Using Emacs as email client
Wednesday 12 October 2022

Engelbecken, Berlin

Couple months ago I decided to reduce the number of online services I’m using and depend on my local machine setup, My local setup is linux on all machines (Except for the gaming PC) so it would be possible to get rid of the Gmail interface for example in favor or a local application.

As I depend on Emacs as an editor and other tasks I wanted to try using it as an email client, The way I used was “Mu4e” which is a package that can read/send emails from emacs interface, here is how I had this setup working.

Downloading all emails locally with OfflineIMAP

Offline IMAP is a python program that can connect to many IMAP servers and sync your emails from the IMAP servers to local machine directory. which means I’ll have all my email offline I can search, read and move them around like any other file on the machine.

Offline IMAP is part of Archlinux community packages so install it with

sudo packman -S offlineimap

Then I needed to configure it to sync the emails to ~/mail directory, OfflineIMAP config file lives in ~/.offlineimaprc, the content for me is as follows

[general]
accounts = account1, account2
pythonfile = ~/path/to/mailpass.py
maxsyncaccounts = 2

[Account account1]
localrepository = account1-local
remoterepository = account1-remote
autorefresh = 5
postsynchook = mu index

[Repository account1-local]
type = Maildir
localfolders = ~/mail/account1

[Repository account1-remote]
type = Gmail
remoteuser = account1@gmail.com
remotepasseval = get_pass("~/.ssh/account1.gmail.password")
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
ssl_version = tls1_2

[Account account2]
localrepository = account2-local
remoterepository = account2-remote
autorefresh = 5
postsynchook = mu index

[Repository account2-local]
type = Maildir
localfolders = ~/mail/account2

[Repository account2-remote]
type = Gmail
remoteuser = account2@gmail.com
remotepasseval = get_pass("~/.ssh/account2.password")
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
ssl_version = tls1_2

This configuration uses a password encrypted in a file with my rsa private key, that gets decrypted with a pythong function defined in ~/path/to/mailpass.py

So I get an application specific password for my email account from my gmail account then I encrypt it to a file like so:

1echo -n "<password>" | openssl rsautl -inkey ~/.ssh/id_rsa -encrypt > ~/path/to/email.password

The ~/path/to/mailpass.py is a python file that has one function it takes a file path and decrypt it with openssl

1#! /usr/bin/env python2
2from subprocess import check_output
3
4def get_pass(file):
5    return check_output("cat " + file + "| openssl rsautl -inkey ~/.ssh/id_rsa -decrypt", shell=True).splitlines()[0]

IMAP uses this get_pass() function to get the password, this is an alternative to writing your password directly in the .offlineimaprc file.

Now running offlineimap command should pickup the configuration and start syncing it to ~/mail, this operation takes a long time depending on the number of emails you have in your mail.

To make sure offlineimap runs everytime I login to my user account I enable and start the systemd service that comes with offlineimap package

1systemctl enable offlineimap --user
2systemctl start offlineimap --user

You’ll notice that offlineimap configuration we added this line

postsynchook = mu index

Using Mu to index the emails

which will run mu index command after every sync, mu is a program that uses Xapian to build a full text search database for the email directory, then you can use mu to search in your emails, mu comes with Mu4e which is the emacs interface for mu.

mu package is in archlinux AUR under the name mu so you can install it with the AUR helpe you have, I’m using yay so the command for me is:

1yay -S mu

Doing that will make offlimeimap sync the emails every 5 minutes and then invokes mu index to rebuild the database.

You can use mu to ask for new emails now, for example I have a script that will display the number of unread emails in my INBOX directories

1mu find 'flag:unread AND (maildir:/account1/INBOX OR maildir:/account2/INBOX)' 2> /dev/null | wc -l

This will query for unread emails in the INBOX directories in each account and then count the number of lines in the output.

I have this line in my polybar configuration which is unobtrusive way to know if I have any new emails, instead of the annoying notifications.

Setting up Emacs Mu4e

Now we’ll need to have our Mu4e interface setup in Emacs so we can read new emails.

I’m using spacemacs but the configuration for Gnu emacs shouldn’t be different. First we load the Mu4e package/layer.

For spacemacs users you should add the layer in your ~/spacemacs layers list

1(mu4e :variables
2     mu4e-use-maildirs-extension t
3     mu4e-enable-async-operations t
4     mu4e-enable-notifications t)

And then I load a file called mu4e-config.el, in the ;;;additional files section I require it

1(require 'mu4e-config)

The file lives in ~/dotfiles/emacs/mu4e-config.el and I push this directory
path to the load-path of emacs with

1(push "~/dotfiles/emacs/" load-path)

The file will hold all of our Mu4e configuration

 1(provide 'mu4e-config)
 2
 3(require 'mu4e-contrib)
 4
 5(spacemacs/set-leader-keys "M" 'mu4e)
 6
 7(setq mu4e-inboxes-query "maildir:/account1/INBOX OR maildir:/account2/INBOX")
 8(setq smtpmail-queue-dir "~/mail/queue/cur")
 9(setq mail-user-agent 'mu4e-user-agent)
10(setq mu4e-html2text-command 'mu4e-shr2text)
11(setq shr-color-visible-luminance-min 60)
12(setq shr-color-visible-distance-min 5)
13(setq shr-use-colors nil)
14(setq mu4e-view-show-images t)
15(setq mu4e-enable-mode-line t)
16(setq mu4e-update-interval 300)
17(setq mu4e-sent-messages-behavior 'delete)
18(setq mu4e-index-cleanup nil)
19(setq mu4e-index-lazy-check t)
20(setq mu4e-view-show-addresses t)
21(setq mu4e-headers-include-related nil)
22
23(advice-add #'shr-colorize-region :around (defun shr-no-colourise-region (&rest ignore)))
24
25(with-eval-after-load 'mu4e
26  (mu4e-alert-enable-mode-line-display)
27  (add-to-list 'mu4e-bookmarks
28               '(:name  "All inboxes"
29                 :query mu4e-inboxes-query
30                 :key   ?i))
31
32  (setq mu4e-contexts
33        `( ,(make-mu4e-context
34             :name "account1"
35             :match-func (lambda (msg) (when msg (string-prefix-p "/account1" (mu4e-message-field msg :maildir))))
36             :vars '(
37                     (mu4e-sent-folder . "/account1/[Gmail].Sent Mail")
38                     (mu4e-drafts-folder . "/account1/[Gmail].Drafts")
39                     (mu4e-trash-folder . "/account1/[Gmail].Trash")
40                     (mu4e-refile-folder . "/account1/[Gmail].All Mail")
41                     (user-mail-address . "account1@gmail.com")
42                     (user-full-name . "Emad Elsaid")
43                     (mu4e-compose-signature . (concat "Emad Elsaid\nSoftware Engineer\n"))
44                     (smtpmail-smtp-user . "account1")
45                     (smtpmail-local-domain . "gmail.com")
46                     (smtpmail-default-smtp-server . "smtp.gmail.com")
47                     (smtpmail-smtp-server . "smtp.gmail.com")
48                     (smtpmail-smtp-service . 587)
49                     ))
50           ,(make-mu4e-context
51             :name "account2"
52             :match-func (lambda (msg) (when msg (string-prefix-p "/account2" (mu4e-message-field msg :maildir))))
53             :vars '(
54                     (mu4e-sent-folder . "/account2/[Gmail].Sent Mail")
55                     (mu4e-drafts-folder . "/account2/[Gmail].Drafts")
56                     (mu4e-trash-folder . "/account2/[Gmail].Trash")
57                     (mu4e-refile-folder . "/account2/[Gmail].All Mail")
58                     (user-mail-address . "account2@gmail.com")
59                     (user-full-name . "Emad Elsaid")
60                     (mu4e-compose-signature . (concat "Emad Elsaid\nSoftware Engineer\n"))
61                     (smtpmail-smtp-user . "account2")
62                     (smtpmail-local-domain . "gmail.com")
63                     (smtpmail-default-smtp-server . "smtp.gmail.com")
64                     (smtpmail-smtp-server . "smtp.gmail.com")
65                     (smtpmail-smtp-service . 587)
66                     ))
67           )))

The previous configuration will define 2 different context each for every email account, each context we tell mu4e the directories for sent, draft, trash and archive directories.

We also told mu4e which smtp servers to use for each context. without it we’ll be able to read the emails but not send any emails. You may have noticed that this configuration doesn’t have the password for the SMTP servers.

Emacs uses a file called ~/.authinfo to connect to remote servers, the SMTP servers are not different for emacs, when mu4e tries to connect to the SMTP server over port 587, emacs will use the credentials in this file to connect to it.

The file content should look like this:

machine smtp.gmail.com port 587 login account1 password <password1>
machine smtp.gmail.com port 587 login account2 password <password2>

We also defined a new bookmark that shows us the inbox emails across all accounts, it’s bound to bi.

And we changed bound the Mu4e interface to SPC M where SPC is my leader key in spacemacs configuration.

How to use this setup

So now this is my workflow:

  • offlineimap works in the background syncing my email to ~/mail
  • When I see the unread email count in my status bar I switch to emacs
  • I press SPC M to open Mu4e
  • I press bi to open the all inbox emails bookmark
  • I open the email with RET and archive it with r
  • When I go through all my unread emails list I press x to archive all emails
  • I press q couple times to continue what I was doing in emacs

Searching through emails:

  • I open Mu4e with SPC M
  • press s to search, I write a word I remember about this email then RET
  • I go through the emails with C-j and C-k
  • When I’m done reading the email I was searching for I quit the email interface with q

Sending email:

  • I open Mu4e with SPC M
  • Start a new compose buffer C
  • Fill in the to and subject fields and the message body
  • Press , c to send it or , k to discard it.

Bonus step to sync across machines

Syncing across machines works when imap sync one machine to the remove IMAP server and the other machine do the same, if you want to make it a bit faster you can use syncthing to sync the ~/mail directory to your other machines, for me I back it up to my phone and other 2 machines at the same time.

Backlinks