#+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. GNU hosts a POP server on =fencepost=. I have a =fetchmail= cronjob running on my server that periodically fetches mail from fencepost onto my own mailserver, to which I connect via IMAP; all my mail is then conveniently mixed into one account, and all GNU mail is also run through my Sieve rules and any other scripts. For sending mail, privacy is an important consideration: I really do not want my home IP Address and hostname appearing in the header of every e-mail that I send. I have the option if running Emacs using =torify=, but that is far too slow for receiving the amount of e-mail that I sift through.[fn:Of course I could set up a local IMAP server that downloads my mail periodically to avoid this---and I have done that in the past---but for reasons I do not want to get into right now, I have stopped using it.] The other option is a SOCKS proxy for SMTP only, but it's not obvious to me how to do that, so I'll defer further research for another time. This leaves a SOCKS proxy in general, or tunneling. I have opted for an SSH tunnel. This has a couple benefits: my normal network rules will apply for reading mail---for which privacy is not a concern---and, since I'm connecting to =localhost=, I cannot forget to invoke Emacs in a special way: the connection will fail if I do not have a tunnel set up.[fn:For example, running =torify emacs= or =proxychains emacs= can yield a good result, but if I forget to do so, that's a problem.] #+BEGIN_SRC emacs-lisp (defun mtg/mail-prof/gnu () "Mail profile for GNU Project" (setq user-mail-address "mtg@gnu.org" smtpmail-smtp-server "localhost" ; tunnel smtpmail-smtp-service 5587 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