电子邮件

https://img.alicdn.com/imgextra/i2/581166664/O1CN01Zmz1yL1z6A2v1sKsI_!!581166664.jpg

阅读邮件我使用的软件主要有

  • offlineimap 使用 imap 协议,以 Maildir 格式来同步邮件;类似的软件有 Mbsync
  • mu4e mu 的 Emacs 插件,mu 可以对 Maildir 格式的邮件建索引,便于搜索;类似的软件有 Gnus

在 macOS 上,这两个软件都可以直接使用 brew 安装。

1
brew install offlineimap mu

mu4e 在 mu 的安装目录内,可以通过 brew ls mu 查看,把它添加到 Emacs 的 load-path 中:

1
2
3
(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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[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]
... 大致同上,这里不在赘述

上面的配置比较直观,也有相应注释,这里重点介绍如何设置 remotepassevalnametrans

GPG Auth Source 配置密码

Emacs 中使用 Auth Source 来管理密码,它相当于一个接口层,可以对接多个存储后端,netrc 是最常见的后端,除此之外,还支持 JSON、Secret Service API、pass。很多命令,比如 wgetcurlgit 等都支持从 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 这个函数来获取密码,其实现如下:

1
2
3
4
5
6
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 命令查看邮件服务器中的内容,确认下需要同步哪些目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ 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= 转成了那串字母,这样两边就能正确同步邮件状态了,下面是本地的状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 卡住。这是目前我使用的同步脚本:

1
*/30 * * * * ~/Sync/bin/syncmail.sh >> /tmp/sync-mail.log 2>&1
1
2
3
4
5
6
echo -n 'Begin sync: '
date -Iseconds

# brew install coreutils
# 安装 gtimeout,防止同步时间过长
gtimeout -s KILL 300 offlineimap -o

mu4e 配置

使用 offlineimap 将同步邮件到本地后,在使用前需要使用 mu 建索引,参考命令:

1
2
3
4
5
export XAPIAN_CJK_NGRAM=true
# 只需要执行一次 init,可以指定多个邮件地址
mu init --my-address [email protected] --my-address [email protected] -m ~/.mail
# index 在每次收取邮件后都需要执行,mu4e 可以配置自动执行
mu index

XAPIAN_CJK_NGRAM 环境变量主要是开启对 CJK 的分词,方便用中文搜索邮件。不过 mu 使用的 Xapian 对中文支持比较弱,只能搜索两个字的词,比如搜「雷峰塔」就不行,可参考这里的解决方案。

下面是我用的配置:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
(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 配置多账户
  • 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 上的默认客户端。

  1. 首先把 mu4e 设置为 Emacs 中的默认客户端

    1
    
    (setq mail-user-agent 'mu4e-user-agent)
  2. 打开 Script Editor,输入以下代码后,导出为「应用」

    1
    2
    3
    4
    5
    
    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 内,处于假死的状态。

    https://img.alicdn.com/imgextra/i3/581166664/O1CN013l4ooY1z6A4aTqcko_!!581166664.jpg
    创建 Script 应用
  3. 打开 Mail.app 应用,进入设置,将上面创建的应用设置为默认邮件阅读器

    https://img.alicdn.com/imgextra/i3/581166664/O1CN016ynUV21z6A4ksNeVG_!!581166664.jpg
    在 Mail 中修改默认阅读器

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

https://img.alicdn.com/imgextra/i2/581166664/O1CN01INrhSm1z6A4jCMVPG_!!581166664.jpg
mu4e 作为系统默认邮件客户端效果示意图