Reading Email in Emacs
There are a million tutorials like this out there, but this one is mine.
Previously
I’d set up a system to read email in Emacs before, using notmuch, which I quite liked.
Notmuch and notmuch.el
were very nice for reading, but I didn’t stick with it because getting mail in to the system was somewhat cumbersome.
I’d used Offlineimap to pull mail down, but that was quite slow and due to some sort of race condition or something, it would get wedged fairly frequently. Additionally, the notmuch setup used this script to sync Gmail labels with notmuch tags, which seemed very complex for what I actually needed.
I really wanted to be able to use Emacs for more things though, so I decided to give it another shot.
Current Configuration
mbsync
The main thing I wanted to change was the syncing being both very slow and unreliable, so I decided to give mbsync/isync a try.
It works well enough. The big difference I made from my previous config is to not bother syncing all the million labels I have on my Gmail account, since I don’t really use any of them anymore.
IMAPAccount gmail Host imap.gmail.com User james.nvc@gmail.com PassCmd "gpg2 -q --for-your-eyes-only --no-tty -d ~/.passwd/gmail.gpg" SSLType IMAPS CertificateFile /etc/ssl/certs/ca-certificates.crt IMAPStore gmail-remote Account gmail MaildirStore gmail-local Subfolders Verbatim Path ~/.mail/gmail/ Inbox ~/.mail/gmail/Inbox Channel gmail Master :gmail-remote: Slave :gmail-local: Patterns INBOX ![Gmail]* "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail" "[Gmail]/Drafts" Create Both Expunge Both SyncState * Sync All
notmuch
As previously, I’m using notmuch for viewing the mail, but with my new configuration, I found that removing things from the inbox wasn’t working properly – I could remove them locally, but they wouldn’t be removed in Gmail.
I’m sure there’s a better way of doing it, but what I settled on is just a script (that is run as described below) that moves mail from the “Inbox” folder to “All Mail” when the inbox
tag is removed.
#!/usr/bin/env bash set -euo pipefail notmuch search --output=files --format=text0 -- folder:Inbox and not tag:inbox \ | xargs -0 -I'{}' mv -n '{}' "${HOME}/.mail/gmail/[Gmail]/All Mail/" notmuch new
Automatically Running
To get mbsync running automatically, I used a systemd user units. It was my first time using systemd user units & timers and I quite like it.
First, I create the mbsync service:
[Unit] Description=Mailbox synchronization service JobRunningTimeoutSec=600 [Service] Type=oneshot ExecStartPre=-/home/james/dotfiles/notmuch_archive.sh ExecStart=/usr/bin/mbsync -Va ExecStartPost=/usr/bin/notmuch new
And then a timer unit that runs it.
[Unit] Description=Mailbox synchonization timer [Timer] OnBootSec=2m OnUnitActiveSec=5m Unit=mbsync.service [Install] WantedBy=timers.target
Finally, activate the systemd timer:
systemctl --user daemon-reload systemctl --user start mbsync.timer
This works well, although sometimes mbsync still gets stuck. I use the status bar described below to let me know when that’s happened, and when wedged a simple systemctl --user stop mbsync.service
gets it going again.
Displaying Status
To both show the mail status, as well how long until the next sync/how long the current syncing has been running for (so I can tell if it’s gotten wedged), I put an entry in my i3blocks.conf
like so:
# ... [mail] label=mail command=$HOME/bin/mail_status.sh interval=30 # ...
This defers to the mail_status.sh
script:
#!/usr/bin/env zsh set -euo pipefail if [[ -v BLOCK_BUTTON && ! -z "${BLOCK_BUTTON}" ]]; then systemctl --user stop mbsync.service exit 0 fi INBOX="📨$(notmuch search -- tag:inbox | wc -l)" NEW="👀$(notmuch search -- tag:inbox and tag:unread | wc -l)" UNTIL_RUN=$(systemctl --user list-timers mbsync.timer | "${HOME}/dotfiles/parse_timer.pl" ) printf "%s %s %s" "${UNTIL_RUN}" "${NEW}" "${INBOX}"
This will display the number of emails in the inbox, the number of unread emails in the inbox, and the time until next run.
The “time until running” part uses a hacky perl script to parse the output of systemctl list-timers
, because I’m too lazy to figure out how to get the information from systemctl show
or whatever.
#!/usr/bin/env perl use strict; my $headers = <>; my $start = index($headers, "LEFT"); my $end = index($headers, "LAST"); my $line = <>; my $left = substr($line, $start, $end - $start); $left =~ s/^\s+//; $left =~ s/\s+$//; if ($left ne "n/a") { print($left); } else { my $passed_start = index($headers, "PASSED"); my $passed_end = index($headers, "UNIT"); my $passed = substr($line, $passed_start, $passed_end - $passed_start); print($passed); }
Reading
With this in place, I just use notmuch.el
to read email in Emacs.
My emacs configuration for notmuch is pretty straightforward:
(autoload 'notmuch "notmuch" "notmuch mail" t) ;; setup the mail address and use name (setq mail-user-agent 'message-user-agent user-mail-address "james.nvc@gmail.com" user-full-name "James N. V. Cash" ;; smtp config smtpmail-smtp-server "smtp.gmail.com" smtpmail-smtp-service 465 smtpmail-stream-type 'ssl message-send-mail-function 'message-smtpmail-send-it ;; report problems with the smtp server smtpmail-debug-info t ;; add Cc and Bcc headers to the message buffer message-default-mail-headers "Cc: \nBcc: \n" ;; postponed message is put in the following draft directory message-auto-save-directory "~/.mail/gmail/[Gmail]/Drafts" message-kill-buffer-on-exit t ;; change the directory to store the sent mail message-directory "~/.mail/gmail/[Gmail]/Sent Mail") (with-eval-after-load 'notmuch (setq notmuch-address-selection-function (lambda (prompt collection initial-input) (completing-read prompt (cons initial-input collection) nil t nil 'notmuch-address-history))) (require 'notmuch-address)) (add-hook 'message-mode-hook (lambda () (auto-fill-mode -1))) (add-hook 'message-mode-hook (lambda () (add-to-list 'company-backends 'company-emoji t))) (general-define-key :keymaps '(notmuch-search-mode-map) "j" #'notmuch-search-next-thread "k" #'notmuch-search-previous-thread "g g" #'notmuch-search-first-thread "G" #'notmuch-search-last-thread) (use-package helm-notmuch :defer t) (defun cogent/notmuch-inbox () (interactive) (notmuch-search "tag:inbox" t)) (general-define-key :keymaps 'global "<f5> 5" #'cogent/notmuch-inbox "<f5> 4" #'helm-notmuch "<f5> 3" #'notmuch)
Worth It?
It took a little while to get all this set up – in particular pulling all my mail down (again) took a few days, because Google rate-limits downloads over IMAP – but I am very happy with how this works now. Being able to do everything in Emacs is very satisfying to me; working in Emacs is like wearing a cozy sweater.
I’m also excited to use this set up to also use Emacs for email on my laptop (seeding the maildir by copying it from my primary machine, so I don’t have to re-sync), which I’d never bothered with before.
I’m sure there are easier ways of doing it – my “removing things from the inbox” step seems like I must be missing something – but it seems to be working & I feel good that I actually understand how my whole setup works.