hoowl

Physicist, Emacser, Digitales Spielkind

Emacs: Salutation auto-complete for mail
Published on Jul 25, 2022.

I use email quite extensively in my every-day communication, at least at work. The style is usually half-formal, with a salutation but on a first-name basis as it is customary in Sweden. Since quite a number of the mails I write or reply to are to first-time contacts, I am even more nervous than usual to misspell someone’s name and always feel the need to double- or even tripple-check for typos. So, why not automatize this step, extract the recipients’ names with some Elisp code and provide salutation completion with a single keystroke?

The result will look something like this:

"Animation demonstrating the salutation completion using yassnippet and some made-up email addresses"

This builds on the power of the yas package in Emacs which I already mentioned in a previous post for auto-inserting file templates. Using the lisp function below (inspired by this blog post), we can extract the first name of the recipients of a mail and add them to a yasnippet template to create a personalized greeting for any number of people the mail is addressed to.

The code parses both the TO field of the email as well as the quoted FROM field (“XYZ writes:”) to determine the first names of all recipients. The latter field is only used in case the TO field only contains an email address and matches that of the quoted FROM field. That makes sure that forwarding mails does not mess up the greeting.

In any case, should we only have an email address for the recipient, the first name is guessed using the first (period-separated) part of the address.

This is the code:

 1: (defun hanno/msg-get-names-for-yasnippet ()
 2:   "Return comma separated string of recipients' names for an email.
 3: 
 4: Uses information from both the TO field and the quoted FROM field.
 5: Guesses first name from the (period-separated) email address if
 6: no name string is known."
 7:   (interactive)
 8:   (let (email-name str email-string email-list email-ff email-name-ff tmpname)
 9:     (save-excursion
10:       (goto-char (point-min))
11:       ;; first line in email could be some hidden line containing NO to field
12:       (setq str (buffer-substring-no-properties (point-min) (point-max))))
13:     ;; take name from TO field - match series of names
14:     (when (string-match "^To: \\(.+\\)" str)
15:       (setq email-string (match-string 1 str)))
16:     ;; split to list by comma
17:     (setq email-list
18:       (split-string
19:        ;; flip "last, first" name combos
20:        (replace-regexp-in-string
21:         "\"\\(.*\\),\\(.*\\)\"" "\\2 \\1"
22:         email-string) " *, *"))
23:     ;; extract the name in the FROM field ("XYZ writes:");
24:     ;; sometimes this field contains more info than the TO field
25:     (when (string-match "^\\([^ ,\n]+\\).+writes:$" str)
26:       (setq email-name-ff (match-string 1 str)))
27:     ;; extract the email in the FROM field ("XYZ writes:")
28:     (when (string-match "^.+<\\([^ ,\n]+\\)> writes:$" str)
29:       (setq email-ff (match-string 1 str)))
30:     ;; loop over emails in TO field
31:     (dolist (tmpstr email-list)
32:       ;; get first word of email string
33:       (setq tmpname (car (split-string tmpstr " " 't " ")))
34:       ;; remove whitespace or "" or '<>'
35:       (setq tmpname (replace-regexp-in-string "[ \"<>]" "" tmpname))
36:       ;; only got mail address?
37:       (when (string-match "@" tmpname)
38:         ;; check if it matches FROM field
39:         ;; and that a name was found there
40:         (if (and
41:              email-ff
42:              (string-match email-ff tmpname)
43:              email-name-ff)
44:             ;; replace with name from FROM field
45:             (setq tmpname email-name-ff)
46:           ;; otherwise, extract (first) name from mail address
47:           (progn
48:             (setq tmpname (car (split-string tmpname "@")))
49:             (setq tmpname (car (split-string tmpname "\\."))))))
50:       ;; add to list
51:       (setq email-name (cons (capitalize tmpname) email-name)))
52:     ;; convert to comma-separated string
53:     (mapconcat 'identity (nreverse email-name) ", ")))

I am using mu4e for managing my mail, but this code should work for any composition buffer that builds upon message-mode.

Running yas-new-snippet, create a new snippet for message-mode with the following contents:

# -*- mode: snippet -*-
# name: hej name
# key: hej
# --
Hej ${1:`(mapconcat 'identity (split-string (hanno/msg-get-names-for-yasnippet) ", " t) ", hej ")`},

$0

Mvh,

Hanno

Once active, you only need to write “hej” (the usual informal salutation in Swedish) and press TAB to complete the names. The mapconcat in the snippet makes sure that each person is greeted individually.

This has been working very well for me the past year, and I only tuned the code slightly to better deal with some typical challenges I encounter in my mails. Of course, you can easily extend this to other snippets using different salutation forms or even extract last names instead for first names as above. Let me know should you take this in interesting new directions! :)

Tags: emacs, mail, lisp