Email in Emacs

Email ports (465, 587, 993) are filtered at work to drop all connections not going to *.outlook.com. Until recently, this meant that I couldn’t access my personal email from work with my favorite client, and had to use the web interface of my email provider.

On the other hand, the SSH port (22) isn’t filtered. With SSH port forwarding I am able to route connnections for my personal emails through a server I control. As I want my work email to not go through this server, I had to find a way to configure the IMAP and SMTP clients to use the proxy or not depending on the account.

🧦 Create proxy to remote server with ssh(1)

SSH can be used to forward all the TCP traffic on a certain port of the local machine to a remote machine. This makes it possible to circumvent blocked or filtered ports. SSH’s DynamicForward configuration option lets you specify which port on the local machine will be used.

The SSH session with the dynamically forwarded port needs to keep running in the background in order to be used by IMAP and SMTP clients’ traffic. ServerAliveInterval and ServerAliveCountMax let SSH keep the connection alive.

# ~/.ssh/config

Host *
	ControlMaster auto
	ControlPath ~/.ssh/masters/%r@%h:%p
	ControlPersist yes
	ServerAliveInterval 30
	ServerAliveCountMax 4

Host server.home.example
	DynamicForward 8888
	ExitOnForwardFailure yes

Run this command to create the SSH connection which keeps running in the background, even if the shell is closed (-f).

$ ssh -f server.home.example :

localhost:8888 is now a SOCKS5 proxy ready for use.

🔒 Encrypt password with gpg(1)

It’s probably not a good idea to type your password in a plaintext configuration file. We can use gpg(1) to keep an encrypted copy of the password and decrypt it on-the-fly.

GPG is a complex beast. This page is a good reference if you haven’t used it before.

👮 Configure the agent

gpg-agent(1) caches your passphrase so you don’t need to type it all the time.

Add these lines to your shell’s initialization file (e.g. .bashrc) to enable gpg-agent(1).

# ~/.bashrc

GPG_TTY=$(tty)
export GPG_TTY

🔏 Create encrypted file containing password

Create passwords directory with the correct permissions.

$ mkdir --parents --mode=700 ~/.passwords

Open the password file ~/.passwords/jdoe@home.example.gpg with Emacs and write the password there. Emacs recognizes the .gpg extension and will automatically and transparently encrypt and decrypt the buffer with gpg(1) when the file is opened and saved.

📥 Retrieve and synchronize emails with offlineimap(1)

offlineimap(1) is an IMAP client which lets you download all your email locally, and synchronises changes (deleted, moved, archived) with the server.

To install it on Debian or Ubuntu, simply run:

# apt install offlineimap

I found it really easy to set up. When in doubt about a configuration parameter, you can look it up in this exhaustive configuration file.

It integrates nicely with GPG (or any other program with a command-line interface) by means of a Python 2 script (support for Python 3 is still in the works). In the configuration below, I indicate where to find this script with the pythonfile parameter.

remotepasseval is a Python function call, which returns the password.

The per-account proxy option is used to specify the type, IP address, and port of the proxy.

# ~/.config/offlineimap/config

[general]
accounts = Home
pythonfile = ~/.config/offlineimap/pass.py

[Account Home]
localrepository = HomeLocal
remoterepository = HomeRemote
proxy = SOCKS5:127.0.0.1:8888

[Repository HomeLocal]
type = Maildir
localfolders = ~/Maildir/Home

[Repository HomeRemote]
type = IMAP
remotehost = mail.home.example
remoteuser = jdoe@home.example
remotepasseval = get_pass("jdoe@home.example")
sslcacertfile = /etc/ssl/certs/ca-certificates.crt

# Repeat with Work email WITHOUT the
# `proxy' directive.

To decrypt the password file we can call gpg(1) in a subprocess. When the GPG agent is configured correctly, the passphrase of your private key will be used automatically.

# ~/.config/offlineimap/pass.py

from __future__ import print_function

import os
from subprocess import check_output

def get_pass(user):
    return check_output(
        [
            "gpg",
            "--no-tty",
            "--quiet",
            "--decrypt",
            os.path.join(
                os.environ["HOME"],
                ".passwords",
                "{}.gpg".format(user),
            ),
        ],
    ).strip()

if __name__ == "__main__":
    print(get_pass("jdoe@home.example"))

You can check that the script works by executing it directly on the command-line:

$ python2 ~/.config/offlineimap/pass.py

Now all is in place to receive emails. You can check that it works:

$ offlineimap -o

Without -o (onetime) the command would run periodically in the background; we don’t need that since Emacs will call it regularly.

The first time offlineimap(1) fetches all emails can take ages, so be patient ☕.

📤 Send emails with msmtp(1)

msmtp(1) is an SMTP client, also very easy to set up.

To install it on Debian or Ubuntu, simply run:

# apt install msmtp

Configuration parameter passwordeval takes a command that will be executed verbatim; we can use it to call gpg(1) and get the decrypted password.

# ~/.config/msmtp/config

account         Home
logfile         ~/.cache/msmtp/home.log
from            jdoe@home.example
host            mail.home.example
port            465
proxy_host      127.0.0.1
proxy_port      8888
user            jdoe@home.example
passwordeval    gpg --no-tty -q -d ~/.passwords/jdoe@home.example.gpg
auth            on
tls             on
tls_starttls    off
tls_trust_file  /etc/ssl/certs/ca-certificates.crt

# Repeat for Work email WITHOUT the
# `proxy_host' and `proxy_port' directives.

account defaults : Home

Check that sending emails works:

$ msmtp --debug -t -- \
	jdoe@home.example \
	<<<'Subject: test'

Permissions for msmtp(1) can be restricted by Linux’ AppArmor. If any permission-related message appears during the test, check /etc/apparmor.d/usr.bin.msmtp (or equivalent on your system). ~/.cache/mstmp/*.log is one of the approved paths for log files.

✍️ Read and compose emails with mu4e

mu4e is a sleek Emacs email client which uses mu(1) as a backend to index and search emails.

To install it on Debian or Ubuntu, simply run:

# apt install mu4e

The first time we start mu(1) we need to build the index of all emails.

$ mu index --rebuild

The configuration of mu4e is quite extensive; this is a minimum example which can be extended for multiple email accounts.

(add-to-list 'load-path
             "/usr/share/emacs/site-lisp/mu4e")
(require 'mu4e)

(setq mail-user-agent 'mu4e-user-agent
      mu4e-update-interval 180
      mu4e-get-mail-command "offlineimap -o")

(setq mu4e-headers-date-format "%Y-%m-%d %H:%M"
      mu4e-headers-fields '((:date    . 20)
                            (:flags   . 6)
                            (:from    . 30)
                            (:subject . nil)))

(setq mu4e-compose-format-flowed t
      message-kill-buffer-on-exit t)

(defun my/maildir-matches (rgx msg)
  "Match MSG’s maildir with RGX."
  (when (and rgx msg)
    (if (listp rgx)
        ;; If rgx is a list, try each one
        (or (my/maildir-matches msg (car rgx))
            (my/maildir-matches msg (cdr rgx)))
        ;; Not a list, check rgx
        (let ((maildir (mu4e-message-field msg :maildir)))
          (string-match rgx maildir)))))

(setq message-sendmail-envelope-from 'header
      sendmail-program "msmtp"
      user-full-name "John Doe")
(setq message-send-mail-function
      #'message-send-mail-with-sendmail)

(defun my/choose-msmtp-account ()
  "Choose account label for ‘msmtp’ account
option based on From header in Message buffer."
  (when (message-mail-p)
    (save-excursion
      (let*
          ((from (save-restriction
                   (message-narrow-to-headers)
                   (message-fetch-field "from")))
           (account
            (cond
             ((string-match "jdoe@home.example" from) "Home"))))
        (setq message-sendmail-extra-arguments
              (list '"--account" account))))))

(add-hook 'message-send-mail-hook
          #'my/choose-msmtp-account)

(setq mu4e-contexts
      `(,(make-mu4e-context
          :name "Home"
          :enter-func (lambda ()
                       (mu4e-message "Switch to Home"))
          :match-func (lambda (msg)
                        (my/maildir-matches "^/Home" msg))
          :leave-func mu4e-clear-caches
          :vars '((user-mail-address  . "jdoe@home.example")
                  (mu4e-maildir       . "~/Maildir/Home")
                  (mu4e-sent-folder   . "/Sent")
                  (mu4e-drafts-folder . "/Drafts")
                  (mu4e-trash-folder  . "/Trash")))
        ;; Repeat for Work email.
        ))

Use M-x mu4e RET in Emacs to start mu4e. C-c C-u is used to trigger email synchronization manually. Use D to delete an email, R to reply, C to compose. Emails are sent with C-c C-c.

📚 References