doomemacs/modules/email/mu4e/config.el
Henrik Lissner 659f7bfc71
refactor!: deprecate IS-* OS constants
BREAKING CHANGE: This deprecates the IS-(MAC|WINDOWS|LINUX|BSD) family
of global constants in favor of a native `featurep` check:

  IS-MAC      ->  (featurep :system 'macos)
  IS-WINDOWS  ->  (featurep :system 'windows)
  IS-LINUX    ->  (featurep :system 'linux)
  IS-BSD      ->  (featurep :system 'bsd)

The constants will stick around until the v3 release so folks can still
use it -- and there are still some modules that use it, but I'll phase
those uses out gradually.

Fix: #7479
2024-02-04 17:54:29 -05:00

747 lines
36 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; email/mu4e/config.el -*- lexical-binding: t; -*-
(defvar +mu4e-backend 'mbsync
"Which backend to use. Can either be offlineimap, mbsync or nil (manual).")
(defvar +mu4e-personal-addresses 'nil
"Alternative to mu4e-personal-addresses that can be set for each account (mu4e context).")
;;
;;; Packages
(use-package! mu4e
:commands mu4e mu4e-compose-new
:init
(provide 'html2text) ; disable obsolete package
(when (or (not (or (require 'mu4e-config nil t)
(require 'mu4e-meta nil t)))
(version< mu4e-mu-version "1.4"))
(setq mu4e-maildir "~/.mail"
mu4e-user-mail-address-list nil))
(setq mu4e-attachment-dir
(lambda (&rest _)
(expand-file-name ".attachments" (mu4e-root-maildir))))
:config
(when (version< mu4e-mu-version "1.8")
;; Define aliases to maintain backwards compatibility. The list of suffixes
;; were obtained by comparing mu4e~ and mu4e-- functions in `obarray'.
(dolist (transferable-suffix
'("check-requirements" "contains-line-matching" "context-ask-user"
"context-autoswitch" "default-handler" "get-folder" "get-log-buffer"
"get-mail-process-filter" "guess-maildir" "key-val"
"longest-of-maildirs-and-bookmarks" "maildirs-with-query"
"main-action-str" "main-bookmarks" "main-maildirs" "main-menu"
"main-queue-size" "main-redraw-buffer"
"main-toggle-mail-sending-mode" "main-view" "main-view-queue"
"main-view-real" "main-view-real-1" "mark-ask-target"
"mark-check-target" "mark-clear" "mark-find-headers-buffer"
"mark-get-dyn-target" "mark-get-markpair" "mark-get-move-target"
"mark-in-context" "mark-initialize" "org-store-link-message"
"org-store-link-query" "pong-handler" "read-char-choice"
"read-patch-directory" "replace-first-line-matching"
"request-contacts-maybe" "rfc822-phrase-type" "start" "stop"
"temp-window" "update-contacts" "update-mail-and-index-real"
"update-mail-mode" "update-sentinel-func" "view-gather-mime-parts"
"view-open-file" "view-mime-part-to-temp-file"))
(defalias (intern (concat "mu4e--" transferable-suffix))
(intern (concat "mu4e~" transferable-suffix))
"Alias to provide the API of mu4e 1.8 (mu4e~ ⟶ mu4e--).")
(dolist (transferable-proc-suffixes
'("add" "compose" "contacts" "eat-sexp-from-buf" "filter"
"find" "index" "kill" "mkdir" "move" "ping" "remove"
"sent" "sentinel" "start" "view"))
(defalias (intern (concat "mu4e--server-" transferable-proc-suffixes))
(intern (concat "mu4e~proc-" transferable-proc-suffixes))
"Alias to provide the API of mu4e 1.8 (mu4e~proc ⟶ mu4e--server)."))
(defalias 'mu4e-search-rerun #'mu4e-headers-rerun-search
"Alias to provide the API of mu4e 1.8.")
(defun mu4e (&optional background)
"If mu4e is not running yet, start it.
Then, show the main window, unless BACKGROUND (prefix-argument)
is non-nil."
(interactive "P")
(mu4e--start (and (not background) #'mu4e--main-view))))
(setq mu4e-view-show-addresses t
mu4e-view-show-images t
mu4e-view-image-max-width 800
mu4e-view-use-gnus t))
(pcase +mu4e-backend
(`mbsync
(setq mu4e-get-mail-command "mbsync -a"
mu4e-change-filenames-when-moving t))
(`offlineimap
(setq mu4e-get-mail-command "offlineimap -o -q")))
(setq mu4e-update-interval nil
mu4e-sent-messages-behavior 'sent
mu4e-hide-index-messages t
;; configuration for sending mail
message-send-mail-function #'smtpmail-send-it
smtpmail-stream-type 'starttls
message-kill-buffer-on-exit t ; close after sending
;; start with the first (default) context;
mu4e-context-policy 'pick-first
;; compose with the current context, or ask
mu4e-compose-context-policy 'ask-if-none
;; use helm/ivy/vertico
mu4e-completing-read-function
(cond ((modulep! :completion ivy) #'ivy-completing-read)
((modulep! :completion helm) #'completing-read)
((modulep! :completion vertico) #'completing-read)
(t #'ido-completing-read))
mu4e-attachment-dir
(concat
(if-let ((xdg-download-query (and (executable-find "xdg-user-dir")
(doom-call-process "xdg-user-dir" "DOWNLOAD")))
(xdg-download-dir (and (= 0 (car xdg-download-query)) (cdr xdg-download-query))))
xdg-download-dir
(expand-file-name (or (getenv "XDG_DOWNLOAD_DIR")
"Downloads")
"~"))
"/") ; a trailing / makes it easier to change directory in `read-file-name'
;; no need to ask
mu4e-confirm-quit nil
mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶")
mu4e-headers-thread-orphan-prefix '("┬>" . "┬▶ ")
mu4e-headers-thread-connection-prefix '("" . "")
mu4e-headers-thread-first-child-prefix '("├>" . "├▶")
mu4e-headers-thread-child-prefix '("├>" . "├▶")
mu4e-headers-thread-last-child-prefix '("└>" . "╰▶")
;; remove 'lists' column
mu4e-headers-fields
'((:account-stripe . 1)
(:human-date . 12)
(:flags . 6) ; 3 icon flags
(:from-or-to . 25)
(:subject)))
;; Better search symbols
(letf! ((defun make-help-button (text help-echo)
(with-temp-buffer
(insert " ")
(insert-text-button text
'help-echo help-echo
'mouse-face nil)
(buffer-string)))
(defun make-help-button-cons (text1 text2 help-echo)
(cons (make-help-button text1 help-echo)
(make-help-button text2 help-echo))))
(setq mu4e-headers-threaded-label
(make-help-button-cons "T" (nerd-icons-octicon "nf-oct-git_branch" :v-adjust 0.05)
"Thread view")
mu4e-headers-related-label
(make-help-button-cons "R" (nerd-icons-mdicon "nf-md-link" :v-adjust -0.1)
"Showing related emails")
mu4e-headers-full-label
(make-help-button-cons "F" (nerd-icons-mdicon "nf-md-disc")
"Search is full!")))
;; set mail user agent
(setq mail-user-agent 'mu4e-user-agent
message-mail-user-agent 'mu4e-user-agent)
;; Set the icons only when a graphical frame has been created
(if (display-graphic-p)
(+mu4e-initialise-icons)
;; When it's the server, wait till the first graphical frame
(add-hook!
'server-after-make-frame-hook
(defun +mu4e-initialise-icons-hook ()
(when (display-graphic-p)
(+mu4e-initialise-icons)
(remove-hook 'server-after-make-frame-hook
#'+mu4e-initialise-icons-hook)))))
(plist-put (cdr (assoc :flags mu4e-header-info)) :shortname " Flags") ; default=Flgs
(add-to-list 'mu4e-bookmarks
'("flag:flagged" "Flagged messages" ?f) t)
;; TODO avoid assuming that nerd-icons is present
(defvar +mu4e-header-colorized-faces
'(nerd-icons-green
nerd-icons-lblue
nerd-icons-purple-alt
nerd-icons-blue-alt
nerd-icons-purple
nerd-icons-yellow)
"Faces to use when coloring folders and account stripes.")
(defvar +mu4e-min-header-frame-width 120
"Minimum reasonable with for the header view.")
;; Add a column to display what email account the email belongs to,
;; and an account color stripe column
(defvar +mu4e-header--maildir-colors nil)
(setq mu4e-header-info-custom
'((:account .
(:name "Account"
:shortname "Account"
:help "which account/maildir this email belongs to"
:function
(lambda (msg)
(let ((maildir (replace-regexp-in-string
"\\`/?\\([^/]+\\)/.*\\'" "\\1"
(mu4e-message-field msg :maildir))))
(+mu4e-colorize-str
(replace-regexp-in-string
"^gmail"
(propertize "g" 'face 'bold-italic)
maildir)
'+mu4e-header--maildir-colors
maildir)))))
(:account-stripe .
(:name "Account"
:shortname ""
:help "Which account/maildir this email belongs to"
:function
(lambda (msg)
(let ((account
(replace-regexp-in-string
"\\`/?\\([^/]+\\)/.*\\'" "\\1"
(mu4e-message-field msg :maildir))))
(propertize
(+mu4e-colorize-str "" '+mu4e-header--maildir-colors account)
'help-echo account)))))
(:recipnum .
(:name "Number of recipients"
:shortname ""
:help "Number of recipients for this message"
:function
(lambda (msg)
(propertize (format "%2d"
(+ (length (mu4e-message-field msg :to))
(length (mu4e-message-field msg :cc))))
'face 'mu4e-footer-face))))))
;; Marks usually affect the current view
(defadvice! +mu4e--refresh-current-view-a (&rest _)
:after #'mu4e-mark-execute-all (mu4e-search-rerun))
;; Wrap text in messages
(setq-hook! 'mu4e-view-mode-hook truncate-lines nil)
;; mu4e now uses `display-buffer-alist' so we need to add some rules of our own
(set-popup-rule! "^\\*mu4e-\\(main\\|headers\\)\\*" :ignore t)
;; Html mails might be better rendered in a browser
(add-to-list 'mu4e-view-actions '("View in browser" . mu4e-action-view-in-browser))
(when (fboundp 'make-xwidget)
(add-to-list 'mu4e-view-actions '("xwidgets view" . mu4e-action-view-in-xwidget)))
;; Detect empty subjects, and give users an opotunity to fill something in
(defun +mu4e-check-for-subject ()
"Check that a subject is present, and prompt for a subject if not."
(save-excursion
(goto-char (point-min))
(search-forward "--text follows this line--")
(re-search-backward "^Subject:") ; this should be present no matter what
(let ((subject (string-trim (substring (thing-at-point 'line) 8))))
(when (string-empty-p subject)
(end-of-line)
(insert (read-string "Subject (optional): "))
(message "Sending...")))))
(add-hook 'message-send-hook #'+mu4e-check-for-subject)
;; The header view needs a certain amount of horizontal space to
;; actually show you all the information you want to see
;; so if the header view is entered from a narrow frame,
;; it's probably worth trying to expand it
(defun +mu4e-widen-frame-maybe ()
"Expand the mu4e-headers containing frame's width to `+mu4e-min-header-frame-width'."
(dolist (frame (frame-list))
(when (and (string= (buffer-name (window-buffer (frame-selected-window frame)))
mu4e-headers-buffer-name)
(< (frame-width) +mu4e-min-header-frame-width))
(set-frame-width frame +mu4e-min-header-frame-width))))
(add-hook 'mu4e-headers-mode-hook #'+mu4e-widen-frame-maybe)
(when (fboundp 'imagemagick-register-types)
(imagemagick-register-types))
(when (modulep! :ui workspaces)
(map! :map mu4e-main-mode-map
:ne "h" #'+workspace/other))
(map! :map mu4e-headers-mode-map
:vne "l" #'+mu4e/capture-msg-to-agenda)
;; Functionality otherwise obscured in mu4e 1.6
(when (version<= "1.6" mu4e-mu-version)
(defun +mu4e-view-select-attachment ()
"Use completing-read to select a single attachment.
Acts like a singular `mu4e-view-save-attachments', without the saving."
(if-let ((parts (delq nil (mapcar
(lambda (part)
(when (assoc "attachment" (cdr part))
part))
(mu4e--view-gather-mime-parts))))
(files (+mu4e-part-selectors parts)))
(cdr (assoc (completing-read "Select attachment: " (mapcar #'car files)) files))
(user-error (mu4e-format "No attached files found"))))
(defun +mu4e-view-open-attachment ()
"Select an attachment, and open it."
(interactive)
(mu4e--view-open-file
(mu4e--view-mime-part-to-temp-file (cdr (+mu4e-view-select-attachment)))))
(defun +mu4e-view-select-mime-part-action ()
"Select a MIME part, and perform an action on it."
(interactive)
(let ((labeledparts (+mu4e-part-selectors (mu4e--view-gather-mime-parts))))
(if labeledparts
(mu4e-view-mime-part-action
(cadr (assoc (completing-read "Select part: " (mapcar #'car labeledparts))
labeledparts)))
(user-error (mu4e-format "No parts found")))))
(map! :map mu4e-view-mode-map
:ne "A" #'+mu4e-view-select-mime-part-action
:ne "p" #'mu4e-view-save-attachments
:ne "o" #'+mu4e-view-open-attachment)
(defun +mu4e-part-selectors (parts)
"Generate selection strings for PARTS."
(if parts
(let (partinfo labeledparts maxfnamelen fnamefmt maxsizelen sizefmt)
(dolist (part parts)
(push (list :index (car part)
:mimetype (if (and (string= "text/plain" (caaddr part))
(alist-get 'charset (cdaddr part)))
(format "%s (%s)"
(caaddr part)
(alist-get 'charset (cdaddr part)))
(caaddr part))
:type (car (nth 5 part))
:filename (cdr (assoc 'filename (assoc "attachment" (cdr part))))
:size (file-size-human-readable (with-current-buffer (cadr part) (buffer-size)))
:part part)
partinfo))
(setq maxfnamelen (apply #'max 7 (mapcar (lambda (i) (length (plist-get i :filename))) partinfo))
fnamefmt (format " %%-%ds " maxfnamelen)
maxsizelen (apply #'max (mapcar (lambda (i) (length (plist-get i :size))) partinfo))
sizefmt (format "%%-%ds " maxsizelen))
(dolist (pinfo partinfo)
(push (cons (concat (propertize (format "%-2s " (plist-get pinfo :index)) 'face '(bold font-lock-type-face))
(when (featurep 'nerd-icons)
(nerd-icons-icon-for-file (or (plist-get pinfo :filename) "")))
(format fnamefmt (or (plist-get pinfo :filename)
(propertize (plist-get pinfo :type) 'face '(italic font-lock-doc-face))))
(format sizefmt (propertize (plist-get pinfo :size) 'face 'font-lock-builtin-face))
(propertize (plist-get pinfo :mimetype) 'face 'font-lock-constant-face))
(plist-get pinfo :part))
labeledparts))
labeledparts))))
(map! :localleader
:map mu4e-compose-mode-map
:desc "send and exit" "s" #'message-send-and-exit
:desc "kill buffer" "d" #'message-kill-buffer
:desc "save draft" "S" #'message-dont-send
:desc "attach" "a" #'+mu4e/attach-files)
;; Due to evil, none of the marking commands work when making a visual selection in
;; the headers view of mu4e. Without overriding any evil commands we may actually
;; want to use in and evil selection, this can be easily fixed.
(when (modulep! :editor evil)
(map! :map mu4e-headers-mode-map
:v "*" #'mu4e-headers-mark-for-something
:v "!" #'mu4e-headers-mark-for-read
:v "?" #'mu4e-headers-mark-for-unread
:v "u" #'mu4e-headers-mark-for-unmark))
(add-hook 'mu4e-compose-pre-hook '+mu4e-set-from-address-h)
(defadvice! +mu4e-ensure-compose-writeable-a (&rest _)
"Ensure that compose buffers are writable.
This should already be the case yet it does not always seem to be."
:before #'mu4e-compose-new
:before #'mu4e-compose-reply
:before #'mu4e-compose-forward
:before #'mu4e-compose-resend
(read-only-mode -1))
;; process lock control
(when (featurep :system 'windows)
(setq
+mu4e-lock-file (expand-file-name "~/AppData/Local/Temp/mu4e_lock")
+mu4e-lock-request-file (expand-file-name "~/AppData/Local/Temp/mu4e_lock_request")))
(add-hook 'kill-emacs-hook #'+mu4e-lock-file-delete-maybe)
(advice-add 'mu4e--start :around #'+mu4e-lock-start)
(advice-add 'mu4e-quit :after #'+mu4e-lock-file-delete-maybe))
(unless (modulep! +org)
(after! mu4e
(defun org-msg-mode (&optional _)
"Dummy function."
(message "Enable the +org mu4e flag to use org-msg-mode."))
(defun +mu4e-compose-org-msg-handle-toggle (&rest _)
"Placeholder to allow for the assumtion that this function is defined.
Ignores all arguments and returns nil."
nil)))
(use-package! org-msg
:after mu4e
:when (modulep! +org)
:config
(setq org-msg-options "html-postamble:nil H:5 num:nil ^:{} toc:nil author:nil email:nil tex:dvipng"
org-msg-startup "hidestars indent inlineimages"
org-msg-greeting-name-limit 3
org-msg-default-alternatives '((new . (utf-8 html))
(reply-to-text . (utf-8))
(reply-to-html . (utf-8 html)))
org-msg-convert-citation t
;; The default attachment matcher gives too many false positives,
;; it's better to be more conservative. See https://regex101.com/r/EtaiSP/4.
org-msg-attached-file-reference
"see[ \t\n]\\(?:the[ \t\n]\\)?\\(?:\\w+[ \t\n]\\)\\{0,3\\}\\(?:attached\\|enclosed\\)\\|\
(\\(?:attached\\|enclosed\\))\\|\
\\(?:attached\\|enclosed\\)[ \t\n]\\(?:for\\|is\\)[ \t\n]")
(defvar +org-msg-currently-exporting nil
"Helper variable to indicate whether org-msg is currently exporting the org buffer to HTML.
Usefull for affecting HTML export config.")
(defadvice! +org-msg--now-exporting-a (&rest _)
:before #'org-msg-org-to-xml
(setq +org-msg-currently-exporting t))
(defadvice! +org-msg--not-exporting-a (&rest _)
:after #'org-msg-org-to-xml
(setq +org-msg-currently-exporting nil))
(advice-add #'org-html-latex-fragment :override #'+org-html-latex-fragment-scaled-a)
(advice-add #'org-html-latex-environment :override #'+org-html-latex-environment-scaled-a)
(map! :map org-msg-edit-mode-map
:desc "attach" "C-c C-a" #'+mu4e/attach-files
:localleader
:desc "attach" "a" #'+mu4e/attach-files)
;; I feel like it's reasonable to expect files to be attached
;; in the order you attach them, not the reverse.
(defadvice! +org-msg-attach-attach-in-order-a (file &rest _args)
"Link FILE into the list of attachment."
:override #'org-msg-attach-attach
(interactive (list (read-file-name "File to attach: ")))
(let ((files (org-msg-get-prop "attachment")))
(org-msg-set-prop "attachment" (nconc files (list file)))))
(defvar +mu4e-compose-org-msg-toggle-next t ; t to initialise org-msg
"Whether to toggle ")
(defun +mu4e-compose-org-msg-handle-toggle (toggle-p)
(when (xor toggle-p +mu4e-compose-org-msg-toggle-next)
(org-msg-mode (if org-msg-mode -1 1))
(setq +mu4e-compose-org-msg-toggle-next
(not +mu4e-compose-org-msg-toggle-next))))
(defadvice! +mu4e-maybe-toggle-org-msg-a (fn &optional toggle-p)
:around #'mu4e-compose-new
:around #'mu4e-compose-reply
:around #'mu4e-compose-forward
:around #'mu4e-compose-resend
(interactive "p")
(+mu4e-compose-org-msg-handle-toggle (/= 1 (or toggle-p 0)))
(funcall fn))
(defadvice! +mu4e-draft-open-signature-a (fn &rest args)
"Prevent `mu4e-compose-signature' from being used with `org-msg-mode'."
:around #'mu4e-draft-open
(let ((mu4e-compose-signature (unless org-msg-mode mu4e-compose-signature)))
(apply fn args)))
(map! :map org-msg-edit-mode-map
"TAB" #'org-msg-tab) ; only <tab> bound by default
(defvar +org-msg-accent-color "#c01c28"
"Accent color to use in org-msg's generated CSS.
Must be set before org-msg is loaded to take effect.")
(setq org-msg-enforce-css
(let* ((font-family '(font-family . "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell,\
\"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\";"))
(monospace-font '(font-family . "SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;"))
(font-size '(font-size . "11pt"))
(font `(,font-family ,font-size))
(line-height '(line-height . "1.2"))
(theme-color +org-msg-accent-color)
(bold '(font-weight . "bold"))
(color `(color . ,theme-color))
(table `((margin-top . "6px") (margin-bottom . "6px")
(border-left . "none") (border-right . "none")
(border-top . "2px solid #222222")
(border-bottom . "2px solid #222222")
))
(ftl-number `(,color ,bold (text-align . "left")))
(inline-modes '(asl c c++ conf cpp csv diff ditaa emacs-lisp
fundamental ini json makefile man org plantuml
python sh xml))
(inline-src `((background-color . "rgba(27,31,35,.05)")
(border-radius . "3px")
(padding . ".2em .4em")
(font-size . "90%") ,monospace-font
(margin . 0)))
(code-src
(mapcar (lambda (mode)
`(code ,(intern (concat "src src-" (symbol-name mode)))
,inline-src))
inline-modes))
(base-quote '((padding-left . "5px") (margin-left . "10px")
(margin-top . "20px") (margin-bottom . "0")
(font-style . "italic") (background . "#f9f9f9")))
(quote-palette '("#6A8FBF" "#bf8f6a" "#6abf8a" "#906abf"
"#6aaebf" "#bf736a" "#bfb66a" "#bf6a94"
"#6abf9b" "#bf6a7d" "#acbf6a" "#6a74bf"))
(quotes
(mapcar (lambda (x)
(let ((c (nth x quote-palette)))
`(div ,(intern (format "quote%d" (1+ x)))
(,@base-quote
(color . ,c)
(border-left . ,(concat "3px solid "
(org-msg-lighten c)))))))
(number-sequence 0 (1- (length quote-palette))))))
`((del nil ((color . "grey") (border-left . "none")
(text-decoration . "line-through") (margin-bottom . "0px")
(margin-top . "10px") (line-height . "11pt")))
(a nil (,color))
(a reply-header ((color . "black") (text-decoration . "none")))
(div reply-header ((padding . "3.0pt 0in 0in 0in")
(border-top . "solid #e1e1e1 1.0pt")
(margin-bottom . "20px")))
(span underline ((text-decoration . "underline")))
(li nil (,line-height (margin-bottom . "0px")
(margin-top . "2px")
(max-width . "47em")))
(nil org-ul ((list-style-type . "disc")))
(nil org-ol (,@font ,line-height (margin-bottom . "0px")
(margin-top . "0px") (margin-left . "30px")
(padding-top . "0px") (padding-left . "5px")))
(nil signature (,@font (margin-bottom . "20px")))
(blockquote nil ((padding . "2px 12px") (margin-left . "10px")
(margin-top . "10px") (margin-bottom . "0")
(border-left . "3px solid #ccc")
(font-style . "italic")
(background . "#f9f9f9")))
(p blockquote ((margin . "0") (padding . "4px 0")))
,@quotes
(code nil (,font-size ,monospace-font (background . "#f9f9f9")))
,@code-src
(nil linenr ((padding-right . "1em")
(color . "black")
(background-color . "#aaaaaa")))
(pre nil ((line-height . "1.2")
(color . ,(face-foreground 'default))
(background-color . ,(face-background 'default))
(margin . "4px 0px 8px 0px")
(padding . "8px 12px")
(width . "max-content")
(min-width . "50em")
(border-radius . "5px")
(font-size . "0.9em")
(font-weight . "500")
,monospace-font))
(div org-src-container ((margin-top . "10px")))
(nil figure-number ,ftl-number)
(nil table-number)
(caption nil ((text-align . "left")
(background . ,theme-color)
(color . "white")
,bold))
(nil t-above ((caption-side . "top")))
(nil t-bottom ((caption-side . "bottom")))
(nil listing-number ,ftl-number)
(nil figure ,ftl-number)
(nil org-src-name ,ftl-number)
(img nil ((vertical-align . "middle")
(max-width . "100%")))
(img latex-fragment-inline ((margin . "0 0.1em")))
(table nil (,@table ,line-height (border-collapse . "collapse")))
(th nil ((border . "none") (border-bottom . "1px solid #222222")
(background-color . "#EDEDED") (font-weight . "500")
(padding . "3px 10px")))
(td nil (,@table (padding . "1px 10px")
(background-color . "#f9f9f9") (border . "none")))
(td org-left ((text-align . "left")))
(td org-right ((text-align . "right")))
(td org-center ((text-align . "center")))
(kbd nil ((border . "1px solid #d1d5da") (border-radius . "3px")
(box-shadow . "inset 0 -1px 0 #d1d5da")
(background-color . "#fafbfc") (color . "#444d56")
(font-size . "0.85em")
(padding . "1px 4px") (display . "inline-block")))
(div outline-text-4 ((margin-left . "15px")))
(div outline-4 ((margin-left . "10px")))
(h4 nil ((margin-bottom . "0px") (font-size . "11pt")))
(h3 nil ((margin-bottom . "0px")
,color (font-size . "14pt")))
(h2 nil ((margin-top . "20px") (margin-bottom . "20px")
,color (font-size . "18pt")))
(h1 nil ((margin-top . "20px") (margin-bottom . "0px")
,color (font-size . "24pt")))
(p nil ((text-decoration . "none") (line-height . "1.4")
(margin-top . "10px") (margin-bottom . "0px")
,font-size (max-width . "50em")))
(b nil ((font-weight . "500") (color . ,theme-color)))
(div nil (,@font (line-height . "12pt")))))))
;;
;;; Gmail integration
(when (modulep! +gmail)
(after! mu4e
(defvar +mu4e-gmail-accounts nil
"Gmail accounts that do not contain \"gmail\" in address and maildir.
An alist of Gmail addresses of the format \((\"username@domain.com\" . \"account-maildir\"))
to which Gmail integrations (behind the `+gmail' flag of the `mu4e' module) should be applied.
See `+mu4e-msg-gmail-p' and `mu4e-sent-messages-behavior'.")
;; don't save message to Sent Messages, Gmail/IMAP takes care of this
(setq mu4e-sent-messages-behavior
(lambda () ;; TODO make use +mu4e-msg-gmail-p
(if (or (string-match-p "@gmail.com\\'" (message-sendmail-envelope-from))
(member (message-sendmail-envelope-from)
(mapcar #'car +mu4e-gmail-accounts)))
'delete 'sent)))
(defun +mu4e-msg-gmail-p (msg)
(let ((root-maildir
(replace-regexp-in-string "/.*" ""
(substring (mu4e-message-field msg :maildir) 1))))
(or (string-match-p "gmail" root-maildir)
(member root-maildir (mapcar #'cdr +mu4e-gmail-accounts)))))
;; In my workflow, emails won't be moved at all. Only their flags/labels are
;; changed. Se we redefine the trash and refile marks not to do any moving.
;; However, the real magic happens in `+mu4e-gmail-fix-flags-h'.
;;
;; Gmail will handle the rest.
(defun +mu4e--mark-seen (docid _msg target)
(mu4e--server-move docid (mu4e--mark-check-target target) "+S-u-N"))
(defvar +mu4e--last-invalid-gmail-action 0)
(delq! 'delete mu4e-marks #'assq)
(setf (alist-get 'delete mu4e-marks)
(list
:char '("D" . "")
:prompt "Delete"
:show-target (lambda (_target) "delete")
:action (lambda (docid msg target)
(if (+mu4e-msg-gmail-p msg)
(progn (message "The delete operation is invalid for Gmail accounts. Trashing instead.")
(+mu4e--mark-seen docid msg target)
(when (< 2 (- (float-time) +mu4e--last-invalid-gmail-action))
(sit-for 1))
(setq +mu4e--last-invalid-gmail-action (float-time)))
(mu4e--server-remove docid))))
(alist-get 'trash mu4e-marks)
(list :char '("d" . "")
:prompt "dtrash"
:dyn-target (lambda (_target msg) (mu4e-get-trash-folder msg))
:action (lambda (docid msg target)
(if (+mu4e-msg-gmail-p msg)
(+mu4e--mark-seen docid msg target)
(mu4e--server-move docid (mu4e--mark-check-target target) "+T-N"))))
;; Refile will be my "archive" function.
(alist-get 'refile mu4e-marks)
(list :char '("r" . "")
:prompt "rrefile"
:dyn-target (lambda (_target msg) (mu4e-get-refile-folder msg))
:action (lambda (docid msg target)
(if (+mu4e-msg-gmail-p msg)
(+mu4e--mark-seen docid msg target)
(mu4e--server-move docid (mu4e--mark-check-target target) "-N")))
#'+mu4e--mark-seen))
;; This hook correctly modifies gmail flags on emails when they are marked.
;; Without it, refiling (archiving), trashing, and flagging (starring) email
;; won't properly result in the corresponding gmail action, since the marks
;; are ineffectual otherwise.
(add-hook! 'mu4e-mark-execute-pre-hook
(defun +mu4e-gmail-fix-flags-h (mark msg)
(when (+mu4e-msg-gmail-p msg)
(pcase mark
(`trash (mu4e-action-retag-message msg "-\\Inbox,+\\Trash,-\\Draft"))
(`delete (mu4e-action-retag-message msg "-\\Inbox,+\\Trash,-\\Draft"))
(`refile (mu4e-action-retag-message msg "-\\Inbox"))
(`flag (mu4e-action-retag-message msg "+\\Starred"))
(`unflag (mu4e-action-retag-message msg "-\\Starred"))))))))
;;
;;; Alerts
(use-package! mu4e-alert
:after mu4e
:config
(setq doom-modeline-mu4e t)
(mu4e-alert-enable-mode-line-display)
(mu4e-alert-enable-notifications)
(when (version<= "1.6" mu4e-mu-version)
(defadvice! +mu4e-alert-filter-repeated-mails-fixed-a (mails)
"Filters the MAILS that have been seen already\nUses :message-id not :docid."
:override #'mu4e-alert-filter-repeated-mails
(cl-remove-if (lambda (mail)
(prog1 (and (not mu4e-alert-notify-repeated-mails)
(ht-get mu4e-alert-repeated-mails
(plist-get mail :message-id)))
(ht-set! mu4e-alert-repeated-mails
(plist-get mail :message-id)
t)))
mails)))
(when (featurep :system 'linux)
(mu4e-alert-set-default-style 'libnotify)
(defvar +mu4e-alert-bell-cmd '("paplay" . "/usr/share/sounds/freedesktop/stereo/message.oga")
"Cons list with command to play a sound, and the sound file to play.
Disabled when set to nil.")
(setq mu4e-alert-email-notification-types '(subjects))
(defun +mu4e-alert-grouped-mail-notification-formatter-with-bell (mail-group _all-mails)
"Default function to format MAIL-GROUP for notification.
ALL-MAILS are the all the unread emails"
(when +mu4e-alert-bell-cmd
(start-process "mu4e-alert-bell" nil (car +mu4e-alert-bell-cmd) (cdr +mu4e-alert-bell-cmd)))
(if (> (length mail-group) 1)
(let* ((mail-count (length mail-group))
(first-mail (car mail-group))
(title-prefix (format "You have %d unread emails"
mail-count))
(field-value (mu4e-alert--get-group first-mail))
(title-suffix (format (pcase mu4e-alert-group-by
(`:from "from %s:")
(`:to "to %s:")
(`:maildir "in %s:")
(`:priority "with %s priority:")
(`:flags "with %s flags:"))
field-value))
(title (format "%s %s" title-prefix title-suffix)))
(list :title title
:body (s-join "\n"
(mapcar (lambda (mail)
(format "%s<b>%s</b> • %s"
(cond
((plist-get mail :in-reply-to) "")
((string-match-p "\\`Fwd:"
(plist-get mail :subject)) "")
(t "  "))
(truncate-string-to-width (or (caar (plist-get mail :from))
(cdar (plist-get mail :from)))
20 nil nil t)
(truncate-string-to-width
(replace-regexp-in-string "\\`Re: \\|\\`Fwd: " ""
(plist-get mail :subject))
40 nil nil t)))
mail-group))))
(let* ((new-mail (car mail-group))
(subject (plist-get new-mail :subject))
(sender (caar (plist-get new-mail :from))))
(list :title sender :body subject))))
(setq mu4e-alert-grouped-mail-notification-formatter #'+mu4e-alert-grouped-mail-notification-formatter-with-bell)))