元々は家でゲームがしたい(小学生のことTAITOのSpace Invaderが大ブームになった)という理由で始めたコンピューターとの付き合いが、いつの間にか人とのCommunicationの手段になったのは、明らかにパソコン通信のせいで、参加していたBBSのChat roomに入り浸るようになってからだったように思う。
Unixを触るようになり、UUCPで自宅に居ながらにして電子メールが使えるようになって以来、数年前までは間違いなくE-Mailこそが他者とのCommunicationの中心にあった。 したがって、筆者の場合、E-Mailの読み書きを簡単にできるようなApplicationが最も重要なツールであった時代が長い。
そのため、メールクライアントは色々使ってきた。それなりの期間(最低でも半年以上)利用したツールは
と、幾つもあった。しかし、個人的な要件を満たすMail Readerはあまりなかった。動作するPlatformが限られていたり、表示が望み通りにならなかったり、EoLになったり…
というわけで、Sylpheedを卒業して次のMail Readerに移行することにした。
今回の選択はmu4eである。 mu4eはmuコマンドを利用したメールの検索機能を利用して、Emacs上でMailの読み書きを行うためのツールである。
mu4eを利用してMailを処理するためにはいくつかの方法があるが、今回は以下の構成で行くことにした。
mbsyncは、RemoteのMDA(IMAP Server)からメールを手元に持ってくるツールである。 Mailboxの同期をEmacs上で行わないのでメールの取得とメールの処理を独立して行えるのがメリットである。 なお、mu4eはメールボックスがありさえすれば良いので、mbsyncである必要はない。imapsyncなどいくつかのツールがあるので、好みのものを選べば良いだろう。
今回は、複数のアカウントをまとめて管理する前提で設定を行う。 設定の詳細は、man mbsyncなどしてman pageを見ること。 ここでは、うちで動作した設定を改変して記述してある。 名前付け替えなどいくつかトリッキーなことをしているので、わからなければManualを読むべきである。
なお、本記事においては、動作確認なども兼ねているためpasswordをrawで記載しているが、これはGPGなどを利用して暗号化するべきである。 暗号化の方法などはGoogleで検索すれば大量に出てくるので、そちらを参照のこと。
# # .mbsyncrc - configuration for mbsync. # ##### General Configuration Create Both Expunge Both CopyArrivalDate yes Sync All SyncState * ##### sample@example.com (Dovecot) IMAPAccount sample_example Host dovecot.example.com Port 993 User sample@example.com Pass Ultra-Secret SSLType IMAPS ### IMAP Store configuration IMAPStore mls_seirios-remote Account mls_seirios ### Maildir Store configuration MaildirStore sample_example-local SubFolders Verbatim Path ~/Maildir/sample_example/ Inbox ~/Maildir/sample_example/Inbox ### Channel Configuration Channel sample_example-local Far :sample_example-remote: Near :sample_example-local: Patterns * ##### sample@icloud.com (Apple Mail) IMAPAccount sample_icloud Host imap.mail.me.com Port 993 User sample@icloud.com Pass Apple-Application-password-set-at-Apple SSLType IMAPS AuthMechs PLAIN ### IMAP Store configuration IMAPStore sample_icloud-remote Account sample_icloud ### Maildir Store configuration MaildirStore sample_icloud-local SubFolders Verbatim Path ~/Maildir/sample_icloud/ Inbox ~/Maildir/sample_icloud/Inbox ### Channel Configuration Channel sample_icloud-base Far :sample_icloud-remote: Near :sample_icloud-local: Patterns * !Sent !"Sent Messages" !Spam !"Junk" !Trash !"Deleted Messages" Channel sample_icloud-base-sent Far :sample_icloud-remote:"Sent Messages" Near :sample_icloud-local:Sent Channel sample_icloud-base-spam Far :sample_icloud-remote:"Junk" Near :sample_icloud-local:Spam Channel sample_icloud-base-trash Far :sample_icloud-remote:"Deleted Messages" Near :sample_icloud-local:Trash Group sample_icloud Channel sample_icloud-base Channel sample_icloud-sent Channel sample_icloud-spam Channel sample_icloud-trash ##### sample@gmail.com (Gmail) ##### もし、GmailのIMAP Folderが日本語だった場合、Gmailから表示を英語モードにしておくこと。日本語だとうまくいかないことがある IMAPAccount sample_gmail Host imap.gmail.com Port 993 User sample@gmail.com Pass Gmail-Application-password-set-at-Gmail SSLType IMAPS AuthMechs PLAIN ### IMAP Store configuration IMAPStore sample_gmail-remote Account sample_gmail ### Maildir Store configuration MaildirStore sample_gmail-local SubFolders Verbatim Path ~/Maildir/sample_gmail/ Inbox ~/Maildir/sample_gmail/Inbox ### Channel Configuration Channel sample_gmail-base Far :sample_gmail-remote: Near :sample_gmail-local: Patterns * !"[Gmail]*" !Sent !Spam !Trash Channel sample_gmail-sent Far :sample_gmail-remote:"[Gmail]/Sent Mail" Near :sample_gmail-local:Sent Channel sample_gmail-spam Far :sample_gmail-remote:"[Gmail]/Spam" Near :sample_gmail-local:Spam Channel sample_gmail-trash Far :sample_gmail-remote:"[Gmail]/Trash" Near :sample_gmail-local:Trash Group sample_gmail Channel sample_gmail-base Channel sample_gmail-sent Channel sample_gmail-spam Channel sample_gmail-trash ##### sheo0147@yahoo.co.jp (Yahoo! Japan) IMAPAccount sample_yahoo Host imap.mail.yahoo.co.jp Port 993 User sample@yahoo.co.jp Pass Ultra-Secret SSLType IMAPS ### IMAP Store configuration IMAPStore sample_yahoo-remote Account sample_yahoo ### Maildir Store configuration MaildirStore sample_yahoo-local SubFolders Verbatim Path ~/Maildir/sample_yahoo/ Inbox ~/Maildir/sample_yahoo/Inbox ### Channel Configuration Channel sample_yahoo-base Far :sample_yahoo-remote: Near :sample_yahoo-local: Channel sample_yahoo-spam Far :sample_yahoo-remote:"Bulk Mail" Near :sample_yahoo-local:Spam Group sheo0147_yahoo Channel sample_yahoo-base Channel sample_yahoo-spam
ここまで設定したら、Directoryを作成し、mbsync -aを実行する
$ mkdir ~/Maildir $ mkdir ~/Maildir/sample_example ~/Maildir/sample_icloud ~/Maildir/sample_gmail ~/Maildir/sample_yahoo $ mbsync -a
これで、手元にMailが来たはず
Mailを取得したら、muでメールのIndexを作成する。 muはXapianを利用している。Xapianは原則としてヨーロッパ系言語の検索が主眼なので、日本語検索は厳しいかと思っていたが、FLAG_NGRAMS=“1”を設定することでそれなりに対応できることがわかったので、以下を設定してmuでDBを初期化する
$ export XAPIAN_CJK_NGRAM="t" $ export FLAG_NGRAMS="t" $ mbsync -a $ mu init --maildir=~/Maildir \ --my-address=sample@example.com \ --my-address=sample@icloud.com \ --my-address=sample@gmail.com \ --my-address=sample@yahoo.co.jp $ mu index $ mu info store ....
msmtpはSMTP Clientで、メールを送信する際に利用できる。
近年では、UCE/SPAMのような単なる迷惑メールだけでなく、Cyber攻撃の道具としてもメールが利用されているため、メールを送信するための制限が厳しくなっている。 電子メールの送信にあたっては、送信用のSMTPサーバー(MTA)がそのメールを送付する資格があるかどうかを確認(SPF, DKIM, DMARCを利用する)され、大量のメールを送信する(MailingListを運営している場合など)場合には、更なる検査(ARCを利用)をされる。 例えば sample@example.com がFromとなるメールを送信することができるMXを限定し、そこ以外から送られたものは迷惑メールもしくは攻撃メールの可能性が高いと判断する、などができるようになっている。
したがって、自分のように「複数のメールアカウントを持って」おり、「それぞれを必要に応じて使い分ける」ような使い方をしている場合、メールを送付する際にどのMTAを利用すれば良いかいちいち判断し、適切なMTAからメールを送る必要がある。これを行うためのツールがmsmtpである。
なお、mbsyncと同様、メール送信系をmsmtpにするべき強い理由はない。単にmsmtpの例が非常に多いから利用しているだけである。 また、本記事の設定は動作確認なども兼ねているためpasswordをrawで記載している。しかし、生パスワードを設定ファイルに記載することはSecurity上の重大なリスクになる可能性が高い。したがって、これはGPGなどを利用して暗号化するべきである。 暗号化の方法などはman msmtpするなり、検索するなりすれば大量に出てくるので、そちらを参照のこと。
以下設定。上記mbsyncと合わせてある。
# # .msmtprc - configuration for msmtp. # defaults logfile ~/Maildir/.log/msmtp.log ##### sample@example.com account sample_example auth on host smtp.example.com port 465 protocol smtp from sample@example.com user sample@example.com password Ultra_secret tls on tls_starttls off ##### sample@icloud.com # *** WARNING *** Must need STARTTLS. This site doesn't use SMTPs. account sample_icloud auth on host smtp.mail.me.com port 587 protocol smtp from sample@icloud.com user sample@icloud.com password Apple-Application-password-set-at-Apple tls on tls_starttls on ##### sample@gmail.com account sample_gmail auth on host smtp.gmail.com port 465 protocol smtp from sample@gmail.com user sample@gmail.com password Gmail-Application-password-set-at-Gmail tls on tls_starttls off ##### sample@yahoo.co.jp account sample_yahoo auth on host smtp.mail.yahoo.co.jp port 465 protocol smtp from sample@yahoo.co.jp user sample@yahoo.co.jp password Ultra-Secret tls on tls_starttls off account default : sample_example
mu4eの設定は、以下の理由で巨大になっています。こんなに難しいことしなくてもいい気はするんだけど…
まぁ、色々なところから色々設定持ってきたり、PerplexityやMS copilotのお世話になったりしてます。 難しいcodeは書いてないから、コメント見ながら読めばわかると思いたい。 というかわかるということにします。
;;; -*- lexical-binding: t; -*-
;;; ~/.config/emacs/mu4e-init.el
;;;
;;; last updated: 2026/01/02
;;; Author: HEO SeonMeyong <seirios@seirios.org>
;;; Microsoft Copilot (Rewrite and support leaf)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Tips
;;; * Create new refile folder
;;; $ mkdir -p ~/Maildir/[Mailbox]/some/where/{new,cur/tmp}
;;; $ chmod -R 700 ~/Maildir/[Mailbox]/some/where/{new,cur/tmp}
;;; $ mbsync -a
;;; $ mu index
;;; * 要するに、正しいpermissionで正しくFolder(Directory)を作成してmbsyncで同期してindexを張ればOK
;;; * 逆に削除はDirectoryごと全部削除すればよさそう
;;; Informations(Links)
;;; * https://www.djcbsoftware.nl/code/mu/mu4e/HV-Custom-headers.html
;;; 定義・機能一覧:
;;; - my/which: Homebrew と環境 PATH の両方で実行ファイルを探索するヘルパー
;;; - my-mu4e-update-mail-and-index-async: 非同期 mbsync + mu index
;;; - my/mu4e-start-background-update: タイマーで定期バックグラウンド更新開始
;;; - my/mu4e-stop-background-update: タイマー停止
;;; - キーバインド: U → 非同期更新 (mu4e メインビュー/ヘッダビュー)
;;; - ポリシー: 起動時自動更新は無効・インデックス更新は常にバックグラウンド
;;;
;;; よく調整するパラメータ (本ファイル内のコメントを検索してください):
;;; * 定期更新間隔分: (my/mu4e-start-background-update 30) ; 30分
;;; * mbsync/mu/msmtp のパス探索候補: my/which の引数リスト
;;; * mu4e-context-policy / mu4e-compose-context-policy:
;;; 'pick-first / 'ask / 'ask-if-none / nil
;;; * mu4e-headers-results-limit: ヘッダの最大取得件数 (性能/読みやすさのトレードオフ)
;;; * display-buffer-alist: 本文ビューを右側へ固定表示するウィンドウ設定
;;; ----------------------------------------------------------------------
;;; 小さなユーティリティ(実行ファイル探索と簡易コンテキスト生成)
(defun my/which (&rest candidates)
"CANDIDATES のうち、最初に見つかった実行ファイルの絶対パスを返す。ない場合は nil。
候補は、絶対パス文字列とコマンド名の混在でよい。
- 絶対パスが渡された場合: 実行可能ならそのパスを返す
- コマンド名が渡された場合: `executable-find` で PATH 上を探索する
例:
(my/which \"/usr/local/bin/mbsync\" \"/opt/homebrew/bin/mbsync\" \"mbsync\")"
(seq-some (lambda (c)
(cond
((file-name-absolute-p c) (and (file-executable-p c) c))
(t (executable-find c))))
candidates))
(defun my/mk-context (name maildir-prefix email &optional signature)
"mu4e 用コンテキストを簡潔に生成する。
NAME は識別名、MAILDIR-PREFIX は \"/XXXX\" 形式、EMAIL は送信元。
SIGNATURE を与えると `message-signature` を設定する。
例:
(my/mk-context \"1seirios_seirios\" \"/seirios_seirios\" \"seirios@seirios.org\" \"=====\nHEO ...\")"
(make-mu4e-context
:name name
:enter-func (lambda () (mu4e-message (format "Enter %s context" email)))
:leave-func (lambda () (mu4e-message (format "Leave %s context" email)))
:match-func (lambda (msg)
(when msg
(string-prefix-p maildir-prefix
(mu4e-message-field msg :maildir))))
:vars (append
`((user-mail-address . ,email)
(user-full-name . "HEO SeonMeyong")
(mu4e-drafts-folder . ,(concat maildir-prefix "/Drafts"))
(mu4e-sent-folder . ,(concat maildir-prefix "/Sent"))
(mu4e-trash-folder . ,(concat maildir-prefix "/Trash")))
(when signature
`((message-signature . ,signature))))))
;;;; mu4e のロードパスを動的に追加(Intel / Apple Silicon 両対応)
(dolist (cand '("/usr/local/share/emacs/site-lisp/mu/mu4e/"
"/opt/homebrew/share/emacs/site-lisp/mu/mu4e/"))
(when (file-directory-p cand)
(add-to-list 'load-path cand)))
;;;; 起動時の自動更新を制御するトグル
;; Toggle for legacy mu4e auto-update (built-in interval & startup hook)
;; - When t: mu4e の定期更新 (`mu4e-update-interval`) を使う
;; - When nil: 組み込みの auto update は無効にして、代わりに自作タイマーを使う
(defvar my/mu4e-auto-update t
"mu4eの組み込み自動更新を使うなら t。nil なら起動時/定期更新とも無効。")
(leaf mu4e
:load-path "/usr/local/share/emacs/site-lisp/mu/mu4e/" ; Homebrew の mu4e パス
:require t
:preface
;; MIMEのHTML等を抑止(ロード前定義でOK)
;; これにより本文表示は極力 text/plain を優先する。
(with-eval-after-load "mm-decode"
(add-to-list 'mm-discouraged-alternatives "text/html")
(add-to-list 'mm-discouraged-alternatives "text/richtext"))
:custom
;; ---- 全体動作/UI系をまとめて :custom で設定 ----
;; インデックス更新は常にバックグラウンドで(UIフリーズを避ける)
((mu4e-index-update-in-background . t) ; Run mu index update asynchronously to avoid UI blocking
;; Maildir と添付保存先
(mu4e-maildir . "~/Maildir")
(mu4e-attachment-dir . "~/Desktop")
;; mbsync でメール移動時、ファイル名を変更(重複衝突などを回避)
(mu4e-change-filenames-when-moving . t)
;; 終了確認やモードライン表示
(mu4e-confirm-quit . nil)
(mu4e-modeline-show-global . t)
(mu4e-modeline-max-width . 100)
;; 表示:本文ビューを右側へ固定(display-buffer-alist 併用)
(mu4e-split-view . "vertical")
;; ミニバッファに出る「Retrieving…」などの雑音を極力隠す
(mu4e-hide-index-messages . t)
;; コンテキストの選択ポリシー(起動時は最初を採用)
(mu4e-context-policy . 'pick-first)
;; ヘッダ表示系(関連メッセージ非表示、結果上限、スレッド表示等)
(mu4e-headers-include-related . nil)
(mu4e-headers-results-limit . 5000)
(mu4e-headers-show-threads . t)
(mu4e-headers-date-format . "%Y-%m-%dT%H:%M:%S")
(mu4e-headers-fields
. '((:date . 20) (:flags . 6) (:from . 16) (:to . 16) (:subject . nil)))
;; 本文ビューのフィールド
(mu4e-view-date-format . "%Y-%m-%dT%H:%M:%S")
(mu4e-view-fields
. '(:from :to :cc :subject :flags :date :maildir :mailing-list
:tags :attachments :signature :decryption :message-id
:path :user-agent))
;; Compose 時のコンテキスト選択ポリシー(マッチしなければ確認)
(mu4e-compose-context-policy . 'ask-if-none)
;; 署名の自動挿入は無効(コンテキストで signature を持つもののみ)
(mu4e-compose-signature-auto-include . nil)
;; 送信系(mu4e を使い、sendmail 経由で送る)
(mail-user-agent . 'mu4e-user-agent)
(send-mail-function . 'sendmail-send-it)
(message-send-mail-function . 'message-send-mail-with-sendmail)
(message-kill-buffer-on-exit . t)
(message-sendmail-envelope-from . 'header)
;; 既定ブックマーク(InboxやSPAMなどショートカット検索)
(mu4e-bookmarks
. '((:name "Unread messages" :query "flag:unread AND NOT flag:trashed" :favorite t :key ?u)
(:name "seirios.seirios" :query "maildir:/seirios_seirios/Inbox" :favorite t :key ?1)
(:name "sheo.seirios" :query "maildir:/sheo_seirios/Inbox" :favorite t :key ?2)
(:name "mls.seirios" :query "maildir:/mls_seirios/Inbox" :favorite t :key ?3)
(:name "seirios.icloud" :query "maildir:/seirios_icloud/Inbox" :favorite t :key ?4)
(:name "sheo0147.yahoo" :query "maildir:/sheo0147_yahoo/Inbox" :favorite t :key ?5)
(:name "seirios.RRcom" :query "maildir:/seirios_RRcom/Inbox" :favorite t :key ?6)
(:name "seirios.RRnet" :query "maildir:/seirios_RRnet/Inbox" :favorite t :key ?7)
(:name "sheo0147.gmail" :query "maildir:/sheo0147_gmail/Inbox" :favorite t :key ?8)
(:name "seirios.wm.gmail" :query "maildir:/seirios.wm_gmail/Inbox" :favorite t :key ?9)
(:name "seirios.silanui" :query "maildir:/seirios_silanui/Inbox" :favorite t :key ?0)
(:name "SPAM/UCE"
:query "maildir:/seirios_seirios/Spam OR maildir:/sheo_seirios/Spam OR maildir:/mls_seirios/Spam OR maildir:/seirios_icloud/Spam OR maildir:/sheo0147_yahoo/Spam OR maildir:/seirios_RRcom/Spam OR maildir:/seirios_RRnet/Spam OR maildir:/sheo0147_gmail/Spam OR maildir:/seirios.wm_gmail/Spam OR maildir:/seirios_silanui/Spam"
:favorite t :key ?s)
(:name "Trash"
:query "maildir:/seirios_seirios/Trash OR maildir:/sheo_seirios/Trash OR maildir:/mls_seirios/Trash OR maildir:/seirios_icloud/Trash OR maildir:/sheo0147_yahoo/Trash OR maildir:/sheo0147_gmail/Trash OR maildir:/seirios_RRnet/Trash OR maildir:/seirios_RRcom/Trash OR maildir:/seirios.wm_gmail/Trash OR maildir:/seirios_silanui/Trash"
:favorite t :key ?t)))
;; Maildir ショートカット(ジャンプキー)
(mu4e-maildir-shortcuts
. '(
(:maildir "/seirios_seirios/UT" :name "UT" :key ?u)
(:maildir "/seirios_seirios/ISII" :name "ISII" :key ?i)
(:maildir "/seirios_seirios/silanui" :name "しらぬい" :key ?s)
(:maildir "/seirios_seirios/NCom" :name "NCom" :key ?n)
(:maildir "/seirios_seirios/NTT-MM" :name "N-MM" :key ?m)
(:maildir "/seirios_RRnet/Rookie" :name "Rookie" :key ?r)
(:maildir "/seirios_seirios/Finance" :name "Fin" :key ?F)
(:maildir "/seirios_seirios/WIDE" :name "WIDE" :key ?W)
(:maildir "/seirios_seirios/Logs" :name "Logs" :key ?L)
(:maildir "/seirios_seirios/Logs/ShadowServer" :name "影鯖" :key ?S)
(:maildir "/seirios_seirios/Logs/DMARC" :name "DMARC" :key ?D)
(:maildir "/seirios_seirios/Individual/Personal" :name "個人" :key ?P)
(:maildir "/seirios_seirios/Individual/Family" :name "家族" :key ?f)
(:maildir "/seirios_seirios/Individual/Game" :name "Game" :key ?G)
(:maildir "/seirios_seirios/Individual/Services" :name "Service" :key ?P)
(:maildir "/seirios_seirios/Activity/JNSA" :name "JNSA" :key ?J)
(:maildir "/seirios_seirios/Activity/ISC2" :name "ISC" :key ?I)
(:maildir "/seirios_seirios/Activity/GitHub" :name "GHUB" :key ?g)
(:maildir "/seirios_seirios/Activity/ISOG-J" :name "ISOG" :key ?j)
;; 以下は「一覧に見せるため」の要素。:key がないものはジャンプキー無し。
(:maildir "/seirios_seirios/ISII/GA")
(:maildir "/seirios_seirios/Individual")
(:maildir "/seirios_seirios/Individual/Tech")
(:maildir "/seirios_seirios/Individual/UnivD2")
(:maildir "/seirios_seirios/Activity")
(:maildir "/seirios_seirios/Activity/ISACA")
(:maildir "/seirios_seirios/Olds")
(:maildir "/seirios_RRnet/Olds")
(:maildir "/seirios_seirios/Trash")
(:maildir "/seirios_icloud/Trash")
(:maildir "/sheo0147_yahoo/Trash")
(:maildir "/sheo0147_gmail/Trash")
(:maildir "/seirios_RRnet/Trash")
(:maildir "/sheo_seirios/Trash")
(:maildir "/seirios_RRcom/Trash")
(:maildir "/seirios_silanui/Trash")
(:maildir "/seirios.wm_gmail/Trash")
(:maildir "/mls_seirios/Trash"))))
:config
;; ---- 実行ファイルの場所を自動検出(Homebrew/Path両対応)----
;; mu/msmtp の実行パスは GUI 起動時(PATHが引き継がれないケース)でも見つかるよう、
;; 絶対パス候補 → PATH の順に探索します。
(setq mu4e-mu-binary (my/which "/usr/local/bin/mu" "/opt/homebrew/bin/mu" "mu"))
(setq sendmail-program (my/which "/usr/local/bin/msmtp" "/opt/homebrew/bin/msmtp" "msmtp"))
;; メールを取得するための設定
(with-eval-after-load 'mu4e
;; U(取得)で使うコマンドは常に設定しておく(スマートラッパーを使わない場面向け互換)
(let ((mbsync (my/which "/usr/local/bin/mbsync" "/opt/homebrew/bin/mbsync" "mbsync")))
(setq mu4e-get-mail-command (and mbsync (concat mbsync " -a")))) ; Keep string form for compatibility (some commands read this variable)
;; 起動時/定期の自動を使うかはトグルで制御
(if my/mu4e-auto-update
(progn
;; 自動更新ON: mu4e の built-in interval を有効化(起動時フックも既定動作)
(setq mu4e-update-interval (* 10 60)) ; 10分(任意で変更)
;; 既定の起動時更新フックは mu4e が勝手に登録するため、ここでは何もしない
)
;; 自動更新OFF: built-in interval を止め、起動時の更新フックも外す
(setq mu4e-update-interval nil)
(remove-hook 'mu4e-main-mode-hook #'mu4e-update-mail-and-index)
;; U で取得したいので mu4e-get-mail-command は nil にしない(上で保持)
)
;; ビューの分割(本文は右側へ表示)
(add-to-list 'display-buffer-alist
`(,(regexp-quote mu4e-view-buffer-name)
display-buffer-in-side-window
(side . right) (window-width . 0.5)))
;; モードライン名の微調整(ヘッダ検索中は短く)
(setq mode-name "mu4e-headers")
(add-hook 'mu4e-headers-search-hook (lambda (_q) (setq mode-name "mu4e")))
;; ---- コンテキスト(簡潔化)----
;; 複数アカウント構成(各 :vars で Drafts/Sent/Trash を設定)
(setq mu4e-contexts
(list
(my/mk-context "1seirios_seirios" "/seirios_seirios" "seirios@seirios.org"
(concat "=====\n" "HEO SeonMeyong"))
(my/mk-context "2sheo_seirios" "/sheo_seirios" "sheo@seirios.org")
(my/mk-context "3mls_seirios" "/mls_seirios" "mls@seirios.org")
(my/mk-context "4seirios_icloud" "/seirios_icloud" "seirios@mac.com")
(my/mk-context "5sheo0147_yahoo" "/sheo0147_yahoo" "sheo0147@yahoo.co.jp")
(my/mk-context "6seirios_RRcom" "/seirios_RRcom" "seirios@rusty-raven.com")
(my/mk-context "7seirios_RRnet" "/seirios_RRnet" "seirios@rusty-raven.net")
(my/mk-context "8sheo0147_gmail" "/sheo0147_gmail" "sheo0147@gmail.com")
(my/mk-context "9seirios.wm_gmail" "/seirios.wm_gmail" "seirios.wm@gmail.com")
(my/mk-context "0seirios_silanui" "/seirios_silanui" "seirios@silanui.com")))
;; ---- Refile ルール(データ駆動)----
;; ルールは (FOLDER FIELD REGEX) のリスト。
;; FIELD は 'to'/'cc'/'bcc'/'rcpt'/'from'/'subj'/'msgid'/'list'/'any' が指定可能。
;; `my-mu4e-refile-message` は、MSG に適用して最初にマッチした folder を返す。
(defvar my-mu4e-refile-rules
'(("/seirios_seirios/ISII" "rcpt" "redmine@interlink\\.or\\.jp$")
("/seirios_seirios/Olds/NTT-Myanmar" "rcpt" "cnip@ml\\.ntt\\.com")
("/seirios_seirios/Olds/KDS" "rcpt" "support@zscaler\\.com$")
("/seirios_seirios/Olds/KDS" "rcpt" "zsc-support@kddi-dsec\\.com$")
("/seirios_seirios/Olds/KDS" "from" "do-not-reply@kds\\.seirios\\.org$")
("/seirios_seirios/Olds/KDS" "rcpt" "zsc-support@k-evolva\\.com$")
("/seirios_seirios/Activity/GitHub" "to" "seirios\\+gh@seirios\\.org")
("/seirios_seirios/Logs/DMARC" "from" "noreply-dmarc-support@google\\.com$")
("/seirios_seirios/Logs/DMARC" "from" "dmarcreport@microsoft\\.com$")
("/seirios_seirios/Logs/DMARC" "from" "reporting@dmarc25\\.jp$")
("/seirios_seirios/Logs/DMARC" "from" "noreply@dmarc\\.yahoo\\.com$")
("/seirios_seirios/Logs/DMARC" "from" "dmarc_support@corp\\.mail\\.ru$")
("/seirios_seirios/Logs/DMARC" "from" "dmarc-reports@lolipop\\.jp$")
("/seirios_seirios/Logs/ShadowServer" "from" "@shadowserver\\.org$")
("/seirios_seirios/Logs" "to" "mgmt\\.seirios\\.org$")
("/seirios_seirios/Logs" "to" "mgmt\\.rookie-inc\\.com$")
("/seirios_seirios/Logs" "from" "^root@")
("/seirios_seirios/Logs" "from" "^www@bbf-wb")
("/seirios_seirios/Logs" "subj" "^Cron")
("/seirios_seirios/Logs" "subj" "run\\.output$")
("/seirios_seirios/Individual/Game" "from" "nintendo\\.(com|net|co\\.jp)$")
("/seirios_seirios/Individual/Game" "from" "fortnite@mail\\.epicgames\\.com$")
("/seirios_seirios/Individual/Game" "from" "familysafety@microsoft\\.com$")
("/seirios_seirios/Finance" "from" "smbc\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "tokyostarbank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "surugabank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "mizuhobank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "japannetbank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "paypay-bank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "sevenbank\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "nicos\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "jaccs\\.co\\.jp$")
("/seirios_seirios/Finance" "from" "viewsnet\\.jp$")
("/seirios_seirios/Finance" "from" "jrepoint\\.jp$")
("/seirios_seirios/Finance" "from" "vpass\\.ne\\.jp$")
("/seirios_seirios/Finance" "from" "paypal\\.com$")
("/seirios_seirios/Finance" "from" "credit\\.orix\\.co\\.jp$")
("/seirios_seirios/Individual/Services" "from" "amazon\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "uqmobile\\..jp$")
("/seirios_seirios/Individual/Services" "from" "auction-master@mail\\..yahoo\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "payment-master@mail\\..yahoo\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "ana\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "member@point\\..recruit\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "iijmio\\..jp$")
("/seirios_seirios/Individual/Services" "from" "booking\\..com$")
("/seirios_seirios/Individual/Services" "from" "Apple\\..com$")
("/seirios_seirios/Individual/Services" "from" "banggood\\..com$")
("/seirios_seirios/Individual/Services" "from" "lambdanote\\..com$")
("/seirios_seirios/Individual/Services" "from" "community_cycle_info@docomo-cycle\\..jp$")
("/seirios_seirios/Individual/Services" "from" "connpass\\..com$")
("/seirios_seirios/Individual/Services" "from" "dropbox\\..com$")
("/seirios_seirios/Individual/Services" "from" "ebay\\..com$")
("/seirios_seirios/Individual/Services" "from" "evernote\\..com$")
("/seirios_seirios/Individual/Services" "from" "fiberjp\\..com$")
("/seirios_seirios/Individual/Services" "from" "gandi\\..net$")
("/seirios_seirios/Individual/Services" "from" "icloud\\..com$")
("/seirios_seirios/Individual/Services" "from" "sakura\\..ad\\..jp$")
("/seirios_seirios/Individual/Services" "from" "mydocomo\\..com$")
("/seirios_seirios/Individual/Services" "from" "suicainternetservice\\..com$")
("/seirios_seirios/Individual/Services" "from" "@interlink\\..or\\..jp$")
("/seirios_seirios/Individual/Services" "from" "flets\\..com$")
("/seirios_seirios/Individual/Services" "from" "itunes\\..com$")
("/seirios_seirios/Individual/Services" "from" "makuake\\..com$")
("/seirios_seirios/Individual/Services" "from" "macdvdripperpro\\..com$")
("/seirios_seirios/Individual/Services" "from" "omnigroup\\..com$")
("/seirios_seirios/Individual/Services" "from" "eki-net\\..com$")
("/seirios_seirios/Individual/Services" "from" "cloudsign\\..jp$")
("/seirios_seirios/Individual/Services" "from" "biccamera\\..com$")
("/seirios_seirios/Individual/Services" "from" "morisawa\\..co\\..jp$")
("/seirios_seirios/Individual/Services" "from" "kuronekoyamato\\..co\\..jp$")
("/seirios_seirios/WIDE" "msgid" "wide\\.ad\\.jp")
("/seirios_seirios/WIDE" "subj" "^\\[(wide|two-core|two|ixp-ops|irc-wg|dns) ")
("/seirios_seirios/WIDE" "rcpt" "as2500@nspixp\\.wide\\.ad\\.jp")
("/seirios_seirios/WIDE" "rcpt" "(ixp|lens-wg)@wide\\.ad\\.jp")
("/seirios_seirios/Activity/ISC2" "from" "/@isc2\\.org$")
("/seirios_seirios/Activity/JNSA" "list" "jnsa\\.org$")
("/seirios_seirios/Activity/ISOG-J" "list" "member\\.isog-j\\.org$")
("/seirios_seirios/Activity/ISC2" "from" "isc2\\.org$")
("/seirios_seirios/Activity/ISC2" "from" "isc2chapter\\.jp$")
("/seirios_seirios/Spam" "from" "nikkeibp\\.(co\\.jp|com)$")
("/seirios_seirios/Spam" "from" "itmedia\\.jp$")
("/seirios_seirios/Spam" "from" "info@twitter\\.com")))
(defun my-mu4e-refile-message (msg)
"ルールに基づき MSG のリファイル先を返す。未マッチなら nil。
- 'any は to/cc/bcc/from の全てに対して正規表現判定を行う
- 'rcpt は to/cc/bcc の受信側集合で判定"
(cl-loop for (folder field regex) in my-mu4e-refile-rules
for addresses = (pcase field
("any" (append (mu4e-message-field msg :to)
(mu4e-message-field msg :cc)
(mu4e-message-field msg :bcc)
(mu4e-message-field msg :from)))
("rcpt" (append (mu4e-message-field msg :to)
(mu4e-message-field msg :cc)
(mu4e-message-field msg :bcc)))
("subj" (list (mu4e-message-field msg :subject)))
("msgid" (list (mu4e-message-field msg :msgid)))
("list" (list (mu4e-message-field msg :list)))
(_ (mu4e-message-field msg (intern (concat ":" field)))))
when (seq-some (lambda (addr)
(and addr (string-match-p regex
(downcase (if (listp addr)
(or (plist-get addr :email) "")
addr)))))
addresses)
return folder
finally return nil))
;; mu4e が参照するリファイル先決定関数。ルールマッチしなければ現在フォルダ。
(setq mu4e-refile-folder
(lambda (msg)
(or (my-mu4e-refile-message msg)
(mu4e-message-field msg :maildir))))
;; ヘッダビュー内で「自動的にrefileマークを付ける」補助コマンド
(defun my-mu4e-auto-refile-process (msg)
(let ((target (funcall mu4e-refile-folder msg)))
(when (and target
(not (string= target (mu4e-message-field msg :maildir))))
(mu4e-mark-set 'refile target))))
(defun my-mu4e-auto-refile ()
"現在フォルダの新着/未読を検索し、ルールに基づき refile マークを付ける。"
(interactive)
(let* ((current (mu4e-message-field (mu4e-message-at-point) :maildir))
(query (concat "maildir:" current " AND (flag:new OR flag:unread) AND NOT flag:trashed")))
(mu4e-headers-search query)
(add-hook 'mu4e-headers-found-hook #'my-mu4e-auto-refile-hook)))
(defun my-mu4e-auto-refile-hook ()
(remove-hook 'mu4e-headers-found-hook #'my-mu4e-auto-refile-hook)
(goto-char (point-min))
(while (not (eobp))
(let ((msg (mu4e-message-at-point)))
(my-mu4e-auto-refile-process msg))
(forward-line))
;; マーク後、次の未読へ移動
(mu4e-headers-next nil))
(keymap-set mu4e-headers-mode-map "e" #'my-mu4e-auto-refile)
;; ---- 送信補助(msmtp選択/Cc/Bcc/件名空確認)----
;; Fromヘッダと msmtp アカウントの対応表(追加・変更はここへ)
(defvar my/msmtp-accounts
'(("seirios@seirios.org" . "seirios_seirios")
("sheo@seirios.org" . "sheo_seirios")
("mls@seirios.org" . "mls_seirios")
("seirios@mac.com" . "seirios_icloud")
("sheo0147@yahoo.co.jp" . "sheo0147_yahoo")
("seirios@rusty-raven.com" . "seirios_RRcom")
("seirios@rusty-raven.net" . "seirios_RRnet")
("sheo0147@gmail.com" . "sheo0147_gmail")
("seirios.wm@gmail.com" . "seirios.wm_gmail")
("seirios@silanui.com" . "seirios_silanui")))
(defun my-mu4e-set-msmtp-account ()
"From ヘッダに基づき msmtp アカウントを自動選択する。
- ヘッダから送信者アドレスを取り出し、`my/msmtp-accounts` で最初に一致したものを採用
- 一致がない場合は何もしない(ユーザ手動選択に委ねる)"
(when (message-mail-p)
(save-excursion
(let* ((from (save-restriction
(message-narrow-to-headers)
(or (message-fetch-field "from") "")))
(acct (seq-some (lambda (pair)
(and (string-match (regexp-quote (car pair)) from)
(cdr pair)))
my/msmtp-accounts)))
(when acct
(message "Using msmtp account: %s" acct)
(setq message-sendmail-extra-arguments (list "-a" acct)))))))
(add-hook 'message-send-hook #'my-mu4e-set-msmtp-account)
(defun my-add-cc-and-bcc ()
"Compose 中に Cc に自アドレスがなければ追記し、Bcc を空で付与する。
- Cc 行が存在しない場合は新規に付与
- 既に自アドレスが含まれていれば何もしない"
(save-excursion
(goto-char (point-min))
(if (re-search-forward "^Cc:" nil t)
(unless (re-search-forward (concat (regexp-quote user-mail-address))
(line-end-position) t)
(end-of-line)
(insert (if (looking-back "[, \t]" (max (point-min) (1- (point))))
user-mail-address
(concat ", " user-mail-address))))
(message-add-header (format "Cc: %s\n" user-mail-address))))
(save-excursion (message-add-header "Bcc: \n")))
(add-hook 'mu4e-compose-mode-hook #'my-add-cc-and-bcc)
(defun confirm-empty-subject ()
"件名が空なら送信前に確認ダイアログを出す。
誤送信防止のための軽量ガード。"
(let ((sub (message-field-value "Subject")))
(or (and sub (not (string-match "\\`[ \t]*\\'" sub)))
(yes-or-no-p "Really send without Subject? ")
(keyboard-quit))))
(add-hook 'message-send-hook #'confirm-empty-subject)
;; === ヘッダ表示のページング(N=残り全件を一気に追記) ===
(defun my/mu4e--headers-last-message-id ()
"ヘッダバッファ末尾のメッセージID(:message-id)を返す。なければ nil。"
(when (mu4e-current-buffer-type-p 'headers)
(save-excursion
(goto-char (point-max))
(forward-line -1)
(plist-get (mu4e-message-at-point) :message-id))))
(defun my-mu4e-append-all-remaining ()
"現在クエリの『末尾からさらに先(残り全件)』を一気に追記する。
この呼び出し中のみ `mu4e-search-full` を t にして上限を外す。"
(interactive)
(when (mu4e-current-buffer-type-p 'headers)
(let* ((mu4e-search-full t) ;; ★ この関数の間だけ全件モード
(mu4e-search-sort-field 'date)
(mu4e-search-sort-direction 'descending)
;; 念のため「ほぼ無限」上限もローカルに設定(古いmu4eへの互換)
(mu4e-headers-results-limit most-positive-fixnum)
(last-msg (my/mu4e--headers-last-message-id)))
(when-let ((query (mu4e-last-query)))
(message "[mu4e] append all remaining headers ...")
;; append=t で結果を追記。last-msg をアンカーに、それより古い側を取得。
(mu4e-search query nil nil t last-msg)
;; 視認性のため末尾へ移動
(goto-char (point-max))
(forward-line -1)
;; スレッド折りたたみを使っている場合は全畳みで見通しを確保
(when (bound-and-true-p mu4e-thread-folding-mode)
(mu4e-headers-fold-all))
(message "[mu4e] all remaining headers appended.")))))
;; キーバインドの差し替え(N)
(keymap-set mu4e-headers-mode-map "N" #'my-mu4e-append-all-remaining)
;; === 非同期取得+インデックス更新(背景実行)と定期バックグラウンド更新 ===
;; Parameters:
;; - mbsync path resolution: my/which tries /usr/local/bin, /opt/homebrew/bin, then PATH
;; - Timer interval: call (my/mu4e-start-background-update MINUTES) to change
;; - Log buffer: " *mu4e-mbsync*" stores stdout/stderr of mbsync for diagnostics
;; Behavior:
;; - U: triggers async mbsync followed by mu4e-index-update when exit-code=0
;; - Startup: built-in mu4e auto update disabled; we rely on custom timer
;; - Index updates run with mu4e-index-update-in-background=t to avoid blocking
(setq mu4e-index-update-in-background t)
(defun my-mu4e-update-mail-and-index-async ()
"Run mbsync asynchronously and, upon success, trigger mu4e index update in background.
ENV/PATH NOTES:
- GUI起動のEmacsでは Homebrew の PATH が見えないことがある。必要なら exec-path-from-shell を併用。
LOGGING:
- mbsync の標準出力/エラーはバッファ \" *mu4e-mbsync*\" に格納。
ERROR HANDLING:
- 非ゼロ終了コードの場合はバッファを表示して詳細を確認できる。"
(interactive)
(let* ((mbsync (my/which "/usr/local/bin/mbsync" "/opt/homebrew/bin/mbsync" "mbsync"))
(args '("-a")))
(unless mbsync
(user-error "mbsync が見つかりません。PATHやインストールを確認してください。"))
(let ((buf (get-buffer-create " *mu4e-mbsync*")))
(with-current-buffer buf (erase-buffer))
(message "[mu4e] start mbsync -a (background)")
(let ((proc (apply #'start-process "mu4e-mbsync" buf mbsync args)))
(set-process-query-on-exit-flag proc nil)
(set-process-sentinel
proc
(lambda (p event)
(when (eq (process-status p) 'exit)
(let ((code (process-exit-status p)))
(if (= code 0)
(progn
(message "[mu4e] mbsync done. start mu index (background)")
(mu4e-update-index)) ; 非同期インデックス更新(UI非ブロッキング)
(progn
(message "[mu4e] mbsync failed (exit=%d). バッファ %s を確認"
code (buffer-name (process-buffer p)))
(display-buffer (process-buffer p))))))))))))
;; Uキーを非同期版に差し替え(メイン/ヘッダ両方)
(define-key mu4e-main-mode-map (kbd "U") ; U = async mbsync + mu index (non-blocking)
#'my-mu4e-update-mail-and-index-async)
(define-key mu4e-headers-mode-map (kbd "U") ; U = async mbsync + mu index (non-blocking)
#'my-mu4e-update-mail-and-index-async)
;; mu4e 自体の同期更新コマンドを使う場面へ互換のため文字列版も保持
(let ((mbsync (my/which "/usr/local/bin/mbsync" "/opt/homebrew/bin/mbsync" "mbsync")))
(setq mu4e-get-mail-command (and mbsync (concat mbsync " -a")))) ; Keep string form for compatibility
;; --- 定期バックグラウンド更新(独自タイマー) ---
(defvar my/mu4e-background-timer nil
"バックグラウンド更新用のタイマー。nil なら未稼働。")
(defun my/mu4e-start-background-update (minutes)
"MINUTES 分間隔で非同期取得+インデックス更新を開始する。
Parameters:
MINUTES 整数。間隔(分)を指定(例: 10)
Behavior:
- 既存タイマーがあればキャンセル。
- 最初の実行は 10 秒後、その後 MINUTES ごとに繰り返し。
- 非同期関数 my-mu4e-update-mail-and-index-async を使用。
Tuning:
- 初回遅延 10 秒は必要に応じて変更可能(run-at-time の第一引数)。"
(when my/mu4e-background-timer
(cancel-timer my/mu4e-background-timer))
(setq my/mu4e-background-timer
(run-at-time 30 (* minutes 60) #'my-mu4e-update-mail-and-index-async))
(message "[mu4e] background update every %d minutes" minutes))
(defun my/mu4e-stop-background-update ()
"バックグラウンド更新タイマーを停止する。
従量制ネットワークや一時的に負荷を避けたい場合に使用。"
(interactive)
(when my/mu4e-background-timer
(cancel-timer my/mu4e-background-timer)
(setq my/mu4e-background-timer nil)
(message "[mu4e] background update stopped")))
;; 既定では 10 分間隔で開始(好みに合わせて値を変更)
(my/mu4e-start-background-update 30)))
;;; 追加: スレッド折りたたみ(見通しを良くするUI拡張)
(leaf mu4e-thread-folding
:vc (:url "https://github.com/rougier/mu4e-thread-folding")
:require t
:config
(define-key mu4e-headers-mode-map (kbd "<tab>") #'mu4e-headers-toggle-at-point)
(define-key mu4e-headers-mode-map (kbd "<S-tab>") #'mu4e-headers-toggle-fold-all)
(define-key mu4e-headers-mode-map (kbd "<left>") #'mu4e-headers-fold-at-point)
(define-key mu4e-headers-mode-map (kbd "<S-left>") #'mu4e-headers-fold-all)
(define-key mu4e-headers-mode-map (kbd "<right>") #'mu4e-headers-unfold-at-point)
(define-key mu4e-headers-mode-map (kbd "<S-right>") #'mu4e-headers-unfold-all)
(add-hook 'mu4e-headers-mode-hook #'mu4e-thread-folding-mode))
;;; 必要なら明示ロード(smtpmail は組み込みだが、leaf で明示しておくと見通しが良い)
(leaf smtpmail :require t)