Mu4e customization
This files holds all my customization made to the mu4e mail client.
Initialization
Autoload
mu4e
autoload is desactivated, thus we have to deal with it
ourselves.
(autoload 'mu4e "/usr/share/emacs/site-lisp/mu4e/mu4e.elc" "Start mu4e daemon and show its main window." t)
Then, for that magic to works, I associate the mu4e
function to a
keybinding, which will load it at first press.
(global-set-key (kbd "<f5>") #'mu4e)
Finally, I need to load the rest of the present file after mu4e
has
been loaded.
(with-eval-after-load 'mu4e ;; Only if mu4e.elc has not been already loaded (unless (fboundp 'ed/compose-new-mail) (let ((gc-cons-threshold most-positive-fixnum)) (load-file (expand-file-name "mu4e.elc" user-emacs-directory)))))
Set Up variables
First, I set common variables. I made the choice to use an external
call to w3m
to render html email, instead of the default shr
, as I
find the result more usable.
(setq mu4e-attachment-dir "~/Téléchargements" mu4e-compose-complete-only-after "2012-01-01" mu4e-compose-context-policy 'ask mu4e-compose-crypto-reply-policy 'sign-and-encrypt mu4e-compose-dont-reply-to-self t mu4e-compose-reply-to-address nil ;; will be configured later by context mu4e-compose-signature nil ;; will be configured later by context mu4e-confirm-quit nil mu4e-context-policy 'pick-first mu4e-drafts-folder nil ;; will be configured later by context mu4e-get-mail-command "offlineimap -o -u quiet" mu4e-headers-date-format "%d/%m/%Y" mu4e-headers-include-related t mu4e-headers-skip-duplicates t mu4e-headers-time-format "%H:%M" mu4e-hide-index-messages t ;;mu4e-html2text-command "html2text -utf8 -nobs -width 72" mu4e-html2text-command "w3m -dump -T text/html -cols 72 -o display_link_number=true -o auto_image=false -o display_image=false -o ignore_null_img_alt=true" mu4e-split-view 'horizontal mu4e-trash-folder nil ;; will be configured later by context mu4e-update-interval 600 ;; in seconds or nil to desactivate mu4e-view-show-addresses t)
Some variables belong to the underlaying packages mail
and message
and have an effect on mu4e
behavior.
(setq message-citation-line-format "%a %d %b %Y à %R, %n a écrit :\n" message-citation-line-function 'message-insert-formatted-citation-line message-kill-buffer-on-exit t message-send-mail-function 'smtpmail-send-it mail-user-agent 'mu4e-user-agent)
Passwords for the various SMTP servers are stored in a shared
~/.authinfo.gpg
file.
(setq smtpmail-auth-credentials (expand-file-name "~/.authinfo.gpg"))
I also set my crypto choices via the following variables:
(setq mml-secure-openpgp-sign-with-sender t
mml-secure-openpgp-encrypt-to-self t)
Configure Flyspell to avoid mail headers
(defun ed/flyspell-skip-mail-headers (begin _end _ignored) "Returns non-nil if BEGIN position is in mail header." (save-excursion (goto-char (point-min)) (let ((end-header (re-search-forward "^--text follows this line--[[:space:]]*$" nil t))) (when end-header (< begin end-header))))) (with-eval-after-load 'flyspell (add-hook 'flyspell-incorrect-hook #'ed/flyspell-skip-mail-headers))
Various utility functions
This section defines functions, which helps me in my email work flow.
Preview mail file in a new buffer
The first one may be called externally to display an email content when we know its file path.
(defun ed/preview-some-mail-at (path) (interactive "fPath: ") (call-process "mu" nil (switch-to-buffer (generate-new-buffer "*mail preview*") t) t "view" (expand-file-name path)) (with-current-buffer "*mail preview*" (goto-char (point-min)) (mu4e~fontify-cited) (mu4e~fontify-signature) (while (re-search-forward "^\\(\\w+:\\) \\(.*\\)$" nil t) (let ((key (match-string 1)) (value (match-string 2))) (beginning-of-line) (delete-region (point) (line-end-position)) (insert (concat (propertize key 'face 'mu4e-header-key-face) " ")) (if (or (string= key "From:") (string= key "To:")) (insert (propertize value 'face 'mu4e-special-header-value-face)) (insert (propertize value 'face 'mu4e-header-value-face))))) (forward-line) (beginning-of-line) (insert "\n") (read-only-mode) (local-set-key (kbd "q") #'kill-this-buffer)))
Display current mail in browser
The second function is called as a mu4e Action, to display the current email body in your external browser. This is useful when event W3M fails at rendering an HTML soup.
(defun ed/mu4e-msgv-action-view-in-browser (msg) "View the body of the message in a web browser." (interactive) (let ((html (mu4e-msg-field (mu4e-message-at-point t) :body-html)) (tmpfile (format "%s/%d.html" temporary-file-directory (random)))) (unless html (error "No html part for this message")) (with-temp-file tmpfile (insert "<html>" "<head><meta http-equiv=\"content-type\"" "content=\"text/html;charset=UTF-8\">" html)) (browse-url (concat "file://" tmpfile)))) (add-to-list 'mu4e-view-actions '("View in browser" . ed/mu4e-msgv-action-view-in-browser) t)
Notify current mail as a spam
The following function is called as a mu4e Action, to send it to the Signal Spam french service. If everything goes fine, then the message is marked for deletion.
(defcustom ed/signalspam-login nil "Login used to connect to the Signal Spam API." :type 'string :group 'mu4e) (defcustom ed/signalspam-pass nil "Password used to connect to the Signal Spam API." :type 'string :group 'mu4e) (defun ed/send-message-to-signal-spam (msg) (if (or (not ed/signalspam-login) (not ed/signalspam-pass) (string= ed/signalspam-login "") (string= ed/signalspam-pass "")) (message "Please add your signal-spam login and password") (let ((path (or (mu4e-message-field msg :path) ""))) (if (or (string= path "") (not (file-readable-p path))) (message "Mail file `%s' is not accessible" path) (message (replace-regexp-in-string "[ \t\n]*$" "" (shell-command-to-string (format "/usr/bin/python3 %s \"%s\" \"%s\" \"%s\"" (expand-file-name "signalspam.py" package-user-dir) ed/signalspam-login ed/signalspam-pass path))))) (when (eq major-mode 'mu4e-headers-mode) ;; in any case, mark it as deleted (mu4e-mark-set 'delete msg) (mu4e-headers-next))))) (add-to-list 'mu4e-headers-actions '("Signal spam" . ed/send-message-to-signal-spam) t) (add-to-list 'mu4e-view-actions '("Signal spam" . ed/send-message-to-signal-spam) t)
The previous function needs the following python code to run.
#!/usr/bin/env python3 import os import sys import requests from base64 import b64encode def submit_to_signal_spam(username, password, mail_path): if not os.path.isfile(mail_path): print("Mail non trouvé :/") return False mail_content = b"" with open(mail_path, "rb") as f: mail_content = f.read() requests.post( "https://www.signal-spam.fr/api/signaler", auth=(username, password), data={"message": b64encode(mail_content)}) print("Signalement ok") return True if __name__ == "__main__": if len(sys.argv) != 4: print("Usage: {} LOGIN PASS MSG_PATH".format(sys.argv[0])) sys.exit(1) if submit_to_signal_spam(sys.argv[1], sys.argv[2], sys.argv[3]): sys.exit() sys.exit(1)
You also need to set the two variables ed/signalspam-login
and
ed/signalspam-pass
to their correct values (the login and password of
your Signal Spam account).
Extract a mail header by name for a given path
The first one extract one email header given by name and return its
value as a string. It expects the sed
command to be available on your
system.
(defun ed/get-mail-header (header-name path) (replace-regexp-in-string "[ \t\n]*$" "" (shell-command-to-string (concat "sed -n '/^" header-name ":/I{:loop t;h;n;/^ /{H;x;s/\\n//;t loop};x;p}' '" path "' | sed -n 's/^" header-name ": \\(.*\\)$/\\1/Ip'"))))
On example of use of the previous function is the following, which extracts the email client name of your correspondant.
(defun ed/get-origin-mail-system-header (msg) (let ((path (or (mu4e-message-field msg :path) ""))) (if (or (string= path "") (not (file-readable-p path))) "no path found" (let ((xmailer (ed/get-mail-header "x-mailer" path)) (useragent (ed/get-mail-header "user-agent" path))) (if (string= xmailer useragent) xmailer (cond ((string= xmailer "") useragent) ((string= useragent "") xmailer) (t (concat xmailer " (xmailer)\n" useragent " (user-agent)"))))))))
GPG related functions
All the following functions interact with an email headers to expose or find out more about cryptography settings in use.
(defun ed/get-openpgp-header (msg) (let ((path (or (mu4e-message-field msg :path) ""))) (if (or (string= path "") (not (file-readable-p path))) "Mail file is not accessible" (ed/get-mail-header "openpgp" path)))) (defcustom ed/gpg-pub-keys '() "List of GnuPG public keys used to sign or encrypt my email." :type '(alist :key-type (string :tag "mu4e context name") :value-type (string :tag "gpg mail header content")) :group 'mu4e-crypto) (defun ed/insert-gpg-headers (sign-or-encrypt) (save-excursion (goto-char (point-min)) (let ((pgp-info (cdr (assoc (mu4e-context-name (mu4e-context-current)) ed/gpg-pub-keys)))) (when pgp-info (insert "Openpgp: " pgp-info) (if (string= sign-or-encrypt "encrypt") (mml-secure-message-sign-encrypt) (mml-secure-message-sign)))))) (defun ed/sign-this-message () "Insert mml gpg command and gnupg header" (interactive) (ed/insert-gpg-headers "sign")) (defun ed/encrypt-this-message () "Insert mml gpg command and gnupg header" (interactive) (ed/insert-gpg-headers "encrypt"))
The last one is responsible to decypher old fashioned inline encyphered emails.
(defun ed/decrypt-inline-pgp () "Decrypt a PGP MESSAGE block in the current buffer." (interactive) (save-excursion (let* ((pm (point-max)) (beg (progn (re-search-forward "^-----BEGIN PGP MESSAGE-----$" pm t 1) (match-beginning 0))) (end (re-search-forward "^-----END PGP MESSAGE-----$" pm t 1))) (if (and beg end) (epa-decrypt-region beg end) (message "No encrypted region found."))))) (add-to-list 'mu4e-view-actions '("Decrypt inline PGP" . ed/decrypt-inline-pgp) t)
Get the signature for a given email address
This function returns a signature string (or an empty one) if the given address match one of the signature paths configuration.
(defun ed/get-signature-for (email-addr) "Return the right signature for the given EMAIL-ADDR. The signatures must be stored in a list named `ed/signature-paths-list'." (let ((sig-path (cdr (assoc email-addr ed/signature-paths-list)))) (unless sig-path (setq sig-path ed/default-signature-path)) (with-temp-buffer (insert-file-contents sig-path) (buffer-string))))
Here is an exemple of ed/signature-paths-list
list:
Open compose window from an external call
These functions allow me to open the compose window, with recipients and
subject extracted from the given mailto:
string.
(defun ed/parse-mailto-string (mailto-string) (let ((data (split-string mailto-string "[ :?&=]")) to is-subject subject) (setq to (pop data)) (when (string= to "mailto") (setq to (pop data))) (dolist (item data) (cond ((string= "subject" item) (setq is-subject t)) (is-subject (setq is-subject nil) (setq subject item)))) (list to subject))) (defun ed/quick-mu4e-pong-handler (data) "Handle 'pong' responses from the mu server." (setq mu4e~server-props (plist-get data :props)) ;; save info from the server (let ((doccount (plist-get mu4e~server-props :doccount))) (mu4e~check-requirements))) (defun ed/quick-mu4e-start () "Quickly start mu4e, avoiding as trouble as possible." ;; Load a context as soon as possible to avoid error messages about ;; missing folders (mu4e~context-autoswitch nil mu4e-context-policy) (setq mu4e-pong-func #'(lambda (info) (ed/quick-mu4e-pong-handler info))) (mu4e~proc-ping)) (defun ed/compose-new-mail (mailto-string) "Compose a new mail with metadata extracted from MAILTO-STRING." (ed/quick-mu4e-start) (let* ((mailto (ed/parse-mailto-string mailto-string)) (to (car mailto)) (subject (cadr mailto))) (mu4e~compose-mail to subject)))
The following function setup Emacs as a possible email client.
(defun ed/setup-emacs-mailto-target-desktop-file () "Write xdg desktop file allowing emacs to be used as mailto target." (interactive) (with-temp-file (expand-file-name "~/.local/share/applications/emacsmail.desktop") (insert "[Desktop Entry] Name=Compose message in Emacs GenericName=Compose a new message with Mu4e in Emacs Comment=Open mu4e compose window MimeType=x-scheme-handler/mailto; Exec=emacs -l /usr/share/emacs/site-lisp/mu4e/mu4e.elc --eval '(ed/compose-new-mail \"%u\")' Icon=emacs Type=Application Terminal=false Categories=Network;Email; StartupWMClass=Emacs ")) (with-temp-buffer (call-process "update-desktop-database" nil (current-buffer) nil (expand-file-name "~/.local/share/applications")) (buffer-string)))
Customize mail list view columns and message view headers
I try to reproduce as much as possible the vintage Alpine mail client look and feel.
(add-to-list 'mu4e-header-info-custom '(:acctshortname . (:name "Account short name" :shortname "Acct" :help "3 first letter of related root maildir" :function (lambda (msg) (let ((account-name (or (mu4e-message-field msg :maildir) ""))) (if (string= account-name "") "" (substring (replace-regexp-in-string "^/\\(\\w+\\)/.*$" "\\1" account-name) 0 3))))))) (add-to-list 'mu4e-header-info-custom '(:foldername . (:name "Folder information" :shortname "Folder" :help "Message short storage information" :function (lambda (msg) (let ((shortaccount) (maildir (or (mu4e-message-field msg :maildir) "")) (mailinglist (or (mu4e-message-field msg :mailing-list) ""))) (if (not (string= mailinglist "")) (setq mailinglist (mu4e-get-mailing-list-shortname mailinglist))) (when (not (string= maildir "")) (setq shortaccount (substring (replace-regexp-in-string "^/\\(\\w+\\)/.*$" "\\1" maildir) 0 3)) (setq maildir (replace-regexp-in-string ".*/\\([^/]+\\)$" "\\1" maildir)) (if (> (length maildir) 8) (setq maildir (concat (substring maildir 0 7) "…"))) (setq maildir (concat "[" shortaccount "]" maildir))) (cond ((and (string= maildir "") (not (string= mailinglist ""))) mailinglist) ((and (not (string= maildir "")) (string= mailinglist "")) maildir) ((and (not (string= maildir "")) (not (string= mailinglist ""))) (concat maildir " (" mailinglist ")")) (t ""))))))) (add-to-list 'mu4e-header-info-custom '(:useragent . (:name "User-Agent" :shortname "UserAgt." :help "Mail client used by correspondant" :function ed/get-origin-mail-system-header))) (add-to-list 'mu4e-header-info-custom '(:openpgp . (:name "PGP Info" :shortname "PGP" :help "OpenPGP information found in mail header" :function ed/get-openpgp-header))) (setq mu4e-view-fields '(:from :to :cc :subject :flags :date :maildir :mailing-list :tags :useragent :attachments :openpgp :signature :decryption) mu4e-headers-fields '((:flags . 5) (:human-date . 12) ;(:acctshortname . 4) (:foldername . 25) (:from-or-to . 25) ;(:size . 6) (:subject . nil)) mu4e-compose-hidden-headers '("^Face:" "^X-Face:" "^Openpgp:" "^X-Draft-From:" "^X-Mailer:" "^User-agent:"))
Something relatively ugly is the visible trailing whitespaces. We can locally remove it with this.
(add-hook 'mu4e-headers-mode-hook
#'(lambda () (setq-local show-trailing-whitespace nil)))
Add some magic to the default behaviors
Fix message width to 72 chars and expose mu4e in the mail headers when the compose window is opened. Also add local keybinding for GPG related operations (see ). Start flyspell mode in the compose window too.
(add-hook 'mu4e-compose-mode-hook #'(lambda () (set-fill-column 72) (save-excursion (save-restriction (message-narrow-to-headers) (message-add-header (concat "X-Mailer: mu4e " mu4e-mu-version "; emacs " emacs-version "\n")))) (run-with-idle-timer 2 nil #'(lambda () (let ((gc-cons-threshold most-positive-fixnum)) ;(flyspell-mode) (flycheck-mode)))))) (add-hook 'mu4e-view-mode-hook #'mu4e-view-fill-long-lines) (define-key mu4e-compose-mode-map (kbd "C-c <return> C-s") #'ed/sign-this-message) (define-key mu4e-compose-mode-map (kbd "C-c <return> C-e") #'ed/encrypt-this-message)
Automatically add GPG headers if necessary when answering an email (for exemple to sign-encrypt an answer if the original message was encyphered).
;; Waiting for mu4e to be shipped with this (add-hook 'mu4e-compose-mode-hook #'(lambda () (let ((msg mu4e-compose-parent-message)) (when msg (cond ((member 'encrypted (mu4e-message-field msg :flags)) (ed/encrypt-this-message)) ((member 'signed (mu4e-message-field msg :flags)) (ed/sign-this-message))))))) (add-hook 'mu4e-compose-mode-hook #'epa-mail-mode) (add-hook 'mu4e-view-mode-hook #'epa-mail-mode)
I don't like the trash system. So I just mark messages for deletion and remap keys to expose this feature as much as possible.
(define-key mu4e-headers-mode-map (kbd "<backspace>") #'mu4e-headers-mark-for-delete) (define-key mu4e-headers-mode-map (kbd "d") #'mu4e-headers-mark-for-delete) (define-key mu4e-headers-mode-map (kbd "D") #'mu4e-headers-mark-for-trash)
Announce
Now time to announce that my own customizations are ready.
(provide 'ed/mu4e)