Tweaking email contact completion in mu4e

The excellent emacs email client mu4e has very good completion of email contacts, which are ranked in a smart way. I have made a few tweaks to this to better suit my needs which I’ll describe below. In a nutshell, they let me

  • add favourites to the start of the completion list
  • offer a list of contacts as soon as I compose an email
  • insert a contact anywhere

For my favourite contacts, I create a text file with contacts in the form

First Last <>

I then create a list of contacts by appending the sorted list of mu4e contacts to this list of favourites and removing duplicates. This forms the final contact list that is offered for completion.

The favourite list used to be more useful before the sorting of contacts in mu4e was improved in the last release. It still has one nice use though, which is that I can specify the name I want to appear for each contact, which otherwise mu4e takes from the message header. This is useful since my email templates make use of this name to address the email, but sometimes these are unhelpful. For example “B Maughan <>” in the default mu4e contact list would cause my template to start “Hi B,” but if I put “Ben Maughan <>” in my favourites file, this is the one that will end up in the TO field and so I will get a nice “Hi Ben,” in my template.

I wrap this in a function using the ivy completion library, based on an example by Jon Kitchin that used helm, and bind it to S-TAB so that when I hit SHIFT and TAB in the TO field of an email I get this tweaked version of the address completion.

;;need this for hash access
(require 'subr-x)

;;my favourite contacts - these will be put at front of list
(setq bjm/contact-file "/full/path/to/fave-contacts.txt")

(defun bjm/read-contact-list ()
  "Return a list of email addresses"
    (insert-file-contents bjm/contact-file)
    (split-string (buffer-string) "\n" t)))

;;ivy contact completion
;;based on
(defun bjm/ivy-select-and-insert-contact (&optional start)
  ;;make sure mu4e contacts list is updated - I was having
  ;;intermittent problems that this was empty but couldn't see why
  (let ((mail-abbrev-mode-regexp mu4e~compose-address-fields-regexp)
        (eoh ;; end-of-headers
           (goto-char (point-min))
           (search-forward-regexp mail-header-separator nil t)))
        ;;append full sorted contacts list to favourites and delete duplicates
         (delq nil (delete-dups (append (bjm/read-contact-list) (mu4e~sort-contacts-for-completion (hash-table-keys mu4e~contacts)))))))
    (when (and eoh (> eoh (point)) (mail-abbrev-in-expansion-header-p))
      (let* ((end (point))
              (or start
                    (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
                    (goto-char (match-end 0))
              (ivy-read "Contact: "
                        :re-builder #'ivy--regex
                        :sort nil
                        :initial-input (buffer-substring-no-properties start end))))
        (unless (equal contact "")
          (kill-region start end)
          (insert contact))))))

;;bind it
(define-key mu4e-compose-mode-map (kbd "<S-tab>") 'bjm/ivy-select-and-insert-contact)

Next I add this function to the hook that runs when I compose an email in mu4e, which launches me straight into address completion:

;;launch automatically
(add-hook 'mu4e-compose-mode-hook 'bjm/ivy-select-and-insert-contact)

Finally, here is a function that lets you insert a contact from your list anywhere in your email (not just the address fields). It will also work in any other buffer once you have started mu4e for the first time to initialise the contacts list.

;;ivy contacts for use anywhere
;;based on
(defun bjm/ivy-select-and-insert-contact-anywhere ()
  (let (contacts-list contact)
    ;;append full sorted contacts list to favourites and delete duplicates
    (setq contacts-list
          (delq nil (delete-dups (append (bjm/read-contact-list) (mu4e~sort-contacts-for-completion (hash-table-keys mu4e~contacts))))))
    (setq contact
          (ivy-read "Contact: "
                    :re-builder #'ivy--regex
                    :sort nil))
        (unless (equal contact "")
          (insert contact))))