docwhat's avatardocwhat's blog

GTD and Mutt

I found this article about Getting things done with mutt. It talks about using 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 different editlabel script and my way doesn’t require patching and recompiling mutt.

Here are my muttrc changes:

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><tag -prefix><mark -as-new><tag -prefix><save -message>=ACTION<enter>"  "Mark message as ACTION"
macro pager \Cr  "<exit><tag -entry></tag><tag -prefix><mark -as-new><tag -prefix><save -message>=RESPOND<enter>" "Mark message as RESPOND"
macro pager \Cw  "<exit><tag -entry></tag><tag -prefix><mark -as-new><tag -prefix><save -message>=WAITFOR<enter>" "Mark message as WAITFOR"
macro index y "</enter><enter -command>set editor=\"~/comp/editlabel\"\n\
<edit><enter -command>set editor=\"emacs -nw\"\n\
<sync -mailbox><next -undeleted>" "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 occurrences 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.


  1. UPDATE 2007-10-08: Added mark-as-new when saving, so that they will attract my notice via the mailbox changing.
  2. UPDATE 2007-10-09: Fixed the mark-as-new stuff…had to use tagging to make it work.
  3. UPDATE 2010-03-01: Updated ‘y’ macro based on feedback from Mark Fardal.
  4. UPDATE 2014-08-11: Fixed broken formatting due to age. Fixed some minor typos.


Gravatar for sr

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

Gravatar for klaus weidner
Klaus Weidner


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

macro index,pager y "<enter-command>set editor=\"editlabel\"\n\
<shell-escape>rm -f ~/.label_saved_action<enter>\
<shell-escape>rm -f ~/.label_saved_action<enter>\
<enter-command>set editor=Mutt-edit\n" "Edit Label"

\*\*\*\* editlabel

#!/usr/bin/python -utWall

import os
import readline
import sys

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():
        return map(lambda s: s.rstrip(), open(labelfile, 'r').readlines())
    except IOError:

def label_completions(text, state):
    labels = get_label_file()
    candidates = filter(lambda x: x.startswith(text), labels)
        return candidates[state]

def my_input(prompt, default=None, completions=None):
    if default is not None:
        def pre_input_hook():
        val = []

    readline.parse_and_bind("tab: complete")
    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()
    return result

def write_label(filename, label):
    header = ['X-Label:']
    tmpfile = filename + '.tmp'
    if label:
        header.append(' ')
    header = '"%s"' % ((''.join(header)).replace('"', '\"'))
    cmd = ' '.join(['formail',

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: x not in known_labels, label.split())
        if len(unknown) == 0:
            not_added = 0
            for new in unknown:
                if ask_yn("label '"+new+"' not known, add?", 'n'):
                    open(labelfile, "a").write(new+'\n')
                    not_added += 1
            if not_added == 0:

    return label

def do_action(action, infile, new_label=None):
    if action == 'a' or action == '+':
        if new_label is None:
            new_label = edit_label("")
        current_labels = get_label(infile)
        updated_labels = current_labels
        for new in new_label.split():
            if new not 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 is None:
            rm_label = edit_label("")
        current_labels = get_label(infile)
        updated_labels = ""
        for new in current_labels.split():
            if new not 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 is 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'):
        except IOError:

        actions = open(actfile, 'r').readlines()
        action = actions[0].rstrip()
        new_label = actions[1].rstrip()
        do_action(action, infile, new_label)
    except IOError:
        # ask user
        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')
Gravatar for marc

Your version of editlabel is exactly what I’m looking for (in particular, applying it to tagged message).

Unfortunately, I get the following error when I run it:

Traceback (most recent call last): File “editlabel”, line 148, in newlabel = doaction(action, infile) File “editlabel”, line 122, in doaction newlabel = editlabel(label) File “editlabel”, line 77, in editlabel knownlabels = set(getlabel_file()) TypeError: ‘NoneType’ object is not iterable

I get this when running editlabel , where is a mail file. Can you assist me?


Gravatar for docwhat

“editlabel” is only 64 lines long; I suspect that you cut-and-pasted it and your editor wrapped lines. Try downloading the file directly instead of cut-and-paste.

Submit a Comment


The personal blog of Christian Höltje.
docwhat docwhat contact