1
0
Fork 0
dotfiles/emacs.d/mail.org

23 KiB

Mike Gerwitz's Emacs Mail Configuration

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: Mutt. My 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:

  ;; -*- lexical-binding: t -*-

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:

  (setq-default user-full-name "Mike Gerwitz")

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:

  (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)

That said, there are certain settings that are reasonable defaults; they can be overridden if needed.

  (require 'smtpmail)
  (setq send-mail-function         'smtpmail-send-it
        message-send-mail-function 'smtpmail-send-it
        smtpmail-debug-info        t)

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, and an offlineimap job that copies all messages to my local box; all my mail is 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.]

  (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 "localhost"
                                      (nnimap-inbox           "INBOX")
                                      (nnimap-record-commands t)
                                      (nnimap-stream          network)
                                      (nnimap-authenticator   login))))

<<<Gnus>>>

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.

  ;; prevents annoying prompts that try to save your ass from doing
  ;; something stupid
  (setq gnus-novice-user      nil
        gnus-interactive-exit nil)

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).

  (setq-default gnus-large-newsgroup 500)

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 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.

    (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)

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.

  (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"))

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.

  ;; 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))

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!

  (setq gnus-gcc-mark-as-read 1)

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 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.

  (setq gnus-sorted-header-list '("^From:"
                                  "^Newsgroups:"
                                  "^Subject:"
                                  "^Date:"
                                  "^Envelope-To:"
                                  "^Followup-To:"
                                  "^Reply-To:"
                                  "^Organization:"
                                  "^Summary:"
                                  "^Abstract:"
                                  "^Keywords:"
                                  "^To:"
                                  "^[BGF]?Cc:"
                                  "^Posted-To:"
                                  "^List-Id:"
                                  "^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
                                        "\\|"))

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:

  (setq gnus-save-all-headers t)

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.

  (setq mm-verify-option 'always)

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.

  ;; >:@
  (setq-default mm-discouraged-alternatives '("text/html"))

Groups

Group lines are displayed as follows:

  (setq gnus-group-line-format
        "%M%m%S%5,5y/%-5,5t %*%B%-40,40g %ud\n")
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.

  (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)
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.

  (add-hook 'gnus-group-mode-hook
            'gnus-topic-mode)
Last Visit Timestamp

On a similar note, it's also helpful to know when I last looked at a group. Sometimes. Not often.

  (add-hook 'gnus-select-group-hook
            'gnus-group-set-timestamp)

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 Gnus manual Gnus manual):

  (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)
        "")))

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.

   ;; 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     "└─> ")

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).

  (setq gnus-auto-select-subject
        'unseen-or-unread)
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.

  (setq gnus-summary-stop-at-end-of-message t)
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.

  (setq gnus-build-sparse-threads 'some)

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.

  (setq gnus-summary-thread-gathering-function
        'gnus-gather-threads-by-subject)

Window Layout

My display width permits and article view that contains a tree view beside the summary, with the article rendered below both.

  (gnus-add-configuration
   '(article
     (horizontal 1.0
                 (vertical 0.50
                           (summary 1.0 point)
                           (tree 0.25))
                 (article 1.0))))

The tree mention above refers to the tree buffer:

  (setq gnus-use-trees              t
        gnus-generate-tree-function 'gnus-generate-vertical-tree
        gnus-tree-minimize-window   nil)

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.

  (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)))

The t keybinding in Mutt toggles marks, but Gnus offers no function to do so; I provide one via gnus-summary-toggle-processable:

  (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)

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.

  (bbdb-initialize 'gnus 'message)

  ;; this is the key for fixing a brokern BBDB3+Gnus integration
  (setq bbdb-mua-update-interactive-p
        '(query . create))

In order to build a comprehensive database, I want contacts to be added with ease; en masse if need be.

  (bbdb-mua-auto-update-init 'gnus 'message)

  ;; return more than just the first address of a message
  (setq bbdb-message-all-addresses t)

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.

  (setq bbdb-pop-up-window-size     0.15
        bbdb-mua-pop-up-window-size 0.15)

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.

  (add-to-list
   'command-switch-alist
   '("gnus" . (lambda (&rest ignore)
                (add-hook 'emacs-startup-hook 'gnus-unplugged t)
                (add-hook 'gnus-after-exiting-gnus-hook
                          'save-buffers-kill-emacs))))