This guide provides a quick setup for configuring Notmuch with GNU Emacs to handle multiple email addresses and enable automatic encryption.

Notmuch has been incredibly useful for me over the past year, and I hope this guide helps new users overcome initial challenges.

Emacs Notmuch with Multiple Addresses & Auto Encryption

Video notes:

Summary

  • Emacs email workflow demo using notmuch.el.
    • Sending mails from multiple addresses, using smtpmail.
    • Auto encrypt emails, if a pgp key is available, using mml-sec.

Why notmuch

  • Builtin Emacs support, with notmuch.el.
    • Packages such as ol-notmuch bring integration with the rest of the Emacs ecosystem.
  • Notmuch is a tag based system. You do not rely on your email provider for filter rules etc to organize your emails into separate mailboxes.
    • Which is inefficient to begin with when working with thousands++ of emails.
    • This also makes moving to a new email provider easier.
  • Locally storing mail.
  • Great performance for filtering and searching through hundred of thousand mails.

Assumptions that I make for this video

You have a working Emacs configuration

Your mail providers has support for retrieving and sending mail via 3rd party apps

  • You already know how to setup isync/mbsync to retrieve emails from remote server.
  • Your mail provider offers smtp credentials to use third party applications.
    • I personally recommend trying out purelymail.com only costs 10$/year and offers good service.

Example isyncrc configuration ~/.config/isyncrc

IMAPAccount gmail
Host imap.gmail.com
Port 993
User [email protected] # Change this to your username.
PassCmd "pass gmail/account" # Change this to a command that returns your password.
TLSType IMAPS
TLSVersions +1.2
CertificateFile /etc/ssl/certs/ca-certificates.crt

IMAPStore gmail-remote
Account gmail

MaildirStore gmail-local
Path ~/Mail/gmail/
Inbox ~/Mail/gmail/Inbox
Trash ~/Mail/gmail/Trash
SubFolders Verbatim

Channel gmail
Far :gmail-remote:
Near :gmail-local:
Patterns *
Expunge None
CopyArrivalDate yes
Sync All
Create Both
SyncState *

After adjusting the above configuration, run mbsync -a to sync your remote mailbox locally.

Setting up a notmuch

First time installation

  • After installing notmuch for the first time, run notmuch setup for an interactive setup.

Create ~/.scripts/notmuch-hook.sh

  • Bash script which will filter/tag your mail

Example:

#!/bin/sh
notmuch new
# Add +inbox tag to a specific alias/address
notmuch tag +inbox -- to:[email protected]

#Sent messages, change this to your mail addresses
notmuch tag -unread -new -inbox +sent -- from:*@mydomain.org or from:*@mydomain.com or from:[email protected]

# Example for Emacs-devel
notmuch tag +emacs-devel -new -inbox -- from:[email protected] or to:[email protected]

# Example for Github
notmuch tag +github -- from:*github.com

# Filter based on other tags, such as drafts
notmuch tag -inbox -sent -inbox -- tag:draft

Emacs configuration

Notmuch

(use-package notmuch
  :config
  (defun thanos/notmuch-update--command (new-buffer)
    (let ((default-directory "~/.scripts/")) ;; Directory where notmuch script is located.
      (async-shell-command "mbsync -a; ./notmuch-hook.sh" new-buffer)))

  (defun thanos/notmuch-update ()
    (interactive)
    (let* ((buffer (generate-new-buffer "*notmuch update*"))
           (height (max 5 (truncate (* (frame-height) 0.1))))
           (window (split-window-vertically (- height))))
      (with-selected-window window
        (switch-to-buffer buffer)
        (let ((proc (thanos/notmuch-update--command buffer)))
          (set-process-sentinel
           (get-buffer-process buffer)
           (lambda (process event)
             (when (memq (process-status process) '(exit signal))
               (delete-window (get-buffer-window (process-buffer process)))
               (kill-buffer (process-buffer process)))))))))

  (defun thanos/notmuch-search ()
    "Custom notmuch-search wrapper, that makes sure we have newest mail first."
    (interactive)
    (let ((query (notmuch-read-query "Notmuch search: ")))
      (notmuch-search query nil)))

  ;; Configure saved searches to your liking, here's an example:
  (setf notmuch-saved-searches
        `((:name "Week's Inbox" :query "tag:inbox date:7d..today" :sort-order newest-first
                 :key ,(kbd "i"))
          (:name "Inbox" :query "tag:inbox" :sort-order newest-first :key ,(kbd "I"))
          (:name "Unread" :query "tag:unread" :sort-order newest-first :key ,(kbd "u"))
          (:name "Today's message" :query "tag:inbox date:today"
                 :sort-order newest-first :key ,(kbd "t"))
          (:name "sent" :query "tag:sent" :sort-order newest-first :key ,(kbd "s"))
          (:name "drafts" :query "tag:draft" :sort-order newest-first :key ,(kbd "d"))
          (:name "all mail" :query "*" :sort-order oldest-first :key ,(kbd "a"))))
  ;; Tag format configuration
  (setf notmuch-tag-formats
        '(("unread" (propertize tag 'face 'notmuch-tag-unread))))
  :bind (("C-x m" . notmuch-hello)
         :map notmuch-hello-mode-map
         ("u" . notmuch-hello-update)
         ("U" . thanos/notmuch-update)
         ("s" . thanos/notmuch-search)
         :map notmuch-search-mode-map
         ("u" . notmuch-refresh-all-buffers)))

smtpmail

  • This configuration is used to send email from multiple addresses, make sure you have your authentication password saved in ~/.authinfo.gpg which emacs is going to use by default.
(use-package smtpmail
  :config
  ;; Default credentials
  (setf smtpmail-smtp-user (auth-source-pass-get "user" "gmail/user") ;; This is not your password, just the username.  Emacs will search for your pw in ~/.authinfo.gpg.
        smtpmail-smtp-server "smtp.gmail.com"
        smtpmail-smtp-service 465 ;; Adjust this to your mail provider port if needed
        smtpmail-stream-type 'ssl
        message-send-mail-function 'smtpmail-send-it
        message-signature "My signature")

  (defun thanos/mail-config-hook ()
    (let ((from (mail-fetch-field "From"))
          (public "<[email protected]>")
          (gmail "<[email protected]>"))
      (cond ((string-match public from)
             (setf smtpmail-smtp-user (auth-source-pass-get "user" "purelymail/thanosapollo")
                   smtpmail-smtp-server "smtp.purelymail.com"))
            ((string-match gmail from)
             (setf smtpmail-smtp-user "[email protected]"
                   smtpmail-smtp-server "smtp.gmail.com")))))
  ;; You can use :hook instead, but I prefer add-hook under :config personally.
  (add-hook 'message-send-hook #'thanos/mail-config-hook))

mml-sec

(use-package mml-sec
  :config
  (defun gpg-list-emails-available ()
  "Return a list of trusted emails.
Parses output of `gpg --list-keys --with-colons' command into a
list of emails."
  (let* ((command "gpg --list-keys --with-colons")
         (output (shell-command-to-string command))
         (lines (split-string output "\n" t))
         (emails '()))
    (dolist (line lines)
      (when (string-prefix-p "uid:" line)
        (when (string-match "<\\([^>]+\\)>" line)
          (let ((email (downcase (string-trim (match-string 1 line)))))
            (push email emails)))))
    (delete-dups emails)))

  (defun thanos/mail-list-recipients-from-buffer ()
    "Get the list of recipients from the current email buffer."
    (save-excursion
      (goto-char (point-min))
      (let ((to (or (mail-fetch-field "To") ""))
            (cc (or (mail-fetch-field "Cc") ""))
            (bcc (or (mail-fetch-field "Bcc") ""))
            recipients)
        (setq recipients (concat to ", " cc ", " bcc))
        ;; Remove empty strings from the result
        (delq nil
              (mapcar (lambda (addr)
                        (let ((clean-addr (string-trim addr)))
                          (when (not (string-empty-p clean-addr))
                            (downcase
                             (if (string-match "<\\([^>]+\\)>" clean-addr)
                                 (string-trim (match-string 1 clean-addr))
                               clean-addr)))))
                      (split-string recipients "," t))))))

  (defun thanos/mail-sign-encrypt ()
    "Auto-encrypt if all recipients have keys, else just sign."
    (let ((trusted-emails (gpg-list-emails-available))
          (recipients (thanos/mail-list-recipients-from-buffer)))
      (if (cl-every (lambda (r) (member r trusted-emails)) recipients)
          (progn
            (message "Encrypting message.")
            (mml-secure-message-sign-encrypt))
        (progn
          (message "Just signing message.")
          (mml-secure-message-sign)))))

  (add-hook 'message-send-hook #'thanos/mail-sign-encrypt)
  :custom
  (mml-secure-openpgp-signers '("62B758D0F6719938BC09CECA339F736C3A720928"))) ;; Change this to your own key!