2015-08-01 09:43:44 -04:00
#+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.
2015-08-27 23:20:00 -04:00
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.]
2015-08-01 09:43:44 -04:00
#+BEGIN_SRC emacs-lisp
(defun mtg/mail-prof/gnu ()
"Mail profile for GNU Project"
(setq user-mail-address "mtg@gnu.org"
2015-08-27 23:20:00 -04:00
smtpmail-smtp-server "localhost" ; tunnel
smtpmail-smtp-service 5587
2015-08-01 09:43:44 -04:00
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
2015-11-07 23:27:55 -05:00
(horizontal 1.0
(vertical 0.50
(summary 1.0 point)
(tree 0.25))
(article 1.0))))
2015-08-01 09:43:44 -04:00
#+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
2015-08-27 23:20:18 -04:00
* Contacts
The majority of my online communication is done via e-mail. Over the
years---especially on mailing lists---it's easy to accumulate a lot of
contacts, and it's easy to forget who people are. So, I need the ability to
not only store names and e-mail addresses (and auto-complete them!), but
also notes about the person, so that I can remember who they are or other
useful information about them.
Emacs comes with BBDB---the "Insidious" Big Brother Database---which keeps a
database of contacts. I won't pretend that I know how to use it very well,
but I'll do by best to learn.
#+BEGIN_SRC emacs-lisp
(bbdb-initialize 'gnus 'message)
;; this is the key for fixing a brokern BBDB3+Gnus integration
(setq bbdb-mua-update-interactive-p
'(query . create))
#+END_SRC
In order to build a comprehensive database, I want contacts to be added with
ease; en masse if need be.
#+BEGIN_SRC emacs-lisp
(bbdb-mua-auto-update-init 'gnus 'message)
;; return more than just the first address of a message
(setq bbdb-message-all-addresses t)
#+END_SRC
BBDB displays a window ("popup") when adding/editing an entry, or visiting
articles with known entries, from Gnus (for any MUA it's initialized for,
for that matter). The default size shares window space evenly with all
others---far too large.
#+BEGIN_SRC emacs-lisp
(setq bbdb-pop-up-window-size 0.15
bbdb-mua-pop-up-window-size 0.15)
#+END_SRC
2015-08-01 09:43:44 -04:00
* 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