From a1e11e7e28d5be42216b2a3952496007b386a78b Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Sat, 1 Aug 2015 09:43:44 -0400 Subject: [PATCH] Emacs literate e-mail configuration (Gnus) I had forgotten to add this initially, it looks like. --- emacs.d/mail.org | 535 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 535 insertions(+) create mode 100644 emacs.d/mail.org diff --git a/emacs.d/mail.org b/emacs.d/mail.org new file mode 100644 index 0000000..0f64b4e --- /dev/null +++ b/emacs.d/mail.org @@ -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.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