Change the Language of the Citation Line in Mu4e

TL;DR: In this blog post, I will describe Emacs Lisp functions that automatically change the language of the citation line in the email client mu4e, depending on the ispell dictionary language.

I use Emacs for several years now. Since a couple of months, I switched from Claws Mail to Mu4e. Don't get me wrong—Claws Mail is the best graphical e-mail client that I know. It was really easy to handle several accounts with one IMAP account and to have different settings for folders. However, since I spend most of my time in Emacs, I was getting uncomfortable writing e-mails with Claws Mail. I missed all my key bindings that burned in my muscle memory. I tried Gnus a couple of times, but it was quite hard to get a working setup.

I gave Mu4e a try; it was easy to get a working setup. There are a couple of blog posts that helped me a lot. Mu4e is just a Emacs front-end for mu. Mu itself is an e-mail maildir indexer and search engine. Combined with tools like Offlineimap and msmtp you will get a efficient and comfortable email setup.

The Problem

After a couple of months, I'm still satisfied with my choice. One thing, however, that bothered me was the citation line format. I write a lot of e-mails in German and English, but my citation line looked always the same, e.g.:

On Monday, Apr 07 2014, Firstname Lastname wrote:

The unique feature of Emacs is that you can change everything. The idea is that whenever I change the ispell language, the citation line should change the format, too. This is an indicator that I write in German or in English. So, let's moo this:

The Solution

The following animated gif shows the solution and the general idea:

The algorithm works in three simple steps: Firstly, we need to collect all the information to create the citation line. The function message-insert-formatted-citation-line takes the e-mail address from the sender and the date as an argument and inserts the citation line. These arguments are not available if we reply to a message. Therefore, we need to switch to the *mu4e-headers* buffer and save those values in variables.

Secondly, we create the old citation line and save it a variable. This is necessary, since we need to find the line where the old citation line is. The function with-temp-buffer creates a temporary buffer and switches to it. Then we can insert the old citation line and remove all newlines. Then we can save the content of the buffer to a variable with buffer-string.

Lastly, we need to find the old citation line and replace it with the new one. The old citation line can be found with search forward; after we have found it, we can delete the old citation line. Before we can insert the new citation line, we have to change the language. The variable system-time-locale controls the language of the format specifiers of message-citation-line-format. To switch easily between the different ispell languages and the citation lines I created the following variables:

(setq languages
     ;; dictionary . locale
      '(("deutsch" . "de_DE.utf8")
        ("english" . "en_GB.utf8")))

(setq de_DE.utf8-message-citation-line-format "Am %A den %d.%m.%Y, %N schrieb:\n")
(setq en_GB.utf8-message-citation-line-format "On %A, %b %d %Y, %N wrote:\n")

It is important that the different formats begin with name of the locale. Depending on the parameter locale, we then replace the citation line with new one. The source code of the above described function is in the following:

(defun fa/change-message-citation-line (locale)
  (interactive)
  ;; Switch to the buffer *mu4e-headers* and copy the date and the
  ;; sender into variables, since we need it for the citation line.
  (let ((original-date (with-current-buffer "*mu4e-headers*"
                         (mu4e-message-field-at-point :date)))
        (original-from (with-current-buffer "*mu4e-headers*"
                         (concat
                          (caar (mu4e-message-field-at-point :from))
                          " <" (cdar (mu4e-message-field-at-point :from)) ">"))))
    ;; Create the citation line in a temp buffer and save this line to
    ;; a string. This is necessary to find the excat citation line in
    ;; the email.
    (with-temp-buffer
      (message-insert-formatted-citation-line original-from original-date)
      (goto-char (point-min))
      (while (re-search-forward "\n" nil t)
        (replace-match ""))
      (buffer-string)
      (setq foo-citation-line (buffer-string))
      (kill-buffer))
    ;; switch back to the reply-buffer, search the citation line and
    ;; replace it with the new one.
    (save-excursion
      (goto-char (point-min))
      (search-forward foo-citation-line)
      (beginning-of-line)
      (kill-line)
      (setq system-time-locale locale)
      (setq message-citation-line-format
            (symbol-value
             (intern (concat locale
                             "-message-citation-line-format"))))
      (message-insert-formatted-citation-line original-from original-date)
      (previous-line)
      (delete-blank-lines))))

Before I decided that I would like to publish my code, I had a simple solution to circle between two languages. But since there are people out there, which speak and write more than two languages; I made my solution more general. To circle easily between the language variable, we need to treat the association list (alist) like a circular list. Therefore, I wrote the following helper functions:

(defun fa/reverse-nth (list element)
  "This function does the opposite of nth---it returns the number
  of an element in a list."
  (let ((i 0)
        (number-of-element nil))
    (while (and (not number-of-element) (<p i (length list)))
      (when (equal element (nth i list))
        (setq number-of-element i))
      (setq i (1+ i)))
    number-of-element))

(defun fa/assoc-next-element (list element)
    "This function searches for an element in a list and gives you
  the next element. If it is the last element, it returns the
  first, like a circular list."
    (let ((n (fa/reverse-nth list element)))
      (if (null n)
          nil
        (if (null (nth (1+ n) list))
            (nth 0 list)
          (nth (1+ n) list)))))

The function fa/reverse-nth does the opposite of nth. The function nth returns the Nth element of a list; fa/reverse-nth searches for an element in a list and returns N if it founds it. The next function fa/assoc-next-element makes use of that function and increases N to return the next element in an alist. If (nth (1+ n) list) returns nil, the function returns the first element. With both functions, we can cycle through an alist.

The next function changes the ispell language and if the current buffer is inmu4e-compose-mode it also changes the citation line. I have bound this function to the F6 Key. </p>

(defun fa/switch-dictionary ()
  (interactive)
  (let ((change (fa/assoc-next-element
                  languages (assoc ispell-current-dictionary languages))))
    (ispell-change-dictionary (car change))
    ;; Try to change citation line when the current buffer is
    ;; mu4e-compose buffer
    (if (eq 'mu4e-compose-mode (buffer-local-value 'major-mode (current-buffer)))
        (fa/change-message-citation-line (cdr change)))))

(global-set-key (kbd "<f6>") 'fa/switch-dictionary)

This should work as expected. However, most of my time I reply my emails in my mother language; that's why I would like to set German as my default format. Luckily, Mu4e provides hooks for that:

(add-hook 'mu4e-compose-pre-hook
          (defun my-do-compose-stuff ()
            "My settings for message composition"
            (setq system-time-locale "de_DE.utf8")
            (setq message-citation-line-format
                  (symbol-value
                   (intern (concat system-time-locale
                                   "-message-citation-line-format"))))
            (turn-on-orgstruct++)
            (flyspell-mode)))

That's all! Hopefully, this blog post showed you how easy it is to extend Emacs or Mu4e with some basic Emacs Lisp skills. The complete source code of the extension is available on this gist. Happy Hacking!

Creative Commons License