;;; mu4e ;;; REVIEW: Reply to all by default. ;;; https://github.com/djcb/mu/issues/1135 ;;; TODO: Is it possible to mbsync without attachments? ;;; REVIEW: Handle attachments in attached e-mails. ;;; See https://github.com/djcb/mu/issues/454#issuecomment-320616279. ;;; TODO: should go to next link in text e-mails too. ;; We need 'main' to setup pinentry-emacs for GPG. (require 'main) (when (require 'mu4e-maildirs-extension nil t) (mu4e-maildirs-extension)) (defun ambrevar/mu4e-headers () "Like `mu4e' but show the header view. Default to unread messages if the header buffer does not already exist." (interactive) (mu4e~start) (if (get-buffer "*mu4e-headers*" ) (switch-to-buffer "*mu4e-headers*") (mu4e-headers-search "flag:unread AND NOT flag:trashed"))) (setq ;; Attachments mu4e-attachment-dir "~/temp" mu4e-save-multiple-attachments-without-asking t ;; IMAP sync. mu4e-maildir "~/.cache/mail" mu4e-get-mail-command "mbsync -a" mu4e-update-interval 90 mu4e-headers-auto-update nil ; Don't refresh so that we don't lose the current filter upon, e.g. reading e-mails. mu4e-change-filenames-when-moving t ; Preferred for mbsync according to the man page. ;; SMTP message-send-mail-function 'smtpmail-send-it ;; Don't bother me with context on startup. mu4e-context-policy nil ;; Don't keep sent e-mail buffer. message-kill-buffer-on-exit t ;; For reporting bugs, "C-x m", etc. mail-user-agent 'mu4e-user-agent mu4e-compose-dont-reply-to-self t ;; Display mu4e-headers-date-format "%F %R" mu4e-headers-fields '((:human-date . 16) (:flags . 6) (:size . 6) (:mailing-list . 10) (:from . 22) (:subject)) mu4e-headers-time-format "%R" mu4e-view-show-addresses t mu4e-view-show-images t mu4e-view-image-max-width 800 mu4e-hide-index-messages t ;; If you're using a dark theme, and the messages are hard to read, it ;; can help to change the luminosity, e.g.: shr-color-visible-luminance-min 80 ;; Gmail-style threading. mu4e-headers-include-related t ;; Gmail likes format=flowed(?) ;; mu4e-compose-format-flowed ;; Also crypt to self so that we can read sent e-mails. mml-secure-openpgp-encrypt-to-self t ;; Because default completion can be extended (e.g. Helm, Ivy). mu4e-completing-read-function 'completing-read) ;;; Press "aV" to view in browser. (add-to-list 'mu4e-view-actions '("ViewInBrowser" . mu4e-action-view-in-browser) t) ;;; Custom bookmarks (add-to-list 'mu4e-bookmarks (make-mu4e-bookmark :name "Big inbox messages" :query "maildir:\".*inbox.*\" size:1M.." :key ?b)) ;;; Unicode chars for decoration might cause issues with some fonts or in terminals. ;;; https://github.com/djcb/mu/issues/733 ;;; https://github.com/djcb/mu/issues/1062 ;; (setq mu4e-use-fancy-chars t) ;;; REVIEW: Sorting in ascending order is impeded by ;;; `mu4e-search-results-limit': the 500 oldest e-mails will be displayed first. ;;; https://github.com/djcb/mu/issues/809 ;; (setq mu4e-headers-sort-direction 'ascending) ;;; Since we sort in ascending direction, we default to the end of buffer. ;; (add-hook 'mu4e-headers-found-hook 'end-of-buffer) (defvar ambrevar/mu4e-compose-fortune-p nil "Whether or not to include a fortune in the signature.") (defun ambrevar/mu4e-add-fortune-signature () (require 'functions) ; For `call-process-to-string'. (setq mu4e-compose-signature (concat user-full-name "\n" "https://ambrevar.xyz/" (when (and ambrevar/mu4e-compose-fortune-p (executable-find "fortune")) (concat "\n\n" (ambrevar/call-process-to-string "fortune" "-s")))))) (add-hook 'mu4e-compose-pre-hook 'ambrevar/mu4e-add-fortune-signature) (defun ambrevar/mu4e-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-fetch-addresses))) 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 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 'mu4e-compose-pre-hook 'ambrevar/mu4e-select-dictionary) (add-hook 'mu4e-conversation-hook 'ambrevar/mu4e-select-dictionary) ;;; Make some e-mails stand out a bit. (set-face-foreground 'mu4e-unread-face "yellow") (set-face-attribute 'mu4e-flagged-face nil :inherit 'font-lock-warning-face) ;;; Confirmation on every mark execution is too slow to my taste. (defun ambrevar/mu4e-mark-execute-all-no-confirm () (interactive) (mu4e-mark-execute-all t)) (define-key mu4e-headers-mode-map "x" 'ambrevar/mu4e-mark-execute-all-no-confirm) (when (require 'helm-mu nil t) (dolist (map (list mu4e-headers-mode-map mu4e-main-mode-map mu4e-view-mode-map)) (define-key map "s" 'helm-mu))) (defvar ambrevar/mu4e-compose-signed-p t) (defvar ambrevar/mu4e-compose-signed-and-crypted-p nil) (defun ambrevar/mu4e-compose-maybe-signed-and-crypted () "Maybe sign or encrypt+sign message. Message is signed or encrypted+signed when replying to a signed or encrypted message, respectively. Alternatively, message is signed or encrypted+signed if `ambrevar/mu4e-compose-signed-p' or `ambrevar/mu4e-compose-signed-and-crypted-p' is non-nil, respectively. This function is suitable for `mu4e-compose-mode-hook'." (let ((msg mu4e-compose-parent-message)) (cond ((or ambrevar/mu4e-compose-signed-and-crypted-p (and msg (member 'encrypted (mu4e-message-field msg :flags)))) (mml-secure-message-sign-encrypt)) ((or ambrevar/mu4e-compose-signed-p (and msg (member 'signed (mu4e-message-field msg :flags)))) (mml-secure-message-sign-pgpmime))))) (add-hook 'mu4e-compose-mode-hook 'ambrevar/mu4e-compose-maybe-signed-and-crypted) (defun ambrevar/message-fetch-addresses () "Return a list of (NAME EMAIL) from the message header. The \"From\", \"To\", \"Cc\" and \"Bcc\" fields are looked up. Addresses in `mu4e-user-mail-address-list' are filtered out. Duplicates are removed." (require 'cl) (cl-delete-duplicates (seq-remove (lambda (contact) (member (cadr contact) mu4e-user-mail-address-list)) (apply 'append (if (eq major-mode 'mu4e-compose-mode) (save-restriction (message-narrow-to-headers) (mapcar (lambda (addr) (mail-extract-address-components (message-fetch-field addr) t)) (seq-filter 'message-fetch-field '("From" "To" "Cc" "Bcc")))) (unless (buffer-live-p (mu4e-get-headers-buffer)) (mu4e-error "no headers buffer connected")) (let ((msg (or (mu4e-message-at-point 'noerror) (with-current-buffer (mu4e-get-headers-buffer) ;; When loading messages, point might ;; not be over a message yet. (mu4e-message-at-point 'noerror))))) (when msg (delq nil (mapcar (lambda (field) ;; `mu4e-message-field' returns a list of (NAME . EMAIL). (mapcar (lambda (addr) (list (car addr) (cdr addr))) (mu4e-message-field msg field))) '(:from :to :cc :bcc)))))))))) (defun ambrevar/message-send-maybe-crypted () "Crypt message if all recipients have a trusted key. This will prompt the user if only some recipients have a suitable public key. Suitable for `message-send-hook'." (let ((recipients (mapcar 'cadr (ambrevar/message-fetch-addresses))) valid-addresses untrusted-recipients) (dolist (key (epg-list-keys (epg-make-context epa-protocol))) (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)))) (setq untrusted-recipients (seq-difference recipients valid-addresses)) (when (/= (length untrusted-recipients) (length recipients)) ;; Some recipients have valid keys. (mml-secure-message-sign-encrypt) (when (and untrusted-recipients (yes-or-no-p (format "Some recipients don't have a trusted key %S. Sending unencrypted? " untrusted-recipients))) (mml-secure-message-sign) (mu4e-message "Sending unencrypted")))) t) (add-hook 'message-send-hook 'ambrevar/message-send-maybe-crypted) ;; Because it's to tempting to send an e-mail riddled with typos... (add-hook 'mu4e-compose-mode-hook 'flyspell-mode) ;;; Org capture (when (require 'org-mu4e nil t) (dolist (map (list mu4e-view-mode-map mu4e-headers-mode-map)) ;; Org mode has "C-c C-t" for 'org-todo. (define-key map (kbd "C-c C-t") 'org-mu4e-store-and-capture)) (setq org-mu4e-link-query-in-headers-mode nil)) ;;; Gmail trash fix. (defvar ambrevar/mu4e-move-to-trash-patterns nil "List of regexps to match for moving to trash instead of deleting them. Matches are done against the :maildir field of the e-mail at point. See `ambrevar/mu4e-headers-move-to-trash' and `ambrevar/mu4e-view-move-to-trash'.") (defun ambrevar/mu4e-headers-move-to-trash () (interactive) (let ((msg-dir (mu4e-message-field (mu4e-message-at-point) :maildir))) (if (not (seq-filter (lambda (re) (string-match re msg-dir)) ambrevar/mu4e-move-to-trash-patterns)) (mu4e-headers-mark-for-delete) (mu4e-mark-set 'move (funcall mu4e-trash-folder (mu4e-message-at-point))) (mu4e-headers-next)))) (defun ambrevar/mu4e-view-move-to-trash () (interactive) (mu4e~view-in-headers-context (ambrevar/mu4e-headers-move-to-trash) (mu4e~headers-move (or n 1)))) ;;; Don't display trashed messages in bookmarks. This is useful for Gmail where ;;; the "delete" flag is not used. (defvar ambrevar/mu4e-trash-folders nil "List of trash folders to filter out from bookmarks.") (load "~/personal/mail/mu4e.el" t) ;; Do this after setting `ambrevar/mu4e-trash-folders'. (dolist (bookmark mu4e-bookmarks) ;; TODO: Why mu4e-bookmark-query does not work here? (setf (car bookmark) (concat (mapconcat (lambda (s) (format "NOT maildir:\"%s\" and " s)) ambrevar/mu4e-trash-folders "") (car bookmark)))) (defun ambrevar/message-github () "When replying to a github message, clean up all bogus recipients. This function could be useful in `mu4e-compose-mode-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 ((addr (message-fetch-field hdr)) recipients bogus-recipients clean-recipients) (when (stringp addr) (setq recipients (mail-extract-address-components addr t) bogus-recipients (message-bogus-recipient-p addr)) (when bogus-recipients (setq clean-recipients (seq-difference recipients bogus-recipients (lambda (addrcomp addr) (string= (cadr addrcomp) addr)))) ;; See `message-simplify-recipients'. (message-replace-header hdr (mapconcat (lambda (addrcomp) (if (and message-recipients-without-full-name (string-match (regexp-opt message-recipients-without-full-name) (cadr addrcomp))) (cadr addrcomp) (if (car addrcomp) (message-make-from (car addrcomp) (cadr addrcomp)) (cadr addrcomp)))) clean-recipients ", ")))))) (message-sort-headers) ;; 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 'mu4e-compose-mode-hook 'ambrevar/message-github) ;;; Org captures (when (require 'org-mu4e nil t) (require 'init-org) ; For org-agenda-files (add-to-list 'org-capture-templates `("t" "Mark e-mail in agenda" entry (file+headline ,(car org-agenda-files) "E-mails") "* TODO [#A] %?\nSCHEDULED: %(org-insert-time-stamp (org-read-date nil t \"+0d\"))\n%a\n")) ;; TODO: Don't duplicate contacts. (defun ambrevar/mu4e-contact-dwim () "Return a list of (NAME . ADDRESS). If point has an `email' property, move it to the front of the list. Addresses in `mu4e-user-mail-address-list' are skipped." (let ((result (ambrevar/message-fetch-addresses)) (message org-store-link-plist)) ;; Move contact at point to front. (let ((email-at-point (get-text-property (point) 'email)) (contacts result)) (when email-at-point (while contacts (if (not (string= (cadr (car contacts)) email-at-point)) (setq contacts (cdr contacts)) (setq result (delete (car contacts) result)) (push (car contacts) result) (setq contacts nil))))) result)) (defun ambrevar/org-contacts-template-name (&optional return-value) "Like `org-contacts-template-name' for mu4e." (or (car (car (ambrevar/mu4e-contact-dwim))) return-value "%^{Name}")) (defun ambrevar/org-contacts-template-email (&optional return-value) "Like `org-contacts-template-name' for mu4e." (or (cadr (car (ambrevar/mu4e-contact-dwim))) return-value (concat "%^{" org-contacts-email-property "}p"))) (add-to-list 'org-capture-templates `("c" "Add e-mail address to contacts" entry (file+headline ,(car org-contacts-files) "Contacts") "* %(ambrevar/org-contacts-template-name) :PROPERTIES: :EMAIL: %(ambrevar/org-contacts-template-email) :END:"))) (defun ambrevar/mu4e-kill-ring-save-message-id (&optional msg) "Save MSG's \"message-id\" field to the kill-ring. If MSG is nil, use message at point." (interactive) (kill-new (mu4e-message-field (or msg (mu4e-message-at-point)) :message-id))) (provide 'init-mu4e)