I found this article about Getting things done with mutt. It talks about using the the techniques from David Allen’s productivity book “Getting Things Done: The Art of Stress-Free Productivity“. I’ve been reading GTD for a week now (taking my time, you know) and think it’s pretty interesting.
Since Mutt is my favorite email client, I spent a little playing with it and making changes. I’m using a diferent editlabel script and my way doesn’t require patching and recompiling mutt.
Here are my muttrc changes:
### GETTING THINGS DONE ###
# http://blogs.techrepublic.com.com/opensource/?p=106&tag=nl.e011
# http://auriga.wearlab.de/~alb/other/mutt-labels/
set editor="emacs"
set move=yes
set mbox='=archive'
unignore X-Label: # make sure to display X-Label on each message
color header red default '^X-Label:'
mailboxes =ACTION
mailboxes =RESPOND
mailboxes =WAITFOR
mbox-hook =ACTION =archive
mbox-hook =RESPOND =archive
macro pager \Ct "<exit><tag-entry><tag-prefix><mark-as-new><tag-prefix><save-message>=ACTION<enter>" "Mark message as ACTION"
macro pager \Cr "<exit><tag-entry><tag-prefix><mark-as-new><tag-prefix><save-message>=RESPOND<enter>" "Mark message as RESPOND"
macro pager \Cw "<exit><tag-entry><tag-prefix><mark-as-new><tag-prefix><save-message>=WAITFOR<enter>" "Mark message as WAITFOR"
macro index y "<enter-command>set editor=\"~/.mutt/editlabel\"\n\
<edit><sync-mailbox><next-undeleted>\
<enter-command>set editor=emacs\n" "Edit Label"
macro pager y "<enter-command>set editor=\"~/.mutt/editlabel\"\n\
<edit><sync-mailbox><next-undeleted>\
<enter-command>set editor=emacs\n" "Edit Label"
macro index \Cy "<limit>~y " "Limit view to label"
set index_format="%4C %Z %{%b %d} %-15.15L %?M?(#%03M)&(%4l)? %?y?{%.20Y} ?%s"
#### END GTD ###
editlabel also has full readline editing and history, which is nice. I haven’t added tab-completion from the history; I’m not sure if I will or not.
My editlabel also has the advantage that you can delete a label just by deleting the line; ^a ^k if you use emacs.
Note that if you use the above, you’ll have to replace the occurances of “emacs” with whatever editor you use.
I just found a similar post: GTD with Mutt
I like my editlabel better, even if I’m using Python’s evil os.system(). But that narrow-wide trick is neat.
Ciao!
UPDATE 2007-10-08: Added mark-as-new when saving, so that they will attract my notice via the mailbox changing.
UPDATE 2007-10-09: Fixed the mark-as-new stuff…had to use tagging to make it work.

2 Comments
Thank you! Very useful, even outside of GTD: just for the labels.
>I like my editlabel better
The same. But cannot point to anything specific (maybe readline).
>But that narrow-wide trick is neat.
What is it?
>evil os.system()
Maybe a comment about the dependancy on formail command could help?
>I haven’t added tab-completion from the >history; I’m not sure if I will or not.
If this is not too much work, it could be appreciated: for emacs users, tab completion feels natural.
Minor comment: instead of duplicating the command in the muttrc, one can use:
macro index,pager
Hello,
thanks for the writeup, this is very close to what I was looking for.
I’ve been hacking on it a bit to add label completion with the Tab key, and a prompt when using new labels to check if you want to add them to the label list (to avoid typos).
I’m keeping everything in my Inbox (including my own sent messages) and adding labels like “Done” to messages I don’t want to see in the default view (!~y Done), but they are still available for other searches.
Also, I’ve used a dirty hack to make the label function work for multiple tagged messages by saving the actions to do (add/remove/set labels) to a temporary file.
Replace “Mutt-edit” with vim/emacs/editor of your choice, and note that I’ve changed the paths.
***** .muttrc
**** editlabel
#!/usr/bin/python -utWall import readline, sys, os histfile = os.path.join(os.environ["HOME"], ".label_history") labelfile = os.path.join(os.environ["HOME"], ".labels") actfile = os.path.join(os.environ["HOME"], ".label_saved_action") def get_label_file(): try: return map(lambda s: s.rstrip(), open(labelfile, 'r').readlines()) except IOError: return def label_completions(text, state): labels = get_label_file() candidates = filter(lambda x: x.startswith(text), labels) try: return candidates[state] except: return def my_input(prompt, default=None, completions=None): if default is not None: def pre_input_hook(): readline.insert_text(str(default)) readline.redisplay() readline.set_pre_input_hook(pre_input_hook) val = [] readline.parse_and_bind("tab: complete") readline.set_completer(completions) return raw_input(prompt) def get_label(filename): header = 'X-Label: ' fp = file(filename, 'r') result = '' for line in fp.readlines(): if line.startswith(header): result = line[len(header):].strip() break fp.close() return result def write_label(filename, label): header = ['X-Label:'] tmpfile = filename + '.tmp' if label: header.append(' ') header.append(label.rstrip().lstrip()) header = '"%s"' % ((''.join(header)).replace('"', '\"')) cmd = ' '.join([ 'formail', '-I', header, '<', filename, '>', tmpfile, '&&', 'mv', tmpfile, filename ]) os.system(cmd) def ask(prompt, default): print prompt + ' ['+default+'] ', ans = sys.stdin.readline().rstrip() if ans == "": return default return ans def edit_label(label): while True: label = my_input('Label: ', label, label_completions) known_labels = set(get_label_file()) unknown = filter(lambda x: not x in known_labels, label.split()) if len(unknown) == 0: break else: not_added=0 for new in unknown: if ask_yn("label '"+new+"' not known, add?", 'n'): open(labelfile, "a").write(new+'\n') else: not_added += 1 if not_added==0: break return label def do_action(action, infile, new_label=None): if action == 'a' or action == '+': if new_label == None: new_label = edit_label("") current_labels = get_label(infile) updated_labels = current_labels for new in new_label.split(): if not new in set(current_labels.split()): updated_labels += ' ' + new if updated_labels != current_labels: write_label(infile, updated_labels) return new_label elif action == 'r' or action == '-': rm_label = new_label if rm_label == None: rm_label = edit_label("") current_labels = get_label(infile) updated_labels = "" for new in current_labels.split(): if not new in set(rm_label.split()): updated_labels += ' ' + new if updated_labels != current_labels: write_label(infile, updated_labels) return rm_label elif action == 'e' or action == '=': label = get_label(infile) if new_label == None: new_label = edit_label(label) if new_label != label: write_label(infile, new_label) return new_label def ask_yn(prompt, default): return ask(prompt, default) == 'y' if "__main__" == __name__: infile = sys.argv[1] if hasattr(readline, 'read_history_file'): try: readline.read_history_file(histfile) except IOError: pass try: actions = open(actfile, 'r').readlines() action = actions[0].rstrip() new_label = actions[1].rstrip() do_action(action, infile, new_label) except IOError: # ask user os.system("clear") action = ask("(a)dd, (r)emove, or (e)dit labels?", "a") new_label = do_action(action, infile) open(actfile, 'w').write(action + '\n' + new_label + '\n') readline.write_history_file(histfile)