Entries About Feed

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.