我开始在我的笔记本电脑上使用 EndeavourOS+i3WM 以及 Emacs 了。刚开始使用 Emacs,实在是被它的能力上限所折服,巴不得所有事情都让它来做 —— 例如邮件收发。

关于邮箱收发,我最终决定使用这样的邮件客户端流:Mu4e 客户端+mbsync。

首先需要安装以下内容:

bash
1
2
sudo pacman -S isync msmtp msmtp-mta pass
yay -S mu

其中,isync 用于 IMAP 同步,mu 是邮件索引器,msmtp 是 SMTP 客户端用于发送,pass 则是密钥管理器。

配置邮件同步,需要创建 ~/.mbsyncrc

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Create Both
Expunge Both
SyncState *

# === Gmail ===
IMAPAccount gmail
HOST imap.gmail.com
User 邮箱地址
PassCmd "pass show email/gmail"
TLSType IMAPS
AuthMechs LOGIN

IMAPStore gmail-remote
Account gmail

MaildirStore gmail-local
Path ~/Mail/Gmail/
Inbox ~/Mail/Gmail/Inbox
SubFolders Verbatim

Channel gmail
Far :gmail-remote:
Near :gmail-local:
Patterns *

在终端初始化目录: mkdir -p ~*Mail*Gmail

同步前需要生成 GPG 密钥:

bash
1
2
3
4
gpg --full-generate-key
gpg --list-keys
pass init <GPG ID 或者最后 8 位>
pass insert email/gmail

最后一个命令是交互式的,需要自行粘贴邮箱的密码。对于主流的邮箱,需要输入应用密码,Gmail 和 iCloud 都是这样。

使用 mbsync gmail 来测试单个邮箱的连接。全部一起连接用 mbsync -a

初始化 mu

我们需要初始化 mu,告诉它邮件都存放在哪里、哪些是我们的邮箱地址:

bash
1
2
3
mu init --maildir=~/Mail \
--my-address=邮箱地址 \
--my-address=另一个邮箱地址

最后建立索引:

bash
1
mu index

将我校邮箱加进来

CUNY 用的是微软的服务,并且关闭了基本认证,必须使用 OAuth2。

我最初的方案是,弄一个脚本处理 OAuth2 握手并自动刷新令牌,也就是 mutt_oauth2.py 脚本(见 neomutt)。这个脚本需要被伪装成 Mozilla Thunderbird,来绕过应用注册审批。

bash
1
2
3
mkdir -p ~/.mbsync
curl -L -o ~/.mbsync/mutt_oauth2.py https://raw.githubusercontent.com/neomutt/neomutt/master/contrib/oauth2/mutt_oauth2.py
chmod -x ~/.mbsync/mutt_oauth2.py

然后这么调用:

bash
1
python ~/.mbsync/mutt_oauth2.py ~/.mbsync/cuny_token --verbose --authorize --provider microsoft --client-id 9e5f94bc-e8a4-4e73-b8be-63364c29d753 --authflow authcode --email <你的邮箱地址>

出现 Client Secret 时直接按回车,复制终端里给的 URL,最好是在无痕浏览器里登录。登录完会跳转到一个页面,赶紧复制 URL 下来,否则过几秒会跳转到警告页面。这个 URL 里有个 code 参数,复制下来(注意后面的 session_state 参数,删掉),输入到终端里……

然后你会收到 User is authorized but not connected 的消息。

为什么会这样呢?我们的 OAuth2 配置是没有问题的,但是 CUNY IT 部门把邮箱的 IMAP 和 POP 协议都禁用、不让用在应用上。

解决方法是,用 DavMail 在本地机器开启一个 IMAP 端口,mbsync 会连接到它,发送的请求会被转换成 Microsoft EWS 协议,也是 Outlook 网页版和官方客户端使用的协议。DavMail 会向微软的服务器请求数据,并伪装成 IMAP 返回给 mbsync。

创建 ~/.davmail.properties

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
davmail.mode=O365Manual
davmail.uri=https://outlook.office365.com/EWS/Exchange.asmx
davmail.imapPort=1143
davmail.smtpPort=1025
davmail.ldapPort=1389
davmail.popPort=1110
davmail.bindAddress=127.0.0.1
davmail.oauth.clientId=d3590ed6-52b3-4102-aeff-aad2292ab01c
davmail.oauth.redirectUri=urn:ietf:wg:oauth:2.0:oob
davmail.tokenFilePath=davmail.properties
davmail.ssl.nosecurecaldav=false
log4j.rootLogger=WARN
davmail.logFilePath=/var/log/davmail.log
davmail.keepDelay=30

davmail.oauth.clientId 里的内容是微软 Outlook 官方客户端的 ID。
davmail ~/.davmail.properties 启动,它会出现一个 URL,也是建议用无痕浏览器使用。唯一的区别是,这次登录后会一直卡着,一定要在登录前打开 Inspect 的 Network 标签页,里面会有一个 302 状态码的请求,location 值里的 URL 有个 code 参数,取出来粘贴到 DavMail 里。

在另一个终端里运行 mbsync:

bash
1
mbsync cuny

发送邮件

我们需要使用 msmtp 这个外部程序来发信。

如果你有着需要使用 DavMail 才能连接上的邮箱地址,msmtp 也需要通过 DavMail 发送邮件。创建 ~/.msmtprc

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
defaults
auth on
tls off
tls_starttls off
logfile ~/.msmtp.log

account cuny
host 127.0.0.1
port 1025
from 邮箱地址
user 邮箱地址
password "随便写"

account gmail
host smtp.gmail.com
port 587
auth on
tls on
tls_starttls on
user 邮箱地址
from 邮箱地址
passwordeval "pass show email/gmail"
tls_trust_file /etc/ssl/certs/ca-certificates.crt

设置文件的权限:

bash
1
chmod 600 ~/.msmtprc

你可以这样测试发送:

bash
1
echo "This is a test email from msmtp via DavMail." | msmtp --debug --account=发的账户 另一个收的邮箱

配置 Emacs

现在我们的邮件系统由三个组件协同工作:

  • mbsync 负责收
  • mu4e 负责界面和索引
  • msmtp 负责发

我的方案是,在一个配置文件内写好所有的邮箱账号信息,例如这样:

lisp
1
2
3
4
5
6
7
8
9
(:name     "CUNY"                           ; 显示名称
:email "邮箱地址"
:fullname "blabla" ; 发件人名
:maildir "CUNY" ; ~/Mail/ 下的文件夹
:inbox "Inbox" ; 收件箱路径
:sent "Sent" ; 已发送
:drafts "Drafts" ; 草稿
:trash "Trash" ; 垃圾箱
:archive "Archive") ; 归档

多账号就是一个列表: (账号1 账号2 ...)

为了让 mu4e 支持多账号,需要写一个函数自动生成 context。

lisp
1
2
3
4
5
6
7
8
9
(defun my/email--make-context (account)
(make-mu4e-context
:name (plist-get account :name)
:match-func (lambda (msg)
(string-prefix-p "/CUNY" (mu4e-message-field msg :maildir)))
:vars `((user-mail-address . ,(plist-get account :email))
(mu4e-sent-folder . "/CUNY/Sent")
(mu4e-drafts-folder . "/CUNY/Drafts")
...)))

mu4e 很大,建议用 with-eval-after-load 延迟配置:

lisp
1
2
3
(with-eval-after-load 'mu4e
(setq mu4e-maildir "~/Mail")
(setq mu4e-get-mail-command "mbsync -a"))

也可以使用 --read-envelope-from 让 msmtp 自动从邮件头的 From: 字段选择正确的账号发送:

lisp
1
2
3
(setq sendmail-program "/usr/bin/msmtp")
(setq message-send-mail-function 'message-send-mail-with-sendmail)
(setq message-sendmail-extra-arguments '("--read-envelope-from"))