summaryrefslogtreecommitdiff
path: root/modules/mail-config.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-12 11:47:26 -0500
committerCraig Jennings <c@cjennings.net>2025-10-12 11:47:26 -0500
commit092304d9e0ccc37cc0ddaa9b136457e56a1cac20 (patch)
treeea81999b8442246c978b364dd90e8c752af50db5 /modules/mail-config.el
changing repositories
Diffstat (limited to 'modules/mail-config.el')
-rw-r--r--modules/mail-config.el341
1 files changed, 341 insertions, 0 deletions
diff --git a/modules/mail-config.el b/modules/mail-config.el
new file mode 100644
index 00000000..76235ff1
--- /dev/null
+++ b/modules/mail-config.el
@@ -0,0 +1,341 @@
+;;; mail-config --- Settings for Mu4e Email -*- lexical-binding: t; coding: utf-8; -*-
+;; author Craig Jennings <c@cjennings.net>
+;;
+;;; Commentary:
+;; I found Aime Bertrand's blog post to be an excellent walkthrough of how to
+;; setup a Mu4e config.
+;;
+;; https://macowners.club/posts/email-emacs-mu4e-macos/
+;;
+;; on saving attachments:
+;; After running mu4e-view-save-attachments,
+;; - invoke embark-act-all in the completion menu
+;; - followed by RET (mu4e-view-save-attachments) to save all attachments
+;;
+;; - or TAB (vertico-insert)
+;; - followed by , (comma) next to each file you want to save,
+;; - then RET (vertico-exit), to save selected attachments.
+;;
+;;; Code:
+
+(require 'user-constants)
+
+;; ------------------------------ Mark All Headers -----------------------------
+;; convenience function to mark all headers for an action
+
+(defun cj/mu4e-mark-all-headers ()
+ "Mark all headers for a later action.
+
+Prompts user for the action when executing."
+ (interactive)
+ (mu4e-headers-mark-for-each-if
+ (cons 'something nil)
+ (lambda (_msg _param) t)))
+
+;; ---------------------------------- SMTPmail ---------------------------------
+;; send mail to smtp host from smtpmail temp buffer.
+
+(use-package smtpmail
+ :ensure nil ;; built-in
+ :defer .5
+ :config
+ (setq sendmail-program (executable-find "msmtp"))
+ (setq send-mail-function 'message-send-mail-with-sendmail
+ message-send-mail-function 'message-send-mail-with-sendmail)
+ (setq message-sendmail-envelope-from 'header)
+ (setq smtpmail-debug-info t))
+
+;; --------------------------------- Mu4e Email --------------------------------
+
+(use-package mu4e
+ :ensure nil ;; mu4e gets installed by installing 'mu' via the system package manager
+ :load-path "/usr/share/emacs/site-lisp/mu4e/"
+ :defer .5
+ :bind
+ ("C-c m". mu4e)
+ (:map mu4e-headers-mode-map
+ ("M" . cj/mu4e-mark-all-headers)
+ ("D" . mu4e-headers-mark-for-trash)
+ ("d" . mu4e-headers-mark-for-delete))
+ (:map mu4e-view-mode-map
+ ("r" . mu4e-compose-wide-reply)
+ ("R" . mu4e-compose-reply))
+ :hook
+ (mu4e-view-mode . turn-on-visual-line-mode)
+ :config
+ (setq gnus-blocked-images "http") ;; block external images (i.e., 1 px trackers)
+ (setq mail-user-agent 'mu4e-user-agent) ;; default to mu4e for email
+ (setq message-citation-line-format "On %a %d %b %Y at %R, %f wrote:\n") ;; helps show up properly in Outlook/Gmail threads
+ (setq message-citation-line-function 'message-insert-formatted-citation-line)
+ (setq message-kill-buffer-on-exit t) ;; don't keep message buffers around
+ (setq mu4e-change-filenames-when-moving t) ;; avoid gmail dup UID issues: https://goo.gl/RTCgVa
+ (setq mu4e-completing-read-function 'completing-read) ;; use generic completing read, rather than ido
+ (setq mu4e-compose-context-policy 'ask) ;; ask for context if no context matches
+
+ ;; (setq mu4e-compose-format-flowed t) ;; plain text mails must flow correctly for recipients
+ (setq mu4e-compose-keep-self-cc t) ;; keep me in the cc list
+ (setq mu4e-compose-signature-auto-include nil) ;; don't include signature by default
+ (setq mu4e-confirm-quit nil) ;; don't ask when quitting
+ (setq mu4e-context-policy 'pick-first) ;; start with the first (default) context
+ (setq mu4e-headers-auto-update nil) ;; updating headers buffer on email is too jarring
+ (setq mu4e-root-maildir mail-dir) ;; root directory for all email accounts
+ (setq mu4e-maildir mail-dir) ;; same as above (for newer mu4e)
+ (setq mu4e-sent-messages-behavior 'delete) ;; don't save to "Sent", IMAP does this already
+ (setq mu4e-show-images t) ;; show embedded images
+ (setq mu4e-update-interval nil) ;; disallow automatic checking for new emails
+
+ ;; Format=flowed for better plain text email handling
+ ;; This will be automatically disabled when org-msg is active
+ (setq mu4e-compose-format-flowed t)
+
+ (setq mu4e-html2text-command 'mu4e-shr2text) ;; email conversion to html via shr2text
+ (setq mu4e-mu-binary (executable-find "mu"))
+ (setq mu4e-get-mail-command (concat (executable-find "mbsync") " -a")) ;; command to sync mail
+ (setq mu4e-user-mail-address-list '("c@cjennings.net" "craigmartinjennings@gmail.com"))
+ (setq mu4e-index-update-error-warning nil) ;; don't warn me about spurious sync issues
+
+ ;; ------------------------------ Mu4e Contexts ------------------------------
+
+ (setq mu4e-contexts
+ (list
+ (make-mu4e-context
+ :name "gmail.com"
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p "/gmail" (mu4e-message-field msg :maildir))))
+ :vars '((user-mail-address . "craigmartinjennings@gmail.com")
+ (user-full-name . "Craig Jennings")
+ (mu4e-drafts-folder . "/gmail/Drafts")
+ (mu4e-sent-folder . "/gmail/Sent")
+ (mu4e-starred-folder . "/gmail/Starred")
+ (mu4e-trash-folder . "/gmail/Trash")))
+
+ (make-mu4e-context
+ :name "cjennings.net"
+ :match-func
+ (lambda (msg)
+ (when msg
+ (string-prefix-p "/cmail" (mu4e-message-field msg :maildir))))
+ :vars '((user-mail-address . "c@cjennings.net")
+ (user-full-name . "Craig Jennings")
+ (mu4e-drafts-folder . "/cmail/Drafts")
+ (mu4e-sent-folder . "/cmail/Sent")))))
+
+ (setq mu4e-maildir-shortcuts
+ '(("/cmail/Inbox" . ?i)
+ ("/cmail/Sent" . ?s)
+ ("/gmail/Inbox" . ?I)
+ ("/gmail/Sent" . ?S)))
+
+ ;; ------------------------------ Mu4e Bookmarks -----------------------------
+
+ (setq mu4e-bookmarks
+ `((:name "cjennings inbox"
+ :query "maildir:/cmail/INBOX"
+ :key ?i)
+ (:name "cjennings unread"
+ :query "maildir:/cmail/INBOX AND flag:unread AND NOT flag:trashed"
+ :key ?u)
+ (:name "cjennings starred"
+ :query "maildir:/cmail/INBOX AND flag:flagged"
+ :key ?s)
+ (:name "cjennings large"
+ :query "maildir:/cmail/INBOX AND size:5M..999M"
+ :key ?l)
+ (:name "gmail.com inbox"
+ :query "maildir:/gmail/INBOX"
+ :key ?I)
+ (:name "gmail.com unread"
+ :query "maildir:/gmail/INBOX AND flag:unread AND NOT flag:trashed"
+ :key ?U)
+ (:name "gmail.com starred"
+ :query "maildir:/gmail/INBOX AND flag:flagged"
+ :key ?S)
+ (:name "gmail.com large"
+ :query "maildir:/gmail/INBOX AND size:5M..999M"
+ :key ?L)))
+
+ (defun no-auto-fill ()
+ "Turn off \'auto-fill-mode\'."
+ (auto-fill-mode -1))
+ (add-hook 'mu4e-compose-mode-hook #'no-auto-fill)
+
+ ;; Always BCC myself
+ ;; http://www.djcbsoftware.nl/code/mu/mu4e/Compose-hooks.html
+ (defun cj/add-cc-bcc-header ()
+ "Add CC: and BCC: to myself header."
+ (save-excursion (message-add-header
+ (concat "CC: " "\n")
+ ;; pre hook above changes user-mail-address.
+ (concat "Bcc: " user-mail-address "\n"))))
+ (add-hook 'mu4e-compose-mode-hook 'cj/add-cc-bcc-header)
+
+ ;; remap the awkward mml-attach-file to the quicker mail-add-attachment
+ (define-key mu4e-compose-mode-map [remap mml-attach-file] 'mail-add-attachment)
+
+ ;; don't allow openwith to mess with your attachments
+ (add-to-list 'mm-inhibit-file-name-handlers 'openwith-file-handler)
+
+ ;; use imagemagick to render images, if available
+ (when (fboundp 'imagemagick-register-types)
+ (imagemagick-register-types))
+
+ ;; ------------------------------ HTML Settings ------------------------------
+ ;; also see org-msg below
+
+ ;; Prefer HTML over plain text when both are available
+ (setq mu4e-view-prefer-html t)
+
+ ;; Use a better HTML renderer with more control
+ (setq mu4e-html2text-command
+ (cond
+ ;; Best option: pandoc (if available)
+ ((executable-find "pandoc")
+ "pandoc -f html -t plain --reference-links")
+ ;; Good option: w3m (better tables/formatting)
+ ((executable-find "w3m")
+ "w3m -dump -T text/html -cols 72 -o display_link_number=true")
+ ;; Fallback: built-in shr
+ (t 'mu4e-shr2text)))
+
+ ;; Configure shr (built-in HTML renderer) for better display
+ (setq shr-use-colors nil) ; Don't use colors in terminal
+ (setq shr-use-fonts nil) ; Don't use variable fonts
+ (setq shr-max-image-proportion 0.7) ; Limit image size
+ (setq shr-width 72) ; Set width for HTML rendering
+ (setq shr-bullet "• ") ; Nice bullet points
+
+ ;; Block remote images by default (privacy/security)
+ (setq mu4e-view-show-images t)
+ (setq mu4e-view-image-max-width 800)
+
+ ;; ------------------------------- View Actions ------------------------------
+ ;; define view and article menus
+
+ (defun cj/search-for-sender (msg)
+ "Search for messages sent by the sender of the message at point."
+ (mu4e-search
+ (concat "from:"
+ (mu4e-contact-email (car (mu4e-message-field msg :from))))))
+
+ ;; Custom function to toggle remote content and bind it in view mode
+ (defun cj/mu4e-toggle-remote-images ()
+ "Toggle display of remote images in current message."
+ (interactive)
+ (require 'mu4e-view)
+ (setq-local gnus-blocked-images
+ (if (equal gnus-blocked-images "http")
+ nil
+ "http"))
+ (mu4e-view-refresh))
+
+ ;; first letter is the keybinding
+ (setq mu4e-headers-actions
+ '(("bview in browser" . mu4e-action-view-in-browser)
+ ("asave attachment" . mu4e-view-mime-part-action)
+ ("oorg-contact-add" . mu4e-action-add-org-contact)
+ ("xsearch for sender" . cj/search-for-sender)
+ ("tshow this thread" . mu4e-action-show-thread)
+ ))
+
+ ;; first letter is the keybinding
+ (setq mu4e-view-actions
+ '(("bview in browser" . mu4e-action-view-in-browser)
+ ("asave attachments" . mu4e-view-mime-part-action)
+ ("esave attachments" . mu4e-view-save-attachments)
+ ("oorg-contact-add" . mu4e-action-add-org-contact)
+ ("Itoggle remote images" . cj/mu4e-toggle-remote-images)
+ ))
+ ;; ("ssave message to attach later" . mu4e-action-capture-message)
+ (setq mu4e-compose-complete-addresses nil)
+
+ ;; ---------------------------- Address Completion ---------------------------
+
+ ;; Disable company-mode in compose buffers
+ (defun cj/disable-company-in-mu4e-compose ()
+ "Disable company mode in mu4e compose buffers."
+ (company-mode -1))
+
+ (add-hook 'mu4e-compose-mode-hook #'cj/disable-company-in-mu4e-compose)
+
+ ;; NOTE: Key bindings for TAB and comma are now handled by
+ ;; mu4e-org-contacts-integration module which provides:
+ ;; - Smart TAB completion in email headers
+ ;; - Comma-triggered completion
+ ;; - Integration with org-contacts database
+
+ ;; Also disable company in org-msg-edit-mode
+ (with-eval-after-load 'org-msg
+ (add-hook 'org-msg-edit-mode-hook #'cj/disable-company-in-mu4e-compose))
+
+ ;; Don't spell-check email addresses and headers
+ (defun cj/disable-ispell-in-email-headers ()
+ "Disable ispell in email header fields."
+ (make-local-variable 'ispell-skip-region-alist)
+ (add-to-list 'ispell-skip-region-alist
+ '("^To:\\|^Cc:\\|^Bcc:" . "^[^:]*$"))
+ (add-to-list 'ispell-skip-region-alist
+ '("^From:" . "^[^:]*$"))
+ (add-to-list 'ispell-skip-region-alist
+ '("^Subject:" . "^[^:]*$")))
+
+ (add-hook 'mu4e-compose-mode-hook #'cj/disable-ispell-in-email-headers)
+ (add-hook 'message-mode-hook #'cj/disable-ispell-in-email-headers)
+ (add-hook 'org-msg-edit-mode-hook #'cj/disable-ispell-in-email-headers)
+
+ (require 'mu4e-org-contacts-integration)
+ (cj/activate-mu4e-org-contacts-integration)) ;; end use-package mu4e
+
+
+;; ---------------------------------- Org-Msg ----------------------------------
+;; user composes org mode; recipient receives html
+
+(use-package org-msg
+ :after (org mu4e)
+ :load-path "~/code/org-msg/"
+ :config
+
+ ;; inline CSS, no postamble, no TOC, no stars or footers
+ (setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil")
+
+ ;; hide org markup, show inline images
+ (setq org-msg-startup "hidestars inlineimages")
+
+ ;; new and html emails get the option for both text and html,
+ ;; text emails get text only replies
+ (setq org-msg-default-alternatives
+ '((new . (text html))
+ (reply-to-html . (text html))
+ (reply-to-text . (text))))
+
+ ;; Convert Org Citations to Blockquote
+ (setq org-msg-convert-citation t)
+
+ ;; enforce css usage; default renders too small
+ (setq org-msg-enforce-css t)
+
+ ;; always kill buffers on exit
+ (setq message-kill-buffer-on-exit nil)
+
+ ;; Override just the problematic styles with important tags
+ (setq org-msg-extra-css
+ (concat
+ "<style type=\"text/css\">\n"
+ "body { font-size: 14px !important; line-height: 1.6 !important; }\n"
+ "p { font-size: 14px !important; margin: 10px 0 !important; }\n"
+ "li { font-size: 14px !important; }\n"
+ "pre { font-size: 13px !important; }\n"
+ "code { font-size: 13px !important; }\n"
+ "</style>"))
+
+ ;; turn on org-msg in all compose buffers
+ (org-msg-mode +1))
+
+(advice-add #'mu4e-compose-reply
+ :after (lambda (&rest _) (org-msg-edit-mode)))
+(advice-add #'mu4e-compose-wide-reply
+ :after (lambda (&rest _) (org-msg-edit-mode)))
+
+(provide 'mail-config)
+;;; mail-config.el ends here