init-message.el 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. ;; Message mode
  2. ;; This is common to Gnus, mu4e, notmuch, etc.
  3. ;;; TODO: Is it possible to mbsync without attachments?
  4. (require 'init-smtpmail)
  5. (setq user-full-name "Pierre Neidhardt"
  6. mm-default-directory "~/Downloads" ; Where to save attachments.
  7. ;; Necessary since Emacs 27:
  8. mml-secure-openpgp-sign-with-sender t
  9. ;; Don't keep sent e-mail buffer. (Also see mu4e-conversation-kill-buffer-on-exit.)
  10. message-kill-buffer-on-exit t
  11. message-send-mail-function 'smtpmail-send-it
  12. ;; The following is only useful for sending mail with msmtp?
  13. mail-specify-envelope-from t
  14. mail-envelope-from 'header)
  15. ;; TODO: Use the following to automatically set the From: field when replying.
  16. ;; Might not be necessary.
  17. ;; `message-alternative-emails'
  18. ;; Also see the `gnus-alias' and `smtpmail-multi' packages.
  19. ;; https://old.reddit.com/r/emacs/comments/5iievm/nice_email_configuration_using_emacs_mbsync/
  20. (defun ambrevar/message-recipients (&optional include-from)
  21. "Return a list of all recipients in the message, looking at TO, CC and BCC.
  22. Each recipient is in the format of `mail-extract-address-components'."
  23. (mapcan (lambda (header)
  24. (let ((header-value (message-fetch-field header)))
  25. (and
  26. header-value
  27. (mail-extract-address-components header-value t))))
  28. `(,@(when include-from '("From")) "To" "Cc" "Bcc")))
  29. ;; Sign messages by default. TODO: Which method?
  30. (add-hook 'message-setup-hook 'mml-secure-sign-pgpmime)
  31. ;; (add-hook 'message-setup-hook 'mml-secure-message-sign-pgpmime)
  32. ;; Also crypt to self so that we can read sent e-mails.
  33. (setq mml-secure-openpgp-encrypt-to-self t)
  34. (defvar ambrevar/trust-threshold '(marignal full ultimate))
  35. (defun ambrevar/trusted-addresses ()
  36. "Return the list of trusted email addresses in the PGP keyring.
  37. Trust is defined as per `ambrevar/trust-threshold'."
  38. (let (valid-addresses)
  39. (dolist (key (epg-list-keys (epg-make-context epa-protocol)) valid-addresses)
  40. (dolist (user-id (epg-key-user-id-list key))
  41. (when (memq (epg-user-id-validity user-id) '(marginal full ultimate))
  42. (push (cadr (mail-extract-address-components (epg-user-id-string user-id)))
  43. valid-addresses))))))
  44. (defun ambrevar/message-sign-encrypt-if-all-keys-trusted ()
  45. "Add MML tag to encrypt message when there is a key for each recipient.
  46. Consider adding this function to `message-send-hook' to
  47. systematically send encrypted emails when possible."
  48. (let* ((recipients (ambrevar/message-recipients))
  49. (untrusted-recipients (seq-difference (mapcar #'cadr recipients)
  50. (ambrevar/trusted-addresses))))
  51. (if untrusted-recipients
  52. (message "Not encrypting because of untrusted %s." untrusted-recipients)
  53. (mml-secure-message-sign-encrypt))))
  54. ;; TODO: Test and report upstream (Emacs + Notmuch).
  55. (add-hook 'message-send-hook #'ambrevar/message-sign-encrypt-if-all-keys-trusted)
  56. ;; Fix replying to GitHub.
  57. ;; We could leverage `message-alter-recipients-function' but it does not seem to
  58. ;; be called with Notmuch for instance.
  59. (defun ambrevar/cleanup-github-recipients ()
  60. "When replying to a github message, clean up all bogus recipients.
  61. Also remove signature.
  62. This function is useful in `message-setup-hook'."
  63. (interactive)
  64. (let ((to (message-fetch-field "To")))
  65. (when (and to
  66. (string-match (rx "@reply.github.com" string-end)
  67. (cadr (mail-extract-address-components to))))
  68. (dolist (hdr '("To" "Cc" "Bcc"))
  69. (let ((header-value (message-fetch-field hdr)))
  70. (when header-value
  71. (message-replace-header
  72. hdr
  73. (mapconcat (lambda (addrcell)
  74. (format "\"%s\" <%s>" (car addrcell) (cadr addrcell)))
  75. (cl-delete-if
  76. (lambda (addrcell)
  77. (string-match (rx "@noreply.github.com" string-end)
  78. (cadr addrcell)))
  79. (mail-extract-address-components header-value t))
  80. ", ")))))
  81. ;; Delete signature if any.
  82. (delete-region (save-excursion
  83. (message-goto-signature)
  84. (unless (eobp)
  85. (forward-line -1))
  86. (point))
  87. (point-max))
  88. ;; Deleting trailing blank lines.
  89. (save-excursion
  90. (goto-char (point-max))
  91. (delete-blank-lines)
  92. (delete-blank-lines)))))
  93. (add-hook 'message-setup-hook 'ambrevar/cleanup-github-recipients)
  94. (defvar ambrevar/message-compose-fortune-p nil
  95. "Whether or not to include a fortune in the signature.")
  96. (defun ambrevar/message-add-signature-and-maybe-fortune ()
  97. "Insert signature using `user-full-name'.
  98. A fortune is appended if `ambrevar/message-compose-fortune-p' is non-nil."
  99. (require 'functions) ; For `call-process-to-string'.
  100. ;; Return the signature and set it for mu4e.
  101. (setq mu4e-compose-signature
  102. (concat
  103. user-full-name "\n"
  104. "https://ambrevar.xyz/"
  105. (when (and ambrevar/message-compose-fortune-p
  106. (executable-find "fortune"))
  107. (concat "\n\n"
  108. (ambrevar/call-process-to-string "fortune" "-s"))))))
  109. ;; (add-hook 'message-setup-hook 'ambrevar/message-add-signature-and-maybe-fortune)
  110. (setq message-signature 'ambrevar/message-add-signature-and-maybe-fortune)
  111. (when (require 'org-contacts nil t)
  112. (defun ambrevar/message-select-dictionary ()
  113. "Set dictionary according to the LANGUAGE property of the first
  114. \"To:\" recipient found in the Org contacts file."
  115. (interactive)
  116. (let ((addresses (mapcar 'cadr (ambrevar/message-recipients)))
  117. address-lang-map)
  118. (setq address-lang-map
  119. (cl-loop for contact in (org-contacts-filter)
  120. ;; The contact name is always the car of the assoc-list
  121. ;; returned by `org-contacts-filter'.
  122. for language = (cdr (assoc-string "LANGUAGE" (nth 2 contact)))
  123. ;; Build the list of the user email addresses.
  124. for email-list = (org-contacts-split-property
  125. (or (cdr (assoc-string org-contacts-email-property
  126. (nth 2 contact))) ""))
  127. if (and email-list language)
  128. ;; Build an alist of (EMAIL . LANGUAGE).
  129. nconc (cl-loop for email in email-list
  130. collect (cons (downcase email) language))))
  131. (while addresses
  132. (if (not (assoc (car addresses) address-lang-map))
  133. (setq addresses (cdr addresses))
  134. (ispell-change-dictionary (cdr (assoc (car addresses) address-lang-map)))
  135. (setq addresses nil)))))
  136. (add-hook 'message-setup-hook 'ambrevar/message-select-dictionary)
  137. (defun ambrevar/message-select-sender ()
  138. "Set the sender according to the SENDER property of the first
  139. \"To:\" recipient found in the Org contacts file."
  140. (interactive)
  141. (let* ((addresses (mapcar #'cadr (ambrevar/message-recipients)))
  142. (sender (cl-loop for contact in (org-contacts-filter)
  143. for email-list = (org-contacts-split-property
  144. (or (cdr (assoc-string org-contacts-email-property
  145. (nth 2 contact))) ""))
  146. when (cl-loop for email in email-list
  147. thereis (string= (downcase email)
  148. (downcase (car addresses))))
  149. return (cdr (assoc-string "SENDER" (nth 2 contact))))))
  150. (when sender
  151. (ambrevar/notmuch-change-sender sender))))
  152. (add-hook 'message-send-hook 'ambrevar/message-select-sender))
  153. ;; Because it's to tempting to send an e-mail riddled with typos...
  154. (add-hook 'message-setup-hook 'flyspell-mode)
  155. ;; Org capture for emails in org-contacts
  156. (when (require 'org-contacts nil 'noerror)
  157. ;; TODO: Don't duplicate contacts.
  158. (defun ambrevar/message-complete-address ()
  159. (require 'subr-x)
  160. ;; Need to get last message buffer since Org capture happens in a different
  161. ;; buffer.
  162. (let ((last-buffer
  163. (cl-loop for buffer in (buffer-list)
  164. when (with-current-buffer buffer
  165. (or (derived-mode-p 'notmuch-show-mode)
  166. (derived-mode-p 'message-mode)))
  167. return buffer)))
  168. (save-window-excursion
  169. (with-current-buffer last-buffer
  170. (let* ((recipients (ambrevar/message-recipients 'include-from))
  171. (addresses-names (mapcar
  172. (lambda (s)
  173. (concat (cadr s) " " (car s)))
  174. recipients))
  175. (email-at-point (let ((email (thing-at-point 'email)))
  176. (when email
  177. (string-trim email "<" ">"))))
  178. (default (when email-at-point (seq-find (lambda (addr)
  179. (string-prefix-p email-at-point
  180. addr))
  181. addresses-names)))
  182. (address-name (completing-read "Address: " addresses-names
  183. nil nil nil nil default))
  184. (idx (string-match " " address-name))
  185. (address (substring address-name 0 idx))
  186. (name (substring address-name idx)))
  187. (list address name))))))
  188. (defun ambrevar/org-capture-contact-format (address name)
  189. (format "%s
  190. :PROPERTIES:
  191. :EMAIL: %s
  192. :END:" name address))
  193. (add-to-list 'org-capture-templates
  194. `("C" "Add e-mail address to contacts" entry (file+headline ,(car org-contacts-files) "Contacts")
  195. "* %(apply 'ambrevar/org-capture-contact-format (ambrevar/message-complete-address))")))
  196. ;; The following is an alternative using the template format string. It has some missing features though:
  197. ;; - Can't use (thing-at-point 'email) as a default.
  198. ;; - Need to manually match Name and Address.
  199. ;; (add-to-list 'org-capture-templates
  200. ;; `("c" "Add e-mail address to contacts" entry (file+headline ,(car org-contacts-files) "Contacts")
  201. ;; "* %^{Name|%:fromname|%:to-names|%:cc-names}
  202. ;; :PROPERTIES:
  203. ;; :EMAIL: %^{Address|%:fromaddress|%:to-addresses|%:cc-addresses}
  204. ;; :END:"))
  205. (provide 'init-message)