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

返回本页常规视图.

Emacs 配置

编译

Ubuntu 依赖

sudo apt update
sudo apt build-dep -y emacs

macOS 依赖

brew install mailutils libxml2

编译步骤

# git clone https://git.savannah.gnu.org/git/emacs.git

./autogen.sh
./configure --prefix $HOME/.x
make && make install

主题

当 Emacs 运行在 Tmux 中时,如果颜色显示不正确,需要在 .tmux.conf 中配置终端颜色和模式,关键是设置 default-terminal 以支持 256 色,推荐使用 tmux-256color 以获得最佳兼容性。

set -g default-terminal "tmux-256color"
set -as terminal-overrides ",xterm-256color:Tc"

然后在 Emacs 中启用 True Color 支持(按需):

;; Enable True Color support in Emacs when running inside Tmux
(when (getenv "TMUX")
  (unless (display-graphic-p)
    (add-to-list 'default-frame-alist '(background-mode . dark))
    (set-terminal-parameter nil 'background-mode 'dark)
    (setq term-term-name "xterm-256color")
    (setenv "COLORTERM" "truecolor")))

配置说明:

  • set -g default-terminal "tmux-256color" :此选项告诉 tmux 默认的终端模式。使用 tmux-256color 比 screen-256color 更能正确处理表情符号和真彩色。
  • set -ag terminal-overrides ...:Tc :这个设置强制 tmux 内部使用真彩色 (24-bit color),这对于 Neovim 或现代终端主题至关重要。

配置完成后,请执行 tmux source-file ~/.tmux.conf 重新加载配置。

1 - RSS

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

  • elfeed-org 使用 org 文件来定义 feeds 列表,它可以很方便进行打标签,重命名
  • elfeed-dashboard 把定义的 feeds 列表按标签展示
(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 安装。

brew install offlineimap mu

mu4e 在 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]
... 大致同上,这里不在赘述

上面的配置比较直观,也有相应注释,这里重点介绍如何设置 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 这个函数来获取密码,其实现如下:

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>&1
echo -n 'Begin sync: '
date -Iseconds

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

mu4e 配置

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

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 对中文支持比较弱,只能搜索两个字的词,比如搜「雷峰塔」就不行,可参考这里的解决方案。

下面是我用的配置:

(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 中的默认客户端

    (setq mail-user-agent 'mu4e-user-agent)
  2. 打开 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 内,处于假死的状态。

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