mu4e
まくら
元々は家でゲームがしたい(小学生のことTAITOのSpace Invaderが大ブームになった)という理由で始めたコンピューターとの付き合いが、いつの間にか人とのCommunicationの手段になったのは、明らかにパソコン通信のせいで、参加していたBBSのChat roomに入り浸るようになってからだったように思う。
Unixを触るようになり、UUCPで自宅に居ながらにして電子メールが使えるようになって以来、数年前までは間違いなくE-Mailこそが他者とのCommunicationの中心にあった。 したがって、筆者の場合、E-Mailの読み書きを簡単にできるようなApplicationが最も重要なツールであった時代が長い。
そのため、メールクライアントは色々使ってきた。それなりの期間(最低でも半年以上)利用したツールは
- rmail (Emacs)
- Gnus (Emacs)
- mew (Emacs) おそらく、これが一番長い
- Bekky (Windows)
- WinBiff (Windows)
- Apple Mail (macOS)
- Thunderbird (macOS/Windows/FreeBSD)
- Shylpheed (macOS)
と、幾つもあった。しかし、個人的な要件を満たすMail Readerはあまりなかった。動作するPlatformが限られていたり、表示が望み通りにならなかったり、EoLになったり…
というわけで、Sylpheedを卒業して次のMail Readerに移行することにした。
準備
今回の選択はmu4eである。 mu4eはmuコマンドを利用したメールの検索機能を利用して、Emacs上でMailの読み書きを行うためのツールである。
mu4eを利用してMailを処理するためにはいくつかの方法があるが、今回は以下の構成で行くことにした。
- IMAPを利用して手元にメールを同期するsoftware → mbsync(isync)
- smtpを利用してメールを送信するsoftware → msmtp
- Emacs → GNU Emacs 30
- メール処理 → mu, mu4e
mbsync
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が来たはず
- yahooの設定の際にPatternsを削除しているのは意図的である。
- 原因はよくわかっていないが、Yahooの場合だけ、Patternsを登録しているとInboxの同期に失敗する
- とりあえずPatternsを設定しなければうまく行くという不思議なことが起きているので、現時点では外す
muの初期化
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
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
mu4eの設定は、以下の理由で巨大になっています。こんなに難しいことしなくてもいい気はするんだけど…
- MultiAccount対応
- Mailの取り扱いはAccountに関係なくシームレスに行える
- どこのINBOXに来たメールでも、別のサーバーにrefileできるようにしてある
- refile ruleを大きく書き換えて、正規表現も込みにしたruleをかけるようにした
- 送信サーバーは、メールのFromフィールドを見て自動で決定するようにした
まぁ、色々なところから色々設定持ってきたり、PerplexityやMS copilotのお世話になったりしてます。 難しいcodeは書いてないから、コメント見ながら読めばわかると思いたい。 というかわかるということにします。
- mu4e-init.el
;;; -*- 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)
