360 lines
15 KiB
EmacsLisp
360 lines
15 KiB
EmacsLisp
;;; 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: <tab> 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. (We want to keep it with mu4e-conversation.)
|
|
message-kill-buffer-on-exit nil
|
|
|
|
;; 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
|
|
|
|
;; 'sent is good for most providers. Gmail requires 'delete.
|
|
mu4e-sent-messages-behavior 'sent
|
|
|
|
;; 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
|
|
'("maildir:\".*inbox.*\" size:1M.." "Big inbox messages" ?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 (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 '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)
|
|
;; TODO: Replace by cl-loop.
|
|
(cl-delete-duplicates
|
|
(seq-remove
|
|
(lambda (contact) (member (cadr contact) mu4e-user-mail-address-list))
|
|
(seq-map (lambda (contact) (list (car contact) (and (cadr contact) (downcase (cadr contact)))))
|
|
(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))
|
|
|
|
;; Fix replying to GitHub.
|
|
(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
|
|
(defun ambrevar/org-mail-date (&optional msg)
|
|
(with-current-buffer (mu4e-get-headers-buffer)
|
|
(mu4e-message-field (or msg (mu4e-message-at-point)) :date)))
|
|
(add-to-list 'org-capture-templates
|
|
`("t" "Mark e-mail in agenda" entry (file+headline ,(car org-agenda-files) "E-mails")
|
|
"* %?\nSCHEDULED: %(org-insert-time-stamp (org-read-date nil t \"++7d\" nil (ambrevar/org-mail-date)))\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)))
|
|
|
|
(require 'patch-mu4e-account)
|
|
|
|
(load "~/personal/mail/mu4e.el" t)
|
|
|
|
(provide 'init-mu4e)
|