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
- A complete guide to email in Emacs using mu and mu4e by Gregory J. Stein, 2017-03-02.
- Configuring Emacs mu4e with nullmailer, offlineimap and multiple identities by Charl P. Botha, 2014-06-06.