Emacs literate e-mail configuration (Gnus)
I had forgotten to add this initially, it looks like.org
parent
9ec0bea609
commit
a1e11e7e28
|
@ -0,0 +1,535 @@
|
|||
#+TITLE: Mike Gerwitz's Emacs Mail Configuration
|
||||
#+AUTHOR: Mike Gerwitz
|
||||
#+EMAIL: mtg@gnu.org
|
||||
|
||||
Before beginning on my Emacs-related mail configuration, I want to
|
||||
shout out to my previous e-mail cl that served me well for so many
|
||||
years: [[http://www.mutt.org/][Mutt]]. My [[https://gitlab.com/mikegerwitz/dotfiles/blob/master/muttrc][personal Mutt configuration]] is still publicly
|
||||
available.
|
||||
|
||||
The reason that an Emacs MUA won out was because the lure and
|
||||
flexibility of a client written in a Lisp, and written to be easily
|
||||
customized and hooked in a Lisp, is powerful. But it's not for
|
||||
everyone; I still recommend Mutt to others.
|
||||
|
||||
As always with my code, I use lexical variable binding:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp :padline no
|
||||
;; -*- lexical-binding: t -*-
|
||||
#+END_SRC
|
||||
|
||||
* Identity
|
||||
My identity isn't as trivial as it sounds, since I may be sharing this
|
||||
configuration with multiple environments. But I'm pretty sure my name
|
||||
won't be changing:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq-default user-full-name "Mike Gerwitz")
|
||||
#+END_SRC
|
||||
|
||||
My e-mail address depends on who I am representing (e.g. myself or my
|
||||
employer). I therefore defer these configuration options to a
|
||||
configurable value that specifies a local profile:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defcustom mtg/mail-profile
|
||||
nil
|
||||
"Function used to perform mail setup
|
||||
The function should set all necessary mail options."
|
||||
:type 'function
|
||||
:group 'mtg)
|
||||
|
||||
;;(funcall 'mtg/mail-profile)
|
||||
#+END_SRC
|
||||
|
||||
That said, there are certain settings that are reasonable defaults;
|
||||
they can be overridden if needed.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(require 'smtpmail)
|
||||
(setq send-mail-function 'smtpmail-send-it
|
||||
message-send-mail-function 'smtpmail-send-it
|
||||
smtpmail-debug-info t)
|
||||
#+END_SRC
|
||||
|
||||
** GNU Profile
|
||||
Nearly all of my communications are related to software. As a member
|
||||
of the GNU Project, I'm both proud and want to do my best to bring
|
||||
attention to it. Communications using this address do _not_
|
||||
necessarily mean that I'm speaking on behalf of the GNU Project.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun mtg/mail-prof/gnu ()
|
||||
"Mail profile for GNU Project"
|
||||
(setq user-mail-address "mtg@gnu.org"
|
||||
smtpmail-local-domain "gnu.org"
|
||||
smtpmail-smtp-server "fencepost.gnu.org"
|
||||
smtpmail-smtp-service 587
|
||||
|
||||
gnus-select-method '(nnimap "mail.mikegerwitz.com"
|
||||
(nnimap-inbox "INBOX")
|
||||
(nnimap-record-commands t)
|
||||
(nnimap-stream tls))))
|
||||
#+END_SRC
|
||||
|
||||
* <<<Gnus>>>
|
||||
[[gnus.org][Gnus]] is a popular message reader (that is, newsgroups and the like, in
|
||||
addition to E-mail) for Emacs. It is highly customizable via user
|
||||
options and hooks. Given that it is written in Elisp, it is also
|
||||
customizable through redefining or advising existing functions.
|
||||
|
||||
Of course, that's assuming that I know what the hell I'm doing. I'd
|
||||
like to think that I do.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;; prevents annoying prompts that try to save your ass from doing
|
||||
;; something stupid
|
||||
(setq gnus-novice-user nil
|
||||
gnus-interactive-exit nil)
|
||||
#+END_SRC
|
||||
|
||||
** Receiving Mail
|
||||
The notion of a "large" newsgroup (or mailbox) has changed over the
|
||||
years as network connections have continued to improve. With that
|
||||
said, some hosts do better than others, some ISPs do better than
|
||||
others, and some user choices may have an impact. For example, I use
|
||||
Tor for many communications.
|
||||
|
||||
I have found that 500 messages is a decent amount not only for
|
||||
preventing fetching too much data, but to help grok it as well. More
|
||||
messages can always be retrieved (using =/ N=).
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq-default gnus-large-newsgroup 500)
|
||||
#+END_SRC
|
||||
|
||||
This would be less of a concern if Emacs were not single-threaded, or
|
||||
if Gnus handled the message retrieval in another process or
|
||||
thread.
|
||||
|
||||
** Writing and Sending Mail
|
||||
Nearly every conversation I have with someone online is via e-mail; I
|
||||
do not use any "social media" websites, and any websites that I do use
|
||||
for communication have, or would do well to have, an e-mail interface.
|
||||
|
||||
*** Replying
|
||||
There are important considerations when replying to
|
||||
mail. Specifically, when replying, you would do best to provide
|
||||
proper unambiguous context and a summary reference for both the
|
||||
recipient and any readers of the message. There are [[https://en.wikipedia.org/wiki/Posting_style][plenty of
|
||||
opinions on posting style]], but the de-facto standard in technical
|
||||
discussions (and discussions with reasonable human beings) is the
|
||||
interleave style, whereby responses follow the appropriate /portion/
|
||||
of quoted text, followed (potentially) by more quotes and
|
||||
replies.
|
||||
|
||||
The foundation for a good reply is a good quote. I enjoyed Mutt's
|
||||
default style, which included a timestamp in the quote heading, which
|
||||
I find important. Further, Gnus adds an empty line after the heading,
|
||||
which I do not like, as I believe it makes the block quotations more
|
||||
difficult to grok at first glance.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq message-citation-line-function
|
||||
'message-insert-formatted-citation-line
|
||||
|
||||
message-citation-line-format
|
||||
"On %a, %b %d, %Y at %H:%M:%S %z, %N wrote:"
|
||||
|
||||
gnus-face-9
|
||||
'gnus-face-tree-marker)
|
||||
#+END_SRC
|
||||
|
||||
A proper reply then involves first placing a greeting, apropos, or
|
||||
summary above the quote heading, and then inserting replies within
|
||||
certain parts of the quote block (creating block fragments), and
|
||||
deleting fragments that are unneeded or inapplicable for the reply.
|
||||
|
||||
*** Importing Messages
|
||||
Some messages may be written externally (e.g. generated by a program,
|
||||
or even written by another person). I provide functions to import
|
||||
them as articles into a group.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun gnus-import-article-quick (group file)
|
||||
"Import FILE as an article into GROUP.
|
||||
FILE is imported as-is, without any assurances that it constitutes a valid
|
||||
message.
|
||||
Return (FILE . article-id) pair."
|
||||
(interactive "sGroup: \nfImport file: ")
|
||||
(with-current-buffer (gnus-get-buffer-create " *import file*")
|
||||
(erase-buffer)
|
||||
(nnheader-insert-file-contents file)
|
||||
(let* ((result (gnus-request-accept-article group nil t t))
|
||||
(article-id (cdr result)))
|
||||
(kill-buffer (current-buffer))
|
||||
`(,file . ,article-id))))
|
||||
|
||||
|
||||
(defalias 'gnus-import-draft-quick
|
||||
(apply-partially 'gnus-import-article-quick "nndraft:drafts"))
|
||||
#+END_SRC
|
||||
|
||||
Git is able to generate a patch series for sending commits via
|
||||
e-mail. This function helps to import them into Gnus for manual
|
||||
editing and sending, rather than using its own mail facilities for
|
||||
sending the messages automatically.
|
||||
|
||||
These functions may be buggy; I seldom use them.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;; TODO: gracefully fail when no wildcard matches
|
||||
(defun gnus-import-patch-set (path &optional edit patch-ext)
|
||||
(interactive "DPath: \nP")
|
||||
(let* ((ext (or patch-ext ".patch"))
|
||||
(result (mapcar 'gnus-import-draft-quick
|
||||
(file-expand-wildcards (concat path "/*" ext))))
|
||||
(root-id (cdar result)))
|
||||
(gnus-group-read-group t nil "nndraft:drafts")
|
||||
(gnus-summary-goto-article root-id t t)
|
||||
(when edit
|
||||
(gnus-summary-edit-article))
|
||||
result))
|
||||
|
||||
(defun gnus-create-patch-set (repo-path topic mainline)
|
||||
(interactive "DRepository path: \nsTopic branch: \nsMainline: ")
|
||||
(cd repo-path)
|
||||
(call-process "git"
|
||||
nil
|
||||
(list (gnus-get-buffer-create " *create-patch-set*") t)
|
||||
nil
|
||||
"format-patch"
|
||||
(concat mainline ".." topic))
|
||||
(gnus-import-patch-set repo-path t))
|
||||
#+END_SRC
|
||||
|
||||
*** Archiving
|
||||
I archive my sent messages to both reference and reflect upon what I
|
||||
have said. But I don't want to feel obligated to re-read what I
|
||||
wrote!
|
||||
|
||||
#+BEGIN_SRC
|
||||
(setq gnus-gcc-mark-as-read 1)
|
||||
#+END_SRC
|
||||
|
||||
** Reading Mail
|
||||
The vast majority of my interaction with other human beings is through
|
||||
my MUA; its presentation is important. I have not customized it as
|
||||
heavily as have [[https://gitlab.com/mikegerwitz/dotfiles/blob/master/muttrc][I have Mutt]] (yet), but it's a start.
|
||||
|
||||
*** Headers
|
||||
Headers are important: they convey metadata that helps to provide
|
||||
context and validity to a message. Certain headers I use for
|
||||
cross-referencing externally, such as =Message-ID=.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-sorted-header-list '("^From:"
|
||||
"^Newsgroups:"
|
||||
"^Subject:"
|
||||
"^Date:"
|
||||
"^Envelope-To:"
|
||||
"^Followup-To:"
|
||||
"^Reply-To:"
|
||||
"^Organization:"
|
||||
"^Summary:"
|
||||
"^Abstract:"
|
||||
"^Keywords:"
|
||||
"^To:"
|
||||
"^[BGF]?Cc:"
|
||||
"^Posted-To:"
|
||||
"^Mail-Copies-To:"
|
||||
"^Mail-Followup-To:"
|
||||
"^Apparently-To:"
|
||||
"^Resent-From:"
|
||||
"^User-Agent:"
|
||||
"^X-detected-operating-system:"
|
||||
"^Message-ID:"
|
||||
"^References:"
|
||||
"^Gnus-Warning:")
|
||||
gnus-visible-headers (mapconcat 'identity
|
||||
gnus-sorted-header-list
|
||||
"\\|"))
|
||||
#+END_SRC
|
||||
|
||||
Considering that I just expressed my interest in headers, it would
|
||||
stand to reason that I would not want them stripped upon saving a
|
||||
message somewhere:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-save-all-headers t)
|
||||
#+END_SRC
|
||||
|
||||
*** Signature Verification
|
||||
Always try to verify GPG/PGP-signed messages when possible. If this fails
|
||||
because the key is not available, the message conveniently displays the key
|
||||
id.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq mm-verify-option 'always)
|
||||
#+END_SRC
|
||||
|
||||
*** Message Formats
|
||||
I like plain text. It is the universal language: all standard Unix
|
||||
utilities are made to operate on plain text; they can be manipulated
|
||||
and piped to other utilities to create sophisticated processes for
|
||||
operating on it. It also views well on a terminal, on which I live.
|
||||
|
||||
I therefore discourage use of HTML e-mails. Gnus is able to render
|
||||
HTML emails on a terminal fairly well, but that is not what I want; I
|
||||
don't want to read your HTML e-mails at all.
|
||||
|
||||
Some people do send me HTML-only e-mails; in that case, there's the
|
||||
option of forcing the HTML rendering, or viewing the e-mail in my web
|
||||
browser. You know---the place where HTML belongs.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;; >:@
|
||||
(setq-default mm-discouraged-alternatives '("text/html"))
|
||||
#+END_SRC
|
||||
|
||||
*** Groups
|
||||
Group lines are displayed as follows:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-group-line-format
|
||||
"%M%m%S%5,5y/%-5,5t %*%B%-40,40g %ud\n")
|
||||
#+END_SRC
|
||||
|
||||
**** Sorting
|
||||
It makes sense for me to have the most read groups appear at the top
|
||||
of my group list. Ranks will be adjusted and the groups re-sorted
|
||||
after returning from summary mode.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-groups-sort-groups
|
||||
'gnus-group-sort-by-rank)
|
||||
|
||||
(add-hook 'gnus-summary-exit-hook
|
||||
'gnus-summary-bubble-group)
|
||||
|
||||
(add-hook 'gnus-summary-exit-hook
|
||||
'gnus-group-sort-groups-by-rank)
|
||||
#+END_SRC
|
||||
|
||||
**** Topics
|
||||
I subscribe to a number of mailing lists, and further organize my mail
|
||||
into a number of groups; it helps to have them organized
|
||||
hierarchically. Gnus offers a "topic" minor mode that offers this
|
||||
feature.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(add-hook 'gnus-group-mode-hook
|
||||
'gnus-topic-mode)
|
||||
#+END_SRC
|
||||
|
||||
**** Last Visit Timestamp
|
||||
On a similar note, it's also helpful to know when I last looked at a
|
||||
group. Sometimes. Not often.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(add-hook 'gnus-select-group-hook
|
||||
'gnus-group-set-timestamp)
|
||||
#+END_SRC
|
||||
|
||||
The default timestamp format for =%d= is ISO 8601, which isn't very useful,
|
||||
because it requires too much effort to visually parse. In the definition of
|
||||
=gnus-group-line-format= above, there is a format specifier =%ud=, which is
|
||||
user-defined; I define it here (largely derived from the [[info:gnus][Gnus manual]] Gnus manual):
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun gnus-user-format-function-d (headers)
|
||||
(let ((time (gnus-group-timestamp gnus-tmp-group)))
|
||||
(if time
|
||||
(format-time-string "%a %d %b %Y, %T" time)
|
||||
"")))
|
||||
#+END_SRC
|
||||
|
||||
*** Summary Mode
|
||||
Gnus' summary mode displays a list of messages. The proper way to
|
||||
display messages is in threads, which display responses
|
||||
hierarchically. This is especially important for discussions in
|
||||
mailing lists, as they can get incredibly lengthy, there can be many
|
||||
discussions happening concurrently, and it's easy to lose context.
|
||||
|
||||
My tree display is inspired by Mutt.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
;; mutt-inspired tree display
|
||||
(setq gnus-summary-line-format "%U%R%z %d %-23,23f (%4,4L) %*%9{%B%}%s\n"
|
||||
gnus-sum-thread-tree-root ""
|
||||
gnus-sum-thread-tree-false-root "──> "
|
||||
gnus-sum-thread-tree-leaf-with-other "├─> "
|
||||
gnus-sum-thread-tree-vertical "│ "
|
||||
gnus-sum-thread-tree-single-leaf "└─> ")
|
||||
#+END_SRC
|
||||
|
||||
When entering summary mode by selecting a group with
|
||||
=gnus-topic-read-group= (default =SPC=, as opposed to
|
||||
=gnus-topic-select-group=, which defaults to =RET=), an article
|
||||
(message) is selected automatically for reading. I want to see any
|
||||
unseen articles first, otherwise unread (the latter there may be many
|
||||
of, such as in groups associated with high-volume mailing lists).
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-auto-select-subject
|
||||
'unseen-or-unread)
|
||||
#+END_SRC
|
||||
|
||||
**** Articles
|
||||
An /article/ in Gnus terminology, as far as we're concerned here, is a
|
||||
message. When reading, hitting =SPC= will (by default) scroll down a
|
||||
page. When reaching the end of an article, the next =SPC= press will
|
||||
silently move to the next article.
|
||||
|
||||
This is a problem, because the previous message may end close to the
|
||||
bottom of the window, and then it may not be immediately apparent that
|
||||
you are reading a new message. I've been bitten by this before, and
|
||||
it can be profoundly confusing. Maybe not for you, but I don't like
|
||||
it.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-summary-stop-at-end-of-message t)
|
||||
#+END_SRC
|
||||
|
||||
**** Sparse Threads
|
||||
I choose to allow Gnus to hide articles that have been read, which
|
||||
helps keep the groups clean looking and easier to grok new
|
||||
material. But threads---especially on mailing lists---may be very
|
||||
long, and may go on for months. The context of the parent thread is
|
||||
very important.
|
||||
|
||||
=A T= can be used to fetch the full thread (the best it can). But
|
||||
that can be overkill in cases where threads are quite large. Gnus has
|
||||
a concept of "sparse" threads, in which it will attempt to build
|
||||
a partial thread of parent messages (even if they are read), and will
|
||||
even leave gaps where it detects missing messages (e.g. off-list
|
||||
replies). The latter alone is useful in avoiding confusion.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-build-sparse-threads 'some)
|
||||
#+END_SRC
|
||||
|
||||
Building threads on its own isn't a trivial task; you'd think that
|
||||
looking at the =References= header would be enough, but that breaks if
|
||||
somebody posts a message with a broker MUA or newsreader. And I can
|
||||
tell you from experience that this is not as infrequent as I would
|
||||
like, even on technical mailing lists.
|
||||
|
||||
So, even though building threads by subject is not always accurate, it
|
||||
will have to do. This is the default behavior.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-summary-thread-gathering-function
|
||||
'gnus-gather-threads-by-subject)
|
||||
#+END_SRC
|
||||
|
||||
|
||||
*** Window Layout
|
||||
My display width permits and article view that contains a tree view
|
||||
beside the summary, with the article rendered below both.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(gnus-add-configuration
|
||||
'(article
|
||||
(vertical 1.0
|
||||
(horizontal 0.25
|
||||
(summary 0.50 point)
|
||||
(tree 1.0))
|
||||
(article 1.0))))
|
||||
#+END_SRC
|
||||
|
||||
The =tree= mention above refers to the tree buffer:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(setq gnus-use-trees t
|
||||
gnus-generate-tree-function 'gnus-generate-vertical-tree
|
||||
gnus-tree-minimize-window nil)
|
||||
#+END_SRC
|
||||
|
||||
** Keybindings
|
||||
I come from the land of Mutt, and I appreciate the concise keybindings
|
||||
that it provides for many operations. I duplicate some of those here.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(add-hook 'gnus-group-mode-hook
|
||||
(lambda ()
|
||||
(local-set-key "j" 'gnus-group-next-unread-group)
|
||||
(local-set-key "k" 'gnus-group-prev-unread-group)
|
||||
|
||||
;; re-bind jump (originally `j')
|
||||
(local-set-key "\M-j" 'gnus-group-jump-to-group)))
|
||||
|
||||
(add-hook 'gnus-summary-mode-hook
|
||||
(lambda ()
|
||||
;; `t' by default toggles headers, which we mapped above
|
||||
(local-set-key "t" 'gnus-summary-toggle-processable)
|
||||
|
||||
;; The original keybindings are dangerous for a vim user! They
|
||||
;; are still accessible, respectively, via `G j'; `M k'; and
|
||||
;; `TAB' within the article buffer.
|
||||
(local-set-key "j" 'next-line)
|
||||
(local-set-key "k" 'previous-line)
|
||||
(local-set-key "\t" 'gnus-summary-next-unread-subject)
|
||||
|
||||
;; mutt uses `s' for "save", which can be used to move between
|
||||
;; IMAP folders (in this case, groups)
|
||||
(local-set-key "\C-s" 'gnus-summary-isearch-article)
|
||||
(local-set-key "s" 'gnus-summary-move-article)
|
||||
|
||||
;; the original has other bindings
|
||||
(local-set-key "d" 'gnus-summary-mark-as-expirable)))
|
||||
|
||||
(add-hook 'gnus-article-mode-hook
|
||||
(lambda ()
|
||||
;; consistency with summary buffer (and mutt)
|
||||
(local-set-key "h" 'gnus-summary-toggle-header)
|
||||
(local-set-key "v" 'gnus-article-view-part)))
|
||||
#+END_SRC
|
||||
|
||||
The =t= keybinding in Mutt toggles marks, but Gnus offers no function
|
||||
to do so; I provide one via =gnus-summary-toggle-processable=:
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(defun gnus-summary-toggle-processable (n)
|
||||
"Toggle process mark on the next N articles.
|
||||
If N is negative, mark backward instead; consistent with behavior of
|
||||
`gnus-summary-mark-as-processable'."
|
||||
(interactive "p")
|
||||
(cl-labels ((next (n direction article)
|
||||
(when (and (> n 0) article)
|
||||
;; toggle article (this also updates point, selecting
|
||||
;; the next article if available)
|
||||
(funcall
|
||||
(if (memq article gnus-newsgroup-processable)
|
||||
'gnus-summary-unmark-as-processable
|
||||
'gnus-summary-mark-as-processable)
|
||||
direction)
|
||||
|
||||
;; process next (the above call already selected the
|
||||
;; next article, so we don't have the return value;
|
||||
;; instead, assume that no other articles are
|
||||
;; available if the article at point matches the
|
||||
;; previously processed article)
|
||||
(let ((next-article (gnus-summary-article-number)))
|
||||
(unless (eq next-article article)
|
||||
(next (1- n) direction next-article))))))
|
||||
(next (abs n)
|
||||
(if (< n 0) -1 1)
|
||||
(gnus-summary-article-number)))
|
||||
n)
|
||||
#+END_SRC
|
||||
|
||||
* Command Line
|
||||
I generally invoke Gnus in a fresh Emacs process, for various reasons that I
|
||||
won't get into here right now. To make this a bit easier, I add a =gnus=
|
||||
command switch that immediately invokes Gnus and then kills Emacs once it's
|
||||
done.
|
||||
|
||||
#+BEGIN_SRC emacs-lisp
|
||||
(add-to-list
|
||||
'command-switch-alist
|
||||
'("gnus" . (lambda (&rest ignore)
|
||||
(add-hook 'emacs-startup-hook 'gnus t)
|
||||
(add-hook 'gnus-after-exiting-gnus-hook
|
||||
'save-buffers-kill-emacs))))
|
||||
#+END_SRC
|
Loading…
Reference in New Issue