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

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

While GNU's SMTP server is usually what I use, life is a little more complicated. Personal e-mail that really should only be handled by my own servers needs to be sent out using my own SMTP server; otherwise, replies to my personal e-mail would have to go out under my GNU address, which isn't desirable.

This overlaps a little bit with my Gnus configuration below.

  ;; default is mtg@gnu.org, so we need only worry about overrides
  (setq gnus-posting-styles
        '(((header "to" ".+@mikegerwitz.com")
           (address "mike@mikegerwitz.com")
           (signature-file "~/.signature.mtgcom")
           ("X-Message-SMTP-Method" "smtp mail.mikegerwitz.com 587"))))

  (defun mail-mikegerwitz.com-p ()
    )

Gnus supports an internal X-Message-SMTP-Method header that will direct mail to the desired SMTP server. Pretty simple! Any mail at my domain will reach me. Chances are, I want to reply from my main e-mail address (especially if I want to GPG-sign my message), but I can change that manually if need be. I often enter unique e-mail addresses for services I sign up for to track who sells my data to spammers or might have been compromised.

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

Signing and Encrypting

Every message I sent is cryptographically signed using my GPG key. When I have my recipients' keys, they're also encrypted.

BEGIN_SRC emacs-lisp (setq mml-secure-openpgp-signers 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!

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

When I can—that is, when my recipients support it—I GPG-encrypt my e-mail. Problem is, I can't read what I sent unless I also encrypt the message to myself.

  (setq mml-secure-openpgp-encrypt-to-self t)

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:"
                                  "^Mail-Copies-To:"
                                  "^Mail-Followup-To:"
                                  "^Apparently-To:"
                                  "^Resent-From:"
                                  "^User-Agent:"
                                  "^X-detected-operating-system:"
                                  "^Message-ID:"
                                  "^References:"
                                  "^List-Id:"
                                  "^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)

Signature verification doesn't really help me if I can't see that it's signed to begin with. In Gnus, it seems that the easiest way to do this is to "buttonize" it, which will delimit the part that is signed.

  (setq gnus-buttonized-mime-types
        '("multipart/signed"
          "multipart/encrypted"))

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

Correspondence

CloudFlare

TODO: CloudFlare rant and rationale.

  # -*- mode: snippet -*-
  # name: Message to webmaster of CloudFlare JS-only verification
  # key: cloudflare-js
  # --
  I recently tried to view ${1:an article} on ${2:your website} at
  ${3:foo.com}.  For reasons of privacy, I and many others use Tor for all
  Web traffic; unfortunately, CloudFlare often recognizes Tor exit nodes as a
  threat[0], and so invokes its DDoS mitigation.

  Normally, the default behavior for CloudFlare---that I've noticed---is
  to display a CAPTCHA to the user.  This works without JavaScript enabled
  and allows the user to pass once it has been answered correctly.  This
  isn't a pleasant experience, but it's surmountable.

  But ${4:$2 has} chosen to enable CloudFlare's JavaScript-only DDoS
  protection (which they call a "JavaScript Challenge").

  There are a number of reasons why users may choose to disable
  JavaScript:  There are a host of security concerns (including attacks
  that have de-anonymized Tor users; malware distribution; user spying;
  and more).  There is also the issue of software freedom: most websites
  serve proprietary JavaScript programs that deny users the right to study,
  share, and modify them.[1]  This is antithetical to a free Web and is
  used to exert control over users.

  I understand that ${5:$2} is likely not willing to disable
  CloudFlare's DDoS mitigation services, but would you please consider
  using the CAPTCHA-based approach so that all users---including those
  that disable JavaScript for privacy, security, and ethical
  considerations---have access to the articles and information that
  $4 provides?  Otherwise, some users may choose to access the web page
  without Tor, potentially endangering their anonymity, or may choose to
  accept non-free programs that could compromise their freedom and security.


  [0]: https://support.cloudflare.com/hc/en-us/articles/203306930
  [1]: https://www.gnu.org/philosophy/javascript-trap.html


  Please help us work our way back toward a Web that is free for everyone to
  access with the software their choose and trust.

  Thank you for your consideration,