Sup email client keybindings and filters

Lately I’ve been playing with Sup, a geeky console-based email client written in Ruby. I’ve used it enough to understand it and settle in a bit, but unfortunately also enough to realize that it’s probably not right for me. Along the way, though, I’ve done some work with keybindings and message filters that I’d like to share with you.

Before I get to that, though, a brief review of Sup. What first attracted me to Sup was the fact that it’s written in Ruby (with fairly clean code, from what I’ve seen), and has a fairly extensive system of hooks for customizing and extending the client behavior (the hooks are also written in Ruby). Also, it has a nice label-based filing system and powerful search utilities (a la Gmail), rather than the tired old folders metaphor. It runs in the console and is entirely keyboard driven, the workflow is pretty quick and easy, and you compose messages in the text editor of your choice (emacs, vim, etc.).

Those are the strong points, for me. Unfortunately, there are a number of issues that made me decide to look elsewhere. Foremost is the console-based interface using Curses, which holds it back in a variety of ways. One issue I ran into was that there’s no way to define keybindings that use the Alt key; Curses simply doesn’t support that. Sup also doesn’t seem to be able to render non-Latin characters, which is occasionally an issue for me. And naturally, being console-based, modern luxuries like HTML mail and previews of attached images are out of the question.

So, Sup turns out not to be my cup of tea after all. But, maybe it is yours, so I’ll share the fruits of my brief foray.

  • The first obstacle was the fact that Sup, like many Unix-style “mail user agents”, has no facilities for the actual fetching or sending of email over a network. Instead, it leverages programs like fetchmail, offlineimap, and sendmail to handle those things. This Unix-style mail system was something I had no prior experience in, so I had a lot to learn. After a lot of research and a fair bit of frustration, my advice is to forget the traditional fetchmail and sendmail, and instead use getmail and msmtp. They are a lot simpler to set up, and do everything most people need.

  • After I got my email imported into Sup, I wanted to change some key bindings to be more comfortable. In particular, being an Emacs user, I am used to Ctrl-n/Ctrl-p to move the cursor up and down lines, while Sup defaults to the vi-style hjkl bindings (which make no sense for me, using the Dvorak keyboard layout). Unfortunately, I could find very little documentation about how to change the keybindings, other than the fact that there was a “keybindings.rb” hook that I could write… if I knew what to write in it.

    Undeterred, I dove into the source code and learned all about the keybindings system. Then, I turned that knowledge into a comprehensive CustomizingKeys wiki page, so that the next guy wouldn’t have to dig through the code like I did. That was a fun afternoon project.

  • Next, I realized I needed a system to filter my incoming mail, automatically apply appropriate labels, and so on. Sup doesn’t have a filtering system like that built in, but it’s fairly straightforward to create one by taking advantage of Sup’s hook system. There is some documentation about this on the AutoAddLabelsToNewMessages wiki page, but as I started writing my filters, I began to realize that, although writing ruby code for every filter gives you a lot of flexibility and power, it also involves a lot of redundant code and is not very easy to read or maintain.

    So, I started writing my own filter system, which loads rules from a YAML file (see the example filters file). I’ve posted the result to my sup-filters repository on GitHub. Actually, what I’ve posted there is even more polished and better documented than what I used myself. Even after I had decided I wasn’t going to use Sup myself, I spent a couple days cleaning it up for other people, to call it a job well done. That’s Just How I Roll ™.

Anyway, if you use Sup, I hope you will find this stuff useful. As for myself, I will miss Sup’s labels and easy searching, but it’s time to continue my quest for a well-fitting email client. Next on my list to try is Wanderlust. If you have any recommendations for other clients to try, leave a comment!

Snippet: Hash#with_only, #merge_existing

Here are some potentiall useful one-liner methods for extracting and merging only parts of Hashes. I was surprised to find out that Hash didn’t already have built-in methods to do these things. Maybe they’re considered so obvious that they don’t need to be built-in, but I think the readability gain is worth it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Hash

  # Return a copy of the receiver with only the given keys.
  # 
  #   >> {:a => 1, :b => 2, :c => 3}.with_only(:a, :c)
  #   => {:a => 1, :c => 3}
  # 
  def with_only( *keys )
    reject{ |k,v| not keys.include?(k) }
  end

  # Return a copy of the receiver without the given keys.
  # 
  #   >> {:a => 1, :b => 2, :c => 3}.without(:a, :c)
  #   => {:b => 2}
  # 
  def without( *keys )
    reject{ |k,v| keys.include?(k) }
  end


  # Like #merge, but only accepts keys that the receiver already has.
  # 
  #   >> {:a => 1, :b => 2}.merge_existing({:a => 0, :c => 0})
  #   => {:a => 0, :b => 2}
  # 
  def merge_existing( other )
    merge( other.with_only(*self.keys) )
  end

  # Like #merge_existing, but modifies the receiver.
  # 
  def merge_existing!( other )
    merge!( other.with_only(*self.keys) )
  end

  alias :update_existing :merge_existing!

end

Snippet: Range#compare

Here’s a little idea that popped into my head for enhancing the Range class in Ruby:

1
2
3
4
5
6
7
8
9
10
11
class Range
  def compare( value )
    if member?( value )
      0
    elsif value <= self.begin
      -1
    elsif value >= self.end
      1
    end
  end
end

Then you can perform broad comparisons, like so:

1
2
3
4
5
6
7
8
9
10
def try_compare( x, r=(1..10) )
  case r.compare x
  when -1
    "#{x} is below the range #{r}"
  when 1
    "#{x} is above the range #{r}"
  when 0
    "#{x} is inside the range #{r}"
  end
end

Example:

1
2
3
4
5
6
>> try_compare( 3 )
"3 is inside the range 1..10"
>> try_compare( -1 )
"-1 is below the range 1..10"
>> try_compare( 11 )
"11 is above the range 1..10"

I haven’t decided whether this is useful, or just daft.