Save Ukraine

My personal mail setup

Christian Kruse,

Requirements

While it may seem odd that I use Emacs to read and write my mail, it is just a logical consequence on my requirements. I need a MUA which is fast, can display mails in threads, integrates with Org mode, has a good search and lets me edit my mail in Emacs.

I think everybody can understand why I want a fast mail client; having to wait for mail is the least thing I want to do. I don't need to explain that 😉 The same for a good search. But let me explain the other requirements.

Editing with Emacs is a nice to have, since as a Software developer I do text editing with Emacs most of the day. I am very good at editing text with Emacs, and it provides a lot of comfort.

Threading is different. Most FLOSS projects communicate via mailing lists. Discussing things in mailing lists works pretty well, you can take time to make a proper argument, you can prove-read your mail before sending and the barrier of entry is pretty darn low. But to be able to follow a discussion you absolutely have to be able to see to what mail the poster refers to; you have to be able to follow the discussion thread. This is what threading is for. If you have a mail thread with, lets say, 120 mails you will never be able to understand the whole discussion without the threading information.

The integration with Org mode is essential for me. Every time I get a todo or request or just want to answer that mail later I capture a Org mode todo containing a link to the mail itself. Thus I can hit C-c C-o on the link and the mail client opens with the mail. Without this feature I would totally forget most of my todos.

Notmuch reading a emacs-hackers mail

While most modern MUAs provide speed and a good search most of them totally fail in threading. Thunderbird can build proper threads, but Thunderbird sucks for various reasons. KMail can thread but I don't want to use KDE. Evolution is just plain broken, it crashes several times a day. Mutt is ok, but it lacks of comfort. Claws is fine as long as your mail folders are not that big, it lacks of speed. And all of them fail when it comes to a tight Org mode integration. What's left? Well, mail in Emacs.

Handling mail in Emacs

So while I initially thought that people reading mail with Emacs are crazy I became frustrated enough to give mail in Emacs a chance. To read mails with Emacs you basically have four choices:

  • Gnus, the default news reader for Emacs
  • Wanderlust, a MUA based on the traditional principles of a mail client
  • Mu with Mu4e, a mail client using Xapian via mu to index local maildirs
  • Notmuch, also a mail client based on Xapian indexing local mail dirs

I tried them all (and a few others).

Wanderlust

First I tried Wanderlust, it sounded the least complicated. But oh boy, nothing did I know. The documentation is horrible, I had to solve most of my problems with reading code and very old blog posts. And damn, this thing is so slow. I used it for just a week or something like that, I wasn't able to keep it using.

Gnus

Next was Gnus. Gnus is cool when your mail folders are small or if you limit them to just a few mails. But searching is just, meh. It uses the IMAP search capabilities, which are, in my opinion, very limited. Also setting up Gnus is complicated, but you can find tons of documentation for that, the Practical guide to use Gnus with Gmail helped me a lot. You can still find my config in my Emacs repository. But the lack of a good search and the mediocre speed let me move the the next alternative.

Mu/Mu4e

Mu/Mu4e was a relief. It is ridiculously fast and the search capabilities just rock. It follows a mix of a traditional, folder based mail client and a search based mail client. I was totally happy with Mu4e for about a year, and I still would use it if I didn't discover notmuch. It has a few problems, but none of them are major bummers. You can find my Mu4e configuration still in my Emacs repository.

Notmuch

Notmuch capturing a todo from an email

But then, via a blog post by a fellow Emacs user, I discovered notmuch. Notmuch is a purely tag and search based mail client using Xapian for indexing, similar to Mu4e. When Mu4e is blazing fast then notmuch is lightning fast. Opening a mail folder with 120k mails is done in under second; opening a mail with 30MB is just instant. In fact speed is the concept: the tagline is "Not much mail." I'm still using it so I will explain this setup.

Basic concept

Handling email can basically be divided into four topics:

  • receiving mail
  • reading mail
  • composing mail
  • sending mail

Notmuch covers exactly one topic: reading mail. The other topics have to be covered by something else. Let's start with sending mail since this is the easiest:

Sending mail

I send my mail using Msmtp. Msmtp is a SMTP client making it pretty darn easy to get mail to your mail server. It ships with three shell scripts: msmtp-enqueue.sh for putting mails into a queue, msmtp-listqueue.sh for listing the mail queue and msmtp-runqueue.sh for sending mails from the mail queue. I put them to ~/dev/mail/ and reference them in the mail tagging script and configured them as the sendmail command in my Emacs configuration. The Msmtp configuration file only contains the mail server and the authorization data:

# Set default values for all following accounts.
defaults
tls on
#tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile ~/.msmtp.log

account Defunct
host host
from cjk@defunct.ch
tls_starttls off
port 465
auth on
user user
password password

Receiving mail

To receive mail I use OfflineIMAP. It syncs a local collection of maildirs to your IMAP mailbox and vice versa. There is isync/mbsync which is faster, but it can't handle IMAP keep-alive and automatic resyncs, you have to run it via cronjobs. OfflineIMAP, on the other hand, has a daemon mode for IMAP idle and automatic resyncs. My configuration contains nothing special, it is plain simple:

[general]
# List of accounts to be synced, separated by a comma.
accounts = Defunct
ui = quiet

[Account Defunct]
# Identifier for the local repository; e.g. the maildir to be synced via IMAP.
localrepository = Defunct-local
# Identifier for the remote repository; i.e. the actual IMAP, usually non-local.
remoterepository = Defunct-remote
# Status cache. Default is plain, which eventually becomes huge and slow.
status_backend = sqlite
autorefresh = 0.5
quick = 10
newmail_hook = ~/dev/mail/update-mail
postsynchook = ~/dev/mail/update-mail


[Repository Defunct-local]
# Currently, offlineimap only supports maildir and IMAP for local repositories.
type = Maildir
# Where should the mail be placed?
localfolders = ~/Mail/Defunct

[Repository Defunct-remote]
# Remote repos can be IMAP or Gmail, the latter being a preconfigured IMAP.
type = IMAP
folderfilter = lambda foldername: foldername not in ["INBOX.spam"]
remotehost = host
remoteuser = user
remotepass = pass
cert_fingerprint  = fingerprint
idlefolders = ['INBOX']

I start OfflineIMAP via SystemD as described in the Arch wiki. To tag my mail for notmuch I use a small shell script:

basedir="`dirname $0`"
notmuch="/usr/bin/notmuch"
afew="/usr/bin/afew"

if [[ -f /usr/local/bin/notmuch ]]; then
  notmuch="/usr/local/bin/notmuch"
fi

sendmailcommand="$basedir/msmtp-runqueue.sh"

# Check connection status before sending mail
if ! ping -c1 www.google.com > /dev/null 2>&1; then
    # Ping could be firewalled ...
    # '-O -' will redirect the actual html to stdout and thus to /dev/null
    if ! wget -O - www.google.com > /dev/null 2>&1; then
        # Both tests failed. We are probably offline
        # (or google is offline, i.e. the end has come)
        exit 1;
    fi
fi

# We are online: So let's get mail going first
${sendmailcommand} 2>&1 >> ~/.msmtp.log

$afew -m -a

$notmuch new --quiet

$notmuch tag +inbox -- "folder:Defunct/INBOX and not tag:inbox"
$notmuch tag +sent -- "folder:Defunct/Sent and not tag:sent"

# postgresql lists
$notmuch tag +list +pg +hackers -- "folder:Defunct/Lists.pg.hackers and not tag:pg"
$notmuch tag +list +pg +www -- "folder:Defunct/Lists.pg.www and and not tag:pg"
$notmuch tag +list +pg +admin -- "folder:Defunct/Lists.pg.admin and not tag:pg"
$notmuch tag +list +pg +announce -- "folder:Defunct/Lists.pg.announce and not tag:pg"
$notmuch tag +list +pg +bugs -- "folder:Defunct/Lists.pg.bugs and not tag:pg"
$notmuch tag +list +pg +commit -- "folder:Defunct/Lists.pg.committers and not tag:pg"

# emacs lists
$notmuch tag +list +emacs +devel -- "folder:Defunct/Lists.emacs.devel and not tag:emacs"
$notmuch tag +list +emacs +org -- "folder:Defunct/Lists.emacs.org-mode and not tag:emacs"
$notmuch tag +list +emacs +help -- "folder:Defunct/Lists.emacs.help and not tag:emacs"

# CCC lists
$notmuch tag +list +ccc +warpzone -- "folder:Defunct/Lists.ccc.warpzone and not tag:ccc"
$notmuch tag +list +ccc +warpzone +intern -- "folder:Defunct/Lists.ccc.warpzone-intern and not tag:ccc"
$notmuch tag +list +ccc +chaos-west -- "folder:Defunct/Lists.ccc.chaos-west and not tag:ccc"
$notmuch tag +list +ccc +ch +basel +raif +intern -- "folder:Defunct/Lists.ccc.raif-intern and not tag:ccc"
$notmuch tag +list +ccc +ch +basel +raif -- "folder:Defunct/Lists.ccc.chaostreff and not tag:ccc"

# Phoenix lists
$notmuch tag +list +phoenix +talk -- "folder:Defunct/Lists.phoenix.talk and not tag:phoenix"
$notmuch tag +list +phoenix +talk -- "folder:Defunct/Lists.phoenix.core and not tag:phoenix"

# cforum
$notmuch tag +list +cforum -- "folder:Defunct/Lists.cforum and not tag:cforum"

# arch linux lists
$notmuch tag +list +arch +announce -- "folder:Defunct/Lists.arch.announce and not tag:arch"
$notmuch tag +list +arch +dev-public -- "folder:Defunct/Lists.arch.dev-public and not tag:arch"
$notmuch tag +list +arch +general -- "folder:Defunct/Lists.arch.general and not tag:arch"
$notmuch tag +list +arch +security -- "folder:Defunct/Lists.arch.security and not tag:arch"

# hackerspaces.org
$notmuch tag +list +hackerspaces +announce -- "to:announce-de@lists.hackerspaces.org and not tag:announce"
$notmuch tag +list +hackerspaces +discuss -- "to:discuss-de@lists.hackerspaces.org and not tag:discuss"

$notmuch tag +list +sailfish +devel -- "folder:Defunct/Lists.sailfish.devel and not tag:sailfish"

$notmuch tag -inbox +archive -- "folder:Defunct/Archiv and not tag:archive"

$notmuch tag -unread +trash -- 'folder:Defunct/Trash'

Using this script I don't need to worry to keep my tags synced, it sets them based on folders.

Reading and composing mail

Notmuch answering an Emacs-hackers mail

Composing mail when using notmuch in Emacs is done via a derived form of the message mode and the Emacs MIME package. This is tightly coupled to the notmuch configuration, so I will explain the two topics together. First you need to configure notmuch; to do that just run notmuch in the command line. This will start a wizard asking you for the configuration options. Next index the mail with notmuch new - this builds the mail index for Xapian. Now you have to tag your mails, if you look at the script above you get the idea how this is done.

Now that this is out of the way lets get started with the Emacs configuration:

(setq notmuch-search-oldest-first nil
      message-sendmail-envelope-from 'header
      mail-specify-envelope-from 'header
      mail-envelope-from 'header
      notmuch-show-all-multipart/alternative-parts nil
      mime-edit-pgp-signers '("C84EF897")
      mime-edit-pgp-encrypt-to-self t
      mml2015-encrypt-to-self t
      mml2015-sign-with-sender t
      notmuch-crypto-process-mime t
      message-send-mail-function 'message-send-mail-with-sendmail
      sendmail-program "~/dev/mail/msmtp-enqueue.sh"
      message-sendmail-f-is-evil nil
      mail-interactive t
      user-full-name "Christian Kruse"
      user-mail-address "cjk@defunct.ch"
      message-kill-buffer-on-exit t
      mail-user-agent 'message-user-agent
      notmuch-always-prompt-for-sender t
      notmuch-fcc-dirs '((".*" . "Defunct/Sent"))
      notmuch-show-indent-messages-width 4
      notmuch-saved-searches '((:name "inbox" :query "tag:inbox" :key "i")
                               (:name "CForum" :query "folder:Defunct/Lists.cforum")
                               (:name "Warpzone" :query "tag:list and tag:warpzone and not tag:intern")
                               (:name "Warpzone Unread" :query "tag:list and tag:warpzone and not tag:intern and tag:unread")
                               (:name "Warpzone intern" :query "tag:list and tag:warpzone and tag:intern")
                               (:name "Warpzone intern Unread" :query "tag:list and tag:warpzone and tag:intern and tag:unread")
                               (:name "PostgreSQL" :query "tag:pg and tag:list")
                               (:name "PostgreSQL Unread" :query "tag:pg and tag:list and tag:unread")
                               (:name "Phoenix" :query "tag:phoenix and tag:list")
                               (:name "Phoenix Unread" :query "tag:phoenix and tag:list and tag:unread")
                               (:name "Emacs:Devel" :query "tag:emacs and tag:devel and tag:list")
                               (:name "Emacs:Devel Unread" :query "tag:emacs and tag:devel and tag:list and tag:unread")
                               (:name "Emacs:Help" :query "tag:emacs and tag:help and tag:list")
                               (:name "Emacs:Help Unread" :query "tag:emacs and tag:help and tag:list and tag:unread")
                               (:name "Emacs:Org Unread" :query "tag:emacs and tag:org and tag:list and tag:unread")
                               (:name "unread" :query "tag:unread" :key "u")
                               (:name "flagged" :query "tag:flagged" :key "f")
                               (:name "sent" :query "tag:sent" :key "t")
                               (:name "drafts" :query "tag:draft" :key "d")
                               (:name "all mail" :query "*" :key "a")))
  • notmuch-search-oldest-first defines the sort order: I like the newest messages first.
  • message-sendmail-envelope-from and mail-specify-envelope-from tell the message mode, that it should derive the envelope address from the From header of the mail - I use this to change my sender address from time to time, e.g. for registration mails when I want to use a specific mail address.
  • notmuch-show-all-multipart/alternative-parts defines that notmuch should not show all alternative parts of the mail; this is often just some junk I'm not interested in.
  • mime-edit-pgp-signers defines the standard keys for signing mails.
  • mime-edit-pgp-encrypt-to-self defines that I want a copy encrypted with my public key for encrypted mails; thus I can read this mail again later. Same for mml2015-encrypt-to-self just for a different Emacs mode.
  • mml2015-sign-with-sender says that mml should find the key by the sender address.
  • notmuch-crypto-process-mime says notmuch that it should decrypt encryted messages and check mail signatures.
  • message-send-mail-function defines the mail sending function, a wrapper around sendmail in this case. sendmail-program defines the sendmail program and message-sendmail-f-is-evil tells message mode that the -f flag for sendmail is not supported (since we use notmuch-enqueue.sh as a sendmail replacement).
  • mail-interactive tells Emacs to report sending errors back to me and not let the MTA take care of this.
  • user-full-name and user-mail-address define my name and mail address.
  • message-kill-buffer-on-exit tells message mode to kill message editing buffers when the mail has been sent or sending has been canceled.
  • mail-user-agent tells Emacs which mail delivery package I want to use. message-user-agent in our case.
  • notmuch-always-prompt-for-sender tells notmuch to ask me for the sender address when composing or forwarding a message.
  • notmuch-fcc-dirs is a ruleset for the sent folder; in this case it is just my sent folder of my single account.
  • notmuch-show-indent-messages-width defines the indention width for threading.
  • notmuch-saved-searches contains standard searches.

For Org mode integration we load org-notmuch:

(require 'org-notmuch)

Now lets define some shortcuts and we're done:

(define-key notmuch-show-mode-map "\C-c\C-o" 'browse-url-at-point)

(define-key notmuch-search-mode-map "g"
  'notmuch-poll-and-refresh-this-buffer)
(define-key notmuch-hello-mode-map "g"
  'notmuch-poll-and-refresh-this-buffer)

(define-key notmuch-search-mode-map "d"
  (lambda ()
    "toggle deleted tag for thread"
    (interactive)
    (if (member "deleted" (notmuch-search-get-tags))
        (notmuch-search-tag '("-deleted"))
      (notmuch-search-tag '("+deleted" "-inbox" "-unread")))))

(define-key notmuch-search-mode-map "!"
  (lambda ()
    "toggle unread tag for thread"
    (interactive)
    (if (member "unread" (notmuch-search-get-tags))
        (notmuch-search-tag '("-unread"))
      (notmuch-search-tag '("+unread")))))


(define-key notmuch-show-mode-map "d"
  (lambda ()
    "toggle deleted tag for message"
    (interactive)
    (if (member "deleted" (notmuch-show-get-tags))
        (notmuch-show-tag '("-deleted"))
      (notmuch-show-tag '("+deleted" "-inbox" "-unread")))))



(define-key notmuch-search-mode-map "a"
  (lambda ()
    "toggle archive"
    (interactive)
    (if (member "archive" (notmuch-search-get-tags))
        (notmuch-search-tag '("-archive"))
      (notmuch-search-tag '("+archive" "-inbox" "-unread")))))


(define-key notmuch-show-mode-map "a"
  (lambda ()
    "toggle archive"
    (interactive)
    (if (member "archive" (notmuch-show-get-tags))
        (notmuch-show-tag '("-archive"))
      (notmuch-show-tag '("+archive" "-inbox" "-unread")))))


(define-key notmuch-hello-mode-map "i"
  (lambda ()
    (interactive)
    (notmuch-hello-search "tag:inbox")))

(define-key notmuch-hello-mode-map "u"
  (lambda ()
    (interactive)
    (notmuch-hello-search "tag:unread")))

(define-key notmuch-hello-mode-map "a"
  (lambda ()
    (interactive)
    (notmuch-hello-search "tag:archive")))

These should be self-explanatory.

This is my basic email configuration. What's left to do? Well, two things: I have a setup to move mails out of my inbox using afew and notifications for new mails. For notifications I don't have a working implementation since this is very low in my priority. But I can explain my afew setup to you:

Moving mails with afew

I use afew to move mails to different folders, so that other MUAs I'm using don't show me just a very long list of mails. The configuration is very basic, I just use the „move mails” feature. My ~/.config/afew/config is as follows:

[MailMover]
folders = Defunct/INBOX

# rules
Defunct/INBOX = 'tag:archive':Defunct/Archiv 'tag:cforum':Defunct/Lists.cforum 'tag:deleted':Defunct/Trash

It basically says: use the MailMover plugin and look in Defunct/INBOX; in the inbox move mails found by the defined searches to the specific folders. I call afew in my update-mails script above.

That's it. This is my mail setup. Do you have questions? Are you missing something? Drop me a mail!