I get a lot of email. I'm also pretty sure you get a lot of email. However, email is still not a solved problemThis is evidenced by the fact that a quick Google search yields no less than ten viable options for email clients on my Mac. . Each potential email client is acceptable on it's own, yet none of them satisfied all of my desired features:
- The ability to access my email without an internet connectionI travel quite a lot, so this was very important to me. .
- Easily move messages between different folders, which is how I keep all of my emails organized by project.
- Quick yet powerful search of all my mail messages.
- Having an auto-updating status indicator that shows me how many unread messages I have.
- Managing multiple accounts (Gmail for personal emails and Microsoft Exchange for work emails) and syncing local changes so that my phone can still be up-to-date.
If you follow this blog, you'll recognize that I've gotten a bit carried away with migrating the different aspects of my life to operate within the Emacs environment. So it was only a matter of time until I finally decided to give it a shot, and I converged upon a solution which happily satisfies all of the above constraints. Every email service is a bit different so YMMV, but this setup works for me.
Here's a screenshot of what we'll be setting up:
My brief adventure with Gnus
There are a ton of tutorials available for reading one's email with Gnus, so it was a natural starting point for my quest. After setting it up, Gnus starts into the standard group summary list, which will display all of the folders it discovered in your various mail accounts. To me, this seemed a bit much, since I have a ton of folders, but this alone wasn't enough to deter me. Unfortunately, I also found that it was quite slowIn fact, the slowness of Gnus is rather well documented on the Emacs Wiki and that the interface was rather cumbersome. The suggested solution to this problem is to host a local email server (groan), which syncs with the Gmail and your other email accounts.
In order to get Gnus working properly, Sacha Chua recommends installing two tools:
offlineimap for the email synchronization and
dovecot for hosting a local IMAP server, since that's how Gnus is able to read the messages. I was able to get
offlineimap working relatively quickly (more on that later), and before too long I had a local copy of all my emails since the dawn of time. By contrast,
dovecot had me scratching my head. Not only could I not get it to work, but it seemed like an unnecessary amount of complexity; I was hosting a local webserver so that my Emacs mail client could read emails that were already saved to my system. So it was at this point that I moved on in search of a better way.
An introduction to mu
After a bit of searching around, I came across a fantastic tool called mu. At it's core, mu is a simple command line tool for searching through emailsSee the mu "cheatsheet" for examples of more powerful search features within mu. . Simply type
mu find $SEARCH into the terminal to query your emails.
It's a cute little tool, and is especially nice for allowing you to quickly check for any new emailYou can easily search for unread emails with
flag:unread. without leaving the terminal. Yet this still doesn't solve all my problems; sure I have an offline copy of my messages and I can search them with ease, but how do I read them, move them around, or interact with them in other ways?
This is where mu4e comes in, the Emacs email client included with mu. It's this that provides me with all of the functionality that I desire: being able to search an offline copy of my emails, easily move them around, and send/reply to different mail servers.
In addition, mu4e has the ability to auto-complete email addresses from names, follow rules about where to archive mail that matches certain filters (like keywords in the subject line) and, via an Emacs package, display a status icon in the modeline when I have new mail messages. In the next few sections, I'll describe how I got everything to work, and any pitfalls I encountered along the way.
Getting set up with mu and OfflineIMAP
As advertised, mu is really just for indexing and searching emails, and relies on other software to maintain a local copy of your messages, which it can then use. To do this, I chose to use the popular OfflineIMAPOn macOS, I installed this with
brew install offlineimap; on Ubuntu, this can be done with apt. , since it's relatively easy to get setup. I have my OfflineIMAP manage two different accounts, Gmail and Exchange, and sync changes between the online services every 5 minutes. Rather than ramble on about how everything should be set up, I'll just reproduce some of the important parts of my configuration file here (taken from my
And example OfflineIMAP configuration python
[general] accounts = Gmail, Exchange maxsyncaccounts = 2 [Account Gmail] localrepository = LocalGmail remoterepository = RepositoryGmail autorefresh = 5 quick = 10 postsynchook = mu index --maildir ~/Maildir status_backend = sqlite [Reposiroty LocalGmail] type = Maildir localfolders = ~/Maildir/Gmail [Reposiroty RepositoryGmail] type = Gmail maxconnections = 2 remoteuser = YOUR_GMAIL_USERNAME remotepass = YOUR_GMAIL_PASSWORD folderfilter = lambda foldername: foldername not in ['[Gmail]/All Mail', '[Gmail]/Important'] sslcacertfile = /usr/local/etc/openssl/cert.pem # This will only work for macOS ## Try one of the following for Ubuntu or Arch: # sslcacertfile = /etc/ssl/certs/ca-certificates.crt # sslcacertfile = OS-DEFAULT # These are effectively the same as the above [Account Exchange] [Repository LocalExchange] [Repository RemoteExchange]
You'll notice a few things about this configurationYou may also notice that arbitrary python code can be specified as part of the configuration. . First, as I have it listed above, you have to enter your password directly into this file, which you probably don't want to do; there's a great Stack Exchange post on how to use GPG and python to encrypt your password. Second, I have included a
folderfilter to avoid storing the All Mail and Important folders that Gmail annoyingly creates. Finally, I call
mu index whenever the sync is complete, via
postsynchook, to ensure that my mu database is as up-to-date as much as possibleBy default, mu looks to
~/Maildir for mail, but I like to include it for clarity. .
Once this is setup, calling
offlineimap from the command line will sync with the remote repositories every 5 minutes. However, this requires keeping the terminal window open. This can be solved by creating a daemon process. On macOS, this is built in to brew, and calling
brew services start offlineimap will get everything started; for Linux, you can follow these instructions on the Arch Linux wiki.
With this step complete, the command line version of mu should now be syncing with the remote server(s) without any issues.
Before even getting to the Emacs configuration file, you should ensure that mu4e is properly installed. Since mu4e is included with the installation of muOn macOS, this is only partially true. See to insure that the install includes mu4e. you need to include mu4e. This can be done with something like
(add-to-list 'load-path "/usr/local/share/emacs/site-lisp/mu/mu4e"). Now, upon reopening Emacs,
M-x mu4e should open a simple window with some shortcuts. Typing
J will bring up a menu for selecting a mail folder. Chose one, and you should be presented with something resembling the screenshot above.
When in the headers view, which displays your email messages, you can easily navigate through different messages using
p and hitting
return will open a message, allowing you to read it. In addition, mu4e includes some very useful marking capabilities:
d marks a message for deletion,
r for refiling/archiving, and
m for moving (after a target directory is specified). Simply press
x to "execute" the marks. In addition, with
* you can "bulk mark" emails; pressing
x after some messages have been marked with
x will allow you to perform an action to all of them. See the mu4e user manual for more details.
I mentioned above that I have two different email addresses and rely on mu4e to manage them both. In the previous screenshot, you can see that I've marked messages for archiving with
r and deletionSee the section below for a caveat about deletion, to avoid premeturely deleting your messages! with
d yet the behavior for the different messages changes depending on their mu4e context. By setting the
mu4e-contexts variable, mu4e will search through the list of options, see if the message of interest matches
:match-func and sets some local variables, like the
mu4e-refile-folder. In the snippet below, I check to see if the mail directory (
/Gmail and, if it does, sets the trash and refile folders accordingly.
Defining mu4e contexts lisp
(setq mu4e-contexts `( ,(make-mu4e-context :name "Gmail" :match-func (lambda (msg) (when msg (string-prefix-p "/Gmail" (mu4e-message-field msg :maildir)))) :vars '( (mu4e-trash-folder . "/Gmail/[Gmail].Trash") (mu4e-refile-folder . "/Gmail/[Gmail].Archive") )) ,(make-mu4e-context :name "Exchange" :match-func (lambda (msg) (when msg (string-prefix-p "/Exchange" (mu4e-message-field msg :maildir)))) :vars '( (mu4e-trash-folder . "/Exchange/Deleted Items") (mu4e-refile-folder . exchange-mu4e-refile-folder) )) ))
For my Exchange server, I have a slightly more complicated procedure; rather than including a specific refile folder, I define a function
exchange-mu4e-refile-folder which does some more filtering. Apparently I don't want any emails from this fictitious going to the typical archive folder. So, whenever I get a message which includes "[some-mailing-list]" in the subject, I can still refile the message with
r and know that it will go to the correct folder.
A custom refiling function lisp
(defun exchange-mu4e-refile-folder (msg) "Function for chosing the refile folder for my Exchange email. MSG is a message p-list from mu4e." (cond ;; FLA messages ((string-match "\\[some-mailing-list\\]" (mu4e-message-field msg :subject)) "/Exchange/mailing-list") (t "/Exchange/Archive") ) )
Alerts for new mail
Now that we can receive email, move it around and keep everything in sync with our different IMAP servers, the next task is to ensure that we're alerted whenever new mail arrives. Fortunately, there's another Emacs package for doing just this: mu4e-alert. The procedure for using mu4e-alert is relatively simple. Whenever you call
mu4e-alert-enable-mode-line-display, your modeline will be updated to include a little envelope icon and the current count of unread messagesThe format of the modeline display can be changed by customizing
You're expectedly a bit annoyed, thinking I thought the icon would update itself! Fortunately, Emacs has the
run-with-timer for just this purpose. However, there remains a small issue: whenever mu4e is open, it maintains a connection to the server. This means that
mu index cannot be run by the OfflineIMAP process whenever mu4e is left open, and new mail will not appear. This is far from ideal. Again, I have a slightly hacky solution. By calling
mu4e~proc-kill periodically, we can sever mu4e's connection to the server. The only consequence of this is that I may occasionally try to archive messages in my inbox that I've already moved on my phone, an issue which is easily remedied by refreshing my mu4e buffer.
My complete mu4e-alert configuration, which relies on John Wiegley's use-package, is as follows:
Mu4e-alert configuration lisp
(use-package mu4e-alert :ensure t :after mu4e :init (setq mu4e-alert-interesting-mail-query (concat "flag:unread maildir:/Exchange/INBOX " "OR " "flag:unread maildir:/Gmail/INBOX" )) (mu4e-alert-enable-mode-line-display) (defun gjstein-refresh-mu4e-alert-mode-line () (interactive) (mu4e~proc-kill) (mu4e-alert-enable-mode-line-display) ) (run-with-timer 0 60 'gjstein-refresh-mu4e-alert-mode-line) )
There's one other hiccup that I haven't yet mentioned; some email servers (cough Gmail cough) will mark messages as unread whenever they are moved to other folders, including the trash. As a result, I've customized my
mu4e-alert-interesting-mail-query variable to check for unread messages in only my inbox folders.
Using mu4e to send mail
Unfortunately IMAP, the protocol for checking email and moving them around, cannot be used to send emails: for that you need to configure SMTP. This process isn't particularly difficult, but it does include a bunch of code, most of which is adapted from the mu4e documentationIf you only have a single account, most of this is unnecessary. . After setting the default values for many of the SMTP parameters, we create a list of account-specific parameter values which are loaded upon composing a message by the
my-mu4e-set-account function. I've included most of my configuration here for the sake of completeness.
Configuration for sending mail lisp
;; I have my "default" parameters from Gmail (setq mu4e-sent-folder "/Users/Greg/Maildir/sent" ;; mu4e-sent-messages-behavior 'delete ;; Unsure how this should be configured mu4e-drafts-folder "/Users/Greg/Maildir/drafts" user-mail-address "firstname.lastname@example.org" smtpmail-default-smtp-server "smtp.gmail.com" smtpmail-smtp-server "smtp.gmail.com" smtpmail-smtp-service 587) ;; Now I set a list of (defvar my-mu4e-account-alist '(("Gmail" (mu4e-sent-folder "/Gmail/sent") (user-mail-address "YOUR.GMAIL.USERNAME@gmail.com") (smtpmail-smtp-user "YOUR.GMAIL.USERNAME") (smtpmail-local-domain "gmail.com") (smtpmail-default-smtp-server "smtp.gmail.com") (smtpmail-smtp-server "smtp.gmail.com") (smtpmail-smtp-service 587) ) ;; Include any other accounts here ... )) (defun my-mu4e-set-account () "Set the account for composing a message. This function is taken from: https://www.djcbsoftware.nl/code/mu/mu4e/Multiple-accounts.html" (let* ((account (if mu4e-compose-parent-message (let ((maildir (mu4e-message-field mu4e-compose-parent-message :maildir))) (string-match "/\\(.*?\\)/" maildir) (match-string 1 maildir)) (completing-read (format "Compose with account: (%s) " (mapconcat #'(lambda (var) (car var)) my-mu4e-account-alist "/")) (mapcar #'(lambda (var) (car var)) my-mu4e-account-alist) nil t nil nil (caar my-mu4e-account-alist)))) (account-vars (cdr (assoc account my-mu4e-account-alist)))) (if account-vars (mapc #'(lambda (var) (set (car var) (cadr var))) account-vars) (error "No email account found")))) (add-hook 'mu4e-compose-pre-hook 'my-mu4e-set-account)
Pitfalls and additional tweaks
I already touched upon a few of the minor issues I encountered when getting everything here to work properly, including how moved messages will occasionally be marked as unread. The biggest uh oh I had to deal with stemmed from some unexptected behavior with OfflineIMAP. Apparently, whenever a message is marked with the trash label
T, which happens whenever you 'delete' a message with
d, OfflineIMAP won't sync it back to the server and, worse still, may delete it entirely. Even though I've marked an item for deletion, I'm comforted by the fact that I can recover a message if I accidentally move it to the trash.
Avoiding this issue requires modifying the way the delete mark
d operates. I simply replaced
-N in the definition of the trash mark. It was a simple (if rather verbose) fix, so I've included it here in its entirety.
Avoid trashing when deleting lisp
(defun remove-nth-element (nth list) (if (zerop nth) (cdr list) (let ((last (nthcdr (1- nth) list))) (setcdr last (cddr last)) list))) (setq mu4e-marks (remove-nth-element 5 mu4e-marks)) (add-to-list 'mu4e-marks '(trash :char ("d" . "▼") :prompt "dtrash" :dyn-target (lambda (target msg) (mu4e-get-trash-folder msg)) :action (lambda (docid msg target) (mu4e~proc-move docid (mu4e~mark-check-target target) "-N"))))
Finally, here are a few more tweaks to the mu4e settings that I frequently use.
Other tweaks lisp
;; Include a bookmark to open all of my inboxes (add-to-list 'mu4e-bookmarks (make-mu4e-bookmark :name "All Inboxes" :query "maildir:/Exchange/INBOX OR maildir:/Gmail/INBOX" :key ?i)) ;; This allows me to use 'helm' to select mailboxes (setq mu4e-completing-read-function 'completing-read) ;; Why would I want to leave my message open after I've sent it? (setq message-kill-buffer-on-exit t) ;; Don't ask for a 'context' upon opening mu4e (setq mu4e-context-policy 'pick-first) ;; Don't ask to quit... why is this the default? (setq mu4e-confirm-quit nil)
I'll try to keep this document up-to-date as I experiment more, however I'm already quite happy with my setup after a couple of weeks of trying it out. There are plenty of features that I haven't touched upon as well, including the ability to link to email messages via org-mode, in which I do much of my work. At any rate, it's just another excuse for me to never leave my Emacs environment.