forsooth!

By necessity, by proclivity, and by delight, we all quote.

Email in spacemacs


Motivation

Email is tedious. Especially at work I get hundreds of emails every day, which need to be managed somehow. A lot of those emails are noise, and ideally I’d only ever have to worry about emails that are signal.

Additionally, writing email requires a good editor with vim bindings (duh!), spacemacs delivers. Navigating, tagging, and searching emails with modal keybindings also makes dealing with them less annoying.

This documents my setup, which uses spacemail, isync, msmtp, and notmuch.

Setup

isync/mbsync

isync is a command line tool that syncs a remote IMAP account and a local mail directory. I simply run it periodically to poll for new emails. My fork supports the macOS Keychain for password retrieval.

My ~/.mbsyncrc looks like this:

IMAPAccount forsooth
Host imap.forsooth.org
User or
KeychainName <KeychainName>
KeychainAccount <KeychainAccount>
SSLType IMAPS
AuthMechs LOGIN

IMAPStore forsooth-remote
Account forsooth

MaildirStore forsooth-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/Mail/or@forsooth.com/
Inbox ~/Mail/or@forsooth.com/INBOX/

Channel forsooth
Master :forsooth-remote:
Slave :forsooth-local:
Patterns "INBOX"

Create Slave
SyncState *

This syncs everything in the folder “INBOX”. I leave everything serverside in that folder, because it avoids moving emails around physically and syncing back and forth with IMAP. All the tagging is done locally via notmuch.

In order to sync multiple folders, the list can be extended to include them, it also supports regular expressions. Either way the Calendar folder of Exchange accounts should be ignored, as Exchange does some non-RFC things that might break the sync (at least that used to happen to me with offlineimap, I haven’t seen it with mbsync).

For my work account I also require a specific SSL certificate for the handshake, and the auth configuration differs a little.

CertificateFile "/usr/local/etc/openssl/certs/some-authority.pem"
  1. Credentials via macOS Keychain (recommended)

    My config uses KeychainName and KeychainAccount to retrieve the password from the macOS Keychain. This requires Keychain support added in my fork, see above, until it is merged upstream (perhaps).

  2. Credentials via GPG (discouraged)

    An alternative to the Keychain is retrieving the password for the account from ~/.authinfo.gpg, an encrypted file containing login information for SMTP and IMAP:

    machine imap.forsooth.org login or@forsooth.org port 465 password <hunter2>
    machine imap.forsooth.org login or@forsooth.org port 993 password <hunter2>
    

    In that case you’d have to set a password command like this:

    PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"
    

    Note that anyone can do this, if they have access to your machine while you are logged in, if you use gpg-agent to avoid constant password prompts. The Keychain method is more secure.

Migrate from offlineimap

If you started with offlineimap and want to switch to mbsync, then that’s easy, just follow the steps outlined here.

notmuch

notmuch processes a local mail directory, building a database that tags and indexes all mails for text searches.

My ~/.notmuch-config is given below, each setting is described in detail in the notmuch documentation and the template for that file.

[database]
path=/home/or/Mail

[user]
name=or
primary_email=or@forsooth.org

[new]
tags=unread;inbox;new;
ignore=

[search]
exclude_tags=deleted;spam;

[maildir]
synchronize_flags=true

[crypto]
gpg_path=gpg

Importantly there is the setup of the user that sends the email and the tags new mail gets automatically.

Here is the rough structure of the script to process new email, ideally running after offlineimap syncs the mail.

# find out how many threads are unread before the sync
previous_unread_count=$(notmuch count --output=threads tag:unread)
notmuch new

# count new and unread email after the sync
new_count=$(notmuch count tag:new)
unread_count=$(notmuch count --output=threads tag:unread)

# if something changed...
if [[ "$new_count" -gt 0 && \
      "$unread_count" -gt 0 && \
      "$unread_count" -ne "$previous_unread_count" ]]; then
  # use $new_count and $unread_count for some notification, omitted here
fi

# remove the "new" tag from all emails that have it
notmuch tag -new tag:new

tagging rules

The main script containing all the rules is ~/Mail/.notmuch/hooks/post-new, which may look like this:

notmuch tag +sent folder:sent
notmuch tag +draft folder:drafts

notmuch tag +me tag:new to:or@forsooth.org
notmuch tag +cron tag:new from:"Cron Daemon"
notmuch tag +invitation-response tag:new 'vcalendar AND (subject:"accepted" OR subject:"declined" OR subject:"tentative")'
notmuch tag +ticket tag:new 'subject:ticket'
notmuch tag +mailing-list tag:new some-topic@mailinglist.com
notmuch tag +build tag:new from:build-tools@some-service.com
notmuch tag +review tag:new subject:"Code Review"
notmuch tag +cake tag:new cake

notmuch tag +noise -inbox -unread tag:new AND tag:build AND NOT tag:me AND "successfully"
notmuch tag +noise -inbox -unread tag:new AND tag:invitation-response
notmuch tag +noise -inbox -unread tag:new AND tag:cron
notmuch tag +noise -inbox -unread tag:new AND tag:review AND NOT tag:me
notmuch tag -inbox tag:new AND tag:mailing-list
notmuch tag -inbox -unread from:or@forsooth.org AND NOT to:or@forsooth.org

notmuch tag +muted "$(notmuch search --output=threads tag:muted)"
notmuch tag +noise -inbox -unread tag:new AND tag:muted

notmuch tag -unread -inbox tag:new AND tag:sent

notmuch tag +signal tag:inbox

This is a normal bash script, which allows some pretty cool rules that’d be hard or impossible to apply in the usual email clients.

In the example config I always filter for mails with the tag new, which makes the processing much faster. Additional tags are applied based on several rules, which identify them as coming from cronjobs or a build service. Many such emails don’t have to be read, so they get the inbox tag removed right away. I also give them a noise tag and the remaining emails a signal tag for some statistics later on.

You may wonder why inbox and unread tags are used in the way they are. I usually filter for tag:inbox AND tag:unread in spacemacs, as you’ll see in the next section, but they both serve a different purpose:

  • inbox - the email is in the inbox and probably needs some action
  • unread - the email is worth looking at but hasn’t been read yet

An example is the mailing-list tag, which keeps an email’s unread tag, but removes inbox, because usually there’s a lot of traffic and I just read through them, without expecting each of them to require a response or work.

Anything with both tags will show up in the inbox, after reading it will lose the unread tag, but remain in the inbox until it is archived explicitly.

Note that any email in a thread given the tag muted will also get muted and removed from inbox.

Another example of how this scripting can do a bit more than just tagging is the following block, which pages me whenever an email mentions cake in the office.

  cake_query="tag:new AND tag:cake AND tag:unread AND NOT from:or@forsooth.org"
  num_cake=$(notmuch search $cake_query | wc -l | awk '{print $1}')
  if [[ "$num_cake" > 0 ]]; then
      cake_email="/tmp/cake_email.txt"
      cat  > $cake_email <<EOF
From: pager@forsooth.org
To: pager@forsooth.org
Subject: "${num_cake}x" cake!

There's cake!
EOF
      sendmail -t < $cake_email
  fi

msmtp

In order to send emails via SMTP, I now use msmtp, which can act as a sendmail replacement but sends via an SMTP server. Previously I set up spacemacs to send via SMTP, but that required the .authinfo.gpg method above to retrieve the password. I forked msmtp to also support macOS Keychain access.

My ~/.msmtprc looks like this:

defaults
tls on
# might need this for your server
# tls_trust_file /usr/local/etc/openssl/certs/some-authority.pem

account or
host smtp.forsooth.org
from or@forsooth.org
auth on
user or
keychain_name <KeychainName>
keychain_account <KeychainAccount>

account default : or
  1. Credentials via macOS Keychain (recommended)

    Just like in the mbsync configuration, this specifies a KeychainName and KeychainAccount to look up the password in the macOS Keychain. This is currently requires my fork, see above, but hopefully it can be merged upstream.

  2. Credentials via GPG (discouraged)

    Again, the password can be read from .authinfo.gpg instead. The configuration would look like this:

    passwordeval "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"
    

    But as mentioned above, this allows anyone with access to an unlocked machine to run the command and to retrieve your password. The Keychain method is more secure.

spacemacs

Finally, we need a way to read and write emails. This is where spacemacs comes in.

With a small layer defined in ~/.emacs.d/private/layers/spacemail/packages.el my .spacemacs config contains:

(defun dotspacemacs/layers ()
  ;; ...
  dotspacemacs-configuration-layer-path '("~/.emacs.d/private/layers/")
  dotspacemacs-configuration-layers '(;; ...
                                      spacemail
                                      ;; ...
                                      )

  ;; ...
  )

(defun dotspacemacs/user-config ()
  ;; ...
  ;; use msmtp as a sendmail replacement to send mails
  (setq message-send-mail-function 'message-send-mail-with-sendmail)
  (setq sendmail-program "/usr/local/bin/msmtp")
  ;; get the "From:" address from the envelope Emacs generates
  (setq message-sendmail-envelope-from 'header)
  ;; ...
  )

And that is it. In the package file above there are several shortcuts set up. Most importantly SPC a m i for inbox/unread mail and SPC a m n for new mail.

Once in a mail listing useful shortcuts are (evil mode):

  • h, j, k, l navigate
  • RET open mail on cursor
  • s search via filter, e.g. tag:build, cake AND NOT tag:me, hey there
  • / search in buffer
  • t tag mail on cursor; use -<tag> or +<tag>
  • Z to switch to tree view
  • r respond to sender
  • R respond to all

And now all your vim bindings and spacemacs features will work when writing emails, which is much more nicer than some typical TextWidget.

For instance, you can format paragraphs quickly and wrap them at the proper width, including quoted text, which can just be selected with V and reformated with gq, while keeping the > prefix correctly at the beginning of each line.

Last but not least you can take org-mode links to emails! Inside an email just hit SPC a o l to store a link (this is not specific for the emails, it works in other files as well) and insert it in an org-mode document via ​, i l. This is very nice to make TODOs to respond to specific emails, or attach an important thread to a projet or task.

#+TITLE: Email in spacemacs
#+DATE: <2017-10-22 Sun>
#+AUTHOR: @or
#+CATEGORY: tech
#+SUMMARY: Manage, read, write, send emails in spacemacs
#+SLUG: email-in-spacemacs
#+TAGS: spacemacs, notmuch, email, offlineimap

** Motivation
Email is tedious. Especially at work I get hundreds of emails every day, which
need to be managed somehow. A lot of those emails are noise, and ideally I'd
only ever have to worry about emails that are signal.

Additionally, writing email requires a good editor with vim bindings (duh!),
spacemacs delivers. Navigating, tagging, and searching emails with modal
keybindings also makes dealing with them less annoying.

This documents my setup, which uses [[https://github.com/or/spacemail][spacemail]], =isync=, =msmtp=, and =notmuch=.

** Setup
*** isync/mbsync
[[http://isync.sourceforge.net/][isync]] is a command line tool that syncs a remote IMAP account and a
local mail directory. I simply run it periodically to poll for new emails.
[[https://github.com/or/isync][My fork]] supports the macOS Keychain for password retrieval.

My =~/.mbsyncrc= looks like this:
#+begin_src conf
IMAPAccount forsooth
Host imap.forsooth.org
User or
KeychainName <KeychainName>
KeychainAccount <KeychainAccount>
SSLType IMAPS
AuthMechs LOGIN

IMAPStore forsooth-remote
Account forsooth

MaildirStore forsooth-local
Subfolders Verbatim
# The trailing "/" is important
Path ~/Mail/or@forsooth.com/
Inbox ~/Mail/or@forsooth.com/INBOX/

Channel forsooth
Master :forsooth-remote:
Slave :forsooth-local:
Patterns "INBOX"

Create Slave
SyncState *
#+end_src

This syncs everything in the folder "INBOX". I leave everything serverside in
that folder, because it avoids moving emails around physically and syncing back
and forth with IMAP. All the tagging is done locally via =notmuch=.

In order to sync multiple folders, the list can be extended to include them, it
also supports regular expressions.
Either way the Calendar folder of Exchange accounts should be ignored, as
Exchange does some non-RFC things that might break the sync (at least that used
to happen to me with =offlineimap=, I haven't seen it with =mbsync=).

For my work account I also require a specific SSL certificate for the handshake,
and the auth configuration differs a little.

#+begin_src conf
CertificateFile "/usr/local/etc/openssl/certs/some-authority.pem"
#+end_src

**** Credentials via macOS Keychain (recommended)
My config uses =KeychainName= and =KeychainAccount= to retrieve the password
from the macOS Keychain. This requires Keychain support added in my fork, see
above, until it is merged upstream (perhaps).

**** Credentials via GPG (discouraged)
An alternative to the Keychain is retrieving the password for the account from
=~/.authinfo.gpg=, an encrypted file containing login information for SMTP and IMAP:
#+begin_src conf
machine imap.forsooth.org login or@forsooth.org port 465 password <hunter2>
machine imap.forsooth.org login or@forsooth.org port 993 password <hunter2>
#+end_src
In that case you'd have to set a password command like this:
#+begin_src conf
PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"
#+end_src
Note that anyone can do this, if they have access to your machine while you are
logged in, if you use gpg-agent to avoid constant password prompts. The Keychain
method is more secure.

*** Migrate from offlineimap
If you started with offlineimap and want to switch to =mbsync=, then that's
easy, just follow the steps outlined [[https://github.com/or/spacemail#migrate-from-offlineimap][here]].

*** notmuch
[[https://notmuchmail.org/][notmuch]] processes a local mail directory, building a database that tags and
indexes all mails for text searches.

My =~/.notmuch-config= is given below, each setting is described in detail in
the notmuch documentation and the template for that file.
#+begin_src conf
[database]
path=/home/or/Mail

[user]
name=or
primary_email=or@forsooth.org

[new]
tags=unread;inbox;new;
ignore=

[search]
exclude_tags=deleted;spam;

[maildir]
synchronize_flags=true

[crypto]
gpg_path=gpg
#+end_src

Importantly there is the setup of the user that sends the email and the tags new
mail gets automatically.

Here is the rough structure of the script to process new email, ideally running
after offlineimap syncs the mail.
#+begin_src sh
  # find out how many threads are unread before the sync
  previous_unread_count=$(notmuch count --output=threads tag:unread)
  notmuch new

  # count new and unread email after the sync
  new_count=$(notmuch count tag:new)
  unread_count=$(notmuch count --output=threads tag:unread)

  # if something changed...
  if [[ "$new_count" -gt 0 && \
        "$unread_count" -gt 0 && \
        "$unread_count" -ne "$previous_unread_count" ]]; then
    # use $new_count and $unread_count for some notification, omitted here
  fi

  # remove the "new" tag from all emails that have it
  notmuch tag -new tag:new
#+end_src

*** tagging rules
The main script containing all the rules is =~/Mail/.notmuch/hooks/post-new=,
which may look like this:
#+begin_src sh
  notmuch tag +sent folder:sent
  notmuch tag +draft folder:drafts

  notmuch tag +me tag:new to:or@forsooth.org
  notmuch tag +cron tag:new from:"Cron Daemon"
  notmuch tag +invitation-response tag:new 'vcalendar AND (subject:"accepted" OR subject:"declined" OR subject:"tentative")'
  notmuch tag +ticket tag:new 'subject:ticket'
  notmuch tag +mailing-list tag:new some-topic@mailinglist.com
  notmuch tag +build tag:new from:build-tools@some-service.com
  notmuch tag +review tag:new subject:"Code Review"
  notmuch tag +cake tag:new cake

  notmuch tag +noise -inbox -unread tag:new AND tag:build AND NOT tag:me AND "successfully"
  notmuch tag +noise -inbox -unread tag:new AND tag:invitation-response
  notmuch tag +noise -inbox -unread tag:new AND tag:cron
  notmuch tag +noise -inbox -unread tag:new AND tag:review AND NOT tag:me
  notmuch tag -inbox tag:new AND tag:mailing-list
  notmuch tag -inbox -unread from:or@forsooth.org AND NOT to:or@forsooth.org

  notmuch tag +muted "$(notmuch search --output=threads tag:muted)"
  notmuch tag +noise -inbox -unread tag:new AND tag:muted

  notmuch tag -unread -inbox tag:new AND tag:sent

  notmuch tag +signal tag:inbox

#+end_src
This is a normal bash script, which allows some pretty cool rules that'd be hard
or impossible to apply in the usual email clients.

In the example config I always filter for mails with the tag =new=, which makes
the processing much faster. Additional tags are applied based on several rules,
which identify them as coming from cronjobs or a build service. Many such emails
don't have to be read, so they get the =inbox= tag removed right away. I also
give them a =noise= tag and the remaining emails a =signal= tag for some
statistics later on.

You may wonder why =inbox= and =unread= tags are used in the way they are.
I usually filter for =tag:inbox AND tag:unread= in spacemacs, as you'll see in the
next section, but they both serve a different purpose:
- inbox - the email is in the inbox and probably needs some action
- unread - the email is worth looking at but hasn't been read yet

An example is the =mailing-list= tag, which keeps an email's =unread= tag,
but removes =inbox=, because usually there's a lot of traffic and I just read
through them, without expecting each of them to require a response or work.

Anything with both tags will show up in the inbox, after reading it will lose
the =unread= tag, but remain in the =inbox= until it is archived explicitly.

Note that any email in a thread given the tag =muted= will also get muted and
removed from =inbox=.

Another example of how this scripting can do a bit more than just tagging is the
following block, which pages me whenever an email mentions cake in the office.
#+begin_src sh
  cake_query="tag:new AND tag:cake AND tag:unread AND NOT from:or@forsooth.org"
  num_cake=$(notmuch search $cake_query | wc -l | awk '{print $1}')
  if [[ "$num_cake" > 0 ]]; then
      cake_email="/tmp/cake_email.txt"
      cat  > $cake_email <<EOF
From: pager@forsooth.org
To: pager@forsooth.org
Subject: "${num_cake}x" cake!

There's cake!
EOF
      sendmail -t < $cake_email
  fi
#+end_src

*** msmtp
In order to send emails via SMTP, I now use [[https://marlam.de/msmtp/][msmtp]], which can act as a =sendmail=
replacement but sends via an SMTP server.
Previously I set up spacemacs to send via SMTP, but that required the
=.authinfo.gpg= method above to retrieve the password. I [[https://github.com/or/msmtp-mirror][forked msmtp]] to also
support macOS Keychain access.

My =~/.msmtprc= looks like this:
#+begin_src conf
defaults
tls on
# might need this for your server
# tls_trust_file /usr/local/etc/openssl/certs/some-authority.pem

account or
host smtp.forsooth.org
from or@forsooth.org
auth on
user or
keychain_name <KeychainName>
keychain_account <KeychainAccount>

account default : or
#+end_src

**** Credentials via macOS Keychain (recommended)
Just like in the =mbsync= configuration, this specifies a =KeychainName= and
=KeychainAccount= to look up the password in the macOS Keychain. This is
currently requires my fork, see above, but hopefully it can be merged upstream.

**** Credentials via GPG (discouraged)
Again, the password can be read from =.authinfo.gpg= instead. The configuration
would look like this:
#+begin_src conf
passwordeval "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.authinfo.gpg"
#+end_src
But as mentioned above, this allows anyone with access to an unlocked machine to
run the command and to retrieve your password. The Keychain method is more secure.

*** spacemacs
Finally, we need a way to read and write emails. This is where spacemacs comes
in.

With a small layer defined in [[https://github.com/or/spacemail/blob/master/lib/spacemail/packages.el][~/.emacs.d/private/layers/spacemail/packages.el]] my
=.spacemacs= config contains:
#+begin_src lisp
  (defun dotspacemacs/layers ()
    ;; ...
    dotspacemacs-configuration-layer-path '("~/.emacs.d/private/layers/")
    dotspacemacs-configuration-layers '(;; ...
                                        spacemail
                                        ;; ...
                                        )

    ;; ...
    )

  (defun dotspacemacs/user-config ()
    ;; ...
    ;; use msmtp as a sendmail replacement to send mails
    (setq message-send-mail-function 'message-send-mail-with-sendmail)
    (setq sendmail-program "/usr/local/bin/msmtp")
    ;; get the "From:" address from the envelope Emacs generates
    (setq message-sendmail-envelope-from 'header)
    ;; ...
    )
#+end_src

And that is it. In the package file above there are several shortcuts set up.
Most importantly =SPC a m i= for inbox/unread mail and =SPC a m n= for new mail.

Once in a mail listing useful shortcuts are (evil mode):
- =h=, =j=, =k=, =l= navigate
- =RET= open mail on cursor
- =s= search via filter, e.g. =tag:build=, =cake AND NOT tag:me=, =hey there=
- =/= search in buffer
- =t= tag mail on cursor; use -<tag> or +<tag>
- =Z= to switch to tree view
- =r= respond to sender
- =R= respond to all

And now all your vim bindings and spacemacs features will work when writing
emails, which is much more nicer than some typical TextWidget.

For instance, you can format paragraphs quickly and wrap them at the proper
width, including quoted text, which can just be selected with =V= and reformated
with =gq=, while keeping the =>= prefix correctly at the beginning of each line.

Last but not least you can take org-mode links to emails! Inside an email just
hit =SPC a o l= to store a link (this is not specific for the emails, it works
in other files as well) and insert it in an org-mode document via =​, i l=. This
is very nice to make TODOs to respond to specific emails, or attach an important
thread to a projet or task.