+mail.el 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. ;;; -*- lexical-binding: t; -*-
  2. ;;; Config is mostly from https://kkatsuyuki.github.io/notmuch-conf/ aadsa
  3. ;;; This is all modified from
  4. ;;; https://github.com/fuxialexander/doom-emacs-private-xfu/
  5. ;;;; Notmuch
  6. (def-package! notmuch
  7. :commands (notmuch
  8. notmuch-tree
  9. notmuch-tree-mode
  10. notmuch-search
  11. notmuch-search-mode
  12. notmuch-hello
  13. notmuch-hello-mode
  14. notmuch-show
  15. notmuch-show-mode
  16. notmuch-message-mode)
  17. :init
  18. (map!
  19. (:leader
  20. (:prefix "o"
  21. (:desc "APP: Email" "e" #'=mail))))
  22. :config
  23. (setq notmuch-fcc-dirs nil
  24. notmuch-show-logo nil
  25. notmuch-message-headers-visible nil
  26. message-kill-buffer-on-exit t
  27. message-send-mail-function 'message-send-mail-with-sendmail
  28. mail-specify-envelope-from t
  29. message-sendmail-envelope-from 'header
  30. mail-envelope-from 'header
  31. notmuch-search-oldest-first nil
  32. send-mail-function 'sendmail-send-it
  33. ;; sendmail-program "/usr/local/bin/msmtp"
  34. notmuch-search-result-format '(("date" . "%12s ")
  35. ("count" . "%-7s ")
  36. ("authors" . "%-30s ")
  37. ("subject" . "%-100s ")
  38. ("tags" . "(%s)"))
  39. notmuch-tag-formats '(("unread"
  40. (propertize tag 'face 'notmuch-tag-unread)))
  41. notmuch-hello-sections '(notmuch-hello-insert-saved-searches
  42. notmuch-hello-insert-alltags)
  43. notmuch-saved-searches '(
  44. (:name "inbox" :query "tag:inbox not tag:deleted" :key "i")
  45. (:name "sent" :query "tag:sent" :key "s")
  46. (:name "archived":query "tag:archived" :key "a")
  47. (:name "drafts" :query "tag:draft" :key "d"))
  48. notmuch-archive-tags '("-inbox" "-unread" "+archived"))
  49. (set-evil-initial-state! '(notmuch-hello-mode
  50. notmuch-show-mode
  51. notmuch-search-mode
  52. notmuch-tree-mode
  53. notmuch-message-mode) 'normal)
  54. ;; (add-hook 'notmuch-tree-mode-hook '+mail/buffer-face-mode-notmuch)
  55. ;; (add-hook 'notmuch-search-mode-hook '+mail/buffer-face-mode-notmuch)
  56. ;; (add-hook 'notmuch-message-mode-hook '+mail/buffer-face-mode-notmuch)
  57. (add-hook 'notmuch-message-mode-hook (lambda () (set (make-local-variable 'company-backends) '(notmuch-company (company-ispell :with company-yasnippet)))))
  58. (add-hook 'notmuch-tree-mode-hook (lambda () (setq-local line-spacing nil)))
  59. (remove-hook 'message-mode-hook #'turn-on-auto-fill)
  60. (remove-hook 'notmuch-message-mode-hook #'turn-on-auto-fill)
  61. (push 'notmuch-tree-mode evil-snipe-disabled-modes)
  62. (push 'notmuch-hello-mode evil-snipe-disabled-modes)
  63. (push 'notmuch-search-mode evil-snipe-disabled-modes)
  64. (push 'notmuch-show-mode evil-snipe-disabled-modes)
  65. (advice-add #'notmuch-start-notmuch-sentinel :override #'+mail/notmuch-start-notmuch-sentinel)
  66. (advice-add #'notmuch-show :override #'+mail/notmuch-show-reuse-buffer)
  67. (advice-add #'notmuch-hello-insert-searches :override #'+mail/notmuch-hello-insert-searches)
  68. (advice-add #'notmuch-hello-insert-saved-searches :override #'+mail/notmuch-hello-insert-saved-searches)
  69. (advice-add #'notmuch-hello-insert-buttons :override #'+mail/notmuch-hello-insert-buttons)
  70. ;; (set! :popup "\\*notmuch-hello\\*" '((size . 20) (side . left)) '((quit . t) (modeline . nil)))
  71. (set-popup-rule! "\\*offlineimap\\*" :side 'bottom :size 0.4 :quit t)
  72. (push (lambda (buf) (string-match-p "^\\*notmuch" (buffer-name buf)))
  73. doom-real-buffer-functions)
  74. (map! (:after notmuch
  75. (:map notmuch-show-mode-map
  76. :nmv "o" #'ace-link-notmuch-show
  77. :nmv "i" #'+mail/open-message-with-mail-app-notmuch-show
  78. :nmv "I" #'notmuch-show-view-all-mime-parts
  79. :nmv "q" #'notmuch-bury-or-kill-this-buffer
  80. (:when (featurep! :completion ivy)
  81. :nmv "s" #'counsel-notmuch)
  82. (:when (featurep! :completion helm)
  83. :nmv "s" #'helm-notmuch)
  84. :nmv "t" #'notmuch-tree-from-show-current-query
  85. :nmv "N" #'notmuch-mua-new-mail
  86. :nmv "n" #'notmuch-show-next-thread-show
  87. :nmv "r" #'notmuch-show-reply
  88. :nmv "<tab>" #'notmuch-show-toggle-visibility-headers
  89. :nmv "R" #'notmuch-show-reply-sender
  90. :nmv "p" #'notmuch-show-previous-thread-show)
  91. (:map notmuch-hello-mode-map
  92. :nmv "o" #'ace-link-notmuch-hello
  93. :nmv "t" #'notmuch-tree
  94. :nmv "k" #'widget-backward
  95. :nmv "n" #'notmuch-mua-new-mail
  96. :nmv "N" #'notmuch-mua-new-mail
  97. :nmv "j" #'widget-forward
  98. (:when (featurep! :completion ivy)
  99. :nmv "s" #'counsel-notmuch)
  100. (:when (featurep! :completion helm)
  101. :nmv "s" #'helm-notmuch)
  102. :nmv "q" #'+mail/quit
  103. :nmv "r" #'notmuch-hello-update)
  104. (:map notmuch-search-mode-map
  105. :nmv "j" #'notmuch-search-next-thread
  106. :nmv "k" #'notmuch-search-previous-thread
  107. :nmv "t" #'notmuch-tree-from-search-thread
  108. ;; :nmv "RET" #'notmuch-tree-from-search-thread
  109. :nmv "RET" #'notmuch-search-show-thread
  110. :nmv "N" #'notmuch-mua-new-mail
  111. :nmv "T" #'notmuch-tree-from-search-current-query
  112. :nmv ";" #'notmuch-search-tag
  113. :nmv "," #'notmuch-jump-search
  114. :nmv "d" #'+mail/notmuch-search-delete
  115. :nmv "a" #'notmuch-search-archive-thread
  116. ;; :nmv "q" #'notmuch
  117. :nmv "q" #'+mail/quit
  118. :nmv "R" #'notmuch-search-reply-to-thread-sender
  119. :nmv "r" #'notmuch-search-reply-to-thread
  120. :nmv "go" #'+notmuch-exec-offlineimap
  121. (:when (featurep! :completion ivy)
  122. :nmv "s" #'counsel-notmuch)
  123. (:when (featurep! :completion helm)
  124. :nmv "s" #'helm-notmuch)
  125. :nmv "x" #'+mail/notmuch-search-spam)
  126. (:map notmuch-tree-mode-map
  127. :nmv "j" #'notmuch-tree-next-message
  128. :nmv "k" #'notmuch-tree-prev-message
  129. :nmv "S" #'notmuch-search-from-tree-current-query
  130. (:when (featurep! :completion ivy)
  131. :nmv "s" #'counsel-notmuch)
  132. (:when (featurep! :completion helm)
  133. :nmv "s" #'helm-notmuch)
  134. :nmv "t" #'notmuch-tree
  135. :nmv ";" #'notmuch-tree-tag
  136. :nmv "RET" #'notmuch-tree-show-message
  137. :nmv "q" #'notmuch-tree-quit
  138. :nmv "s-n" #'notmuch-mua-new-mail
  139. :nmv "r" #'notmuch-search-reply-to-thread-sender
  140. :nmv "a" #'notmuch-tree-archive-message-then-next
  141. :nmv "A" #'notmuch-tree-archive-thread
  142. :nmv "i" #'+mail/open-message-with-mail-app-notmuch-tree
  143. :nmv "d" #'+mail/notmuch-tree-delete
  144. :nmv "x" #'+mail/notmuch-tree-spam)
  145. (:map notmuch-message-mode-map
  146. :localleader
  147. :desc "Send and Exit" doom-localleader-key #'notmuch-mua-send-and-exit
  148. :desc "Kill Message Buffer" "k" #'notmuch-mua-kill-buffer
  149. :desc "Save as Draft" "s" #'message-dont-send
  150. :desc "Attach file" "f" #'mml-attach-file))))
  151. ; Use w3m to parse HTML email
  152. (setq mm-text-html-renderer 'w3m)
  153. (setq w3m-fill-column 72)
  154. ;;;; counsel-notmuch
  155. (when (featurep! :completion ivy)
  156. (def-package! counsel-notmuch
  157. :commands counsel-notmuch
  158. :after notmuch))
  159. ;;;; helm-notmuch
  160. (when (featurep! :completion helm)
  161. (def-package! helm-notmuch
  162. :commands helm-notmuch
  163. :after notmuch))
  164. ;;;; org-mime
  165. (def-package! org-mime
  166. :after (org notmuch))
  167. :config (setq org-mime-library 'mml)
  168. ;;;###autoload
  169. (defun =mail ()
  170. "Activate (or switch to) `notmuch' in its workspace."
  171. (interactive)
  172. (if-let* ((buf (cl-find-if (lambda (it) (string-match-p "^\\*notmuch" (buffer-name (window-buffer it))))
  173. (doom-visible-windows))))
  174. (select-window (get-buffer-window buf))
  175. (notmuch-search "tag:inbox")))
  176. ;; (call-interactively 'notmuch-hello-sidebar)
  177. ;;;###autoload
  178. (defun +mail/quit ()
  179. (interactive)
  180. ;; (+popup/close (get-buffer-window "*notmuch-hello*"))
  181. (doom-kill-matching-buffers "^\\*notmuch"))
  182. ;;;###autoload
  183. (defun +mail/notmuch-search-delete ()
  184. (interactive)
  185. (notmuch-search-add-tag
  186. (list "+deleted" "-inbox" "-unread"))
  187. (notmuch-search-next-thread))
  188. ;;;###autoload
  189. (defun +mail/notmuch-tree-delete ()
  190. (interactive)
  191. (notmuch-tree-add-tag
  192. (list "+trash" "-inbox" "-unread"))
  193. (notmuch-tree-next-message))
  194. ;;;###autoload
  195. (defun +mail/notmuch-search-spam ()
  196. (interactive)
  197. (notmuch-search-add-tag
  198. (list "+spam" "-inbox" "-unread"))
  199. (notmuch-search-next-thread))
  200. ;;;###autoload
  201. (defun +mail/notmuch-tree-spam ()
  202. (interactive)
  203. (notmuch-tree-add-tag
  204. (list "+spam" "-inbox" "-unread"))
  205. (notmuch-tree-next-message))
  206. ;;;###autoload
  207. (defun +mail/open-message-with-mail-app-notmuch-tree ()
  208. (interactive)
  209. (let* ((msg-path (car (plist-get (notmuch-tree-get-message-properties) :filename)))
  210. (temp (make-temp-file "notmuch-message-" nil ".eml")))
  211. (shell-command-to-string (format "cp '%s' '%s'" msg-path temp))
  212. (start-process-shell-command "email" nil (format "thunderbird '%s'" temp))))
  213. ;; Override
  214. ;;;###autoload
  215. (defun +mail/notmuch-start-notmuch-sentinel (proc event)
  216. "Process sentinel function used by `notmuch-start-notmuch'."
  217. (let* ((err-file (process-get proc 'err-file))
  218. (err-buffer (or (process-get proc 'err-buffer)
  219. (find-file-noselect err-file)))
  220. (err (when (not (zerop (buffer-size err-buffer)))
  221. (with-current-buffer err-buffer (buffer-string))))
  222. (sub-sentinel (process-get proc 'sub-sentinel))
  223. (real-command (process-get proc 'real-command)))
  224. (condition-case err
  225. (progn
  226. ;; Invoke the sub-sentinel, if any
  227. (when sub-sentinel
  228. (funcall sub-sentinel proc event))
  229. ;; Check the exit status. This will signal an error if the
  230. ;; exit status is non-zero. Don't do this if the process
  231. ;; buffer is dead since that means Emacs killed the process
  232. ;; and there's no point in telling the user that (but we
  233. ;; still check for and report stderr output below).
  234. (when (buffer-live-p (process-buffer proc))
  235. (notmuch-check-async-exit-status proc event real-command err))
  236. ;; If that didn't signal an error, then any error output was
  237. ;; really warning output. Show warnings, if any.
  238. (let ((warnings
  239. (when err
  240. (with-current-buffer err-buffer
  241. (goto-char (point-min))
  242. (end-of-line)
  243. ;; Show first line; stuff remaining lines in the
  244. ;; errors buffer.
  245. (let ((l1 (buffer-substring (point-min) (point))))
  246. (skip-chars-forward "\n")
  247. (cons l1 (unless (eobp)
  248. (buffer-substring (point) (point-max)))))))))
  249. (when warnings
  250. (notmuch-logged-error (car warnings) (cdr warnings)))))
  251. (error
  252. ;; Emacs behaves strangely if an error escapes from a sentinel,
  253. ;; so turn errors into messages.
  254. (message "%s" (error-message-string err))))
  255. (when err-buffer
  256. (set-process-query-on-exit-flag (get-buffer-process err-buffer) nil)
  257. (kill-buffer err-buffer))
  258. (when err-file (ignore-errors (delete-file err-file)))))
  259. ;;;###autoload
  260. (defun +mail/notmuch-show-reuse-buffer (thread-id &optional elide-toggle parent-buffer query-context buffer-name)
  261. "Run \"notmuch show\" with the given thread ID and display results.
  262. ELIDE-TOGGLE, if non-nil, inverts the default elide behavior.
  263. The optional PARENT-BUFFER is the notmuch-search buffer from
  264. which this notmuch-show command was executed, (so that the
  265. next thread from that buffer can be show when done with this
  266. one).
  267. The optional QUERY-CONTEXT is a notmuch search term. Only
  268. messages from the thread matching this search term are shown if
  269. non-nil.
  270. The optional BUFFER-NAME provides the name of the buffer in
  271. which the message thread is shown. If it is nil (which occurs
  272. when the command is called interactively) the argument to the
  273. function is used.
  274. Returns the buffer containing the messages, or NIL if no messages
  275. matched."
  276. (interactive "sNotmuch show: \nP")
  277. (let ((buffer-name (generate-new-buffer-name
  278. (or (concat "*notmuch-" buffer-name "*")
  279. (concat "*notmuch-" thread-id "*"))))
  280. ;; We override mm-inline-override-types to stop application/*
  281. ;; parts from being displayed unless the user has customized
  282. ;; it themselves.
  283. (mm-inline-override-types
  284. (if (equal mm-inline-override-types
  285. (eval (car (get 'mm-inline-override-types 'standard-value))))
  286. (cons "application/*" mm-inline-override-types)
  287. mm-inline-override-types)))
  288. (switch-to-buffer (get-buffer-create buffer-name))
  289. ;; No need to track undo information for this buffer.
  290. (setq buffer-undo-list t)
  291. (notmuch-show-mode)
  292. ;; Set various buffer local variables to their appropriate initial
  293. ;; state. Do this after enabling `notmuch-show-mode' so that they
  294. ;; aren't wiped out.
  295. (setq notmuch-show-thread-id thread-id
  296. notmuch-show-parent-buffer parent-buffer
  297. notmuch-show-query-context (if (or (string= query-context "")
  298. (string= query-context "*"))
  299. nil query-context)
  300. notmuch-show-process-crypto notmuch-crypto-process-mime
  301. ;; If `elide-toggle', invert the default value.
  302. notmuch-show-elide-non-matching-messages
  303. (if elide-toggle
  304. (not notmuch-show-only-matching-messages)
  305. notmuch-show-only-matching-messages))
  306. (add-hook 'post-command-hook #'notmuch-show-command-hook nil t)
  307. (jit-lock-register #'notmuch-show-buttonise-links)
  308. (notmuch-tag-clear-cache)
  309. (let ((inhibit-read-only t))
  310. (if (notmuch-show--build-buffer)
  311. ;; Messages were inserted into the buffer.
  312. (current-buffer)
  313. ;; No messages were inserted - presumably none matched the
  314. ;; query.
  315. (kill-buffer (current-buffer))
  316. (ding)
  317. (message "No messages matched the query!")
  318. nil))))
  319. ;;;###autoload
  320. (defun +mail/notmuch-hello-insert-searches (title query-list &rest options)
  321. "Insert a section with TITLE showing a list of buttons made from QUERY-LIST.
  322. QUERY-LIST should ideally be a plist but for backwards
  323. compatibility other forms are also accepted (see
  324. `notmuch-saved-searches' for details). The plist should
  325. contain keys :name and :query; if :count-query is also present
  326. then it specifies an alternate query to be used to generate the
  327. count for the associated search.
  328. Supports the following entries in OPTIONS as a plist:
  329. :initially-hidden - if non-nil, section will be hidden on startup
  330. :show-empty-searches - show buttons with no matching messages
  331. :hide-if-empty - hide if no buttons would be shown
  332. (only makes sense without :show-empty-searches)
  333. :filter - This can be a function that takes the search query as its argument and
  334. returns a filter to be used in conjuction with the query for that search or nil
  335. to hide the element. This can also be a string that is used as a combined with
  336. each query using \"and\".
  337. :filter-count - Separate filter to generate the count displayed each search. Accepts
  338. the same values as :filter. If :filter and :filter-count are specified, this
  339. will be used instead of :filter, not in conjunction with it."
  340. (widget-insert (propertize title 'face 'org-agenda-structure))
  341. (if (and notmuch-hello-first-run (plist-get options :initially-hidden))
  342. (add-to-list 'notmuch-hello-hidden-sections title))
  343. (let ((is-hidden (member title notmuch-hello-hidden-sections))
  344. (widget-push-button-prefix "")
  345. (widget-push-button-suffix "")
  346. (start (point)))
  347. (if is-hidden
  348. (widget-create 'push-button
  349. :notify `(lambda (widget &rest ignore)
  350. (setq notmuch-hello-hidden-sections
  351. (delete ,title notmuch-hello-hidden-sections))
  352. (notmuch-hello-update))
  353. (propertize " +" 'face 'org-agenda-structure))
  354. (widget-create 'push-button
  355. :notify `(lambda (widget &rest ignore)
  356. (add-to-list 'notmuch-hello-hidden-sections
  357. ,title)
  358. (notmuch-hello-update))
  359. " -"))
  360. (widget-insert "\n")
  361. (when (not is-hidden)
  362. (let ((searches (apply 'notmuch-hello-query-counts query-list options)))
  363. (when (or (not (plist-get options :hide-if-empty))
  364. searches)
  365. (widget-insert "\n")
  366. (notmuch-hello-insert-buttons searches)
  367. (indent-rigidly start (point) notmuch-hello-indent))))))
  368. ;;;###autoload
  369. (defun +mail/notmuch-hello-insert-saved-searches ()
  370. "Insert the saved-searches section."
  371. (let ((searches (notmuch-hello-query-counts
  372. (if notmuch-saved-search-sort-function
  373. (funcall notmuch-saved-search-sort-function
  374. notmuch-saved-searches)
  375. notmuch-saved-searches)
  376. :show-empty-searches notmuch-show-empty-saved-searches)))
  377. (when searches
  378. (widget-insert (propertize "Notmuch" 'face 'org-agenda-date-today))
  379. (widget-insert "\n\n")
  380. (widget-insert (propertize "Saved searches" 'face 'org-agenda-structure))
  381. (widget-insert "\n\n")
  382. (let ((start (point)))
  383. (notmuch-hello-insert-buttons searches)
  384. (indent-rigidly start (point) notmuch-hello-indent)))))
  385. ;;;###autoload
  386. (defun +mail/notmuch-hello-insert-buttons (searches)
  387. "Insert buttons for SEARCHES.
  388. SEARCHES must be a list of plists each of which should contain at
  389. least the properties :name NAME :query QUERY and :count COUNT,
  390. where QUERY is the query to start when the button for the
  391. corresponding entry is activated, and COUNT should be the number
  392. of messages matching the query. Such a plist can be computed
  393. with `notmuch-hello-query-counts'."
  394. (let* ((widest (notmuch-hello-longest-label searches))
  395. (tags-and-width (notmuch-hello-tags-per-line widest))
  396. (tags-per-line (car tags-and-width))
  397. (column-width (cdr tags-and-width))
  398. (column-indent 0)
  399. (count 0)
  400. (reordered-list (notmuch-hello-reflect searches tags-per-line))
  401. ;; Hack the display of the buttons used.
  402. (widget-push-button-prefix "")
  403. (widget-push-button-suffix ""))
  404. ;; dme: It feels as though there should be a better way to
  405. ;; implement this loop than using an incrementing counter.
  406. (mapc (lambda (elem)
  407. ;; (not elem) indicates an empty slot in the matrix.
  408. (when elem
  409. (if (> column-indent 0)
  410. (widget-insert (make-string column-indent ? )))
  411. (let* ((name (plist-get elem :name))
  412. (query (plist-get elem :query))
  413. (oldest-first (case (plist-get elem :sort-order)
  414. (newest-first nil)
  415. (oldest-first t)
  416. (otherwise notmuch-search-oldest-first)))
  417. (search-type (eq (plist-get elem :search-type) 'tree))
  418. (msg-count (plist-get elem :count)))
  419. (widget-insert (format "\n%5s "
  420. (notmuch-hello-nice-number msg-count)))
  421. (widget-create 'push-button
  422. :notify #'notmuch-hello-widget-search
  423. :notmuch-search-terms query
  424. :notmuch-search-oldest-first oldest-first
  425. :notmuch-search-type search-type
  426. name)
  427. (setq column-indent
  428. (1+ (max 0 (- column-width (length name)))))))
  429. (setq count (1+ count))
  430. (when (eq (% count tags-per-line) 0)
  431. (setq column-indent 0)
  432. (widget-insert "\n")))
  433. reordered-list)
  434. ;; If the last line was not full (and hence did not include a
  435. ;; carriage return), insert one now.
  436. (unless (eq (% count tags-per-line) 0)
  437. (widget-insert "\n"))))
  438. (defun +notmuch-exec-offlineimap ()
  439. "Execute offlineimap"
  440. (interactive)
  441. (set-process-sentinel
  442. (start-process-shell-command "offlineimap"
  443. "*offlineimap*"
  444. "offlineimap -o")
  445. '(lambda (process event)
  446. (notmuch-refresh-all-buffers)
  447. (message "Done!"))))
  448. ; Kill email message buffers when you close them
  449. (setq message-kill-buffer-on-exit t)
  450. (setq message-default-mail-headers "Cc: \nBcc: \n")
  451. (setq message-auto-save-directory "~/Mail/colin@onec.me/Drafts/")
  452. (setq message-directory "~/Mail/colin@onec.me/")