电子邮件

阅读邮件我使用的软件主要有
- offlineimap 使用 imap 协议,以 Maildir 格式来同步邮件;类似的软件有 Mbsync
- mu4e mu 的 Emacs 插件,mu 可以对 Maildir 格式的邮件建索引,便于搜索;类似的软件有 Gnus
在 macOS 上,这两个软件都可以直接使用 brew 安装。
brew install offlineimap mumu4e 在 mu 的安装目录内,可以通过 brew ls mu 查看,把它添加到 Emacs 的 load-path 中:
(let* ((mu4e-dir "/opt/homebrew/opt/mu/share/emacs/site-lisp/mu/mu4e/"))
(when (file-exists-p mu4e-dir)
(add-to-list 'load-path mu4e-dir)))这样 mu4e 就算安装成功了。
offlineimap 配置
配置文件推荐使用 xdg 标准的 ~/.config/offlineimap/config ,尽量避免直接在家目录创建 ~/.offlineimaprc 。
[general]
# 默认开启的账户,可以是多个
accounts = ljc, outlook
# 定义额外的 python 脚本,里面有获取密码的函数
pythonfile = ~/.config/offlineimap/pass.py
# 开始配置第一个账户
[Account ljc]
# 一个账户主要包含 local 与 remote 这两个配置项
localrepository = LocalLJC
remoterepository = RemoteLJC
# 可选配置项,邮件同步时间:这里只同步 365 天之内的
# 需要注意,一些邮件提供商也存在同步限制,比如 QQ 邮箱默认只同步 30 天内的数据,可以在设置中修改
maxage = 365
[Repository LocalLJC]
type = Maildir
localfolders = ~/.mail/ljc
# 需要与 RemoteLJC 里面的 nametrans 配置起来使用,解释见正文
nametrans = lambda folder: re.sub('^=', '&UXZO1mWHTvZZOQ-/', folder)
[Repository RemoteLJC]
nametrans = lambda folder: re.sub('^&UXZO1mWHTvZZOQ-\/', '=', folder)
# 配置 ssl 证书,macOS 下安装 openssl 即可
sslcacertfile = /usr/local/etc/[email protected]/cert.pem
# 过滤掉一些目录,不进行同步
folderfilter = lambda f: 'kK5O9l9SaGM' not in f and '&UXZO1mWHTvZZOQ-' != f
# 下面是 IMAP 的配置,我这里是使用的腾讯企业邮箱
type = IMAP
remotehost = imap.exmail.qq.com
remoteport = 993
remoteuser = [email protected]
# 调用 pass.py 中的函数获取密码
remotepasseval = get_password_emacs("imap.exmail.qq.com", "[email protected]", "993")
# 也可通过下面配置直接设置明文密码,不推荐
# remotepass = YOUR_MAIL_PASSWORD
[Account outlook]
... 大致同上,这里不在赘述上面的配置比较直观,也有相应注释,这里重点介绍如何设置 remotepasseval 与 nametrans 。
GPG Auth Source 配置密码
Emacs 中使用 Auth Source 来管理密码,它相当于一个接口层,可以对接多个存储后端,netrc 是最常见的后端,除此之外,还支持 JSON、Secret Service API、pass。很多命令,比如 wget、curl、git 等都支持从 netrc 读取密码,因此这里只介绍它的用法:
machine mymachine login myloginname password mypassword port myport
上面是 netrc 文件的通用格式,下面是我邮箱的相关配置:
# cat ~/.authinfo machine imap.exmail.qq.com login [email protected] password 123456 port 993 machine smtp.exmail.qq.com login [email protected] password 123456 port 465
993 是 imap 端口,接受邮件用;465 是 smtp 端口,发送邮件用。为了安全,还可以对 netrc 使用 GPG 加密,Emacs 会自动解密读取。
(setq auth-sources '("~/.authinfo.gpg" "~/.authinfo" "~/.netrc"))
Emacs 默认从上面几个地方找文件,读者可按需修改。
我上面对配置中使用了 get_password_emacs 这个函数来获取密码,其实现如下:
def get_password_emacs(machine, login, port):
s = "machine %s login %s password ([^ ]*) port %s\n" % (
machine, login, port)
p = re.compile(s)
authinfo = os.popen("gpg -q --no-tty -d ~/.config/authinfo.gpg").read()
return p.search(authinfo).group(1)nametrans 文件名转换
配置好密码后,就可以进行邮件同步了。但是在同步前,可以通过 offlineimap --info 命令查看邮件服务器中的内容,确认下需要同步哪些目录
$ offlineimap --info
OfflineIMAP 7.3.3
Licensed under the GNU GPL v2 or any later version (with an OpenSSL exception)
imaplib2 v2.101 (bundled), Python v2.7.17, OpenSSL 1.1.1i 8 Dec 2020
imaplib2: 2.101 (bundled)
Remote repository 'RemoteLJC': type 'IMAP'
Host: imap.exmail.qq.com Port: None SSL: True
Establishing connection to imap.exmail.qq.com:993 (RemoteLJC)
Server supports ID extension.
Server welcome string: * OK [CAPABILITY IMAP4 IMAP4rev1 ID AUTH=PLAIN AUTH=LOGIN NAMESPACE] QQMail IMAP4Server ready
Server capabilities: ('IMAP4', 'IMAP4REV1', 'XLIST', 'MOVE', 'IDLE', 'XAPPLEPUSHSERVICE', 'NAMESPACE', 'CHILDREN', 'ID', 'UIDPLUS')
folderfilter= lambda f: 'kK5O9l9SaGM' not in f and '&UXZO1mWHTvZZOQ-' != f
nametrans= lambda folder: re.sub('^&UXZO1mWHTvZZOQ-\/', '=', folder)
Folderlist:
&UXZO1mWHTvZZOQ- (disabled)
&UXZO1mWHTvZZOQ-/clojure -> =clojure
&UXZO1mWHTvZZOQ-/GitHub -> =GitHub
&UXZO1mWHTvZZOQ-/golang -> =golang
Deleted Messages
Drafts
INBOX
Junk
Sent Messages在 Folderlist 下面就是需要同步的目录,腾讯企业邮箱比较奇葩,自定义文件夹前会有一串字母 &UXZO1mWHTvZZOQ- ,为了保证同步下来的目录中没有这一串字母,这里使用 nametrans 配置把它转化为了 = 符号。其他邮箱(比如:Gmail、Yandex)的目录都是比较规整的,这时则不需要设置 namestrans 。
由于 IMAP 的同步是双向的,本地的状态也需要返回给远端,因此在 LocalLJC 中又使用 nametrans 把 = 转成了那串字母,这样两边就能正确同步邮件状态了,下面是本地的状态:
Local repository 'LocalLJC': type 'Maildir'
nametrans= lambda folder: re.sub('^=', '&UXZO1mWHTvZZOQ-/', folder)
Folderlist:
INBOX
Drafts
Junk
=GitHub -> &UXZO1mWHTvZZOQ-/GitHub
Deleted Messages
=golang -> &UXZO1mWHTvZZOQ-/golang
=clojure -> &UXZO1mWHTvZZOQ-/clojure
Sent Messages确认好目录同步无误后,直接运行 offlineimap 就可以同步邮件了。
定期同步
推荐使用 crontab 进行定期同步,虽然说 mu4e 中也提供了 mu4e-get-mail-command 用来配置同步的命令,
但在 Emacs 外同步更安全些,可以防止 Emacs 卡住。这是目前我使用的同步脚本:
*/30 * * * * ~/Sync/bin/syncmail.sh >> /tmp/sync-mail.log 2>&1echo -n 'Begin sync: '
date -Iseconds
# brew install coreutils
# 安装 gtimeout,防止同步时间过长
gtimeout -s KILL 300 offlineimap -omu4e 配置
使用 offlineimap 将同步邮件到本地后,在使用前需要使用 mu 建索引,参考命令:
export XAPIAN_CJK_NGRAM=true
# 只需要执行一次 init,可以指定多个邮件地址
mu init --my-address [email protected] --my-address [email protected] -m ~/.mail
# index 在每次收取邮件后都需要执行,mu4e 可以配置自动执行
mu indexXAPIAN_CJK_NGRAM 环境变量主要是开启对 CJK 的分词,方便用中文搜索邮件。不过 mu 使用的 Xapian 对中文支持比较弱,只能搜索两个字的词,比如搜「雷峰塔」就不行,可参考这里的解决方案。
下面是我用的配置:
(use-package mu4e
:ensure nil
:if (executable-find "mu")
:commands (mu4e)
:bind (:map mu4e-view-mode-map
("9" . scroll-down-command)
("0" . scroll-up-command)
:map mu4e-search-minor-mode-map
("/" . mu4e-search-maildir)
:map mu4e-main-mode-map
("g" . mu4e-update-mail-and-index)
:map mu4e-headers-mode-map
("<backspace>" . scroll-down-command)
("j" . mu4e-headers-next)
("k" . mu4e-headers-prev)
("r" . mu4e-headers-mark-for-read)
("!" . mu4e-headers-flag-all-read)
("f" . mu4e-headers-mark-for-flag))
:custom ((mu4e-headers-fields '((:human-date . 12)
(:flags . 6)
(:from-or-to . 22)
(:thread-subject . nil)))
(mu4e-view-fields '(:from :to :cc :bcc :subject :flags
:date :maildir :mailing-list :tags))
(mu4e-modeline-show-global nil)
(mu4e-hide-index-messages t))
:init
(setq user-mail-address "[email protected]"
user-full-name "Jiacai Liu"
mail-user-agent 'mu4e-user-agent
mu4e-debug t
message-send-mail-function 'smtpmail-send-it
;; https://emacs.stackexchange.com/a/45216/16450
message-citation-line-format "\nOn %a, %b %d, %Y at %r %z, %N wrote:\n"
message-citation-line-function 'message-insert-formatted-citation-line
;; https://github.com/djcb/mu/issues/1798
mm-discouraged-alternatives '("text/html" "text/richtext")
;; mu4e 展示邮件时,使用的时间格式
gnus-article-time-format "%a, %Y-%m-%d %T %z"
gnus-article-date-headers '(user-defined original))
:config
(require 'mu4e-contrib)
(setq mu4e-contexts
(list
(make-mu4e-context
:name "ljc"
:match-func (lambda (msg)
(when msg
(string-prefix-p "/ljc" (mu4e-message-field msg :maildir))))
:vars '((mu4e-sent-folder . "/ljc/Sent")
(mu4e-trash-folder . "/ljc/Trash")
(mu4e-refile-folder . "/ljc/Archive")
(mu4e-drafts-folder . "/ljc/Drafts")
(user-mail-address . "[email protected]")
(smtpmail-smtp-service . 465)
(smtpmail-smtp-user . "[email protected]")
(smtpmail-smtp-server . "smtp.yandex.com")
(smtpmail-stream-type . ssl)))
(make-mu4e-context
:name "yandex"
:match-func (lambda (msg)
(when msg
(string-prefix-p "/yandex" (mu4e-message-field msg :maildir))))
:vars '((mu4e-sent-folder . "/yandex/Sent")
(mu4e-trash-folder . "/yandex/Trash")
(mu4e-refile-folder . "/yandex/Archive")
(mu4e-drafts-folder . "/yandex/Drafts")
(user-mail-address . "[email protected]")
(smtpmail-smtp-user . "[email protected]")
(smtpmail-smtp-service . 465)
(smtpmail-smtp-server . "smtp.yandex.com")
(smtpmail-stream-type . ssl))))
mu4e-compose-complete-only-personal t
mu4e-view-show-addresses t
mu4e-view-show-images nil
mu4e-attachment-dir "~/Downloads"
mu4e-sent-messages-behavior 'sent
mu4e-context-policy 'pick-first
mu4e-compose-context-policy 'ask-if-none
mu4e-compose-dont-reply-to-self t
mu4e-confirm-quit nil
mu4e-headers-date-format "%+4Y-%m-%d"
mu4e-view-html-plaintext-ratio-heuristic most-positive-fixnum
mu4e-update-interval (* 30 60)
;; get mail in external process, like crontab
mu4e-get-mail-command "true"
;; mu4e-get-mail-command "gtimeout 120 offlineimap -o -q -u basic -l /tmp/mu4e.log"
mu4e-compose-format-flowed t
mu4e-completing-read-function 'ido-completing-read
mu4e-bookmarks '((:name "All Inbox"
:query "maildir:/ljc/INBOX"
:key ?i)
(:name "Unread messages"
:query "flag:unread AND NOT flag:trashed"
:key ?u)
(:name "Today's messages"
:query "date:today..now AND NOT flag:trashed"
:key ?t)
(:name "Last 7 days"
:query "date:7d..now AND NOT flag:trashed"
:hide-unread t
:key ?w)
(:name "Flagged"
:query "flag:flagged"
:key ?f)
(:name "Sent"
:query "maildir:/ljc/Sent"
:key ?s)))
(add-to-list 'mu4e-view-actions '("browser" . mu4e-action-view-in-browser) t)
(defun my/mu4e-pre-update-hook ()
(let ((inhibit-message t))
(message "Update and index mu4e at %s" (format-time-string "%D %-I:%M %p"))))
(defun my/mu4e-stop-update-task ()
(interactive)
(when mu4e--update-timer
(cancel-timer mu4e--update-timer)
(setq mu4e--update-timer nil)))
(setq mu4e-update-pre-hook 'my/mu4e-pre-update-hook)
(add-to-list 'mu4e-view-fields :bcc))核心配置主要有以下几个:
mu4e-contexts配置多账户message-send-mail-function配置邮件发送方式为smtpmail-send-it,在进行发送时会利用 authinfo 进行密码查找,用户、服务域名由以下几个变量来控制,更具体可以参考文档:smtpmail-smtp-service端口smtpmail-smtp-user用户名smtpmail-smtp-server服务域名
mu4e-update-interval配置自动同步邮件与建索引间隔mu4e-get-mail-command配置同步邮件的命令mu4e-view-actions增加mu4e-action-view-in-browser,这样在阅读邮件时,按a b在浏览器打开邮件,这对那些只有 HTML 格式的邮件来说比较重要message-citation-line-format定义符合 Gmail 的引用格式,Gmail 会用...将引用隐藏起来gnus-article-time-format设置邮件时间显示格式,具体可参见这里。
mu4e 查询语法
| 查询语句 | 含义 |
|---|---|
date:today..now | 今天的邮件 |
from:jim and not flag:attach | 来自 jim 且没有附件的邮件 |
"maildir:/Sent Items" and rupert | 发送目录内,包含 rupert 的邮件 |
subject:wombat and date:20170601..20170630 | 指定时间内,主题中包含 wombat 的邮件 |
macOS 默认客户端
在网页中点击含有 mailto:[email protected] 的链接时,会调用操作系统默认邮件客户端来处理,这里介绍一种方式来把 mu4e 设置为 macOS 上的默认客户端。
首先把 mu4e 设置为 Emacs 中的默认客户端
(setq mail-user-agent 'mu4e-user-agent)打开 Script Editor,输入以下代码后,导出为「应用」
on open location myurl set text item delimiters to {":"} display notification text item 2 of myurl with title "Emacs Compose" do shell script "/usr/local/bin/emacsclient -c -n --eval '(browse-url-mail \"" & myurl & "\")'" end open location上面代码中,
-c表示创建一个新 frame,-n表示不等待 Emacs Server 的返回,不加这个选项的话,执行这个应用时,会一直驻留在 Dock 内,处于假死的状态。
创建 Script 应用 打开 Mail.app 应用,进入设置,将上面创建的应用设置为默认邮件阅读器

在 Mail 中修改默认阅读器
完成上面三步操作配置就好了,之后点击含有 mailto 的链接时,会自动在当前 active 的 buffer 中打开 mu4e-compose 界面,同时右上角会跳出提示框。
