2023-12-03 18:57:36 +00:00
#+title : Configuring Emacs for Email with Notmuch
#+author : Howard X. Abrams
#+date : 2020-09-16
#+tags : emacs email
2021-11-15 06:08:09 +00:00
A literate configuration file for email using Notmuch.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none
2022-03-09 18:45:37 +00:00
;;; ha-email --- Email configuration using Notmuch. -*- lexical-binding: t; -* -
;;
2023-02-23 17:35:36 +00:00
;; © 2020-2023 Howard X. Abrams
2022-06-18 00:25:47 +00:00
;; Licensed under a Creative Commons Attribution 4.0 International License.
2022-03-09 18:45:37 +00:00
;; See http://creativecommons.org/licenses/by/4.0/
;;
;; Author: Howard X. Abrams <http://gitlab.com/howardabrams >
;; Maintainer: Howard X. Abrams
;; Created: September 16, 2020
;;
;; This file is not part of GNU Emacs.
;;
;; *NB:* Do not edit this file. Instead, edit the original literate file at:
;; ~/other/hamacs/ha-email.org
;; And tangle the file to recreate this one.
;;
;;; Code:
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* Introduction
2024-02-26 05:21:41 +00:00
To use this system, begin with ~SPC a M~ (after a ~SPC a n~ to asychronously download new mail … which probably should be running beforehand).
2021-11-15 17:46:52 +00:00
When the Notmuch interface up, hit ~J~ to jump to one of the Search boxes (described below). Typically, this is ~i~ for the Imbox, check out the focused message from people I care). Hit ~q~ to return.
2022-06-18 00:25:47 +00:00
Next type ~s~ to view and organize mail I've never seen before. We need to focus the display, so /creating auto filtering rules/ is important. Move the point to the message and hit one of the following to automatically move the sender to a pre-defined box:
2021-11-15 17:46:52 +00:00
2022-06-18 00:25:47 +00:00
- ~I~ :: screened email important enough to go to my Imbox
- ~S~ :: spam … so much goes here
2021-11-15 17:46:52 +00:00
- ~P~ :: receipts go to this *Paper Trail* , which takes a *tag* as the name of the store
2022-06-18 00:25:47 +00:00
- ~f~ :: mailing lists and other email that might be nice to read go to *The Feed*
2022-02-02 19:43:10 +00:00
2021-12-01 19:04:02 +00:00
** Email Addresses
2022-06-18 00:25:47 +00:00
The configuration files below expect email addresses (I store passwords and other encrypted information elsewhere). These email addresses are /not/ private, but I figured I would annoy any screenscraping spam-inducing crawlers out there, while still allowing others to follow my lead on configuring Emacs and Email.
2021-12-01 19:04:02 +00:00
2024-03-07 04:02:25 +00:00
#+name : email-address-1
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none :tangle no :results silent
2022-09-24 04:59:33 +00:00
(rot13-string "ubjneq@ubjneqnoenzf.pbz")
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-01 19:04:02 +00:00
2024-03-07 04:02:25 +00:00
#+name : email-address-2
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none :tangle no :results silent
2022-09-24 04:59:33 +00:00
(rot13-string "ubjneq.noenzf@tznvy.pbz")
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-01 19:04:02 +00:00
2024-03-07 04:02:25 +00:00
#+name : email-address-3
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :exports none :tangle no :results silent
2021-12-01 19:04:02 +00:00
(rot13-string "ubjneq@shmmlgbnfg.pbz")
2022-06-18 00:25:47 +00:00
#+end_src
2021-12-01 19:04:02 +00:00
2022-09-24 04:59:33 +00:00
To use these, we set the =:noweb yes= (to pull in the /name/ of the code block) but put a pair of parens after the name to have it evaluated. For instance, the configuration for sending mail through the MUA in Emacs:
#+begin_src emacs-lisp :noweb yes
(setq send-mail-function 'smtpmail-send-it
message-send-mail-function 'smtpmail-send-it
smtpmail-starttls-credentials '(("smtp.gmail.com" 587 nil nil))
smtpmail-auth-credentials `(("smtp.gmail.com" 587 ,<<email-address-1 >> nil))
smtpmail-default-smtp-server "smtp.gmail.com"
smtpmail-smtp-server "smtp.gmail.com"
smtpmail-smtp-service 587)
#+end_src
2021-11-15 17:46:52 +00:00
* Installation and Basic Configuration
2021-11-15 06:08:09 +00:00
To begin, we need the code. On Ubuntu, this is:
2022-06-18 00:25:47 +00:00
#+begin_src shell :tangle no
2022-09-24 04:59:33 +00:00
sudo apt install -y notmuch
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
And on MacOS, we use =brew= :
2022-06-18 00:25:47 +00:00
#+begin_src shell :tangle no
2022-09-24 04:59:33 +00:00
brew install notmuch
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
Next, we need some basic configuration settings and some global keybindings:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :noweb yes
2021-11-15 06:08:09 +00:00
(use-package notmuch
:init
2021-11-15 17:46:52 +00:00
(setq mail-user-agent 'notmuch-user-agent
notmuch-mail-dir (format "%s/%s" (getenv "HOME") ".mail")
2021-11-15 06:08:09 +00:00
notmuch-hooks-dir (expand-file-name ".notmuch/hooks" notmuch-mail-dir)
2021-11-15 17:46:52 +00:00
notmuch-show-logo nil
2021-11-15 06:08:09 +00:00
notmuch-message-deleted-tags '("+deleted" "-inbox" "-unread")
notmuch-archive-tags '("-inbox" "-unread" "+archived")
notmuch-show-mark-read-tags '("-inbox" "-unread" "+archived")
notmuch-search-oldest-first nil
notmuch-show-indent-content nil)
:bind (:map notmuch-hello-mode-map
2022-09-24 04:59:33 +00:00
("U" . notmuch-retrieve-messages)
2022-05-31 18:51:56 +00:00
("C" . notmuch-mua-new-mail)
:map notmuch-tree-mode-map
2023-10-13 03:59:53 +00:00
("C" . notmuch-mua-new-mail)))
#+end_src
And some leader-based keybindings:
#+begin_src emacs-lisp :noweb yes
(use-package notmuch
2022-09-24 04:59:33 +00:00
:config
(ha-leader ; Should I put these under an "m" heading?
2024-03-04 18:54:45 +00:00
"a n" '("new mail" . notmuch-retrieve-messages)
2022-09-24 04:59:33 +00:00
"a c" '("compose mail" . compose-mail))
2024-03-02 03:30:31 +00:00
(major-mode-hydra-define notmuch-hello-mode nil
("Mail"
(("u" notmuch-retrieve-messages "new mail")
("m" notmuch "read mail"))
"Messages"
(("c" notmuch-mua-new-mail "compose")
("C" hey-notmuch-reply-later "reply-later"))))
2022-09-24 04:59:33 +00:00
<<hey-show-keybindings >>
<<hey-search-keybindings >>)
2023-10-13 03:59:53 +00:00
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 17:46:52 +00:00
Also, let's do some basic configuration of Emacs' mail system:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 17:46:52 +00:00
(setq mm-text-html-renderer 'shr
mail-specify-envelope-from t
message-kill-buffer-on-exit t
message-send-mail-function 'message-send-mail-with-sendmail
message-sendmail-envelope-from 'header)
2022-06-18 00:25:47 +00:00
#+end_src
2022-09-24 04:59:33 +00:00
Create a special mail perspective:
#+begin_src emacs-lisp
2024-03-04 18:54:45 +00:00
(ha-leader "a M" `("mail" . ,(ha-app-perspective "mail" #'notmuch)))
2022-09-24 04:59:33 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* Configuration
Do I want to sign messages by default? Nope.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2022-09-24 04:59:33 +00:00
(add-hook 'message-setup-hook 'mml-secure-sign-pgpmime)
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** Addresses
I need to incorporate an address book again, but in the meantime, searching through a history of past email works well enough.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-09-24 04:59:33 +00:00
(setq notmuch-address-selection-function
(lambda (prompt collection initial-input)
(completing-read prompt
(cons initial-input collection)
nil
t
nil
'notmuch-address-history)))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** Sending Messages
Do I need to set up [[https://marlam.de/msmtp/ ][MSMTP ]]? No, as Notmuch will do that work.
To do this, type ~c~ and select an option (including ~r~ to reply).
** Retrieving Messages
When we start notmuch, we need to retrieve the email and then process it. Most of this is actually contained in the Notmuch configuration.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-09-24 04:59:33 +00:00
(defun notmuch-retrieve-messages ()
"Retrieve and process my mail messages."
(interactive)
(async-shell-command "notmuch new"))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* iSync Configuration
2022-06-18 00:25:47 +00:00
Using [[https://isync.sourceforge.io/ ][isync ]] (or is it =mbsync= ) for mail retrieval. I have a couple of Google Mail accounts that I want connected.
2021-11-15 06:08:09 +00:00
2022-09-24 04:59:33 +00:00
There are global settings:
2022-06-18 00:25:47 +00:00
#+begin_src conf :tangle ~/.mbsyncrc :noweb yes
2021-12-01 19:04:02 +00:00
# Note: We now tangle this file from ~/other/hamacs/ha-email.org
Create Both
SyncState *
2022-09-24 04:59:33 +00:00
MaxMessages 100
2021-12-01 19:04:02 +00:00
Sync All # New ReNew Flags
2022-09-24 04:59:33 +00:00
#+end_src
2021-12-01 19:04:02 +00:00
2022-09-24 04:59:33 +00:00
The following section is for my /personal/ (not too general) account.
The file generally can have a =Pass= entry for the encrypted passcode, but to show how to connect to more than one accounts, I'm using a GPG daemon:
#+begin_src conf :tangle ~/.mbsyncrc :noweb yes
# PERSONAL ACCOUNT
2021-12-01 19:04:02 +00:00
IMAPAccount personal
Host imap.gmail.com
2022-09-24 04:59:33 +00:00
User <<email-address-1() >> # Substitute your own email address here
2021-12-01 19:04:02 +00:00
PassCmd "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.mailpass-personal.gpg"
SSLType IMAPS
AuthMechs LOGIN
IMAPStore personal-remote
Account personal
MaildirStore personal-local
Path ~/.mail/personal/
Inbox ~/.mail/personal/INBOX
Flatten .
Channel personal-inbox
2023-10-13 03:59:53 +00:00
Far :personal-remote:
Near :personal-local:
2021-12-01 19:04:02 +00:00
Patterns * !"[Gmail]/Drafts" !"[Gmail]/Spam"
Expunge Both
# Patterns "inbox"
# ExpireUnread no
Channel personal-sent
2023-10-13 03:59:53 +00:00
Far :personal-remote:"[Gmail]/Sent Mail"
Near :personal-local:sent
2021-12-01 19:04:02 +00:00
ExpireUnread yes
Channel personal-trash
2023-10-13 03:59:53 +00:00
Far :personal-remote:"[Gmail]/Trash"
Near :personal-local:trash
2021-12-01 19:04:02 +00:00
ExpireUnread yes
2022-09-24 04:59:33 +00:00
#+end_src
2021-12-01 19:04:02 +00:00
2022-09-24 04:59:33 +00:00
I have other email accounts that could use or ignore.
#+begin_src conf :tangle no
# GMAIL ACCOUNT
2021-12-01 19:04:02 +00:00
IMAPAccount gmail
Host imap.gmail.com
2022-09-24 04:59:33 +00:00
User <<email-address-2() >> # Substitute your own email address here
2021-12-01 19:04:02 +00:00
PassCmd "gpg -q --for-your-eyes-only --pinentry-mode loopback -d ~/.mailpass-google.gpg"
SSLType IMAPS
AuthMechs LOGIN
IMAPStore gmail-remote
Account gmail
MaildirStore gmail-local
Path ~/.mail/gmail/
Inbox ~/.mail/gmail/INBOX
Flatten .
Channel gmail-inbox
Master :gmail-remote:
Slave :gmail-local:
Patterns * !"[Gmail]/Drafts" !"[Gmail]/Spam"
Expunge Both
# Patterns "inbox"
Channel gmail-sent
Master :gmail-remote:"[Gmail]/Sent Mail"
Slave :gmail-local:sent
ExpireUnread yes
Channel gmail-trash
Master :gmail-remote:"[Gmail]/Trash"
Slave :gmail-local:trash
ExpireUnread yes
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* Notmuch Configuration
2022-06-18 00:25:47 +00:00
Notmuch requires these configuration files.
2021-11-15 06:08:09 +00:00
** =notmuch-config=
The general settings file that goes into =~/.notmuch-config= :
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config
2021-11-15 06:08:09 +00:00
# .notmuch-config - Configuration file for the notmuch mail system
# Note: We now tangle this file from ~/other/hamacs/ha-email.org
#
# For more information about notmuch, see https://notmuchmail.org
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
The commentary for each of the subsections came from their man page.
*** Database configuration
2022-06-18 00:25:47 +00:00
The value supported here is =path= which should be the top-level directory where your mail exists and to where =mbsync= will new mail. Files should be individual email messages. Notmuch will store its database within a sub-directory of the path configured here named ".notmuch".
2021-11-15 06:08:09 +00:00
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config
2021-11-15 06:08:09 +00:00
[database]
path=.mail
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
*** User configuration
2022-06-18 00:25:47 +00:00
Here is where you can let notmuch know how you address emails. Valid settings are
2021-11-15 06:08:09 +00:00
- =name= :: Your full name.
- =primary_email= :: Your primary email address.
- =other_email= :: A list (separated by =;= ) of other email addresses at which you receive email.
2022-06-18 00:25:47 +00:00
Notmuch use the email addresses configured here when formatting replies. It will avoid including your own addresses in the recipient list of replies, and will set the From address based on the address in the original email.
2021-11-15 06:08:09 +00:00
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config :noweb yes
2022-09-24 04:59:33 +00:00
[user]
name=Howard Abrams
primary_email=<<email-address-1() >>
other_email=<<email-address-2() >>;<<email-address-3() >>
2022-06-18 00:25:47 +00:00
#+end_src
*NB:* In the configuration above, you may see the addresses are all set to =nil= . If you are copying this from a rendered web page, note that you need to substitute that with your own email address.
2021-11-15 06:08:09 +00:00
*** Configuration for "notmuch new"
2022-06-18 00:25:47 +00:00
Note the following supported options:
- =tags= :: A list (separated by =;= ) of the tags that added to all messages incorporated by "notmuch new".
2021-11-15 06:08:09 +00:00
- =ignore= :: A list (separated by =;= ) of file and directory names that will not be searched for messages by "notmuch new".
NOTE: *Every* file/directory that goes by one of those names will be ignored, independent of its depth/location in the mail store.
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config
2022-09-24 04:59:33 +00:00
[new]
tags=unread;inbox;
ignore=
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
*** Search configuration
The following option is supported here:
- =exclude_tags= :: A ;-separated list of tags that will be excluded from search results by default. Using an excluded tag in a query will override that exclusion.
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config
2022-09-24 04:59:33 +00:00
[search]
exclude_tags=deleted;spam;
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
*** Maildir compatibility configuration
The following option is supported here:
- =synchronize_flags= :: Valid values are true and false. If true, then the following maildir flags (in message filenames) will be synchronized with the corresponding notmuch tags:
| Flag | Tag |
|------+---------------------------------------------|
| D | draft |
| F | flagged |
| P | passed |
| R | replied |
| S | unread (added when 'S' flag is not present) |
The =notmuch new= command will notice flag changes in filenames and update tags, while the =notmuch tag= and =notmuch restore= commands will notice tag changes and update flags in filenames.
2022-06-18 00:25:47 +00:00
#+begin_src conf-unix :tangle ~/.notmuch-config
2022-09-24 04:59:33 +00:00
[maildir]
synchronize_flags=true
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
That should complete the Notmuch configuration.
** =pre-new=
Then we need a shell script called when beginning a retrieval, =pre-new= that simply calls =mbsync= to download all the messages:
2022-06-18 00:25:47 +00:00
#+begin_src shell :tangle ~/.mail/ .notmuch/hooks/pre-new :shebang "#!/bin/bash"
2022-09-24 04:59:33 +00:00
# More info about hooks: https://notmuchmail.org/manpages/notmuch-hooks-5/
# Note: We now tangle this file from ~/other/hamacs/ha-email.org
2021-11-15 06:08:09 +00:00
2022-09-24 04:59:33 +00:00
echo "Starting not-much 'pre-new' script"
2021-11-15 06:08:09 +00:00
2022-09-24 04:59:33 +00:00
mbsync -a
2021-11-15 06:08:09 +00:00
2022-09-24 04:59:33 +00:00
echo "Completing not-much 'pre-new' script"
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** =post-new=
And a =post-new= hook based on a filtering scheme that mimics the Hey.com workflow taken from [[https://gist.githubusercontent.com/frozencemetery/5042526/raw/57195ba748e336de80c27519fe66e428e5003ab8/post-new ][this gist ]] (note we have more to say on that later on) to filter and tag all messages after they have arrived:
2022-06-18 00:25:47 +00:00
#+begin_src shell :tangle ~/.mail/ .notmuch/hooks/post-new :shebang "#!/bin/bash"
2021-11-15 06:08:09 +00:00
# Based On: https://gist.githubusercontent.com/frozencemetery/5042526/raw/57195ba748e336de80c27519fe66e428e5003ab8/post-new
# Note: We now tangle this file from ~/other/hamacs/ha-email.org
#
# Install this by moving this file to <maildir>/.notmuch/hooks/post-new
# NOTE: you need to define your maildir in the vardiable nm_maildir (just a few lines below in this script)
# Also create empty files for:
# 1. thefeed.db (things you want to read every once in a while)
# 2. spam.db (things you never want to see)
# 3. screened.db (your inbox)
# 4. ledger.db (papertrail)
# in the hooks folder.
# More info about hooks: https://notmuchmail.org/manpages/notmuch-hooks-5/
# Note:
# Old emails: notmuch search --output summary NOT date:30d.. and tag:unread
# Ignore old emails: notmuch tag -unread --output summary NOT date:30d.. and tag:unread
echo "Starting not-much 'post-new' script"
export nm_maildir="$HOME/.mail"
export start="-1"
echo Working from $nm_maildir
function timer_start {
echo -n " starting $1"
export start=$(date +"%s")
}
function timer_end {
end=$(date +"%s")
delta=$(($end-$start))
mins=$(($delta / 60))
secs=$(($delta - ($mins*60)))
echo " -- $1 completed: ${mins} minutes, ${secs} seconds"
export start="-1" # sanity requires this or similar
}
timer_start "ledger"
while IFS= read -r line; do
nm_tag=$(echo "$line" | cut -d' ' -f1 -)
nm_entry=$(echo "$line" | cut -d' ' -f2 -)
if [ -n "$nm_entry" ]
then
notmuch tag +archived +ledger/"$nm_tag" -inbox -- tag:inbox and tag:unread and from:"$nm_entry"
fi
echo -n "Handling entry: $nm_tag, $nm_entry"
done < $nm_maildir/.notmuch/hooks/ledger.db
timer_end "ledger"
timer_start "unsubscribable_spam"
for entry in $(cat $nm_maildir/.notmuch/hooks/spam.db)
do
if [ -n "$entry" ]
then
notmuch tag +spam +deleted +archived -inbox -unread -- tag:inbox and tag:unread and from:"$entry"
fi
done
timer_end "unsubscribable_spam"
timer_start "thefeed"
for entry in $(cat $nm_maildir/.notmuch/hooks/thefeed.db)
do
if [ -n "$entry" ]
then
notmuch tag +thefeed +archived -inbox -- tag:inbox and tag:unread and from:"$entry"
fi
done
timer_end "thefeed"
timer_start "Screened"
notmuch tag +screened 'subject:/\[Web\]/ '
for entry in $(cat $nm_maildir/.notmuch/hooks/screened.db)
do
if [ -n "$entry" ]
then
notmuch tag +screened -- from:"$entry" # tag:unread and tag:inbox and
fi
done
timer_end "Screened"
# Projects...
timer_start "Old-Projects"
notmuch tag +old-project 'subject:/.*howardabrams\/node-mocks-http/ '
notmuch tag +old-project 'subject:/.*Pigmice2733/ '
timer_end "Old-Projects"
notmuch tag +screened 'subject:[Web]'
echo "Completing not-much 'post-new' script"
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* Hey
I originally took the following configuration from [[https://youtu.be/wuSPssykPtE ][Vedang Manerikar's video ]], along with [[https://gist.github.com/vedang/26a94c459c46e45bc3a9ec935457c80f ][the code ]]. The ideas brought out were to mimic the hey.com email workflow, and while not bad, I thought that maybe I could improve upon it slowly over time.
To allow me to keep Vedang's and my code side-by-side in the same Emacs variable state, I have renamed the prefix to =hey-= , however, if you are looking to steal my code, you may want to revisit the source.
** Default Searches
A list of pre-defined searches act like "Folder buttons" at the top to quickly see files that match those /buckets/ :
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(setq notmuch-saved-searches '((:name "Imbox"
:query "tag:inbox AND tag:screened AND tag:unread"
:key "i"
:search-type 'tree)
(:name "Previously Seen"
:query "tag:screened AND NOT tag:unread"
:key "I")
(:name "Unscreened"
2021-11-15 17:46:52 +00:00
:query "tag:inbox AND tag:unread AND NOT tag:screened AND NOT date:..14d AND NOT tag:thefeed AND NOT tag:/ledger/ AND NOT tag:old-project"
2021-11-15 06:08:09 +00:00
:key "s")
(:name "New Feed"
:query "tag:thefeed AND tag:unread"
:key "f"
:search-type 'tree)
(:name "Old Feed"
:query "tag:thefeed"
:key "f"
:search-type 'tree)
(:name "New Receipts"
:query "tag:/ledger/ AND tag:unread"
:key "p")
(:name "Papertrail"
:query "tag:/ledger/ "
:key "P")
;; (push '(:name "Projects"
;; :query "tag:project AND NOT tag:unread"
;; :key "x")
;; notmuch-saved-searches)
(:name "Old Projects"
:query "tag:old-project AND NOT tag:unread"
:key "X")))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** Helper Functions
With good bucket definitions, we should be able to scan the mail quickly and deal with the entire lot of them:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(defun hey-notmuch-archive-all ()
"Archive all the emails in the current view."
(interactive)
(notmuch-search-archive-thread nil (point-min) (point-max)))
(defun hey-notmuch-delete-all ()
"Archive all the emails in the current view.
Mark them for deletion by cron job."
(interactive)
(notmuch-search-tag-all '("+deleted"))
(hey-notmuch-archive-all))
(defun hey-notmuch-search-delete-and-archive-thread ()
"Archive the currently selected thread. Add the deleted tag as well."
(interactive)
(notmuch-search-add-tag '("+deleted"))
(notmuch-search-archive-thread))
(defun hey-notmuch-tag-and-archive (tag-changes &optional beg end)
"Prompt the user for TAG-CHANGES.
Apply the TAG-CHANGES to region and also archive all the emails.
When called directly, BEG and END provide the region."
(interactive (notmuch-search-interactive-tag-changes))
(notmuch-search-tag tag-changes beg end)
(notmuch-search-archive-thread nil beg end))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
A key point in organizing emails with the Hey model, is looking at the "from" address:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(defun hey-notmuch-search-find-from ()
"A helper function to find the email address for the given email."
(let ((notmuch-addr-sexp (first
(notmuch-call-notmuch-sexp "address"
"--format=sexp"
"--format-version=1"
"--output=sender"
(notmuch-search-find-thread-id)))))
(plist-get notmuch-addr-sexp :address)))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
And we can create a filter, /search/ and tagging based on this "from" function:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(defun hey-notmuch-filter-by-from ()
"Filter the current search view to show all emails sent from the sender of the current thread."
(interactive)
(notmuch-search-filter (concat "from:" (hey-notmuch-search-find-from))))
(defun hey-notmuch-search-by-from (&optional no-display)
"Show all emails sent from the sender of the current thread.
NO-DISPLAY is sent forward to `notmuch-search'."
(interactive)
(notmuch-search (concat "from:" (hey-notmuch-search-find-from))
notmuch-search-oldest-first nil nil no-display))
(defun hey-notmuch-tag-by-from (tag-changes &optional beg end refresh)
"Apply TAG-CHANGES to all emails from the sender of the current thread.
BEG and END provide the region, but are ignored. They are defined
since `notmuch-search-interactive-tag-changes' returns them. If
REFRESH is true, refresh the buffer from which we started the
search."
(interactive (notmuch-search-interactive-tag-changes))
(let ((this-buf (current-buffer)))
(hey-notmuch-search-by-from t)
;; This is a dirty hack since I can't find a way to run a
;; temporary hook on `notmuch-search' completion. So instead of
;; waiting on the search to complete in the background and then
;; making tag-changes on it, I will just sleep for a short amount
;; of time. This is generally good enough and works, but is not
;; guaranteed to work every time. I'm fine with this.
(sleep-for 0.5)
(notmuch-search-tag-all tag-changes)
(when refresh
(set-buffer this-buf)
(notmuch-refresh-this-buffer))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** Moving Mail to Buckets
We based the Hey buckets on notmuch databases, we combine the =hey-notmuch-add-addr-to-db= with the =hey-notmuch-tag-by-from= functions to move messages.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(defun hey-notmuch-add-addr-to-db (nmaddr nmdbfile)
"Add the email address NMADDR to the db-file NMDBFILE."
(append-to-file (format "%s\n" nmaddr) nil nmdbfile))
(defun hey-notmuch-move-sender-to-thefeed ()
"For the email at point, move the sender of that email to the feed.
This means:
1. All new email should go to the feed and skip the inbox altogether.
2. All existing email should be updated with the tag =thefeed= .
3. All existing email should be removed from the inbox."
(interactive)
(hey-notmuch-add-addr-to-db (hey-notmuch-search-find-from)
(format "%s/thefeed.db" notmuch-hooks-dir))
(hey-notmuch-tag-by-from '("+thefeed" "+archived" "-inbox")))
(defun hey-notmuch-move-sender-to-papertrail (tag-name)
"For the email at point, move the sender of that email to the papertrail.
This means:
1. All new email should go to the papertrail and skip the inbox altogether.
2. All existing email should be updated with the tag =ledger/TAG-NAME= .
3. All existing email should be removed from the inbox."
(interactive "sTag Name: ")
(hey-notmuch-add-addr-to-db (format "%s %s"
tag-name
(hey-notmuch-search-find-from))
(format "%s/ledger.db" notmuch-hooks-dir))
(let ((tag-string (format "+ledger/%s" tag-name)))
(hey-notmuch-tag-by-from (list tag-string "+archived" "-inbox" "-unread"))))
(defun hey-notmuch-move-sender-to-screened ()
"For the email at point, move the sender of that email to Screened Emails.
This means:
1. All new email should be tagged =screened= and show up in the inbox.
2. All existing email should be updated to add the tag =screened= ."
(interactive)
(hey-notmuch-add-addr-to-db (hey-notmuch-search-find-from)
(format "%s/screened.db" notmuch-hooks-dir))
(hey-notmuch-tag-by-from '("+screened")))
(defun hey-notmuch-move-sender-to-spam ()
"For the email at point, move the sender of that email to spam.
This means:
1. All new email should go to =spam= and skip the inbox altogether.
2. All existing email should be updated with the tag =spam= .
3. All existing email should be removed from the inbox."
(interactive)
(hey-notmuch-add-addr-to-db (hey-notmuch-search-find-from)
(format "%s/spam.db" notmuch-hooks-dir))
(hey-notmuch-tag-by-from '("+spam" "+deleted" "+archived" "-inbox" "-unread" "-screened")))
(defun hey-notmuch-reply-later ()
"Capture this email for replying later."
(interactive)
;; You need `org-capture' to be set up for this to work. Add this
;; code somewhere in your init file after `org-cature' is loaded:
;; (push '("r" "Respond to email"
;; entry (file org-default-notes-file)
;; "* TODO Respond to %:from on %:subject :email: \nSCHEDULED: %t\n%U\n%a\n"
;; :clock-in t
;; :clock-resume t
;; :immediate-finish t)
;; org-capture-templates)
(org-capture nil "r")
;; The rest of this function is just a nice message in the modeline.
(let* ((email-subject (format "%s..."
(substring (notmuch-show-get-subject) 0 15)))
(email-from (format "%s..."
(substring (notmuch-show-get-from) 0 15)))
(email-string (format "%s (From: %s)" email-subject email-from)))
(message "Noted! Reply Later: %s" email-string)))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 17:46:52 +00:00
** Bucket Keybindings
2022-02-02 19:43:10 +00:00
A series of keybindings to quickly send messages to one of the pre-defined buckets.
2024-03-07 04:02:25 +00:00
#+name : hey-show-keybindings
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2024-03-02 03:30:31 +00:00
(major-mode-hydra-define notmuch-show-mode nil
("Messages"
(("c" notmuch-mua-new-mail "compose")
("C" hey-notmuch-reply-later "reply-later"))))
2022-02-02 19:43:10 +00:00
2021-11-15 17:46:52 +00:00
(define-key notmuch-show-mode-map (kbd "C") 'hey-notmuch-reply-later)
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 17:46:52 +00:00
The bindings in =notmuch-search-mode= are available when looking at a list of messages:
2021-11-15 06:08:09 +00:00
2024-03-07 04:02:25 +00:00
#+name : hey-search-keybindings
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2024-03-02 03:30:31 +00:00
(major-mode-hydra-define notmuch-search-mode nil
("Mail"
(("r" notmuch-search-reply-to-thread "reply")
("R" notmuch-search-reply-to-thread-sender "reply-all")
("A" hey-notmuch-archive-all "archive all")
("D" hey-notmuch-delete-all "delete all")
("d" hey-notmuch-search-delete-and-archive-thread "delete thread"))
"Search"
(("/" notmuch-search-filter "search")
("L" hey-notmuch-filter-by-from "filter by from")
(";" hey-notmuch-search-by-from "search by from"))
"Messages"
(("s" hey-notmuch-move-sender-to-spam "send to spam")
("i" hey-notmuch-move-sender-to-screened "send to screened")
("p" hey-notmuch-move-sender-to-papertrail "send to papertrail")
("f" hey-notmuch-move-sender-to-thefeed "send to feed")
("C" hey-notmuch-reply-later "reply")
("c" notmuch-mua-new-mail) "compose")))
;; And if I can remember the keybindings...
2021-11-15 17:46:52 +00:00
(define-key notmuch-search-mode-map (kbd "r") 'notmuch-search-reply-to-thread)
(define-key notmuch-search-mode-map (kbd "R") 'notmuch-search-reply-to-thread-sender)
(define-key notmuch-search-mode-map (kbd "/") 'notmuch-search-filter)
(define-key notmuch-search-mode-map (kbd "A") 'hey-notmuch-archive-all)
(define-key notmuch-search-mode-map (kbd "D") 'hey-notmuch-delete-all)
(define-key notmuch-search-mode-map (kbd "L") 'hey-notmuch-filter-by-from)
(define-key notmuch-search-mode-map (kbd ";") 'hey-notmuch-search-by-from)
(define-key notmuch-search-mode-map (kbd "d") 'hey-notmuch-search-delete-and-archive-thread)
(define-key notmuch-search-mode-map (kbd "S") 'hey-notmuch-move-sender-to-spam)
(define-key notmuch-search-mode-map (kbd "I") 'hey-notmuch-move-sender-to-screened)
(define-key notmuch-search-mode-map (kbd "P") 'hey-notmuch-move-sender-to-papertrail)
(define-key notmuch-search-mode-map (kbd "f") 'hey-notmuch-move-sender-to-thefeed)
(define-key notmuch-search-mode-map (kbd "C") 'hey-notmuch-reply-later)
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
** Org Integration
The gods ordained that Mail and Org should dance together, so step one is composing mail with org:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2021-11-15 06:08:09 +00:00
(use-package org-mime
:config
2024-03-02 03:30:31 +00:00
(major-mode-hydra-define notmuch-message-mode nil
("Messages"
(("s" notmuch-mua-send-and-exit "send")
("m" org-mime-htmlize "mime it")))))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 17:46:52 +00:00
A new option is to use [[https://github.com/jeremy-compostella/org-msg ][org-msg ]], so let's try it:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :noweb yes
2021-11-15 17:46:52 +00:00
(use-package org-msg
:init
(setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil \\n:t"
org-msg-startup "hidestars indent inlineimages"
org-msg-greeting-fmt "\nHi%s,\n\n"
2021-12-01 19:04:02 +00:00
org-msg-recipient-names '(("<<email-address-1() >>" . "Howard Abrams"))
2021-11-15 17:46:52 +00:00
org-msg-greeting-name-limit 3
2022-09-24 04:59:33 +00:00
org-msg-default-alternatives '((new . (text html))
(reply-to-html . (text html))
(reply-to-text . (text)))
2021-11-15 17:46:52 +00:00
org-msg-convert-citation t
org-msg-signature "
Regards,
,#+begin_signature
--
,*Howard*
/One Emacs to rule them all/
,#+end_signature"))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
The idea of linking org documents to email could be nice, however, the =ol-notmuch= package in the [[https://elpa.nongnu.org/nongnu/org-contrib.html ][org-contrib ]] package needs a maintainer.
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp :tangle no
2021-11-15 06:08:09 +00:00
(use-package ol-notmuch
:after org
:straight (:type built-in)
:config (add-to-list 'org-modules 'ol-notmuch))
2022-06-18 00:25:47 +00:00
#+end_src
2023-04-03 16:24:52 +00:00
To use, read a message and save a link to it with ~SPC o l~ . Next, in an org document, create a link with ~, l~ . Now, you can return to the message from that document with ~, o~ . Regardless, I may need to store a local copy when I upgrade Org.
2021-11-15 06:08:09 +00:00
* Display Configuration
Using the [[https://github.com/seagle0128/doom-modeline ][Doom Modeline ]] to add notifications:
2022-06-18 00:25:47 +00:00
#+begin_src emacs-lisp
2022-04-01 18:29:45 +00:00
(use-package doom-modeline
:config
(setq doom-modeline-mu4e t))
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
* Technical Artifacts :noexport:
2022-06-18 00:25:47 +00:00
Let's =provide= a name so we can =require= this file:
#+begin_src emacs-lisp :exports none
2021-11-15 17:46:52 +00:00
(provide 'ha-email)
;;; ha-email.el ends here
2022-06-18 00:25:47 +00:00
#+end_src
2021-11-15 06:08:09 +00:00
2024-03-07 04:02:25 +00:00
#+description : A literate configuration file for email using Notmuch.
2021-11-15 06:08:09 +00:00
2024-03-07 04:02:25 +00:00
#+property : header-args:sh :tangle no
#+property : header-args:emacs-lisp :tangle yes
#+property : header-args :results none :eval no-export :comments no mkdirp yes
2021-11-15 06:08:09 +00:00
2024-03-07 04:02:25 +00:00
#+options : num:nil toc:t todo:nil tasks:nil tags:nil date:nil
#+options : skip:nil author:nil email:nil creator:nil timestamp:nil
#+infojs_opt : view:nil toc:t ltoc:t mouse:underline buttons:0 path:http://orgmode.org/org-info.js