-
-
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" |
@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))
@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/