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.
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/mbsyncto retrieve emails from remote server. - Your mail provider offers
smtpcredentials to use third party applications.- I personally recommend trying out
purelymail.comonly costs 10$/year and offers good service.
- I personally recommend trying out
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 setupfor 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.gpgwhich 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!