这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

Emacs 配置

1 - RSS

阅读 RSS 使用的是 elfeed,下面两个插件搭配起来使用,体验会更流程:

  • elfeed-org 使用 org 文件来定义 feeds 列表,它可以很方便进行打标签,重命名
  • elfeed-dashboard 把定义的 feeds 列表按标签展示
  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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
(use-package elfeed
  :custom ((elfeed-use-curl t)
           (elfeed-db-directory "~/Downloads/elfeed/")
           (elfeed-curl-timeout 20)
           (elfeed-curl-extra-arguments `("-x" ,socks-proxy)))
  :bind (:map elfeed-show-mode-map
         ("8" . my/elfeed-toggle-star)
         ("9" . my/elfeed-show-images)
         ("g" . elfeed-show-refresh)
         :map elfeed-search-mode-map
         ("SPC" . scroll-up-command)
         ("DEL" . scroll-down-command)
         ("f" . my/elfeed-search-by-title)
         ("b" . my/elfeed-browse)
         ("8" . my/elfeed-toggle-star))
  :init
  (progn
    (defun my/elfeed-browse ()
      (interactive)
      (let ((entries (elfeed-search-selected)))
        (elfeed-untag entries 'unread)
        (mapc #'elfeed-search-update-entry entries))
      (elfeed-search-browse-url))

    (defun my/elfeed-add-title-tag (entry)
      (when-let* ((feed (elfeed-entry-feed entry))
                  (title (cl-destructuring-bind (&key title &allow-other-keys)
                             (elfeed-feed-meta feed)
                           title)))
        (elfeed-tag entry (intern title))))

    (add-hook 'elfeed-new-entry-hook 'my/elfeed-add-title-tag 1)

    (defun my/elfeed-hook ()
      ;; (variable-pitch-mode)
      ;; (setq-local shr-inhibit-images nil)
      (setq-local line-spacing 0.5)
      (setq-local shr-width 85))

    (defun my/elfeed-search-print-entry (entry)
      (let* ((date (elfeed-search-format-date (elfeed-entry-date entry)))
             (title (or (elfeed-meta entry :title) (elfeed-entry-title entry) ""))
             (title-faces (elfeed-search--faces (elfeed-entry-tags entry)))
             (feed (elfeed-entry-feed entry))
             (feed-title (when feed
                           (or (elfeed-meta feed :title) (elfeed-feed-title feed))))
             (tags (seq-filter
                    (lambda (tag) (not (string-equal feed-title tag)))
                    (mapcar #'symbol-name (elfeed-entry-tags entry))))
             (tags-str (mapconcat
                        (lambda (s) (propertize s 'face 'elfeed-search-tag-face))
                        tags ","))
             (title-width (- (window-width) 10 elfeed-search-trailing-width))
             (title-column (elfeed-format-column
                            title (elfeed-clamp
                                   elfeed-search-title-min-width
                                   title-width
                                   elfeed-search-title-max-width)
                            :left)))
        (insert (propertize date 'face 'elfeed-search-date-face) " ")
        (insert (propertize title-column 'face title-faces 'kbd-help title) " ")
        (when feed-title
          (insert (propertize feed-title 'face 'elfeed-search-feed-face) " "))
        (when tags
          (insert "(" tags-str ")"))))

    (setq elfeed-search-print-entry-function 'my/elfeed-search-print-entry))

  :hook ((elfeed-search-mode elfeed-show-mode) . my/elfeed-hook)
  :config
  (progn
    (setq elfeed-search-filter "@1-months-ago +unread #100")

    (evil-add-hjkl-bindings elfeed-show-mode-map)
    (evil-add-hjkl-bindings elfeed-search-mode-map)

    (defun my/elfeed-clear-queue ()
      "Sometime elfeed update will end with pending task, which popup at minibuffer, this fix the annoying message"
      (interactive)
      (setq elfeed-curl-queue-active 0))

    (defun my/elfeed-show-images ()
      (interactive)
      (let ((shr-inhibit-images nil))
        (elfeed-show-refresh)))
    ;;functions to support syncing .elfeed between machines
    ;;makes sure elfeed reads index from disk before launching
    (defun my/elfeed-open-db-and-load ()
      "Wrapper to load the elfeed db from disk before opening"
      (interactive)
      (elfeed-db-load)
      (elfeed)
      (elfeed-search-update--force))

    ;;write to disk when quiting
    (defun my/elfeed-close-db-and-save ()
      "Wrapper to save the elfeed db to disk before burying buffer"
      (interactive)
      (elfeed-db-save)
      ;; (quit-window)
      )

    (defun my/elfeed-toggle-star ()
      (interactive)
      (let* ((entry (or elfeed-show-entry
                        (first (elfeed-search-selected))))
             (tag (intern "starred"))
             (taggged (elfeed-tagged-p tag entry)))
        (if taggged
            (elfeed-untag entry tag)
          (elfeed-tag entry tag))
        (message "Starred: %s" (not taggged))))

    (defun my/elfeed-search-star ()
      (interactive)
	  (let ((tag (intern "starred"))
            (entries (elfeed-search-selected)))
	    (cl-loop for entry in entries do (elfeed-tag entry tag))
	    (mapc #'elfeed-search-update-entry entries)
	    (unless (use-region-p) (forward-line))))

    (defun my/elfeed-search-unstar ()
      "Remove starred tag from all selected entries."
      (interactive)
	  (let ((tag (intern "starred"))
            (entries (elfeed-search-selected)))
	    (cl-loop for entry in entries do (elfeed-untag entry tag))
	    (mapc #'elfeed-search-update-entry entries)
	    (unless (use-region-p) (forward-line))))

    (setq my/elfeed-export-dir (expand-file-name "~/Sync/"))
    (defun my/elfeed-export (dir)
      (interactive (list (read-directory-name "Export dir: " my/elfeed-export-dir)))
      (require 'f)
      (let* ((sf (elfeed-search-parse-filter "+starred"))
	         (uf (elfeed-search-parse-filter "-unread"))
	         (starred-entries '())
	         (read-entries '())
	         (hash-table (make-hash-table))
             (output (expand-file-name (format-time-string "elfeed-%Y-%m-%d.el" (current-time))
                                       dir)))
        (with-elfeed-db-visit (entry feed)
          (let ((title-and-link  (cons (elfeed-entry-title entry)
                                       (elfeed-entry-link entry))))
	        (when (elfeed-search-filter sf entry feed)
	          (add-to-list 'starred-entries title-and-link))
	        (when (elfeed-search-filter uf entry feed)
	          (add-to-list 'read-entries title-and-link))))

        (puthash :starred starred-entries hash-table)
        (puthash :read read-entries hash-table)
        (f-write-text (prin1-to-string hash-table) 'utf-8 output)

        (message "Export to %s. starred: %d, read: %d" output (length starred-entries) (length read-entries))))

    (defun my/elfeed-import (f)
      (interactive (list (read-file-name "Backup file: " my/elfeed-export-dir)))
      (require 'f)
      (let* ((hash-table (read (f-read-text f)))
             (starred-entries (thread-last (gethash :starred hash-table)
                                           (mapcar 'cdr)))
             (read-entries (thread-last (gethash :read hash-table)
                                        (mapcar 'cdr))))
        (with-elfeed-db-visit (entry feed)
          (let* ((link (elfeed-entry-link entry)))
            (when (member link starred-entries)
              (elfeed-tag entry (intern "starred")))
            (when (member link read-entries)
              (elfeed-untag entry (intern "unread")))))

        (message "Import starred: %d, read: %d" (length starred-entries) (length read-entries))))

    (defun my/elfeed-search-by-title (title)
      (interactive (list (or (when-let* ((entry (car (elfeed-search-selected)))
                                         (feed (elfeed-entry-feed entry)))
                               (or (cl-destructuring-bind (&key title &allow-other-keys)
                                       (elfeed-feed-meta feed)
                                     title)
                                   (elfeed-feed-title feed)))
                             (read-from-minibuffer "Feed Title: "))))
      (unwind-protect
          (let ((elfeed-search-filter-active :live))
            (setq elfeed-search-filter (concat "+" title)))
        (elfeed-search-update :force)))

    (custom-set-faces
     '(elfeed-search-unread-title-face ((((class color) (background light)) (:foreground "#000" :weight normal :strike-through nil))
                                        (((class color) (background dark)) (:foreground "#fff" :weight normal :strike-through nil))))

     '(elfeed-search-title-face ((((class color) (background light)) (:foreground "grey" :strike-through t))
							     (((class color) (background dark)) (:foreground "grey" :strike-through t)))))

    ;; face for starred articles
    (defface elfeed-search-starred-title-face
      '((t :foreground "#f77" :strike-through nil))
      "Marks a starred Elfeed entry.")

    (push '(starred elfeed-search-starred-title-face) elfeed-search-face-alist)))

(use-package elfeed-dashboard
  :ensure nil
  :commands (elfeed-dashboard)
  :bind (:map elfeed-dashboard-mode-map
         ("/" . my/feed-choose-by-tag)
         :map elfeed-search-mode-map
         ("/" . my/feed-choose-by-tag))
  :config
  (setq elfeed-dashboard-file (no-littering-expand-etc-file-name "elfeed-dashboard.org"))
  ;; Disable this update, it cost too much cpu when there are many feeds
  ;; Update feed counts on elfeed-quit
  ;; (advice-add 'elfeed-search-quit-window :after #'elfeed-dashboard-update-links)

  (defun my/feed-choose-by-tag ()
    (interactive)
    (ivy-read "Tag: " (elfeed-db-get-all-tags)
              :action (lambda (tag)
                        (elfeed-dashboard-query (format "+unread +%s" tag))))))

(use-package elfeed-org
  :ensure nil
  :custom ((rmh-elfeed-org-files `(,(no-littering-expand-etc-file-name "elfeed-feeds.org"))))
  :hook (elfeed-dashboard-mode . elfeed-org)
  :init
  (progn
    (defun my/reload-org-feeds ()
      (interactive)
      (rmh-elfeed-org-process rmh-elfeed-org-files rmh-elfeed-org-tree-id))
    (advice-add 'elfeed-dashboard-update :before #'my/reload-org-feeds)))

下面阐述 elfeed 核心配置:

  • elfeed-curl-extra-arguments 可以配置 curl 的参数,比较常用的是配置代理
  • elfeed 默认使用 shr 展示,在加载图片是会卡住,可通过设置 shr-inhibit-imagesnil 来禁用图片,之后通过自定义函数 my/show-feed-images 来开启图片
  • 为了方便记录重要文章,定义 my/elfeed-toggle-star 函数来实现「标星」效果,并可以使用 +starred 来搜索
  • 默认 elfeed 列表行间距太小,可以通过 (setq-local line-spacing 0.3) 调整
  • elfeed 的数据库存放在本地,可以把这个目录防止云盘的同步目录里。我这里实现了两个函数,来对指定的 tag 进行导出/导入,这样相当于只备份元数据,更轻量些。

    • 导出时主要有两个 tag,一个是 +starred ,表示加星的文章;另一个是 -unread ,表示已读的。

查询语法

查询语句含义
@6-months-ago +unread6 个月内的未读文章
-unread +youtube #10已读的与 youtube 相关的前 10 篇文章
+emacs =http://example.org/feed指定 feed 内,与 emacs 有关的文章

常用快捷键

elfeed-search

  • s 在 minibuffer 重新输入搜索词
  • y yank 当前行文章的 URL,并去掉 unread 标签
  • b 调用外部浏览器打开所选行文章
  • g 刷新当前 feed 列表
  • G 重新抓取 feed 记录

elfeed-show

  • TAB 切换到下一个超链接处
  • g 刷新
  • b 调用外部浏览器打开文章

2 - eglot

eglot

3 - 电子邮件

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 配置多账户
  • 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 上的默认客户端。

  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 作为系统默认邮件客户端效果示意图