NotMuch is an email indexing and search tool based on Xapian. Like Gmail, Notmuch relies on tagging and fast searching to organize mail. By default, incoming mail is given the tags “UNREAD” and “INBOX.” When the mail is read and archived the two tags, respectively, are removed.

Notmuch comes bundled with an emacs mail client that relies on message-mode for composing mail.

For more information about Notmuch, see the official wiki.

NotMuch and Gnus

It is also possible to use NotMuch to index mail that is otherwise read in Gnus. See for more details. suggests one way of setting up notmuch to work with Gnus. However, the functions for going from a notmuch result to the message in gnus need some fiddling. In the function below, replace USER with your username in the imap account:

    (defun notmuch-file-to-group (file)
     "Calculate the Gnus group name from the given file name."
     (let ((group (file-name-directory (directory-file-name (file-name-directory file)))))
       (setq group (replace-regexp-in-string ".*/Maildir/" "nnimap+USER:" group))
       (setq group (replace-regexp-in-string "/$" "" group))
       (if (string-match ":$" group)
        	(concat group "INBOX")
          (replace-regexp-in-string ":\\." ":" group))))
     (defun notmuch-goto-message-in-gnus ()
       "Open a summary buffer containing the current notmuch
       (unless (gnus-alive-p) (with-temp-buffer (gnus)))
       (let ((group (notmuch-file-to-group (notmuch-show-get-filename)))
     	 (replace-regexp-in-string "\"" ""
     	  (replace-regexp-in-string "^id:" ""
         (if (and group message-id)
     	  (gnus-summary-read-group group 1) ; have to show at least one old message
     	  (gnus-summary-refer-article message-id)) ; simpler than org-gnus method?
           (message "Couldn't get relevant infos for switching to Gnus."))))
     (define-key notmuch-show-mode-map (kbd "C-c C-c") 'notmuch-goto-message-in-gnus)

Expunging Deleted Mail

The standard way to expunge all the emails tagged with “deleted” is to run:

     notmuch search --output=files tag:deleted | xargs -L 1 rm

However this fails if the path to any of the files contains spaces. In this case you need something like:

     notmuch search --output=files tag:deleted | tr '\n' '\0' | xargs -0 -L 1 rm

This works on GNU/Linux but I can’t see why it should not work elsewhere. I don’t know if there is a simpler way to do it. The best thing to do seems to put the above line followed by:

     notmuch new

in a shell script.

NotMuch and Planner Mode

I have posted code for integration of Notmuch with Planner Mode on the PlannerMode page.

NotMuch and Org-Contacts

When you are initially starting to use org-contacts it can be tedious to add all of your existing contacts into the contacts database. Using notmuch’s [python bindings]( you can quickly get an exhaustive list of contacts from an e-mail account that can be winnowed down to those you care about. Using some simple scripting with org-map-entries after the face can make adding tags, etc much easier.

#!/usr/bin/env python

# imports

from notmuch import Database, Query
from email.Utils import parseaddr
import re

# Functions
# Uses notmuch to gather all contacts.

def get_contacts():
    db = Database(mode=Database.MODE.READ_WRITE)
    msgs = Query(db, '*').search_messages()
    _all_msg_addrs = []

    for msg in msgs:
        _addr_list = []
        addr_hdr = ["to", "from", "cc", "bcc"]
        for hdr in addr_hdr:
            _addr = msg.get_header(hdr)
            _addr_list = _addr.split(",")
            for a in _addr_list:
                # normalize
                normalized = a.lower().strip().encode('ascii', 'ignore')
                # get address tuple from email and remove spaces
                tabled = [e.strip() for e in parseaddr(normalized)]
                # Remove any of that repeated address cruft people always put in
                tabled[0] = re.sub("\<.*\>", "", re.sub("\(.*\)", "", tabled[0]))
                # Strip out any quotes for when we convert to org later
                tabled = [re.sub("\"", " ", x).strip() for x in tabled]
                tabled = [re.sub("\'", " ", x).strip() for x in tabled]
                # I don't want no empty strings
                tabled = [x for x in tabled if x != "" and x != " "]
                # Check to make sure all single line items are address'
                _address = False
                if len(tabled) == 1:
                    if "@" in tabled[0]:
                        _address = True
                    _address = True
                if _address and tabled != [] and tabled not in _all_msg_addrs:
    return _all_msg_addrs

def indexed(x, cons):
    for i in cons:
        if len(i) == 2:
            if x == i[1]:
                return True
    return False

def deduplicate(raw_contacts):
  items = [x for x in raw_contacts if not indexed(x[0], raw_contacts)]
  return items

def make_org(contacts, my_org_contacts):
      pheader = ":PROPERTIES:\n"
      pbackend = """:PHONE:
      with open(my_org_contacts, "w+") as cfile:
          for i in contacts:
              cfile.write("* {0}\n".format(i[0]))
              if len(i) == 1:
                  cfile.write(":EMAIL: {0}\n".format(i[0]))
                  cfile.write(":EMAIL: {0}\n".format(i[1]))

# Main

def main():
    raw_contacts = get_contacts()
    deduped = deduplicate(raw_contacts)
    make_org(deduped, "./")

if __name__ == '__main__':