Emacs and Claws Mail

I promised to write about the way I use Emacs as the external editor for composing emails in Claws. I hope to fulfill that promise by this post.

The major mode

I defined a specialized Emacs major mode for the editing of emails. Almost all of the code is dedicated to quoting, i.e. copying and pasting chunks of the message I'm replying to, and prefixing them with either the plain > mark (I mostly do this in personal messages) or with the more elaborate Foo> mark when following up to Mrs. Foo's mailing list message or newsgroup post. Emacs veterans recognize the latter style of quoting as the one favored by Supercite and love it; others have no idea where it came from and loathe it. Separating the two groups is one of the reasons why I use it.

Why do I need my own mode rather than use Emacs' existing Mail mode? The environment provided by Claws when it throws you into the external editor is a bit different from the one provided by Emacs email packages: Gnus, RMAIL and so on. In particular, the Claws message buffer has no header and no magic string to separate the header from the body. This means I cannot guess the prefix from the header as Emacs likes to do.

On the other hand, when replying, Claws itself already quotes the parent message and places the quote in the buffer, which Emacs doesn't, at least not in the default configuration. Emacs instead just selects the text to be quoted in the buffer with the parent message, and the quoting and insertion happens as a result of further user interaction. So for Claws I needed code to identify the prefix from the "Mrs. Foo wrote:" part of the quote. And while I was at it I might as well select the quoted message text itself, just in case I want to paste it again at some other point in the reply. Here's some of the code for those tasks:

(defun claws-ext-find-citation-line ()
  (goto-char (point-min))
  (when (and (re-search-forward "^>" nil t)
             (looking-back "wrote:\\s *>"))
    (goto-char (match-beginning 0))
    (let ((p (save-excursion
               (beginning-of-line)
               (point))))
      (search-backward "," p 'move)
      (if (looking-at ", *")
          (goto-char (match-end 0))
        t))))

(defsubst claws-ext-get-attribution-from-citation ()
  (save-excursion
    (when (claws-ext-find-citation-line)
      (looking-at "\"?\\(\\w+\\(-\\w+\\)*\\)\\s +"))))

(defun claws-ext-set-region ()
  (when (save-excursion (claws-ext-find-citation-line))
    (claws-ext-find-citation-line)
    (forward-line 1)
    (re-search-forward "^\\s *\\S " nil 'move)
    (beginning-of-line)
    (push-mark (point) t 'activate)
    (if (re-search-forward "^\\([^>]\\|>\\S \\|$\\)" nil 'move)
        (goto-char (match-beginning 0)))))

The mode function itself is fairly simple thanks to the major mode inheritance mechanism:

(define-derived-mode claws-external-mode text-mode "Claws External"
  "Major mode for editing Claws mail messages.
Like Text Mode but with Auto Fill and Supercite loaded."
  (auto-fill-mode 1)
  (sc-minor-mode 1)
  (set (make-local-variable 'fill-paragraph-function)
       'claws-ext-fill-paragraph)
  (make-local-variable 'paragraph-separate)
  (make-local-variable 'paragraph-start)
  (make-local-variable 'adaptive-fill-regexp)
  (unless (boundp 'adaptive-fill-first-line-regexp)
    (setq adaptive-fill-first-line-regexp nil))
  (make-local-variable 'adaptive-fill-first-line-regexp)
  (let ((quote-prefix-regexp
         ;; User should change claws-ext-cite-prefix-regexp if
         ;; the Claws mail yank prefix is set to an abnormal value.
         (concat "\\(" claws-ext-cite-prefix-regexp "\\)[ \t]*")))
    (setq paragraph-start
          (concat
           (regexp-quote mail-header-separator) "$\\|"
           "[ \t]*$\\|"                        ; blank lines
           "-- $\\|"                   ; signature delimiter
           "---+$\\|"             ; delimiters for forwarded messages
           page-delimiter "$\\|"       ; spoiler warnings
           ".*wrote:$\\|"              ; attribution lines
           quote-prefix-regexp "$"     ; empty lines in quoted text
           ))
    (setq paragraph-separate paragraph-start)
    (setq adaptive-fill-regexp
          (concat quote-prefix-regexp "\\|" adaptive-fill-regexp))
    (setq adaptive-fill-first-line-regexp
          (concat quote-prefix-regexp "\\|"
                  adaptive-fill-first-line-regexp)))
  (make-local-variable 'auto-fill-inhibit-regexp)
  ;;(setq auto-fill-inhibit-regexp "^[A-Z][^: \n\t]+:")
  (setq auto-fill-inhibit-regexp nil)
  ;; try to set up Supercite preferred citation name
  (save-match-data
    (when (claws-ext-get-attribution-from-citation)
      (add-to-list
       'sc-attributions
       (cons "firstname" (match-string 1))))
    (claws-ext-set-region)))

Activating the mode

Emacs provides multiple mechanisms to set the major mode for new buffers automatically. For claws-external-mode I chose the local variables mechanism, which requires some machine-readable text to be present near the end of the buffer:

Local Variables:
mode:claws-external
End:

And how do I get this text into my Claws message buffers before Emacs tries to set the mode? I stuff it into my .signature file.

It may seem a little strange to do it this way, as it clutters my signature file with gibberish. But my signature is very short and to the point, so this doesn't really matter. And I have some good reasons not to use the other ways of setting the mode. Read on if you dare.

Running the Emacs client as external editor

Most of the time, I prefer to run just one instance of Emacs (it's the second program I start when I log in, after a terminal emulator) and connect to it with the emacsclient program. So, in principle, it should be emacsclient in the Claws external editor setting. But it turns out there is a time when Emacs is not running -- near the end of my session, and sometimes an email message comes in at that point and is urgent enough to need an immediate reply. Since I have Claws' "Automatically start external editor" option turned on, if emacsclient were indeed the external editor it would fail and complain about not finding an Emacs to talk to.

As is well known every computing problem can be solved by an additional shell script. Here's the one I've written (or, more likely, stolen)

#! /bin/sh

UID=`id -u`

if [ $# -eq 0 ] ; then
    set `mktemp`
fi

if [ -S "/tmp/emacs${UID}/server" ]; then
    emacsclient -c "$@" >/dev/null
else
    emacs "$@"
fi

I call this script "ee" (strangely, there's no commonly used program by that name), and it is the external editor I tell Claws about.

As you can see it just calls emacsclient when a master Emacs is available, otherwise it falls back on calling the full Emacs. This has some implications for Lisp code that runs from here -- such as setting the mode. I cannot rely on server-visit-hook, for one thing, because that won't apply in the fallback case. I could still try to use the auto-mode-alist mechanism based on matching the temporary file name, but since there seems to be no easy way to control that name from Claws, it would be brittle and easily broken by Claws internals changes. So this is why I chose the local variables way.

Comments !

You can send comments on this post by email. Click here to start composing a comment.