diff options
Diffstat (limited to 'lisp/acdw-mail.el')
-rw-r--r-- | lisp/acdw-mail.el | 439 |
1 files changed, 0 insertions, 439 deletions
diff --git a/lisp/acdw-mail.el b/lisp/acdw-mail.el deleted file mode 100644 index bf20c9b..0000000 --- a/lisp/acdw-mail.el +++ /dev/null | |||
@@ -1,439 +0,0 @@ | |||
1 | ;;; acdw-mail.el --- My email configuration -*- lexical-binding: t; -*- | ||
2 | |||
3 | ;;; Code: | ||
4 | |||
5 | (require 'cl-lib) | ||
6 | (require 'message) | ||
7 | |||
8 | ;;; Variables | ||
9 | |||
10 | (defcustom +message-send-dispatch-rules nil | ||
11 | "Alist to set variables based on the current from address." | ||
12 | :group 'message | ||
13 | :type '(alist :key-type (string :tag "From address") | ||
14 | :value-type (alist :tag "Rules" | ||
15 | :key-type (symbol :tag "Variable") | ||
16 | :value-type (sexp :tag "Value")))) | ||
17 | |||
18 | (defcustom +notmuch-spam-tags '("+spam -inbox -unread") | ||
19 | "List of tag changes to apply when marking a thread as spam." | ||
20 | :group 'notmuch | ||
21 | :type '(repeat string)) | ||
22 | |||
23 | ;;; Functions | ||
24 | |||
25 | (defun +message-send-set-variables () | ||
26 | "Set variables for `message-send' depending on the From: header. | ||
27 | Useful in `message-send-hook'." | ||
28 | (unless +message-send-dispatch-rules | ||
29 | (load notmuch-init-file) | ||
30 | (or +message-send-dispatch-rules | ||
31 | (error "`+message-send-dispatch-rules' isn't set!"))) | ||
32 | (let ((from (message-fetch-field "from"))) | ||
33 | (cl-loop for (var . val) in (cl-loop for (address . bindings) | ||
34 | in +message-send-dispatch-rules | ||
35 | if (string-match-p address from) | ||
36 | return bindings) | ||
37 | do (set (make-local-variable var) val)))) | ||
38 | |||
39 | ;; Thanks to Alex Schroeder! | ||
40 | ;; https://www.emacswiki.org/emacs/Change_Signature_Dynamically | ||
41 | (defun +message-check-for-signature-change (&rest ignore) | ||
42 | "Check for a change in the To: or Cc: fields" | ||
43 | (when (and (message--in-tocc-p) | ||
44 | (not (buffer-narrowed-p))) | ||
45 | (save-excursion | ||
46 | (goto-char (point-max)) | ||
47 | (let ((end (point))) | ||
48 | (when (re-search-backward message-signature-separator nil t) | ||
49 | (delete-region (1- (match-beginning 0)) end))) | ||
50 | (message-insert-signature)))) | ||
51 | |||
52 | (defun +message-signature-setup () | ||
53 | (make-local-variable 'after-change-functions) | ||
54 | (push '+message-check-for-signature-change after-change-functions)) | ||
55 | |||
56 | (defun +notmuch-field-match-p (field regexp) | ||
57 | "Return whether message FIELD matches REGEXP." | ||
58 | (string-match-p regexp (or (message-fetch-field field) ""))) | ||
59 | |||
60 | (defun +notmuch-query-concat (&rest queries) | ||
61 | "Concatenate `notmuch' QUERIES with AND." | ||
62 | (mapconcat #'identity queries " AND ")) | ||
63 | |||
64 | ;;;###autoload | ||
65 | (defun +notmuch-goto (&optional prefix) | ||
66 | "Perform a saved `notmuch' search. | ||
67 | Without a PREFIX argument, perform the first search in | ||
68 | `notmuch-saved-searches'. With a single PREFIX argument | ||
69 | (\\[universal-argument]), prompt the user as to which saved | ||
70 | search to perform. With two PREFIX arguments, prompt the user | ||
71 | for a free-form search. With any other PREFIX argument, open | ||
72 | `notmuch-hello'." | ||
73 | (interactive "P") | ||
74 | (pcase prefix | ||
75 | ('nil (notmuch-search (plist-get (car notmuch-saved-searches) :query))) | ||
76 | ('(4) (notmuch-search | ||
77 | (plist-get (cl-find (completing-read "Saved search: " | ||
78 | (mapcar (lambda (elt) | ||
79 | (plist-get elt :name)) | ||
80 | notmuch-saved-searches)) | ||
81 | notmuch-saved-searches | ||
82 | :key (lambda (elt) (plist-get elt :name)) | ||
83 | :test #'equal) | ||
84 | :query))) | ||
85 | ('(16) (notmuch-search)) | ||
86 | (_ (notmuch-hello)))) | ||
87 | |||
88 | (defun +notmuch-search-mark-spam (&optional ham start end) | ||
89 | "Mark the current thread or region as spam. | ||
90 | That is, add the tags in `+notmuch-spam-tags' to the message. | ||
91 | With an optional HAM argument (interactively, | ||
92 | \\[universal-argument]), mark the message as not-spam, or ham, by | ||
93 | reversing the tag changes." | ||
94 | (interactive (cons current-prefix-arg (notmuch-interactive-region))) | ||
95 | (when +notmuch-spam-tags | ||
96 | (notmuch-search-tag (notmuch-tag-change-list +notmuch-spam-tags ham) | ||
97 | start end)) | ||
98 | (when (eq start end) | ||
99 | (notmuch-search-next-thread))) | ||
100 | |||
101 | (defun +notmuch-tree-mark-spam (&optional ham _ _) | ||
102 | "Mark the current message as spam. | ||
103 | That is, add the tags in `+notmuch-spam-tags' to the message. | ||
104 | With an optional HAM argument (interactively, | ||
105 | \\[universal-argument]), mark the message as not-spam, or ham, by | ||
106 | reversing the tag changes." | ||
107 | (interactive (cons current-prefix-arg (notmuch-interactive-region))) | ||
108 | (when +notmuch-spam-tags | ||
109 | (notmuch-tree-tag (notmuch-tag-change-list +notmuch-spam-tags ham))) | ||
110 | (notmuch-tree-next-matching-message)) | ||
111 | |||
112 | (defun +notmuch-define-saved-search (name key search-type &rest queries) | ||
113 | "Wrapper to ease `notmuch-saved-searches' defining. | ||
114 | NAME, KEY, and SEARCH-TYPE are paired with the corresponding keywords in | ||
115 | `notmuch-saved-searches', which see. QUERIES are all concatenated together with | ||
116 | AND. If QUERIES is prepended with more keyword arguments, those are added to | ||
117 | the saved search as well." | ||
118 | (declare (indent 3)) | ||
119 | (let (extra-keywords) | ||
120 | (while (keywordp (car queries)) | ||
121 | (push (cadr queries) extra-keywords) | ||
122 | (push (car queries) extra-keywords) | ||
123 | (setf queries (cddr queries))) | ||
124 | (add-to-list 'notmuch-saved-searches | ||
125 | (append (list :name name | ||
126 | :key key | ||
127 | :search-type search-type | ||
128 | :query (apply #'+notmuch-query-concat queries)) | ||
129 | (reverse extra-keywords)) | ||
130 | :append | ||
131 | (lambda (a b) | ||
132 | (equal (plist-get a :name) | ||
133 | (plist-get b :name)))))) | ||
134 | |||
135 | (defun notmuch-async-poll () | ||
136 | "Run `notmuch-poll' in an async process." | ||
137 | (interactive) | ||
138 | (if (require 'async nil t) | ||
139 | (progn | ||
140 | (message "Polling mail (async)...") | ||
141 | (async-start | ||
142 | (lambda () | ||
143 | (ignore-errors | ||
144 | (push "~/usr/share/emacs/site-lisp/" load-path) | ||
145 | (require 'notmuch-lib) | ||
146 | (notmuch-poll))) | ||
147 | (lambda (_) | ||
148 | (message "Polling mail (async)...done")))) | ||
149 | (user-error "Feature `async' not found!") | ||
150 | (notmuch-poll))) | ||
151 | |||
152 | ;;; https://kitchingroup.cheme.cmu.edu/blog/2015/09/04/Checking-for-email-attachments-before-you-send-email/ | ||
153 | |||
154 | (defun email-says-attach-p () | ||
155 | "Return t if email suggests there could be an attachment." | ||
156 | (save-excursion | ||
157 | (goto-char (point-min)) | ||
158 | (re-search-forward "attach" nil t))) | ||
159 | |||
160 | (defun email-has-attachment-p () | ||
161 | "Return t if the currently open email has an attachment." | ||
162 | (save-excursion | ||
163 | (goto-char (point-min)) | ||
164 | (re-search-forward "<#part" nil t))) | ||
165 | |||
166 | (defun email-pre-send-check-attachment () | ||
167 | (when (and (email-says-attach-p) | ||
168 | (not (email-has-attachment-p))) | ||
169 | (unless | ||
170 | (y-or-n-p | ||
171 | "Your email suggests an attachment, but none was found. Send anyway?") | ||
172 | (error "No attachment. Aborting send.")))) | ||
173 | |||
174 | ;;; Process ical attachments | ||
175 | ;; https://www.reddit.com/r/emacs/comments/8s1ion/comment/e0x5kgd/ | ||
176 | |||
177 | (defun ical2org (&optional replace output-buffer) | ||
178 | "Run ical2org on contents of this buffer. | ||
179 | If REPLACE (interactive prefix argument), replace contents of the | ||
180 | buffer. If no REPLACE nor OUTPUT-BUFFER, output goes to | ||
181 | minibuffer." | ||
182 | (interactive "P") | ||
183 | (shell-command-on-region (point-min) (point-max) | ||
184 | "ical2org.awk" | ||
185 | output-buffer | ||
186 | replace | ||
187 | "*ical2org errors*" | ||
188 | 'display-errors)) | ||
189 | |||
190 | (defun ical2org-capture () | ||
191 | "Run `ical2org' on this buffer, then `org-capture' the result. | ||
192 | Leaves current buffer as-was afterwards." | ||
193 | (interactive) | ||
194 | (let ((buf (current-buffer)) | ||
195 | (ics (buffer-string))) | ||
196 | (ical2org 'replace buf) | ||
197 | (mark-whole-buffer) | ||
198 | (org-capture nil "a") | ||
199 | (mark-whole-buffer) | ||
200 | (delete-region (point-min) (point-max)) | ||
201 | (insert ics))) | ||
202 | |||
203 | (defun notmuch-ics-refile (filename header) | ||
204 | "Refile the current ICS buffer to FILENAME, under HEADER." | ||
205 | (org-refile nil nil | ||
206 | (list header | ||
207 | filename | ||
208 | nil | ||
209 | (with-current-buffer (find-file-noselect filename) | ||
210 | (org-find-exact-headline-in-buffer header)))) | ||
211 | (notmuch-ics-kill)) | ||
212 | |||
213 | (defun notmuch-ics-kill () | ||
214 | "Kill the current ICS buffer and delete its file." | ||
215 | (let ((kill-buffer-hook nil) | ||
216 | (kill-buffer-query-functions nil) | ||
217 | (file (buffer-file-name))) | ||
218 | (set-buffer-modified-p nil) | ||
219 | (kill-buffer-and-window) | ||
220 | (delete-file file))) | ||
221 | |||
222 | (defun notmuch-ics-cancel () | ||
223 | "Cancel saving ICS buffer." | ||
224 | (interactive) | ||
225 | (notmuch-ics-kill)) | ||
226 | |||
227 | (define-derived-mode notmuch-save-ics-mode org-mode | ||
228 | "Org-mode but with a different keymap.") | ||
229 | |||
230 | (defun notmuch-save-ics () | ||
231 | ;; XXX: this is hacky as hell | ||
232 | "Save a .ics file in a message." | ||
233 | (interactive) | ||
234 | (with-current-notmuch-show-message | ||
235 | (notmuch-foreach-mime-part | ||
236 | (lambda (part) | ||
237 | (when (and (listp part) | ||
238 | (or (equal "application/ics" (caadr part)) | ||
239 | ;; (equal "text/calendar" (caadr part)) | ||
240 | )) | ||
241 | (let* ((filename (format "/dev/shm/notmuch-%s.ics" | ||
242 | (time-convert (current-time) 'integer)))) | ||
243 | (mm-save-part-to-file part filename) | ||
244 | (message "Converting ICS to org ...") | ||
245 | (with-current-buffer (find-file-other-window filename) | ||
246 | (ical2org 'replace (current-buffer)) | ||
247 | (deactivate-mark) | ||
248 | (let ((org-mode-hook nil)) | ||
249 | (notmuch-save-ics-mode)) | ||
250 | (local-set-key (kbd "C-c C-c") | ||
251 | (lambda () (interactive) | ||
252 | (notmuch-ics-refile (sync/ "org/diary.org") "Appointments"))) | ||
253 | (local-set-key (kbd "C-c C-k") #'notmuch-ics-cancel) | ||
254 | (setq-local header-line-format | ||
255 | (concat "ICAL buffer. " | ||
256 | "Finish: `C-c C-c'. " | ||
257 | "Abort: `C-c C-k'.")))))) | ||
258 | (mm-dissect-buffer t t)))) | ||
259 | |||
260 | (add-hook 'notmuch-show-hook #'notmuch-save-ics) | ||
261 | |||
262 | |||
263 | ;;; Fixes | ||
264 | |||
265 | ;; https://nmbug.notmuchmail.org/nmweb/show/87bklhricc.fsf%40tethera.net | ||
266 | |||
267 | (defun notmuch--indent-rigidly (start end count) | ||
268 | (cond | ||
269 | ((zerop count) t) | ||
270 | ((< count 0) (indent-rigidly start end count)) | ||
271 | (t | ||
272 | (save-excursion | ||
273 | (let ((startpt (progn (goto-char start) (line-beginning-position))) | ||
274 | (endpt (progn (goto-char end) (line-end-position))) | ||
275 | (spaces (spaces-string count))) | ||
276 | (goto-char startpt) | ||
277 | (while | ||
278 | (progn | ||
279 | (insert spaces) | ||
280 | (cl-incf endpt count) | ||
281 | (and (zerop (forward-line 1)) (bolp) | ||
282 | (<= (point) endpt))))))))) | ||
283 | |||
284 | (with-eval-after-load 'notmuch-show | ||
285 | ;; Redefine `notmuch-show-lazy-part' --- XXX: this is the most braindead way | ||
286 | ;; of doing this | ||
287 | (defun notmuch-show-lazy-part (part-args button) | ||
288 | ;; Insert the lazy part after the button for the part. We would just | ||
289 | ;; move to the start of the new line following the button and insert | ||
290 | ;; the part but that point might have text properties (eg colours | ||
291 | ;; from a message header etc) so instead we start from the last | ||
292 | ;; character of the button by adding a newline and finish by | ||
293 | ;; removing the extra newline from the end of the part. | ||
294 | (save-excursion | ||
295 | (goto-char (button-end button)) | ||
296 | (insert "\n") | ||
297 | (let* ((inhibit-read-only t) | ||
298 | ;; We need to use markers for the start and end of the part | ||
299 | ;; because the part insertion functions do not guarantee | ||
300 | ;; to leave point at the end of the part. | ||
301 | (part-beg (copy-marker (point) nil)) | ||
302 | (part-end (copy-marker (point) t)) | ||
303 | ;; We have to save the depth as we can't find the depth | ||
304 | ;; when narrowed. | ||
305 | (depth (notmuch-show-get-depth))) | ||
306 | (save-restriction | ||
307 | (narrow-to-region part-beg part-end) | ||
308 | (delete-region part-beg part-end) | ||
309 | (apply #'notmuch-show-insert-bodypart-internal part-args) | ||
310 | (notmuch--indent-rigidly part-beg | ||
311 | part-end | ||
312 | (* notmuch-show-indent-messages-width depth))) | ||
313 | (goto-char part-end) | ||
314 | (delete-char 1) | ||
315 | (notmuch-show-record-part-information (cadr part-args) | ||
316 | (button-start button) | ||
317 | part-end) | ||
318 | ;; Create the overlay. If the lazy-part turned out to be empty/not | ||
319 | ;; showable this returns nil. | ||
320 | (notmuch-show-create-part-overlays button part-beg part-end)))) | ||
321 | ) | ||
322 | |||
323 | |||
324 | ;;; Packages | ||
325 | |||
326 | (use-package bbdb | ||
327 | :ensure t | ||
328 | :config | ||
329 | (setopt bbdb-complete-mail-allow-cycling t | ||
330 | bbdb-file (private/ "bbdb")) | ||
331 | (add-hook 'custom-allowed-after-load-hook | ||
332 | (defun bbdb@after-custom () | ||
333 | (require 'bbdb) | ||
334 | (require 'bbdb-message) | ||
335 | (bbdb-initialize 'message)))) | ||
336 | |||
337 | (use-package bbdb-vcard | ||
338 | :ensure t | ||
339 | :after bbdb) | ||
340 | |||
341 | (use-package notmuch | ||
342 | :when (executable-find "notmuch") | ||
343 | :load-path "~/usr/share/emacs/site-lisp/" | ||
344 | :defer 30 | ||
345 | :commands (notmuch-mua-new-mail | ||
346 | notmuch-search | ||
347 | notmuch-hello) | ||
348 | :preface (defdir notmuch/ (sync/ "emacs/notmuch/") | ||
349 | "Notmuch configuration directory." | ||
350 | :makedir) | ||
351 | :config | ||
352 | ;; Options | ||
353 | (setopt notmuch-init-file (notmuch/ "notmuch-init.el" t) | ||
354 | notmuch-address-save-filename (notmuch/ "addresses" t) | ||
355 | notmuch-address-use-company (featurep 'company) | ||
356 | notmuch-search-oldest-first nil | ||
357 | notmuch-archive-tags '("-inbox" "-unread") | ||
358 | notmuch-draft-tags '("+draft" "-inbox" "-unread") | ||
359 | +notmuch-spam-tags '("+spam") | ||
360 | mail-user-agent 'notmuch-user-agent | ||
361 | message-mail-user-agent t | ||
362 | notmuch-show-indent-content nil | ||
363 | message-kill-buffer-on-exit t | ||
364 | message-auto-save-directory nil | ||
365 | message-signature "Case Duckworth\nhttps://www.acdw.net" | ||
366 | send-mail-function #'sendmail-send-it | ||
367 | mail-specify-envelope-from t | ||
368 | message-sendmail-envelope-from 'header | ||
369 | message-envelope-from 'header | ||
370 | notmuch-saved-searches nil | ||
371 | notmuch-poll-script "~/usr/scripts/syncmail" ; XXX: Deprecated option | ||
372 | ) | ||
373 | (load notmuch-init-file) | ||
374 | ;; Key bindings | ||
375 | (keymap-set notmuch-search-mode-map "!" #'+notmuch-search-mark-spam) | ||
376 | (keymap-set notmuch-search-mode-map "RET" #'notmuch-search-show-thread) | ||
377 | (keymap-set notmuch-search-mode-map "M-RET" #'notmuch-tree-from-search-thread) | ||
378 | (keymap-set notmuch-tree-mode-map "!" #'+notmuch-tree-mark-spam) | ||
379 | ;; Saved searches | ||
380 | (+notmuch-define-saved-search "inbox+unread" "m" 'tree | ||
381 | "tag:inbox" "tag:unread" "NOT tag:Spam") | ||
382 | (+notmuch-define-saved-search "inbox" "i" 'tree | ||
383 | "tag:inbox" "NOT tag:Spam") | ||
384 | (+notmuch-define-saved-search "lists+unread" "l" 'tree | ||
385 | "tag:/List/" "tag:unread") | ||
386 | (+notmuch-define-saved-search "lists" "L" 'tree | ||
387 | "tag:/List/") | ||
388 | (+notmuch-define-saved-search "unread" "u" 'tree | ||
389 | "tag:unread" "NOT tag:Spam") | ||
390 | (+notmuch-define-saved-search "flagged" "f" 'tree | ||
391 | "tag:flagged") | ||
392 | (+notmuch-define-saved-search "sent" "t" 'tree | ||
393 | "tag:sent") | ||
394 | (+notmuch-define-saved-search "drafts" "d" 'tree | ||
395 | "tag:draft") | ||
396 | (+notmuch-define-saved-search "all mail" "a" 'tree "*") | ||
397 | ;; Hooks and advice | ||
398 | (add-hook 'message-send-hook #'+message-send-set-variables) | ||
399 | (add-hook 'message-send-hook #'email-pre-send-check-attachment) | ||
400 | (add-hook 'message-setup-hook #'+message-signature-setup) | ||
401 | (autoload 'visual-fill-column-mode "visual-fill-column" nil t) | ||
402 | (add-hook 'notmuch-message-mode-hook #'visual-fill-column-mode) | ||
403 | (add-hook 'notmuch-show-mode-hook #'visual-fill-column-mode) | ||
404 | |||
405 | (define-advice notmuch-bury-or-kill-this-buffer (:after (&rest _) poll-async) | ||
406 | (call-process "swaymail")) | ||
407 | |||
408 | (define-advice notmuch-address-selection-function | ||
409 | (:override (prompt collection _) no-initial-input) | ||
410 | "Call `completing-read' with `notmuch-address-history'. | ||
411 | This version doesn't add any initial-input." | ||
412 | (completing-read prompt collection nil nil nil 'notmuch-address-history)) | ||
413 | |||
414 | (add-to-list 'notmuch-message-headers "List-Post" :append #'equal) | ||
415 | (define-advice notmuch-mua-new-reply (:around (orig &rest r) list-aware) | ||
416 | "Make `notmuch-mua-new-reply' list-aware." | ||
417 | (let ((ml (notmuch-show-get-header :List-Post))) | ||
418 | (apply orig r) | ||
419 | (require 'message) | ||
420 | (when ml | ||
421 | (with-buffer-modified-unmodified | ||
422 | (message-remove-header "To") | ||
423 | (message-add-header | ||
424 | (format "To: %s" (replace-regexp-in-string "<mailto:\\(.*\\)>" "\\1" | ||
425 | ml))) | ||
426 | (message-goto-body))))) | ||
427 | |||
428 | (define-advice notmuch-tag (:filter-args (args) trim) | ||
429 | "Trim whitespace from ends of tags." | ||
430 | (list (car args) (mapcar #'string-trim (cadr args)))) | ||
431 | ;; Load init file | ||
432 | (load notmuch-init-file :noerror)) | ||
433 | |||
434 | (use-package notmuch-tags | ||
435 | :load-path "~/src/emacs/notmuch-tags.el/" | ||
436 | :commands (notmuch-tags-mode)) | ||
437 | |||
438 | (provide 'acdw-mail) | ||
439 | ;;; acdw-mail.el ends here | ||