为什么要用 Mutt

Mutt 是一个运行在终端中的邮件客户端,基本上对于它的第一印象就是麻烦以及不够直观,当然我最开始也是拒绝的,无奈 Linux 下也确实没有一个令我满意的邮件客户端,结果使用了 Mutt 之后发现它其实很契合自己的需求:

  • 大部分邮件只需看一眼标题然后归档即可,少部分邮件也只需要提取 URL 或者附件。
  • 讨厌邮件中包含的各种 Tracker。
  • 需要将所有邮件在本地备份。
  • 可以通过一行命令快速的发送邮件。
  • 喜欢使用纯文本格式的邮件。

方案

Mutt 有许多不同的分支,分别编译了不同的功能和补丁,现在比较主流的是 NeoMutt。由于我需要将所有邮件在本地保留备份,所以没有使用 NeoMutt 自带的 IMAP,而是使用 OfflineIMAP 将邮件与本地 Maildir 同步,存储在 Maildir 的邮件可以被 NeoMutt 直接读取以及操作。至于发送邮件方面由于没有特殊需求,所以我直接使用了 NeoMutt 自带的 SMTP 功能。
可能需要安装的包:neomutt, offlineimap 以及 lynx

配置 OfflineIMAP

OfflineIMAP 的配置文件可以放在 ~/.config/offlineimap/config,这里我简化了一下我的配置文件,用作抛砖引玉。

[general]
accounts = main

[Account main]
localrepository = main-local
remoterepository = main-remote
autorefresh = 0.2
quick = 10
postsynchook = ~/.config/offlineimap/notify.sh

[Repository main-local]
type = Maildir
localfolders = ~/.mail

[Repository main-remote]
type = IMAP
remoteport = 1143
remotehost = 127.0.0.1
remoteuser = username
remotepass = password
folderfilter = lambda foldername: foldername in ['INBOX', 'Archive', 'Sent', 'Trash']
# SSL
ssl = no
starttls = yes
ssl_version = tls1_3
sslcacertfile = ~/.config/offlineimap/relay.cert

其中有几处需要注意的地方:

  1. postsynchook 后面的命令会在收到新邮件后运行,这里我运行的是我自己写的一个简单的 Shell Script,作用是检查新邮件并发送系统通知:

    #!/bin/bash
    set -Eeo pipefail
    
    MAILDIR=~/.mail/INBOX/new/
    WORKDIR="${XDG_CACHE_HOME:-$HOME/.cache}"
    cd $WORKDIR
    
    if [[ -f notifyDB ]]; then
      CURRID=$(cat notifyDB)
    else
      CURRID=0
    fi
    
    for FNAME in $(ls $MAILDIR); do
      ID=$(echo $FNAME | sed 's/_.*//')
      if [[ "$ID" -gt "$CURRID" ]]; then
        EMLFROM=$(cat $MAILDIR$FNAME | grep "^From: ")
        EMLSUBJ=$(cat $MAILDIR$FNAME | grep "^Subject: ")
        notify-send "$EMLFROM" "$EMLSUBJ" -t 5000
        paplay /usr/share/sounds/Oxygen-Im-New-Mail.ogg
      fi
      echo "$ID" > notifyDB
    done
    
  2. localfolders 是存放 Maildir 的路径,需要自己建立。

  3. remoteuserremotepass 这里我直接使用的明文,原因是我连接的是一个在本地运行的 IMAP Relay。一般来讲这里推荐通过一个 Password Manager 来提供密码,详情可以查看这里,或者官方配置文档中的 remoteusereval 以及 remotepasseval

  4. ['INBOX', 'Archive', 'Sent', 'Trash'] 这里表示同步了四个文件夹,文件夹的名字要与服务器对应起来。

  5. SSL 的配置方面我选择了关闭强制 SSL 并开启了 STARTTLS,版本只开启了 TLS1.3,具体请根据自己的邮件供应商自行选择。

  6. sslcacertfile 指向 IMAP 需要用到的证书,一般指向 CA Certificates 所在的位置(不同发行版存放的位置不同)即可。由于我运行在本地的 IMAP Relay 使用的是自签名证书,所以直接选择自己抓取:

    openssl s_client -starttls imap -connect 127.0.0.1:1143 </dev/null 2>/dev/null | openssl x509 -text > ~/.config/offlineimap/relay.cert
    

配置好后使用 offlineimap 命令即可运行 OfflineIMAP,可以先运行一下来查看配置是否正确,确认配置没问题后即可设置后台自动同步,我选择的是用 Systemd 来管理:

systemctl --user enable --now offlineimap

配置 NeoMutt

主配置文件存放在 ~/.config/mutt/muttrc,需要用到的其他配置文件或脚本也可以一并扔到 mutt 目录里。
由于强迫症使然,配置中只设置了与 NeoMutt 默认配置不同的地方,
配置文件中 set 代表把 xxx 设置成 yes,unset 代表把 xxx 设置成 no,而 source 的作用是引用其他文件。

基本设置

前文中我们已经通过 OfflineIMAP 将邮件同步到 Maildir (~/.mail),这里首先进行相应的设置以来让 Mutt 可以管理 Maildir。

set header_cache = ~/.cache/mutt_header_cache
set tmpdir = /dev/shm
set folder = ~/.mail
set mbox_type = Maildir
set spoolfile=+INBOX
set mbox=+Archive
set record=+Sent
set postponed=+Drafts
set trash=+Trash
alternates .+@example.com alt1@gmail.com alt2@gmail.com
mailboxes +INBOX +Archive +Sent +Drafts +Trash +Local

header_cache 顾名思义为存储邮件的 header 的缓存,可以指向文件也可以指向文件夹,设置了之后可以提升读取速度。
tmpdir 为临时文件夹,撰写新邮件和读取附件时会把文件存储在这里,我直接设置成了存储到 tmpfs。
folder 为 mailbox 的位置,而 mbox_type 则表示 mailbox 的类型,这里我使用的是 Maildir。
spoolfile, mbox, record, postponed, trash 则分别表示了 Inbox, Archive, Sent, Drafts, Trash 这五个文件夹的位置,其中 + 展开时会变成 ~/.mail/,也就是 folder 中设置的目录,所以 spoolfile=+INBOX 代表了 spool mailbox (inbox) 存储在 ~/.mail/INBOX
alternates 后面列举了属于用户的邮件地址,可以支持正则表达,使劲往里面填就是了,这个会影响到 NeoMutt 对邮件进行分类和标记。
mailboxes 后面包含了所有 NeoMutt 需要同步的文件夹,其中 + 同样代表了 folder 中设置的目录。

杂项

set sleep_time = 0
set timeout = -1
set assumed_charset = "utf-8:GB18030"
set crypt_use_gpgme
set pgp_default_key="0xXXXXXXXXXXXXXXXX"
source ~/.config/mutt/gpgrc
unset help
unset wait_key

简单介绍一下各行的作用:
sleep_time 设置成 0 的主要原因是可以让切换文件夹的时候不会卡顿。
timeout 设置成了 -1,由于 NeoMutt 会在闲置时停止检查新邮件等行为,所以直接设置成不会进入闲置状态。
assumed_charset 表示了当邮件里没有标明 encoding 时,会按照顺序来尝试 decode。
crypt_use_gpgme 开启 GPGME 支持。
pgp_default_key 直接填写自己的 PGP 公钥 fingerprint 即可。
引用的 gpgrc 是直接复制的 /usr/share/doc/neomutt/samples/gpg.rc,具体包含的内容可以自己查看和订制一下。
help 设置成 no 可以隐藏快捷键提示,这个快捷键提示着实鸡肋,总是提示不到点子上。
wait_key 设置成 no 的时候,在执行外部命令时只有出错时才会提示『按任意键继续』。

SMTP

我的 SMTP 需求很简单,所以直接使用了 NeoMutt 自带的 SMTP,用户密码也直接明文存储,正常情况下推荐使用 msmtp 并配合一个 Password Manager。

set smtp_url=smtp://username:password@127.0.0.1:1025
set ssl_force_tls
set certificate_file = ~/.config/mutt/cert

certificate_file 指向 SMTP 需要用到的证书,与之前 OfflineIMAP 中的证书一样可以直接指向 CA Certificates 所在的位置。

侧栏和状态栏

set sidebar_width=15
unset help
set status_on_top
set status_format = "-%r-[%f]---[Msgs:%?M?%M/?%m%?n? New:%n?%?o? Old:%o?%?d? Del:%d?%?F? Flag:%F?%?t? Tag:%t?%?p? Post:%p?%?b? Inc:%b?%?l? %l?]---(%s/%S)-%>-(%P)---"

NeoMutt 默认的侧栏很宽,所以设置了一下 sidebar_width,另外侧栏默认隐藏,我用起来很舒服就没更改,如果有需求可以通过 set sidebar_visible 来设置。
status_on_top 可以让状态栏显示在顶端,不过需要先把快捷键提示关闭 (unset help),最后 status_format 设置了状态栏显示的内容,如果想 DIY 可以参考这里

邮件列表

邮件列表的显示格式我更改的地方比较少,其中 date_formatindex_format 分别表示了日期的格式以及每一个条目的格式,具体可以查看这里sortsort_aux 则设置成按照主题分类,并且将最新收到的邮件放在前面,最后设置 collapse_all 让相同主题默认折叠。

set date_format = "%m/%d"
set index_format = "%4C  [%Z]  %D  %-15.15F  %s"
set sort = threads
set sort_aux = reverse-last-date-received
set collapse_all

页面

页面 (Pager) 界面则负责管理邮件以及帮助页面的显示格式。

set pager_index_lines = 6
set pager_context = 3
set pager_stop
set tilde
unset markers
unhdr_order *
hdr_order from: to: cc: date: subject:
auto_view text/html
alternative_order text/plain text/enriched text/html
set display_filter="sed -e '/\\[-- Type: text.* --\\]/d' | sed -e '/\\[-- Autoview.* --\\]/d' | sed -e '/\\[-- Type.* --\\]/d' |  sed -e '/\\[-- .*unsupported.* --\\]/d' | sed -e '/\\[-- Attachment #[0-9] --\\]/d' | sed -e 's/Attachment #[0-9]: //g' | sed '/./,/^$/!d'"

这里我设置了 pager_index_lines = 6,可以在显示邮件内容的同时显示五条邮件的索引,因为状态栏会占用一条,所以设置成 6 的时候只会显示 5 条索引。
pager_context 表示了翻页时保留的行数,而 pager_stop 则表示翻页时不会转移到下(上)一封邮件。同时我设置了 set tildeunset markers,分别代表显示空行标识符("~") 以及隐藏自动换行时标识符("+")。
unhdr_order *hdr_order xxx 则分别清除了默认 Header 的显示排序以及设定了新的排序。
auto_view text/html 则指示 NeoMutt 自动将邮件中 text/html 部分转换为 text/plain,这需要 mailcap 里设置 copiousoutput,具体会在后面部分说明。最后alternative_order 表示了优先把哪一部分显示成邮件的主体。
另外单独提一下 display_filter,其作用是对显示邮件时的内容进行过滤和替换,我这里的命令主要是让 MIME 分割线变得好看一些。

Mailcap

当 NeoMutt 遇到自己没法处理的 MIME 类型时,会把 MIME 中的内容存储在临时目录,然后按照 mailcap 里的设定来进行处理,格式是 MIME类型; 命令; copiousoutput,命令中可以包含 %s,表示的是临时文件夹的路径,而 copiousoutput 为可选部分,表示将命令的 output 显示在 NeoMutt 的 Pager 界面。这里以我自己的设置为例:
首先设置 mailcap 文件的位置。

set mailcap_path = ~/.config/mutt/mailcap

对于 MIME 为 text/html 的部分,使用 lynx 来处理。对于其它类型,我直接都扔给了 xdg-open 命令,不过这里有个问题,xdg-open 在判断完后即会返回,然后存储 MIME 内容的临时文件就被删除了,导致真正调用的程序无法打开文件。我使用的权宜之计是先复制一份到 tmpfs,然后再处理,因为命令很短就直接也放在 mailcap 文件里了。

text/html; lynx -assume_charset=%{charset} -display_charset=utf-8 -dump %s; nametemplate=%s.html; copiousoutput
image/*; tmpdir="/dev/shm/mutt_attach_$(whoami)" && rm -rf $tmpdir && mkdir -m 700 "$tmpdir" && cp %s $tmpdir/ && xdg-open "$tmpdir/$(basename %s)"
application/pdf; tmpdir="/dev/shm/mutt_attach_$(whoami)" && rm -rf $tmpdir && mkdir -m 700 "$tmpdir" && cp %s $tmpdir/ && xdg-open "$tmpdir/$(basename %s)"

撰写

对于撰写邮件的设置的解释我直接放在注释里了。

set realname="Coda"                    # 撰写新邮件时使用的名字
set from="i@coda.world"                # 撰写新邮件时使用的邮件地址
set forward_format="Fwd: %s"           # 转发的格式为 "Fwd: 标题"
set forward_quote                      # 转发时将正文放到引用中
set attribution="\n\nOn %d, %n wrote:" # 回复时引用前文的格式
set reverse_name                       # 优先使用收件人的身份回复,而不是使用撰写新邮件时的地址
set fast_reply                         # 回复时不会询问收件人和标题
set include                            # 回复时附上前文
set send_charset="utf-8"               # 发送邮件时使用的编码
set editor="code -w -n"                # 编辑器,我使用的是 vscode,'-w'代表文件关闭后再返回命令
set edit_headers                       # 允许编辑 Header (To: CC: Subject:)

颜色

NeoMutt 的颜色的颜色设置十分详细,不同部分的设置格式也不同,部分设置甚至支持正则表达式,具体格式可以看官方文档中 color 这部分。
建议直接 Google neomutt color scheme 来用别人做好的配色,如果想 DIY 则需要考虑不同 Terminal 中显示的颜色会有差异,可以使用下方命令来获得一个 256 色的展示。

for i in {0..255}; do printf '\e[48;5;%dm%3d ' $i $i; (((i+3) % 18)) || printf '\e[0m\n'; done

快捷键绑定

设置快捷键的方式有两种,分别是 bindmacro,区别是 bind 仅能绑定一条命令,而 macro 可以绑定一系列命令。
bind 的格式如下

bind view(s) key(s) function

macro 的格式仅仅多了个注释,并且是可选的

macro view(s) key(s) functions [description]

其中 view(s) 代表了快捷键所绑定的界面,NeoMutt 定义的界面有 alias, attach, browser, compose, editor, generic, index, mix, pager, pgp, postpone, query 和 smime,其中 generic(通用)比较特殊,当一个界面(pager 除外)没有绑定某快捷键时,会使用 generic 中绑定的快捷键作为后备,比较常用的几个界面有 index (索引), pager (页面), attach (附件) 和 compose (撰写)。
key(s) 就是对应的快捷键了,例如设置成 \cD 就表示 Ctrl+D,设置成 gi 就代表按完 (不需要按住) g 后再按 i 即可,当然这时就需要保证 g 没有绑定任何命令。
而其中 function(s) 表示执行的命令,可以设为 noop,顾名思义表示『不作任何事情』,从而达到取消某个快捷键绑定的效果。
详细的按键列表可以看这里,而关于详细的 function 列表可以看这里
由于强迫症的原因,我先把所有默认的 bind 和 macro 都设置成了 noop,然后又根据自己的需求重新设置了 bind 和 macro,其中 bind 由于又简单又长就不列出来了,只列一下我使用的 macro:

macro index a ":set confirmappend=no delete=yes<Return><tag-prefix><save-message>=Archive<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to Archive mailbox"
macro index i ":set confirmappend=no delete=yes<Return><tag-prefix><save-message>=INBOX<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to INBOX mailbox"
macro index d ":set confirmappend=no delete=yes<Return><tag-prefix><save-message>=Trash<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to Trash mailbox"
macro pager a ":set confirmappend=no delete=yes<Return><save-message>=Archive<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to Archive mailbox"
macro pager i ":set confirmappend=no delete=yes<Return><save-message>=INBOX<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to INBOX mailbox"
macro pager d ":set confirmappend=no delete=yes<Return><save-message>=Trash<Return><sync-mailbox>:set confirmappend=yes delete=ask-yes<Return>" "Move message to Trash mailbox"
macro attach s "<save-entry><bol>~/Downloads/<eol><Return>" "Save attachment to ~/Downloads/"
macro compose K "<attach-key>0xXXXXXXXXXXXXXXXX<Return><Return>" "Attach my gpg key"
macro index,pager \cB "<pipe-message> urlscan<Enter>" "call urlscan to extract URLs out of a message"
macro attach,compose \cB "<pipe-entry> urlscan<Enter>" "call urlscan to extract URLs out of an attachment"
macro index,pager \` "<sync-mailbox>"
bind index,pager g noop
macro index,pager gi "<change-folder>+INBOX<Return>" "Switch to INBOX folder"
macro index,pager ga "<change-folder>+Archive<Return>" "Switch to Archive folder"
macro index,pager gs "<change-folder>+Sent<Return>" "Switch to Sent folder"
macro index,pager gd "<change-folder>+Drafts<Return>" "Switch to Drafts folder"
macro index,pager gt "<change-folder>+Trash<Return>" "Switch to Trash folder"
macro index,pager gl "<change-folder>+Local<Return>" "Switch to Local folder"
bind index,pager gv sidebar-toggle-visible

注意其中 index 和 pager 所绑定的 a、i、d 所执行的命令有着细微的差别,主要是为了可以配合其他快捷键,以便在 index 界面时允许选中多个邮件进行操作。

命令行发送邮件

echo "这是正文" | neomutt -s "这是主题"  -a 附件.jpg receiver@example.com

或者直接将文件的内容作为正文发送

neomutt -s "这是主题"  -a 附件.jpg receiver@example.com < 正文.txt

总结

Linux 下的邮件客户端一直是一个让我比较头疼的问题,要么功能太过简陋(例如不支持GPG),要么界面丑陋并且 Overengineering,要么闭源。而 Mutt 肯定说不上完美,只是它提供了我所需要的功能,又不会有着多余的功能(例如日历),也不会出现一堆难看的图标之类的,而且用久了会发现,这玩意儿的效率还确实不错。