Expiring vs. editing articles in Gnus

I’ve been using Gnus running inside GNU Emacs for my main email and news reader for a few weeks now. It has been a rather pleasing ride so far, and I am only now beginning to appreciate the infinite configurability and customizability that a Lisp based mailer offers.

One of the first things I wanted to tweak was the default keys for expiring and editing articles. A little background information may be useful before we go into the details of how the default keys can be tweaked.

Gnus uses a very newsreaderly method of handling your email messages too. This means that, by default, no email messages will ever be deleted from your local email folders. This is an important detail, so it’s worth repeating: Gnus will never delete email messages. Unless you ask Gnus to delete them forever. The method of asking Gnus to delete a message forever is to “expire” the relevant message/article. An article can be expired in the summary buffer of a mail or news group by hitting “E“.

Some message groups support article editing too. The “nnml” type of message groups does, and it’s a very handy feature too. I often have to edit email headers in email messages that use the wrong charset, for example. The default key for editing an article in Gnus is “e“.

After a few weeks of using Gnus for my email, I realized that I type “E” to expire articles far more often than I type “e” to edit articles. When I say “far more often”, I mean really far more often, as in several hundred of times per day vs. maybe once or twice. This prompted me to look into the manual of Gnus for a simple way to swap the behavior of these two keys. Having to hold down “Shift-E” instead of merely pressing “e” tends to get a bit old after the first few thousands of times you have reached for the shift modifier key.

The solution was, as is very commonly the case with the excellent manual of Gnus, already there. It was just waiting for me to discover it, in section 3.7.5 (Generic Marking Commands):

3.7.5 Generic Marking Commands

Some people would like the command that ticks an article (“!“) go to the next article. Others would like it to go to the next unread article. Yet others would like it to stay on the current article. And even though I haven’t heard of anybody wanting it to go to the previous (unread) article, I’m sure there are people that want that as well.

Multiply these five behaviors with five different marking commands, and you get a potentially complex set of variable to control what each command should do.

To sidestep that mess, Gnus provides commands that do all these different things. They can be found on the “M M” map in the summary buffer. Type “M M C-h” to see them all—there are too many of them to list in this manual.

While you can use these commands directly, most users would prefer altering the summary mode keymap. For instance, if you would like the “!” command to go to the next article instead of the next unread article, you could say something like:

(add-hook 'gnus-summary-mode-hook 'my-alter-summary-map)
(defun my-alter-summary-map ()
  (local-set-key "!" 'gnus-summary-put-mark-as-ticked-next))


(defun my-alter-summary-map ()
  (local-set-key "!" "MM!n"))

That’s it. The sample gnus-summary-mode-hook was all I needed to see. Then I added in my own personal ~/.gnus startup file my own hook:

;; Swap the default behavior of the 'e' and 'E' keys in the group summary
;; buffers.  Using a shifted key to expire articles is somewhat painful.
;; I expire articles far too often, but I only very occasionally edit the
;; contents of email articles.  So it should be easier to expire them.
(add-hook 'gnus-summary-mode-hook 'keramida-alter-summary-map)
(add-hook 'gnus-article-prepare-hook 'keramida-alter-summary-map)
(defun keramida-alter-summary-map ()
  (local-set-key "e" "MMen")
  (local-set-key "E" 'gnus-summary-edit-article))

To apply the changes I didn’t even have to restart Emacs or Gnus. I evaluated the expressions right there, inside my ~/.gnus buffer, by typing “C-x C-e“, and that’s all. They were instantly part of my running Emacs and Gnus session.

Now I can expire articles by simply hitting “e“, and for the rare occasion that I have to manually edit an article’s headers or body, “E” is always there too :-)

Yay for the astonishing configurability of Gnus!

Update (06:38am): I didn’t really like the mapping of “e” to “MMen” very much, because it means the behavior of the “e” depends on whatever happens to be mapped to that key combination. After reading the source of Gnus a bit, I rewrote the hook function to look like this:

(defun keramida-alter-summary-map ()
  (local-set-key "e"
    (lambda (n)
      (interactive "p")
      (with-current-buffer gnus-summary-buffer
        (gnus-summary-put-mark-as-expirable-next n))))
  (local-set-key "E" 'gnus-summary-edit-article))

This works equally well in gnus-summary-mode and gnus-article-mode. I am a bit unsure about the (interactive) property of an anonymous lambda function, so I’ve posted a question to the gnu.emacs.gnus newsgroup, asking if this is considered bad style. Let’s see what other, more experienced Gnusers have to say…

2 thoughts on “Expiring vs. editing articles in Gnus

  1. adamo

    I’ve been meaning to switch to Gnus for years now but never really got around to switching. Thanks for the write up!

  2. keramida Post author

    I was a bit afraid to switch to Gnus for email, but I have been using it for news for ages. It’s practically the only news reader I’ve used for more than a few weeks ever, with the exception, perhaps, of tin more than a decade ago :)

    I don’t know why I was scared of Gnus for email. I guess I was puzzled by the various ways of marking articles as ‘read’, ‘expired’, ‘dormant’, etc.

    As an experiment, I copied my ~/mail/ directory to a safe place, I stopped fetchmail, and started moving email from my ~/mail/whatever folders to Gnus folders. I used the nnml: backend for Gnus folders. It seems to be the most newsreaderly folder format supported by Gnus: each folder has an overview file, the active folders are listed in an active file, email folders open very fast because Gnus has minimal parsing to do (i.e. not as much as when plain UNIX “mbox” files are used as folders), and so on.

    The experiment seemed to have worked well, at least as far as the conversion of email folders was concerned. There is only one detail that may bite someone doing this, but it’s easy to avoid. When Gnus pulls email from UNIX “mbox” files into nnml: folders, some of the original marks are lost. Most importantly, many old messages which were marked as read may appear as unread/new. Having to re-read a few hundred thousand messages sucks! So the conversion of each folder has to be done in two passes:

    * First all old/read messages are pulled into a temporary “mbox” file. Gnus pulls messages from the “mbox” file called foo, into nnml:mail.foo. Then, all messages in nnml:mail.foo are explicitly marked as ‘read’.

    * Then all the remaining messages from the source mallbox are pulled into nnml:mail.foo. This time they are all shown as ‘unread’ by Gnus, but that’s exactly what we want.

    After having respooled all my old messages into nnml:mail.* folders, I had one last step to complete: email splitting. I have used procmail for ages, but there is one feature of nnml-split-fancy that absolutely rules! It supports logical operators of the form:

    (| (“header” “regexp” “mail.foo”)
    (& (“header2” “regexp2” “mail.bar”)
    (“header3” “regexp3” “mail.baz”))
    (“header4” “regexp4” “mail.frob”)

    The | logical operator is a ‘short-circuit OR’ operator, so it delivers a message only once and skips the rest of the filters of the same level. The & logical operator delivers to all the folders that match.

    This has proven to be a very smart way of writing mail splitting filters, even for mailing-list-like aliases. We have some of these at work, and I used to have snippets like the following in my ~/.procmailrc:

    :0 H
    * (^TO_|^From:(.*[^-a-zA-Z0-9_.])?)(.*@.*bytemobile.com)
    # First deliver a copy of all all email address directly
    # to me to a `misc’ folder, so it gets elevated
    # visibility/priority.

    :0 Hc
    * ^TO.*gkeramidas@bytemobile.com
    * !^Subject: PERFORCE change [0-9][0-9]* for review

    :0 afhw
    | formail -A”X-Delivered: YES”

    # For the rest of Bytemobile-specific messages, multiple
    # deliveries are supported, but email that is delivered
    # in one of the other folders is excluded from the main
    # =bytemobile.misc mailbox.

    :0 Hc
    * ^TO.*listaddress1@.*bytemobile.com

    :0 afhw
    | formail -A”X-Delivered: YES”

    # … more pairs of rules here, two for each list …

    * X-Delivered: YES

    :0 E

    Now all these rules have been replaced with the more succint set of nnmail-split-fancy rules:

    ;; Bytemobile email filters. By default a message is delivered at
    ;; least to “mail.bytemobile.misc”, with the exception of Perforce
    ;; commit emails. These are only delivered _once_.

    (any “bytemobile\\.com”

    (| (“subject” “PERFORCE change [0-9][0-9]* for review”

    ;; The rest of email messages are delivered to one or more
    ;; folders under the mail.bytemobile.* hierarchy. If the
    ;; message is addressed to me, then deliver at least one copy
    ;; to “mail.bytemobile.misc”, and optionally to one or more
    ;; other folders.

    (| (to “gkeramidas@bytemobile.com” “mail.bytemobile.misc”)
    (& (to “list1@.*bytemobile\\.com” “mail.bytemobile.list1”)
    (to “list2@.*bytemobile\\.com” “mail.bytemobile.list2”)
    (to “list3@.*bytemobile\\.com” “mail.bytemobile.list3”)
    (to “list4@.*bytemobile\\.com” “mail.bytemobile.list4”)
    (to “list5@.*bytemobile\\.com” “mail.bytemobile.list5”)

    ;; Fallback folder for all the rest of Bytemobile emails.

    For FreeBSD mailing lists, my original procmail filter rules contained:

    ## ==============================================================
    ## FreeBSD email filters.
    ## ==============================================================

    :0 H
    * Sender:.*@.*freebsd.org
    :0 H
    * ^Sender: owner-doc-committers@freebsd.org

    :0 H
    * ^Sender: owner-cvs-\/[^@]*

    :0 H
    * ^Sender: owner-freebsd-\/[^@]*

    :0 H
    * ^TO.*core@freebsd.org

    :0 H
    * ^(To|Cc|Bcc|From):[ ].*freebsd.org

    This has been rewritten to nnmail-split-fancy format now:

    ;; FreeBSD email filters. The `core’ mailbox is treated in a special
    ;; way, so that I won’t lose email posted there in the noise of a
    ;; mailing list.

    (any “freebsd\\.org”

    (| (& (to “core@freebsd\\.org” “mail.freebsd.core”)

    (“sender” “owner-doc-committers@freebsd\\.org” “mail.freebsd.cvs.doc”)
    (“sender” “owner-cvs-\\(\\w+\\)@.*freebsd\\.org” “mail.freebsd.cvs.\\1”)
    (“sender” “owner-freebsd-\\(\\w+\\)@.*freebsd\\.org” “mail.freebsd.\\1”))

    ;; Fallback folder for all the other FreeBSD messages.

    Rewriting the filter which catches all mailing lists was the most challenging part. Now that it’s done I find the nnmail-split-fancy rules far more readable, but that may be just me and my very-well-known fancy about Lisp.

Comments are closed.