1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
;;; rul-mail.el --- Email configuration
;; mml-sec.el
;; Use sender to find GPG key.
(setq mml-secure-openpgp-sign-with-sender t)
(use-package notmuch
:ensure t
:config
;; UI
(setq notmuch-show-logo nil
notmuch-column-control 1.0
notmuch-hello-auto-refresh t
notmuch-hello-recent-searches-max 20
notmuch-hello-thousands-separator ""
notmuch-show-all-tags-list t
notmuch-show-text/html-blocked-images nil
)
(setq notmuch-draft-folder "current/Drafts")
;; Keymaps
(defun rul/capture-mail()
"Capture mail to org mode."
(interactive)
(org-store-link nil)
(org-capture nil "m")
)
(bind-key "c" 'rul/capture-mail notmuch-show-mode-map)
;; Keep uppercase refresh cheap after tag operations; use M-g for a full poll.
(define-key notmuch-show-mode-map "G" 'notmuch-refresh-this-buffer)
(define-key notmuch-search-mode-map "G" 'notmuch-refresh-this-buffer)
(define-key notmuch-show-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer)
(define-key notmuch-search-mode-map (kbd "M-g") 'notmuch-poll-and-refresh-this-buffer)
(define-key notmuch-show-mode-map "R" 'notmuch-show-reply)
(define-key notmuch-search-mode-map "R" 'notmuch-search-reply-to-thread)
;; Spam
(define-key notmuch-show-mode-map "S"
(lambda ()
"mark message as spam"
(interactive)
(notmuch-show-tag (list "+spam" "-inbox" "-unread"))))
(define-key notmuch-search-mode-map "S"
(lambda (&optional beg end)
"mark thread as spam"
(interactive (notmuch-search-interactive-region))
(notmuch-search-tag (list "+spam" "-inbox" "-unread") beg end)))
;; Unspam: restore false-positives back to INBOX. Unlike archive, the file
;; move has to happen inline — the hook's path:current/Spam/** rule would
;; otherwise re-tag +spam on the next post-new, reverting the user's action.
;; Move the file BEFORE changing tags so an interleaved post-new from mbsync
;; can't re-apply +spam between our tag change and our rename.
(defun rul/notmuch-move-to-inbox (query)
"Move files matching QUERY that live under spam/trash maildirs into
~/mail/INBOX/cur/ with fresh uuid names. Reindex if any moved."
(let ((inbox-cur (expand-file-name "~/mail/INBOX/cur/"))
(spam-paths "(path:current/Spam/** or path:current/spam/** or path:current/Trash/** or path:current/trash/**)")
(count 0))
;; Spam/trash are in search.exclude_tags, so include excluded files here.
(dolist (f (process-lines "notmuch" "search" "--exclude=false" "--output=files"
(concat "(" query ") and " spam-paths)))
(when (file-exists-p f)
(let* ((base (file-name-nondirectory f))
(colon (cl-position ?: base :from-end t))
(suffix (if colon (substring base colon) ""))
(uuid (string-trim (shell-command-to-string "uuidgen"))))
(rename-file f (concat inbox-cur uuid suffix))
(cl-incf count))))
(when (> count 0)
(call-process "notmuch" nil nil nil "new" "--no-hooks"))
count))
(defun rul/notmuch-search-unspam (&optional beg end)
"Restore threads from spam/trash: move files back to INBOX first (so a
racing post-new can't re-tag +spam based on path), then tag -spam -trash
-killed +inbox +unread, then refresh."
(interactive (notmuch-search-interactive-region))
(let ((tids (save-excursion
(goto-char beg)
(cl-loop while (< (point) end)
collect (notmuch-search-find-thread-id)
do (forward-line 1)))))
(dolist (tid tids)
(rul/notmuch-move-to-inbox (concat "thread:" tid)))
(notmuch-search-tag (list "-spam" "-trash" "-killed" "+inbox" "+unread") beg end))
(notmuch-refresh-this-buffer))
(defun rul/notmuch-show-unspam ()
"Restore current message from spam/trash back to INBOX."
(interactive)
(let ((mid (notmuch-show-get-message-id)))
(rul/notmuch-move-to-inbox mid)
(notmuch-show-tag (list "-spam" "-trash" "-killed" "+inbox" "+unread")))
(notmuch-refresh-this-buffer))
(define-key notmuch-search-mode-map "U" 'rul/notmuch-search-unspam)
(define-key notmuch-show-mode-map "U" 'rul/notmuch-show-unspam)
;; Archive
;; Stock `a` applies notmuch-archive-tags but doesn't re-run the search, so
;; the thread stays visible in the tag:inbox buffer with stale results. Wrap
;; both `a` and `A` to refresh the buffer so the thread drops out of view.
(setq notmuch-archive-tags (list "-inbox" "+archive"))
(defun rul/notmuch-search-archive (&optional unarchive beg end)
"Archive threads and refresh so they drop out of the inbox view."
(interactive (cons current-prefix-arg (notmuch-search-interactive-region)))
(notmuch-search-archive-thread unarchive beg end)
(notmuch-refresh-this-buffer))
(defun rul/notmuch-show-archive ()
"Archive the current message with +archive -inbox -unread and refresh."
(interactive)
(notmuch-show-tag (list "+archive" "-inbox" "-unread"))
(notmuch-refresh-this-buffer))
(define-key notmuch-search-mode-map "a" 'rul/notmuch-search-archive)
(define-key notmuch-search-mode-map "A" 'rul/notmuch-search-archive)
(define-key notmuch-show-mode-map "a" 'rul/notmuch-show-archive)
(define-key notmuch-show-mode-map "A" 'rul/notmuch-show-archive)
;; Mark as read
(define-key notmuch-search-mode-map "r"
(lambda (&optional beg end)
"mark thread as read"
(interactive (notmuch-search-interactive-region))
(notmuch-search-tag (list "-unread") beg end)
(notmuch-search-next-thread)))
(define-key notmuch-search-mode-map (kbd "RET")
(lambda ()
"Show the selected thread with notmuch-tree if it has more
than one email. Use notmuch-show otherwise."
(interactive)
(if (= (plist-get (notmuch-search-get-result) :total) 1)
(notmuch-search-show-thread)
(notmuch-tree (notmuch-search-find-thread-id)
notmuch-search-query-string
nil
(notmuch-prettify-subject (notmuch-search-find-subject))))))
(defun color-inbox-if-unread () (interactive)
(save-excursion
(goto-char (point-min))
(let ((cnt (car (process-lines "notmuch" "count" "tag:inbox and tag:unread"))))
(when (> (string-to-number cnt) 0)
(save-excursion
(when (search-forward "inbox" (point-max) t)
(let* ((overlays (overlays-in (match-beginning 0) (match-end 0)))
(overlay (car overlays)))
(when overlay
(overlay-put overlay 'face '((:inherit bold) (:foreground "green")))))))))))
(defvar notmuch-hello-refresh-count 0)
(defun notmuch-hello-refresh-status-message ()
(let* ((new-count
(string-to-number
(car (process-lines notmuch-command "count"))))
(diff-count (- new-count notmuch-hello-refresh-count)))
(cond
((= notmuch-hello-refresh-count 0)
(message "You have %s messages."
(notmuch-hello-nice-number new-count)))
((> diff-count 0)
(message "You have %s more messages since last refresh."
(notmuch-hello-nice-number diff-count)))
((< diff-count 0)
(message "You have %s fewer messages since last refresh."
(notmuch-hello-nice-number (- diff-count)))))
(setq notmuch-hello-refresh-count new-count)))
(add-hook 'notmuch-hello-refresh-hook 'color-inbox-if-unread)
(add-hook 'notmuch-hello-refresh-hook 'notmuch-hello-refresh-status-message)
(setq notmuch-hello-sections '(notmuch-hello-insert-saved-searches
notmuch-hello-insert-search
notmuch-hello-insert-recent-searches
notmuch-hello-insert-alltags
))
;; https://git.sr.ht/~tslil/dotfiles/tree/4e51afbb/emacs/notmuch-config.el#L76-82
(defmacro make-binds (mode-map binds argfunc &rest body)
"Create keybindings in `mode-map' using a list of (keystr . arg)
pairs in `binds' of the form ( ... (argfunc arg) body)."
`(progn ,@(mapcar (lambda (pair)
`(define-key ,mode-map (kbd ,(car pair))
(lambda () (interactive) (,argfunc ,(cdr pair)) ,@body)))
(eval binds))))
(defvar notmuch-hello-tree-searches '(("u" . "tag:unread")
("i" . "tag:inbox")
("*" . "*"))
"List of (key . query) pairs to bind in notmuch-hello.")
(make-binds notmuch-hello-mode-map
notmuch-hello-tree-searches
notmuch-search)
) ;; ends use-package notmuch
(use-package notmuch-indicator :ensure t)
(provide 'rul-mail)
|