summary refs log tree commit diff stats
path: root/lisp/acdw-mail.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/acdw-mail.el')
-rw-r--r--lisp/acdw-mail.el439
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.
27Useful 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.
67Without 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
70search to perform. With two PREFIX arguments, prompt the user
71for 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.
90That is, add the tags in `+notmuch-spam-tags' to the message.
91With an optional HAM argument (interactively,
92\\[universal-argument]), mark the message as not-spam, or ham, by
93reversing 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.
103That is, add the tags in `+notmuch-spam-tags' to the message.
104With an optional HAM argument (interactively,
105\\[universal-argument]), mark the message as not-spam, or ham, by
106reversing 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.
114NAME, KEY, and SEARCH-TYPE are paired with the corresponding keywords in
115`notmuch-saved-searches', which see. QUERIES are all concatenated together with
116AND. If QUERIES is prepended with more keyword arguments, those are added to
117the 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.
179If REPLACE (interactive prefix argument), replace contents of the
180buffer. If no REPLACE nor OUTPUT-BUFFER, output goes to
181minibuffer."
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.
192Leaves 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'.
411This 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