123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- ;; Message mode
- ;; This is common to Gnus, mu4e, notmuch, etc.
- ;;; TODO: Is it possible to mbsync without attachments?
- (require 'init-smtpmail)
- (setq user-full-name "Pierre Neidhardt"
- mm-default-directory "~/Downloads" ; Where to save attachments.
- ;; Necessary since Emacs 27:
- mml-secure-openpgp-sign-with-sender t
- ;; Don't keep sent e-mail buffer. (Also see mu4e-conversation-kill-buffer-on-exit.)
- message-kill-buffer-on-exit t
- message-send-mail-function 'smtpmail-send-it
- ;; The following is only useful for sending mail with msmtp?
- mail-specify-envelope-from t
- mail-envelope-from 'header)
- ;; TODO: Use the following to automatically set the From: field when replying.
- ;; Might not be necessary.
- ;; `message-alternative-emails'
- ;; Also see the `gnus-alias' and `smtpmail-multi' packages.
- ;; https://old.reddit.com/r/emacs/comments/5iievm/nice_email_configuration_using_emacs_mbsync/
- (defun ambrevar/message-recipients (&optional include-from)
- "Return a list of all recipients in the message, looking at TO, CC and BCC.
- Each recipient is in the format of `mail-extract-address-components'."
- (mapcan (lambda (header)
- (let ((header-value (message-fetch-field header)))
- (and
- header-value
- (mail-extract-address-components header-value t))))
- `(,@(when include-from '("From")) "To" "Cc" "Bcc")))
- ;; Sign messages by default. TODO: Which method?
- (add-hook 'message-setup-hook 'mml-secure-sign-pgpmime)
- ;; (add-hook 'message-setup-hook 'mml-secure-message-sign-pgpmime)
- ;; Also crypt to self so that we can read sent e-mails.
- (setq mml-secure-openpgp-encrypt-to-self t)
- (defvar ambrevar/trust-threshold '(marignal full ultimate))
- (defun ambrevar/trusted-addresses ()
- "Return the list of trusted email addresses in the PGP keyring.
- Trust is defined as per `ambrevar/trust-threshold'."
- (let (valid-addresses)
- (dolist (key (epg-list-keys (epg-make-context epa-protocol)) valid-addresses)
- (dolist (user-id (epg-key-user-id-list key))
- (when (memq (epg-user-id-validity user-id) '(marginal full ultimate))
- (push (cadr (mail-extract-address-components (epg-user-id-string user-id)))
- valid-addresses))))))
- (defun ambrevar/message-sign-encrypt-if-all-keys-trusted ()
- "Add MML tag to encrypt message when there is a key for each recipient.
- Consider adding this function to `message-send-hook' to
- systematically send encrypted emails when possible."
- (let* ((recipients (ambrevar/message-recipients))
- (untrusted-recipients (seq-difference (mapcar #'cadr recipients)
- (ambrevar/trusted-addresses))))
- (if untrusted-recipients
- (message "Not encrypting because of untrusted %s." untrusted-recipients)
- (mml-secure-message-sign-encrypt))))
- ;; TODO: Test and report upstream (Emacs + Notmuch).
- (add-hook 'message-send-hook #'ambrevar/message-sign-encrypt-if-all-keys-trusted)
- ;; Fix replying to GitHub.
- ;; We could leverage `message-alter-recipients-function' but it does not seem to
- ;; be called with Notmuch for instance.
- (defun ambrevar/cleanup-github-recipients ()
- "When replying to a github message, clean up all bogus recipients.
- Also remove signature.
- This function is useful in `message-setup-hook'."
- (interactive)
- (let ((to (message-fetch-field "To")))
- (when (and to
- (string-match (rx "@reply.github.com" string-end)
- (cadr (mail-extract-address-components to))))
- (dolist (hdr '("To" "Cc" "Bcc"))
- (let ((header-value (message-fetch-field hdr)))
- (when header-value
- (message-replace-header
- hdr
- (mapconcat (lambda (addrcell)
- (format "\"%s\" <%s>" (car addrcell) (cadr addrcell)))
- (cl-delete-if
- (lambda (addrcell)
- (string-match (rx "@noreply.github.com" string-end)
- (cadr addrcell)))
- (mail-extract-address-components header-value t))
- ", ")))))
- ;; Delete signature if any.
- (delete-region (save-excursion
- (message-goto-signature)
- (unless (eobp)
- (forward-line -1))
- (point))
- (point-max))
- ;; Deleting trailing blank lines.
- (save-excursion
- (goto-char (point-max))
- (delete-blank-lines)
- (delete-blank-lines)))))
- (add-hook 'message-setup-hook 'ambrevar/cleanup-github-recipients)
- (defvar ambrevar/message-compose-fortune-p nil
- "Whether or not to include a fortune in the signature.")
- (defun ambrevar/message-add-signature-and-maybe-fortune ()
- "Insert signature using `user-full-name'.
- A fortune is appended if `ambrevar/message-compose-fortune-p' is non-nil."
- (require 'functions) ; For `call-process-to-string'.
- ;; Return the signature and set it for mu4e.
- (setq mu4e-compose-signature
- (concat
- user-full-name "\n"
- "https://ambrevar.xyz/"
- (when (and ambrevar/message-compose-fortune-p
- (executable-find "fortune"))
- (concat "\n\n"
- (ambrevar/call-process-to-string "fortune" "-s"))))))
- ;; (add-hook 'message-setup-hook 'ambrevar/message-add-signature-and-maybe-fortune)
- (setq message-signature 'ambrevar/message-add-signature-and-maybe-fortune)
- (when (require 'org-contacts nil t)
- (defun ambrevar/message-select-dictionary ()
- "Set dictionary according to the LANGUAGE property of the first
- \"To:\" recipient found in the Org contacts file."
- (interactive)
- (let ((addresses (mapcar 'cadr (ambrevar/message-recipients)))
- address-lang-map)
- (setq address-lang-map
- (cl-loop for contact in (org-contacts-filter)
- ;; The contact name is always the car of the assoc-list
- ;; returned by `org-contacts-filter'.
- for language = (cdr (assoc-string "LANGUAGE" (nth 2 contact)))
- ;; Build the list of the user email addresses.
- for email-list = (org-contacts-split-property
- (or (cdr (assoc-string org-contacts-email-property
- (nth 2 contact))) ""))
- if (and email-list language)
- ;; Build an alist of (EMAIL . LANGUAGE).
- nconc (cl-loop for email in email-list
- collect (cons (downcase email) language))))
- (while addresses
- (if (not (assoc (car addresses) address-lang-map))
- (setq addresses (cdr addresses))
- (ispell-change-dictionary (cdr (assoc (car addresses) address-lang-map)))
- (setq addresses nil)))))
- (add-hook 'message-setup-hook 'ambrevar/message-select-dictionary)
- (defun ambrevar/message-select-sender ()
- "Set the sender according to the SENDER property of the first
- \"To:\" recipient found in the Org contacts file."
- (interactive)
- (let* ((addresses (mapcar #'cadr (ambrevar/message-recipients)))
- (sender (cl-loop for contact in (org-contacts-filter)
- for email-list = (org-contacts-split-property
- (or (cdr (assoc-string org-contacts-email-property
- (nth 2 contact))) ""))
- when (cl-loop for email in email-list
- thereis (string= (downcase email)
- (downcase (car addresses))))
- return (cdr (assoc-string "SENDER" (nth 2 contact))))))
- (when sender
- (ambrevar/notmuch-change-sender sender))))
- (add-hook 'message-send-hook 'ambrevar/message-select-sender))
- ;; Because it's to tempting to send an e-mail riddled with typos...
- (add-hook 'message-setup-hook 'flyspell-mode)
- ;; Org capture for emails in org-contacts
- (when (require 'org-contacts nil 'noerror)
- ;; TODO: Don't duplicate contacts.
- (defun ambrevar/message-complete-address ()
- (require 'subr-x)
- ;; Need to get last message buffer since Org capture happens in a different
- ;; buffer.
- (let ((last-buffer
- (cl-loop for buffer in (buffer-list)
- when (with-current-buffer buffer
- (or (derived-mode-p 'notmuch-show-mode)
- (derived-mode-p 'message-mode)))
- return buffer)))
- (save-window-excursion
- (with-current-buffer last-buffer
- (let* ((recipients (ambrevar/message-recipients 'include-from))
- (addresses-names (mapcar
- (lambda (s)
- (concat (cadr s) " " (car s)))
- recipients))
- (email-at-point (let ((email (thing-at-point 'email)))
- (when email
- (string-trim email "<" ">"))))
- (default (when email-at-point (seq-find (lambda (addr)
- (string-prefix-p email-at-point
- addr))
- addresses-names)))
- (address-name (completing-read "Address: " addresses-names
- nil nil nil nil default))
- (idx (string-match " " address-name))
- (address (substring address-name 0 idx))
- (name (substring address-name idx)))
- (list address name))))))
- (defun ambrevar/org-capture-contact-format (address name)
- (format "%s
- :PROPERTIES:
- :EMAIL: %s
- :END:" name address))
- (add-to-list 'org-capture-templates
- `("C" "Add e-mail address to contacts" entry (file+headline ,(car org-contacts-files) "Contacts")
- "* %(apply 'ambrevar/org-capture-contact-format (ambrevar/message-complete-address))")))
- ;; The following is an alternative using the template format string. It has some missing features though:
- ;; - Can't use (thing-at-point 'email) as a default.
- ;; - Need to manually match Name and Address.
- ;; (add-to-list 'org-capture-templates
- ;; `("c" "Add e-mail address to contacts" entry (file+headline ,(car org-contacts-files) "Contacts")
- ;; "* %^{Name|%:fromname|%:to-names|%:cc-names}
- ;; :PROPERTIES:
- ;; :EMAIL: %^{Address|%:fromaddress|%:to-addresses|%:cc-addresses}
- ;; :END:"))
- (provide 'init-message)
|