;;; -*- lexical-binding: t; -*- ;;; ~/.config/emacs/mu4e-init.el ;;; ;;; last updated: 2026/01/02 ;;; Author: HEO SeonMeyong ;;; 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 "") #'mu4e-headers-toggle-at-point) (define-key mu4e-headers-mode-map (kbd "") #'mu4e-headers-toggle-fold-all) (define-key mu4e-headers-mode-map (kbd "") #'mu4e-headers-fold-at-point) (define-key mu4e-headers-mode-map (kbd "") #'mu4e-headers-fold-all) (define-key mu4e-headers-mode-map (kbd "") #'mu4e-headers-unfold-at-point) (define-key mu4e-headers-mode-map (kbd "") #'mu4e-headers-unfold-all) (add-hook 'mu4e-headers-mode-hook #'mu4e-thread-folding-mode)) ;;; 必要なら明示ロード(smtpmail は組み込みだが、leaf で明示しておくと見通しが良い) (leaf smtpmail :require t)