-
-
Save vedang/26a94c459c46e45bc3a9ec935457c80f to your computer and use it in GitHub Desktop.
- Specific Notmuch filters (and saved-searches) for: | |
+ The Feed (newsletters, blogs) | |
+ The Paper trail (receipts, ledger) | |
+ Screened Inbox (mail from folks you actually want to read) | |
+ Previously Seen (important mail that you've already read) | |
+ Unscreened Inbox (potential spam / stuff you don't want) | |
- Elisp Functions to move / categorize emails from a particular sender. | |
+ Adds tags needed by filters defined above to all email sent by a particular sender | |
+ Creates an entry in a DB file, which is used by the Notmuch post-new script when indexing new email, to auto-add the relevant tags. | |
- Shell script (the Notmuch post-new hook) to categorize emails when indexing them. | |
- Demo of this functionality: https://www.youtube.com/watch?v=wuSPssykPtE |
;;; init-notmuch.el --- configuration for using notmuch to manage email | |
;;; Author: Vedang Manerikar | |
;;; Created on: 07th June 2014 | |
;;; Copyright (c) 2014 Vedang Manerikar <[email protected]> | |
;; This file is not part of GNU Emacs. | |
;;; License: | |
;; This program is free software; you can redistribute it and/or | |
;; modify it under the terms of the Do What The Fuck You Want to | |
;; Public License, Version 2, which is included with this distribution. | |
;; See the file LICENSE.txt | |
;;; Commentary: | |
;; Put this file somewhere on your load-path, after notmuch is loaded. | |
;; Eg: | |
;; (require 'notmuch) | |
;; (require 'init-notmuch) | |
;; The variable `notmuch-mail-dir' needs to be defined (for example, | |
;; in your personal.el file) | |
;;; Code: | |
(setq user-mail-address (notmuch-user-primary-email) | |
user-full-name (notmuch-user-name) | |
message-send-mail-function 'message-send-mail-with-sendmail | |
;; we substitute sendmail with msmtp | |
sendmail-program (executable-find "msmtp") | |
message-sendmail-envelope-from 'header | |
mail-specify-envelope-from t | |
notmuch-archive-tags '("-inbox" "-unread" "+archived") | |
notmuch-show-mark-read-tags '("-inbox" "-unread" "+archived") | |
notmuch-search-oldest-first nil | |
notmuch-show-indent-content nil | |
notmuch-hooks-dir (expand-file-name ".notmuch/hooks" notmuch-mail-dir)) | |
;;; My Notmuch start screen: | |
(progn | |
(setq notmuch-saved-searches nil) | |
(push '(:name "Inbox" | |
:query "tag:inbox AND tag:screened AND tag:unread" | |
:key "i" | |
:search-type 'tree) | |
notmuch-saved-searches) | |
(push '(:name "Previously Seen" | |
:query "tag:screened AND NOT tag:unread" | |
:key "I") | |
notmuch-saved-searches) | |
(push '(:name "Unscreened" | |
:query "tag:inbox AND NOT tag:screened" | |
:key "s") | |
notmuch-saved-searches) | |
(push '(:name "The Feed" | |
:query "tag:thefeed" | |
:key "f" | |
:search-type 'tree) | |
notmuch-saved-searches) | |
(push '(:name "The Papertrail" | |
:query "tag:/ledger/" | |
:key "p") | |
notmuch-saved-searches)) | |
;; Integrate with org-mode | |
(require 'ol-notmuch) | |
(eval-after-load 'notmuch-show | |
'(progn | |
;; Bindings in `notmuch-show-mode' | |
(define-key notmuch-show-mode-map (kbd "r") | |
'notmuch-show-reply) | |
(define-key notmuch-show-mode-map (kbd "R") | |
'notmuch-show-reply-sender) | |
(define-key notmuch-show-mode-map (kbd "C") | |
'vedang/notmuch-reply-later) | |
;; Bindings in `notmuch-search-mode' | |
(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") | |
'vedang/notmuch-archive-all) | |
(define-key notmuch-search-mode-map (kbd "D") | |
'vedang/notmuch-delete-all) | |
(define-key notmuch-search-mode-map (kbd "L") | |
'vedang/notmuch-filter-by-from) | |
(define-key notmuch-search-mode-map (kbd ";") | |
'vedang/notmuch-search-by-from) | |
(define-key notmuch-search-mode-map (kbd "d") | |
'vedang/notmuch-search-delete-and-archive-thread) | |
;; The HEY Workflow Bindings | |
(define-key notmuch-search-mode-map (kbd "S") | |
'vedang/notmuch-move-sender-to-spam) | |
(define-key notmuch-search-mode-map (kbd "I") | |
'vedang/notmuch-move-sender-to-screened) | |
(define-key notmuch-search-mode-map (kbd "P") | |
'vedang/notmuch-move-sender-to-papertrail) | |
(define-key notmuch-search-mode-map (kbd "f") | |
'vedang/notmuch-move-sender-to-thefeed) | |
(define-key notmuch-search-mode-map (kbd "C") | |
'vedang/notmuch-reply-later) | |
;; Bindings in `notmuch-tree-mode' | |
(define-key notmuch-tree-mode-map (kbd "C") | |
'vedang/notmuch-reply-later))) | |
(defun vedang/notmuch-archive-all () | |
"Archive all the emails in the current view." | |
(interactive) | |
(notmuch-search-archive-thread nil (point-min) (point-max))) | |
(defun vedang/notmuch-delete-all () | |
"Archive all the emails in the current view. | |
Mark them for deletion by cron job." | |
(interactive) | |
(notmuch-search-tag-all '("+deleted")) | |
(vedang/notmuch-archive-all)) | |
(defun vedang/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 vedang/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)) | |
(defun vedang/notmuch-search-get-from () | |
"A helper function to find the email address for the given email." | |
(let ((notmuch-addr-sexp (car | |
(notmuch-call-notmuch-sexp "address" | |
"--format=sexp" | |
"--format-version=1" | |
"--output=sender" | |
(notmuch-search-find-thread-id))))) | |
(plist-get notmuch-addr-sexp :name-addr))) | |
(defun vedang/notmuch-tree-get-from () | |
"A helper function to find the email address for the given email. | |
Assumes `notmuch-tree-mode'." | |
(plist-get (notmuch-tree-get-prop :headers) :From)) | |
(defun vedang/notmuch-get-from () | |
"Find the From email address for the email at point." | |
(car (notmuch-clean-address (cond | |
((eq major-mode 'notmuch-show-mode) | |
(notmuch-show-get-from)) | |
((eq major-mode 'notmuch-tree-mode) | |
(vedang/notmuch-tree-get-from)) | |
((eq major-mode 'notmuch-search-mode) | |
(vedang/notmuch-search-get-from)) | |
((t nil)))))) | |
(defun vedang/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:" (vedang/notmuch-get-from)))) | |
(defun vedang/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:" (vedang/notmuch-get-from)) | |
notmuch-search-oldest-first | |
nil | |
nil | |
no-display)) | |
(defun vedang/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))) | |
(vedang/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)))) | |
(defun vedang/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 vedang/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) | |
(vedang/notmuch-add-addr-to-db (vedang/notmuch-get-from) | |
(format "%s/thefeed.db" notmuch-hooks-dir)) | |
(vedang/notmuch-tag-by-from '("+thefeed" "+archived" "-inbox"))) | |
(defun vedang/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: ") | |
(vedang/notmuch-add-addr-to-db (format "%s %s" | |
tag-name | |
(vedang/notmuch-get-from)) | |
(format "%s/ledger.db" notmuch-hooks-dir)) | |
(let ((tag-string (format "+ledger/%s" tag-name))) | |
(vedang/notmuch-tag-by-from (list tag-string "+archived" "-inbox" "-unread")))) | |
(defun vedang/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) | |
(vedang/notmuch-add-addr-to-db (vedang/notmuch-get-from) | |
(format "%s/screened.db" notmuch-hooks-dir)) | |
(vedang/notmuch-tag-by-from '("+screened"))) | |
(defun vedang/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) | |
(vedang/notmuch-add-addr-to-db (vedang/notmuch-get-from) | |
(format "%s/spam.db" notmuch-hooks-dir)) | |
(vedang/notmuch-tag-by-from '("+spam" "+deleted" "+archived" "-inbox" "-unread" "-screened"))) | |
(defun vedang/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))) | |
;; Sign messages by default. | |
(add-hook 'message-setup-hook 'mml-secure-sign-pgpmime) | |
(setq notmuch-address-selection-function | |
(lambda (prompt collection initial-input) | |
(completing-read prompt | |
(cons initial-input collection) | |
nil | |
t | |
nil | |
'notmuch-address-history))) | |
(defun disable-auto-fill () | |
"I don't want `auto-fill-mode'." | |
(auto-fill-mode -1)) | |
(add-hook 'message-mode-hook 'disable-auto-fill) | |
(provide 'init-notmuch) | |
;;; init-notmuch.el ends here |
#!/bin/bash | |
# Based On: https://gist.githubusercontent.com/frozencemetery/5042526/raw/57195ba748e336de80c27519fe66e428e5003ab8/post-new | |
# 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/ | |
echo "starting NM post-new script" | |
# NOTE: You will need to define your maildir here! | |
export nm_maildir="$HOME/Documents/mail" | |
export start="-1" | |
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 | |
/usr/local/bin/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 | |
/usr/local/bin/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 | |
/usr/local/bin/notmuch tag +thefeed +archived -inbox -- tag:inbox and tag:unread and from:"$entry" | |
fi | |
done | |
timer_end "thefeed" | |
timer_start "Screened" | |
for entry in $(cat $nm_maildir/.notmuch/hooks/screened.db); do | |
if [ -n "$entry" ]; then | |
/usr/local/bin/notmuch tag +screened -- tag:inbox and tag:unread and from:"$entry" | |
fi | |
done | |
timer_end "Screened" | |
echo "Completing NM post-new script; goodbye" |
@agenbite I haven't deviated a lof of @vedang's approach in the past year. The line I shared in the previous comment was one of the more interesting ones already :) What else would you find interesting to see?
afew looks interesting, never tried that https://afew.readthedocs.io/en/latest/
@DivineDominion, if it's mostly @vedang's approach I'm fine, thanks! :)
@vedang - this is awesome! Thank you!
Here's a version of post-new.sh that uses saved queries instead of tags
(obviously have to update notmuch-saved-searches
as well)
#!/bin/bash
# Based On: https://gist.githubusercontent.com/frozencemetery/5042526/raw/57195ba748e336de80c27519fe66e428e5003ab8/post-new
# 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: You will need to define your maildir here!
export notmuch_command=/usr/bin/notmuch
export nm_maildir=`$notmuch_command config get database.path`
export start="-1"
function set_query() {
query_name=$1
static_query=$2
file=$3
query=""
while IFS="" read -r entry || [ -n "$entry" ]
do
if [ -n "$query" ]; then
query="$query OR "
fi
query="$query(from:$entry)"
done < $nm_maildir/.notmuch/hooks/$file
if [ -n "$query" ]; then
$notmuch_command config set --database query.$query_name "$static_query($query)"
fi
}
set_query ledger "tag:inbox and tag:unread and " ledger.db
set_query thefeed "tag:inbox and tag:unread and " thefeed.db
set_query spam "tag:inbox and tag:unread and " spam.db
set_query screened "tag:inbox and tag:unread and " screened.db
Edit: added the --database
flag to the notmuch config set
command. This makes the saved queries play nice with afew
I keep a tagging query file of this form:
-inbox +papertrail -- tag:inbox from:[email protected]
-inbox +feed -- tag:inbox from:[email protected] subject:"just shared"
...
And then have a postNew
hook that's just
notmuch tag --batch --input=<path>/notmuch-inbox-retag
Note the lack of loops and per-tag files. You can also add arbitrary queries to the batch file if you need something that's not just sender-based (note the Patreon query above). Elisp functions append queries to the file.
I think it's much simpler and more flexible.
Didn't know that was possible, thank you for sharing!
Migrated to the batch input file. While I now have to repeat the +feed
setting for every line, it also makes customizations possible. That's neat.
The helper to add a sender to the feed.db file needed adjustment, of course, to be complete:
(defun vedang/notmuch-add-addr-to-db (nmaddr nmdbfile)
"Add the email address NMADDR to the db-file NMDBFILE with a `from:` prefix."
(append-to-file (format "+feed -- from:%s\n" nmaddr) nil nmdbfile))
Yeah, @DivineDominion, that's what I thought. What is called "db" here is no more than a file containing a set of queries, am I right? These queries define filters, so, in principle, you might use afew to manage those filters.
Could you please share your approach?