More Stupid Gnus Hacks

Table of Contents

1. More Stupid Gnus Hacks

Elsewhere on the site I’ve published some hacks to help make Gmail work a little more smoothly with Gnus. Here I’ll list a few things that I do to make Gnus work the way I wish.

Gnus, like Emacs itself, is highly flexible. It has never happened that I’ve asked myself, “Can I do x-y-z in Gnus?” and found the answer to be in the negative. I doubt there is any other mail or news reader as customizable as Gnus.

That comes at a price. Gnus is filled with obscure commands initiated by obscure keystroke sequences. Gnus is something of an adventure, an exploration, rather than something profoundly simple and intuitive.

Gnus out of the box needed tweaking to do things my way. If you’re an Emacs user and a Gnus user, you’re into tweaking, you practically live for it. So here goes. This is a work in progress and I expect to modify and expand upon it over time.

2. Mailing From Web Pages.

If you want Gnus to be invoked on a ’mailto’ link in a web page (in w3m or eww; we’re talking about within Emacs here), I do this.

(setq browse-url-mailto-function 'rjn-browse-url-mailto)

(defvar rjn-mail-addr)
(defvar rjn-mail-subject)
(defun rjn-browse-url-mailto (messy-mail-addr &optional whatever)
"Fix for url-browse and w3m mailto to work with gnus styles"
 (setq rjn-mail-addr (replace-regexp-in-string "mailto:" "" messy-mail-addr))
 ;; This still isn't quite right because it assumes the subject is
 ;; right after the mailto address.
 (if (string-match "?subject=.*" rjn-mail-addr)
      (setq rjn-mail-subject (match-string 0 rjn-mail-addr))
      (setq rjn-mail-subject (replace-regexp-in-string "?subject=" "" rjn-mail-subject))
;;; No subject.
    (setq rjn-mail-subject nil)
;;; Delete supposedly anything after the mailto address.
 (setq rjn-mail-addr (replace-regexp-in-string "?.*" "" rjn-mail-addr))
 (gnus-msg-mail rjn-mail-addr rjn-mail-subject)

This works for me more or less as you would expect, setting up an email that looks right.

3. Using Message Mode

I want to always use message mode. I do not want to use mail mode sometimes and message mode other times. The following code takes care of that.

;;; We're okay with being in message mode rather than mail mode.
(setq compose-mail-user-agent-warnings nil)

;;; Make 'mail' shortcut use gnus-group-mail. May not be necessary
;;; with the 'defalias' above?
(define-key global-map "\C-xm" 'gnus-msg-mail)

4. Forwarding Mail

Forwarding email in Gnus is a little odd. There is a key sequence, C-c C-f, which invokes gnus-summary-mail-forward, but it only works in the summary buffer. If you’re within an email (in Gnus terms everything is an ’article’), you can give the command M-x message-forward. (Don’t try that in a summary buffer, though! You’ll end up mailing the summary buffer, not the email in question.) But that’s inconsistent.

Actually, gnus-summary-mail-forward also works in the article buffer! It just isn’t tied to a nice keystroke by default. So why not use \C-c\C-f also?

Now, gnus-summary-mail-forward by default includes just about every last mail header and defaults to multipart. In general I didn’t want this. I want to forward a message in-line, with \C-c\C-f, with only a few select headers.

The code below does this, but creates its own problem. If you have a mulitpart message (with attachments) the attachments won’t be forwarded. You’ll have to use the M-2 prefix, that is M-2\C-c\C-f, which makes the whole message multipart (not just the attachments). I have to yet to figure out how to put the message inline and make the attachments a MIME part. Suggestions welcome.

;;; There is no forward key in the article buffer, so make one.
;;; Oddly gnus-summary-mail-forward seems to work correctly.
(define-key gnus-article-mode-map "\C-c\C-f" 'gnus-summary-mail-forward)

;;; Make summary forwarding behave a little better. This also makes
;;; message forwarding and summary forwarding consistent. But note that
;;; if you wish to forward an attachment you must use M-2\C-c\C-f.
(setq message-forward-as-mime nil)
(setq message-forward-show-mml nil)
(setq message-forward-included-headers "^Date\\|^From\\|^To\\|^Subject:")

The mime and mml options may not suit you, and you may want a different selection of headers, but all of that is easy to modify.

5. Wrapping Up Gnus on Emacs Exit

NOTE: I no longer do things this way. TO BE REVISED.

If we close Emacs without first closing Gnus, we may not get a server update if we’re using IMAP to fetch mail directly into Gnus. Here’s a possible fix; it works for me.

;; Add an exit hook. Is this a good idea? Mostly it is, as we
;; want to close gnus and update stuff every time, but once in a
;; while it causes a hang, when gnus itself is hosed.

(defun exit-gnus-on-exit ()
  (if (and (fboundp 'gnus-group-exit)
      (with-current-buffer (get-buffer "*Group*")
	(setq gnus-interactive-exit 'quiet)

And I like to do this to avoid prompting.

(setq gnus-expert-user t)

6. Catching Up in All Groups

I like to keep “caught up” on unread mail, and there’s often a lot of stuff I don’t read. In the group buffer you can easily mark each group as individually caught up with ’c’. But how about catching up all groups at once? This is how I do that.

(defun gnus-catchup-irrelevant ()
  "Catchup displayed groups."
  ;; Seems to work in group or topic mode.
  ;; Setting gnus-expert-user to t recommended.
  ;; If you have more than 100 groups displayed, rethink your
  ;; approach, or change the number below.
  (with-current-buffer "*Group*"
    (message "Clearing groups...")
    (goto-char (point-min))     
    (gnus-group-catchup-current 100)
    (message "...done.")))

(define-key gnus-group-mode-map "K" 'gnus-catchup-irrelevant)

Now, in the group buffer, ’K’ for ’katchup’ does a global catchup. (’C’ is already in use. Finding a free key in Gnus? Not so easy.)

7. Avoiding Slashdot Bans

Do you read Slashdot? I used to read it on the web but it became an unacceptable time sink, especially since I felt compelled to join in discussions. But courtesy is almost non-existent there, so I dropped out and recently (August 2019) picked it up again by RSS. I read selectively and don’t comment.

Slashdot is somewhat strict about limiting polling for new articles. The short version is that you can’t poll more than once every 30 minutes, and if you abuse this your IP address will be banned for at least 72 hours. If you’re reading RSS feeds in Gnus, which I do, it’s easy to go over that limit accidentally. What to do?

Almost anything is possible in Gnus, and I eventually figured out a means of limiting polling. The limit persists even across invocations on Emacs on the same machine (and across machines if you sync as I do). The following code does the job. (You may have to change the Slashdot URL if you use a different feed; they offer several.)

The method relies on advising the ’nnrss function. Advising often has side effects (not good ones) but so far, no problem. Please let me know if you try this and run into anything.

Note that I assume the ’session’ feature is in use; that’s likely to be true.

I added ’slashot-when so that if you wish you can find out how long until you can check Slashdot again, out of curiosity or addiction.

;;; Slashdot customizations.

;;; Make sure we don't get banned by Slashdot for fetching more than
;;; once per half hour. We set up a persistent variable to be saved by
;;; the 'session' feature. This variable contains the last time /. was
;;; fetched and will be carried between sessions and across machines.
;;; We advise nnrss-fetch to check if 30 minutes (actually a bit more
;;; to be safe) has elapsed. If so, we fetch update the timestamp
;;; variable and allow the fetch. If not, we warn and return nil.

(defun rjn-gnus-dont-check-slashdot-too-often (orig-fun url &rest args)
  (if (string= url "")
      (if (> (float-time (time-subtract (current-time)
	       rjn-gnus-slashdot-last-fetched-time )) 1830 )
	      (setq rjn-gnus-slashdot-last-fetched-time (current-time)) 
 	      (apply orig-fun url args))
	      (message "Too soon to fetch Slashdot again")
       (apply orig-fun url args))

(advice-add 'nnrss-fetch :around #'rjn-gnus-dont-check-slashdot-too-often)
(add-to-list 'savehist-additional-variables

;;; In case I get curious about when I can refresh Slashdot.

(defun slashdot-when ()
  "How long until I can check Slashdot again"
  (let* ((diff (- 1830
	   (float-time (time-subtract (current-time)
	      rjn-gnus-slashdot-last-fetched-time )))))
    (message (number-to-string diff))
    (if (< diff 0)
      (message "You can check Slashdot now")
      (message (concat "You can check Slashdot in about "
        (number-to-string (+ 1 (truncate (/ diff 60)))) " minutes")))))
;;; Needed on very first run; and sometimes the variable gets lost -?-
(unless (boundp 'rjn-gnus-slashdot-last-fetched-time)
  (setq rjn-gnus-slashdot-last-fetched-time 0))

8. Adaptive Language Input

I correspond in a number of languages other than English, especially in French and German and even a little Hawaiian once in a while.

Emacs provides a wonderful feature, input mode, which allows for much easier input of accents and the like. This is super-useful for French but applies as well to (many) other languages.

I didn’t want to have to set and reset the input modes every time I sent an email, so I came up with a simple method of having the input mode set based on the email address of the correspondent. For instance, if I write to I want to automatically be in french-postfix mode, and similarly for other users and other languages.

My method utilizes a custom field in BBDB, so if you don’t use BBDB this won’t work for you. The custom field has to be manually added (Emacs can’t really guess if you want to write to someone in Hawaiian).

;;; Try to set input mode when writing to certain addressees.  This
;;; works, with one caveat.  It doesn't work if an addressee is typed
;;; without a TAB following (which calls bbdb-complete-mail). (There
;;; is no problem when /replying/ to an email.)  This all must come
;;; /after/ gnus-alias-init. This relies on defining a 'language'
;;; xfield in BBDB, which is trivial, and populating it with a valid
;;; language input method when needed.

;;; This works with single addressees on the To: line.
;;; I have yet to adapt for multiple addressees.

(defun rjn-change-lang-by-addressee ()
  (let ((addressee (message-fetch-field "To"))
	netaddr person langpref)
    (if addressee
        (setq netaddr (cadr (mail-extract-address-components addressee))))
    (if netaddr
        (setq person (car (bbdb-search (bbdb-records) :mail netaddr))))
    (if person
        (setq langpref (bbdb-record-field person 'language)))
    (if langpref
	(set-input-method langpref))))

;;; Both of the following are necessary. The addition to
;;; message-setup-hook works when doing replies, while the advice on
;;; bbdb-complete-mail works when composing a new mail from scratch
;;; (but TAB completion is necessary to activate it; is there a better
;;; way?).
(add-hook 'message-setup-hook 'rjn-change-lang-by-addressee t)
(advice-add 'bbdb-complete-mail :after #'rjn-change-lang-by-addressee)

And here’s a representative (anonymized) BBDB entry. Note the ’language’ field.

Kumu Hula
           language: hawaiian-postfix

Author: Bob Newell


Created: 2024-06-02 Sun 10:45