diff --git a/docs/modules.org b/docs/modules.org index 2187b1cb5..539f24be7 100644 --- a/docs/modules.org +++ b/docs/modules.org @@ -84,7 +84,7 @@ Modules that reconfigure or augment packages or features built into Emacs. + [[file:../modules/emacs/vc/README.org][vc]] - TODO * :email -+ [[file:../modules/email/mu4e/README.org][mu4e]] =+gmail= - TODO ++ [[file:../modules/email/mu4e/README.org][mu4e]] =+org +gmail= - TODO + [[file:../modules/email/notmuch/README.org][notmuch]] - TODO + wanderlust =+gmail= - TODO diff --git a/init.example.el b/init.example.el index b24c44cb6..457989e54 100644 --- a/init.example.el +++ b/init.example.el @@ -171,7 +171,7 @@ ;;zig ; C, but simpler :email - ;;(mu4e +gmail) + ;;(mu4e +org +gmail) ;;notmuch ;;(wanderlust +gmail) diff --git a/modules/email/mu4e/README.org b/modules/email/mu4e/README.org index 4fa541360..78df081a8 100644 --- a/modules/email/mu4e/README.org +++ b/modules/email/mu4e/README.org @@ -3,21 +3,20 @@ #+SINCE: v2.0 #+STARTUP: inlineimages -* Table of Contents :TOC: +* Table of Contents :TOC:noexport: - [[#description][Description]] + - [[#maintainers][Maintainers]] - [[#module-flags][Module Flags]] - [[#plugins][Plugins]] - [[#prerequisites][Prerequisites]] - - [[#macos][MacOS]] - - [[#arch-linux][Arch Linux]] - [[#nixos][NixOS]] - - [[#opensuse][openSUSE]] - - [[#debianubuntu][Debian/Ubuntu]] - [[#features][Features]] - [[#configuration][Configuration]] - [[#offlineimap][offlineimap]] - [[#mbsync][mbsync]] - [[#mu-and-mu4e][mu and mu4e]] + - [[#orgmsg][OrgMsg]] + - [[#mu4e-alert][mu4e-alert]] - [[#troubleshooting][Troubleshooting]] - [[#no-such-file-or-directory-mu4e][=No such file or directory, mu4e=]] - [[#void-function-org-time-add-error-on-gentoo][~(void-function org-time-add)~ error on Gentoo]] @@ -34,41 +33,50 @@ It uses ~mu4e~ to read my email, but depends on ~offlineimap~ (to sync my email via IMAP) and ~mu~ (to index my mail into a format ~mu4e~ can understand). #+end_quote +** Maintainers ++ [[https://github.com/tecosaur][@tecosaur]] + ** Module Flags -+ ~+gmail~ Enables gmail-specific configuration. ++ =+gmail= Enables gmail-specific configuration for mail ~To~ or ~From~ a gmail + address, or a maildir with ~gmail~ in the name. ++ =+org= Use =org-msg= for composing email in Org, then sending a multipart text + (ASCII export) and HTML message. ** Plugins -+ [[https://github.com/jeremy-compostella/org-msg][org-msg]] ++ [[https://github.com/iqbalansari/mu4e-alert][mu4e-alert]] ++ =+org= + + [[https://github.com/jeremy-compostella/org-msg][org-msg]] * Prerequisites This module requires: -+ Either ~mbsync~ (default) or ~offlineimap~ (to sync mail with) ++ Either ~mbsync~ (recommended, default) or ~offlineimap~ (to sync mail with) + ~mu~, to index your downloaded messages and to provide the ~mu4e~ package. -** MacOS -#+BEGIN_SRC sh -brew install mu -# And one of the following -brew install isync # mbsync -brew install offlineimap -#+END_SRC +#+name: Install Matrix +| Platform | Install command | Base packages | +|---------------+------------------------+---------------------| +| MacOS | ~brew install ~ | =mu= | +| Arch | ~pacman -S ~ | (AUR, ~yay -S~) =mu= | +| openSUSE | ~zypper install ~ | =maildir-utils=, =mu4e= | +| Fedora | ~dnf install ~ | =maildir-utils= | +| Debian/Ubuntu | ~apt-get install ~ | =maildir-utils=, =mu4e= | -** Arch Linux -Run one of the following commands. +The install either the =isync= (=mbsync=) or =offlineimap= package. -#+BEGIN_SRC sh -sudo pacman -S isync # mbsync -# OR -sudo pacman -S offlineimap -#+END_SRC +To send mail, mu4e uses [[https://www.gnu.org/software/emacs/manual/html_mono/smtpmail.html][smtpmail]] (an Emacs library) by default. +You can also run a local SMTP server like =sendmail= or =postfix=, or use an SMTP +forwarder such as =msmtp= (recommended). -Install ~mu~, which is not available in the main repositories but in the AUR, by -using for example the AUR helper ~yay~. - -#+BEGIN_SRC sh -yay -S mu -#+END_SRC +If you use =msmtp=, you'll likely want to add the following to your +=config.el=: +#+begin_src emacs-lisp +(setq sendmail-program "/usr/bin/msmtp" + send-mail-function #'smtpmail-send-it + message-sendmail-f-is-evil t + message-sendmail-extra-arguments '("--read-envelope-from") + message-send-mail-function #'message-send-mail-with-sendmail) +#+end_src ** NixOS #+BEGIN_SRC nix @@ -82,27 +90,16 @@ environment.systemPackages = with pkgs; [ [[https://github.com/Emiller88/dotfiles/blob/5eaabedf1b141c80a8d32e1b496055231476f65e/modules/shell/mail.nix][An example of setting up mbsync and mu with home-manager]] -** openSUSE -Remove ~#~ in ~#sync_program=offlineimap~ to choose ~offlineimap~ instead of -~mbsync~. - -#+BEGIN_SRC sh :dir /sudo:: -sync_program=isync # mbsync -#sync_program=offlineimap - -sudo zypper install maildir-utils $sync_program -#+END_SRC - -** Debian/Ubuntu -Run the command corresponding to the desired backend and the last one. -#+BEGIN_SRC sh -sudo apt-get install isync # mbsync -# or -sudo apt-get install offlineimap -# then -sudo apt-get install maildir-utils mu4e # mu and mu4e respectivly -#+END_SRC -* TODO Features +* Features ++ Tidied mu4e headers view, with flags from =all-the-icons= ++ Consistent coloring of reply depths (across compose and gnus modes) ++ Prettified =mu4e:main= view ++ Cooperative locking of the =mu= process. Another Emacs instance may request + access, or grab the lock when it's available. ++ =org-msg= integration with =+org=, which can be toggled per-message, with revamped style and + an accent color ++ Gmail integrations with the =+gmail= flag ++ Email notifications with =mu4e-alert=, and (on Linux) a customised notification style * Configuration ** offlineimap @@ -126,8 +123,8 @@ You can now proceed with the [[#mu-and-mu4e][mu and mu4e]] section. The steps needed to set up =mu4e= with =mbsync= are very similar to the ones for [[#offlineimap][offlineimap]]. -Start with writing a ~\~/.mbsyncrc~. An example for GMAIL can be found on -[[http://pragmaticemacs.com/emacs/migrating-from-offlineimap-to-mbsync-for-mu4e/][pragmaticemacs.com]]. A non-GMAIL example is available as a gist [[https://gist.github.com/agraul/60977cc497c3aec44e10591f94f49ef0][here]]. The [[http://isync.sourceforge.net/mbsync.html][manual +Start with writing a ~~/.mbsyncrc~. An example for Gmail can be found on +[[http://pragmaticemacs.com/emacs/migrating-from-offlineimap-to-mbsync-for-mu4e/][pragmaticemacs.com]]. A non-Gmail example is available as a gist [[https://gist.github.com/agraul/60977cc497c3aec44e10591f94f49ef0][here]]. The [[http://isync.sourceforge.net/mbsync.html][manual page]] contains all needed information to set up your own. Next you can download your email with ~mbsync --all~. This may take a while, but @@ -135,11 +132,18 @@ should be quicker than =offlineimap= ;). You can now proceed with the [[#mu-and-mu4e][mu and mu4e]] section. +*** Faster syncing +It's possible to use IMAP IDLE to be quickly notified of updates, then use a +tailored =mbsync= command to just fetch the new changes. + +If this is of interest, this approach can be seen [[https://tecosaur.github.io/emacs-config/config.html#fetching][in @tecosaur's config]] where +[[https://gitlab.com/shackra/goimapnotify][goimapnotify]] is used for this. + ** mu and mu4e You should have your email downloaded already. If you have not, you need to set =offlineimap= or =mbsync= up before you proceed. -Before you can use =mu4e= or the cli program =mu=, you need to index your email +Before you can use =mu4e= or the CLI program =mu=, you need to index your email initially. How to do that differs a little depending on the version of =mu= you use. You can check your version with ~mu --version~. @@ -169,6 +173,73 @@ Then configure Emacs to use your email address: t) #+END_SRC +If you use multiple email accounts, defining them with ~set-email-account!~ will +automatically set the appropriate account context when replying to emails in +that account's maildir. ~mu4e-context-policy~ and ~mu4e-compose-context-policy~ +can be modified to change context behavior when opening mu4e and composing +email: + +#+begin_src emacs-lisp +(setq mu4e-context-policy 'ask-if-none + mu4e-compose-context-policy 'always-ask) +#+end_src + +If you send mail from various email aliases for different services, +~+mu4e-personal-addresses~ can be set per-context with ~set-email-account!~. If +you are not replying to an email to or from one of the specified aliases, you +will be prompted for an alias to send from. + +*** Gmail +With the =+gmail= flag, integrations are applied which account for the different +behaviour of Gmail. + +The integrations are applied to addresses with /both/ "@gmail.com" in the +account address and "gmail" in the account maildir, as well as accounts listed +in ~+mu4e-gmail-accounts~. Any domain can be specified, so G Suite accounts can +benefit from the integrations: +#+begin_src emacs-lisp +;; if "gmail" is missing from the address or maildir, the account must be listed here +(setq +mu4e-gmail-accounts '(("hlissner@gmail.com" . "/hlissner") + ("example@example.com" . "/example"))) +#+end_src + +If you only use Gmail, you can improve performance due to the way Gmail +presents messages over IMAP: +#+begin_src emacs-lisp +;; don't need to run cleanup after indexing for gmail +(setq mu4e-index-cleanup nil + ;; because gmail uses labels as folders we can use lazy check since + ;; messages don't really "move" + mu4e-index-lazy-check t) +#+end_src + +Also, note that Gmail's IMAP settings must have "When I mark a message in IMAP +as deleted: Auto-Expunge off - Wait for the client to update the server." and +"When a message is marked as deleted and expunged from the last visible IMAP +folder: Move the message to the trash" for the integrations to work as expected. + +** OrgMsg +With the =+org= flag, =org-msg= is installed, and ~org-msg-mode~ is enabled before +composing the first message. To disable ~org-msg-mode~ by default, simply +#+BEGIN_SRC emacs-lisp :tangle no +(setq mu4e-compose--org-msg-toggle-next nil) +#+END_SRC + +To toggle org-msg for a single message, just apply the universal argument to the +compose or reply command (=SPC u= with ~evil~, =C-u= otherwise). + +The accent color that Doom uses can be customised by setting +~+org-msg-accent-color~ to a CSS color string. + +** mu4e-alert +This provides notifications through the [[https://github.com/jwiegley/alert][alert]] library. + +If you don't like this, simply add +#+begin_src emacs-lisp +(package! mu4e-alert :disable t) +#+end_src +to your [[elisp:(find-file (expand-file-name "packages.el" doom-private-dir))][packages.el]] and it will not be used. + * Troubleshooting ** =No such file or directory, mu4e= You will get =No such file or directory, mu4e= errors if you don't run ~doom diff --git a/modules/email/mu4e/autoload/advice.el b/modules/email/mu4e/autoload/advice.el new file mode 100644 index 000000000..b38bffc59 --- /dev/null +++ b/modules/email/mu4e/autoload/advice.el @@ -0,0 +1,132 @@ +;;; email/mu4e/autoload/advice.el -*- lexical-binding: t; -*- + +;;;###autoload +(defun +mu4e~main-action-str-prettier-a (str &optional func-or-shortcut) + "Highlight the first occurrence of [.] in STR. +If FUNC-OR-SHORTCUT is non-nil and if it is a function, call it +when STR is clicked (using RET or mouse-2); if FUNC-OR-SHORTCUT is +a string, execute the corresponding keyboard action when it is +clicked." + (let ((newstr + (replace-regexp-in-string + "\\[\\(..?\\)\\]" + (lambda(m) + (format "%s" + (propertize (match-string 1 m) 'face '(mode-line-emphasis bold)))) + (replace-regexp-in-string "\t\\*" "\t⚫" str))) + (map (make-sparse-keymap)) + (func (if (functionp func-or-shortcut) + func-or-shortcut + (if (stringp func-or-shortcut) + (lambda()(interactive) + (execute-kbd-macro func-or-shortcut)))))) + (define-key map [mouse-2] func) + (define-key map (kbd "RET") func) + (put-text-property 0 (length newstr) 'keymap map newstr) + (put-text-property (string-match "[A-Za-z].+$" newstr) + (- (length newstr) 1) 'mouse-face 'highlight newstr) + newstr)) + +;;;###autoload +(defun +mu4e~main-keyval-str-prettier-a (str) + "Replace '*' with '⚫' in STR." + (replace-regexp-in-string "\t\\*" "\t⚫" str)) + +;; Org msg LaTeX image scaling + +;;;###autoload +(defun +org-msg-img-scale-css (img-uri) + "For a given IMG-URI, use imagemagick to find its width." + (if +org-msg-currently-exporting + (when (and (not IS-WINDOWS)) ; relies on posix path + (let ((with-call (and (executable-find "identify") + (doom-call-process "identify" "-format" "%w" + (substring img-uri 7))))) ; 7=(length "file://") + (unless width-call + (setq width-call (doom-call-process "file" (substring img-uri 7))) + (setcdr width-call (replace-regexp-in-string "^.*image data, \\([0-9]+\\).*$" "\\1" (cdr width-call))) + (setcar width-call (if (< 0 (string-to-number (cdr width-call))) 0 1))) + (when (= (car width-call) 0) + (list :width + (format "%.1fpx" + (/ (string-to-number (cdr width-call)) + (plist-get org-format-latex-options :scale))))))) + (list :style (format "transform: scale(%.3f)" + (/ 1.0 (plist-get org-format-latex-options :scale)))))) + +;;;###autoload + (defun +org-html-latex-fragment-scaled-a (latex-fragment _contents info) + "Transcode a LATEX-FRAGMENT object from Org to HTML. +CONTENTS is nil. INFO is a plist holding contextual information. + +This differs from `org-html-latex-fragment' in that it uses the LaTeX fragment +as a meaningful alt value, applies a class to indicate what sort of fragment it is +(latex-fragment-inline or latex-fragment-block), and (on Linux) scales the image to +account for the value of :scale in `org-format-latex-options'." + (let ((latex-frag (org-element-property :value latex-fragment)) + (processing-type (plist-get info :with-latex))) + (cond + ((memq processing-type '(t mathjax)) + (org-html-format-latex latex-frag 'mathjax info)) + ((eq processing-type 'html) + (org-html-format-latex latex-frag 'html info)) + ((assq processing-type org-preview-latex-process-alist) + (let ((formula-link + (org-html-format-latex latex-frag processing-type info))) + (when (and formula-link (string-match "file:\\([^]]*\\)" formula-link)) + (let ((source (org-export-file-uri (match-string 1 formula-link))) + (attributes (append (list :alt latex-frag + :class + (concat "latex-fragment-" + (if (equal "\\(" (substring latex-frag 0 2)) + "inline" "block"))) + (when (memq processing-type '(dvipng convert)) + (+org-msg-img-scale-css source))))) + (org-html--format-image source attributes info))))) + (t latex-frag)))) + +;;;###autoload + (defun +org-html-latex-environment-scaled-a (latex-environment _contents info) + "Transcode a LATEX-ENVIRONMENT element from Org to HTML. +CONTENTS is nil. INFO is a plist holding contextual information. + +This differs from `org-html-latex-environment' in that (on Linux) it +scales the image to account for the value of :scale in `org-format-latex-options'." + (let ((processing-type (plist-get info :with-latex)) + (latex-frag (org-remove-indentation + (org-element-property :value latex-environment))) + (attributes (org-export-read-attribute :attr_html latex-environment)) + (label (and (org-element-property :name latex-environment) + (org-export-get-reference latex-environment info))) + (caption (and (org-html--latex-environment-numbered-p latex-environment) + (number-to-string + (org-export-get-ordinal + latex-environment info nil + (lambda (l _) + (and (org-html--math-environment-p l) + (org-html--latex-environment-numbered-p l)))))))) + (plist-put attributes :class "latex-environment") + (cond + ((memq processing-type '(t mathjax)) + (org-html-format-latex + (if (org-string-nw-p label) + (replace-regexp-in-string "\\`.*" + (format "\\&\n\\\\label{%s}" label) + latex-frag) + latex-frag) + 'mathjax info)) + ((assq processing-type org-preview-latex-process-alist) + (let ((formula-link + (org-html-format-latex + (org-html--unlabel-latex-environment latex-frag) + processing-type info))) + (when (and formula-link (string-match "file:\\([^]]*\\)" formula-link)) + (let ((source (org-export-file-uri (match-string 1 formula-link)))) + (org-html--wrap-latex-environment + (org-html--format-image source + (append attributes + (when (memq processing-type '(dvipng convert)) + (+org-msg-img-scale-css source))) + info) + info caption label))))) + (t (org-html--wrap-latex-environment latex-frag info caption label))))) diff --git a/modules/email/mu4e/autoload/email.el b/modules/email/mu4e/autoload/email.el index f413207df..72b867d45 100644 --- a/modules/email/mu4e/autoload/email.el +++ b/modules/email/mu4e/autoload/email.el @@ -15,6 +15,7 @@ OPTIONAL: + `mu4e-trash-folder' + `mu4e-refile-folder' + `mu4e-compose-signature' + + `+mu4e-personal-addresses' DEFAULT-P is a boolean. If non-nil, it marks that email account as the default/fallback account." @@ -22,29 +23,30 @@ default/fallback account." (when (version< mu4e-mu-version "1.4") (when-let (address (cdr (assq 'user-mail-address letvars))) (add-to-list 'mu4e-user-mail-address-list address))) + ;; remove existing context with same label (setq mu4e-contexts (cl-loop for context in mu4e-contexts unless (string= (mu4e-context-name context) label) collect context)) (let ((context (make-mu4e-context :name label - :enter-func (lambda () (mu4e-message "Switched to %s" label)) - :leave-func #'mu4e-clear-caches + :enter-func + (lambda () (mu4e-message "Switched to %s" label)) + :leave-func + (lambda () (progn (setq +mu4e-personal-addresses nil) + (mu4e-clear-caches))) :match-func (lambda (msg) (when msg (string-prefix-p (format "/%s" label) - (mu4e-message-field msg :maildir)))) + (mu4e-message-field msg :maildir) t))) :vars letvars))) - (push context mu4e-contexts) - (when default-p - (setq-default mu4e-context-current context)) + (add-to-list 'mu4e-contexts context (not default-p)) context))) - (defvar +mu4e-workspace-name "*mu4e*" - "TODO") + "Name of the workspace created by `=mu4e', dedicated to mu4e.") (defvar +mu4e--old-wconf nil) (add-hook 'mu4e-main-mode-hook #'+mu4e-init-h) @@ -55,7 +57,13 @@ default/fallback account." (interactive) (require 'mu4e) (if (featurep! :ui workspaces) - (+workspace-switch +mu4e-workspace-name t) + ;; delete current workspace if empty + ;; this is useful when mu4e is in the daemon + ;; as otherwise you can accumulate empty workspaces + (progn + (unless (+workspace-buffer-list) + (+workspace-delete (+workspace-current-name))) + (+workspace-switch +mu4e-workspace-name t)) (setq +mu4e--old-wconf (current-window-configuration)) (delete-other-windows) (switch-to-buffer (doom-fallback-buffer))) @@ -71,9 +79,241 @@ default/fallback account." ;; TODO Interactively select email account (call-interactively #'mu4e-compose-new)) +(defun +mu4e--get-string-width (str) + "Return the width in pixels of a string in the current +window's default font. If the font is mono-spaced, this +will also be the width of all other printable characters." + (let ((window (selected-window)) + (remapping face-remapping-alist)) + (with-temp-buffer + (make-local-variable 'face-remapping-alist) + (setq face-remapping-alist remapping) + (set-window-buffer window (current-buffer)) + (insert str) + (car (window-text-pixel-size))))) -;; -;; Hooks +(cl-defun +mu4e-normalised-icon (name &key set color height v-adjust) + "Convert :icon declaration to icon" + (let* ((icon-set (intern (concat "all-the-icons-" (or set "faicon")))) + (v-adjust (or v-adjust 0.02)) + (height (or height 0.8)) + (icon (if color + (apply icon-set `(,name :face ,(intern (concat "all-the-icons-" color)) :height ,height :v-adjust ,v-adjust)) + (apply icon-set `(,name :height ,height :v-adjust ,v-adjust)))) + (icon-width (+mu4e--get-string-width icon)) + (space-width (+mu4e--get-string-width " ")) + (space-factor (- 2 (/ (float icon-width) space-width)))) + (concat (propertize " " 'display `(space . (:width ,space-factor))) icon))) + +;; Set up all the fancy icons +;;;###autoload +(defun +mu4e-initialise-icons () + (setq mu4e-use-fancy-chars t + mu4e-headers-draft-mark (cons "D" (+mu4e-normalised-icon "pencil")) + mu4e-headers-flagged-mark (cons "F" (+mu4e-normalised-icon "flag")) + mu4e-headers-new-mark (cons "N" (+mu4e-normalised-icon "sync" :set "material" :height 0.8 :v-adjust -0.10)) + mu4e-headers-passed-mark (cons "P" (+mu4e-normalised-icon "arrow-right")) + mu4e-headers-replied-mark (cons "R" (+mu4e-normalised-icon "arrow-right")) + mu4e-headers-seen-mark (cons "S" "") ;(+mu4e-normalised-icon "eye" :height 0.6 :v-adjust 0.07 :color "dsilver")) + mu4e-headers-trashed-mark (cons "T" (+mu4e-normalised-icon "trash")) + mu4e-headers-attach-mark (cons "a" (+mu4e-normalised-icon "file-text-o" :color "silver")) + mu4e-headers-encrypted-mark (cons "x" (+mu4e-normalised-icon "lock")) + mu4e-headers-signed-mark (cons "s" (+mu4e-normalised-icon "certificate" :height 0.7 :color "dpurple")) + mu4e-headers-unread-mark (cons "u" (+mu4e-normalised-icon "eye-slash" :v-adjust 0.05)))) + +(defun +mu4e-colorize-str (str &optional unique herring) + "Apply a face from `+mu4e-header-colorized-faces' to STR. +If HERRING is set, it will be used to determine the face instead of STR. +Will try to make unique when non-nil UNIQUE, +a quoted symbol for a alist of current strings and faces provided." + (unless herring + (setq herring str)) + (put-text-property + 0 (length str) + 'face + (if (not unique) + (+mu4e--str-color-face herring str) + (let ((unique-alist (eval unique))) + (unless (assoc herring unique-alist) + (if (> (length unique-alist) (length +mu4e-header-colorized-faces)) + (push (cons herring (+mu4e--str-color-face herring)) unique-alist) + (let ((offset 0) color color?) + (while (not color) + (setq color? (+mu4e--str-color-face herring offset)) + (if (not (rassoc color? unique-alist)) + (setq color color?) + (setq offset (1+ offset)) + (when (> offset (length +mu4e-header-colorized-faces)) + (message "Warning: +mu4e-colorize-str was called with non-unique-alist UNIQUE-alist alist.") + (setq color (+mu4e--str-color-face herring))))) + (push (cons herring color) unique-alist))) + (set unique unique-alist)) + (cdr (assoc herring unique-alist)))) + str) + str) + +(defun +mu4e--str-color-face (str &optional offset) + "Select a face from `+mu4e-header-colorized-faces' based on +STR and any integer OFFSET." + (let* ((str-sum (apply #'+ (mapcar (lambda (c) (% c 3)) str))) + (color (nth (% (+ str-sum (if offset offset 0)) + (length +mu4e-header-colorized-faces)) + +mu4e-header-colorized-faces))) + color)) + +(defvar +org-capture-emails-file "todo.org" + "Default target for storing mu4e emails captured from within mu4e. +Requires a \"* Email\" heading be present in the file.") + +;; Adding emails to the agenda +;; Perfect for when you see an email you want to reply to +;; later, but don't want to forget about +;;;###autoload +(defun +mu4e/capture-msg-to-agenda (arg) + "Refile a message and add a entry in `+org-capture-emails-file' with a +deadline. Default deadline is today. With one prefix, deadline +is tomorrow. With two prefixes, select the deadline." + (interactive "p") + (let ((sec "^* Email") + (msg (mu4e-message-at-point))) + (when msg + ;; put the message in the agenda + (with-current-buffer (find-file-noselect + (expand-file-name +org-capture-emails-file org-directory)) + (save-excursion + ;; find header section + (goto-char (point-min)) + (when (re-search-forward sec nil t) + (let (org-M-RET-may-split-line + (lev (org-outline-level)) + (folded-p (invisible-p (point-at-eol))) + (from (plist-get msg :from))) + ;; place the subheader + (when folded-p (show-branches)) ; unfold if necessary + (org-end-of-meta-data) ; skip property drawer + (org-insert-todo-heading 1) ; insert a todo heading + (when (= (org-outline-level) lev) ; demote if necessary + (org-do-demote)) + ;; insert message and add deadline + (insert (concat " Respond to " + "[[mu4e:msgid:" + (plist-get msg :message-id) "][" + (truncate-string-to-width + (or (caar from) (cadr from)) 25 nil nil t) + " - " + (truncate-string-to-width + (plist-get msg :subject) 40 nil nil t) + "]] ")) + (org-deadline nil + (cond ((= arg 1) (format-time-string "%Y-%m-%d")) + ((= arg 4) "+1d"))) + + (org-update-parent-todo-statistics) + + ;; refold as necessary + (if folded-p + (progn + (org-up-heading-safe) + (hide-subtree)) + (hide-entry)))))) + ;; refile the message and update + ;; (cond ((eq major-mode 'mu4e-view-mode) + ;; (mu4e-view-mark-for-refile)) + ;; ((eq major-mode 'mu4e-headers-mode) + ;; (mu4e-headers-mark-for-refile))) + (message "Refiled \"%s\" and added to the agenda for %s" + (truncate-string-to-width + (plist-get msg :subject) 40 nil nil t) + (cond ((= arg 1) "today") + ((= arg 4) "tomorrow") + (t "later")))))) + +;;;###autoload +(defun +mu4e/attach-files (&optional files-to-attach) + "When called in a dired buffer, ask for a message to attach the marked files to. +When called in a mu4e:compose or org-msg buffer, `read-file-name'to either +attach a file, or select a folder to open dired in and select file attachments +(using `dired-mu4e-attach-ctrl-c-ctrl-c'). + +When otherwise called, open a dired buffer and enable `dired-mu4e-attach-ctrl-c-ctrl-c'." + ;; TODO add ability to attach files (+dirs) as a single (named) archive + (interactive "p") + (+mu4e-compose-org-msg-handle-toggle (/= 1 files-to-attach)) + (pcase major-mode + ((or 'mu4e-compose-mode 'org-msg-edit-mode) + (let ((mail-buffer (current-buffer)) + (location (read-file-name "Attach: "))) + (if (not (file-directory-p location)) + (pcase major-mode + ('mu4e-compose-mode + (save-excursion + (goto-char (point-max)) + (unless (eq (current-column) 0) + (insert "\n\n") + (forward-line 2)) + (mail-add-attachment location))) + ('org-msg-edit-mode (org-msg-attach-attach location))) + (split-window-sensibly) + (with-current-buffer (dired location) + (setq-local dired-mail-buffer mail-buffer) + (dired-mu4e-attach-ctrl-c-ctrl-c 1))))) + ('dired-mode + (unless (and files-to-attach (/= 1 files-to-attach)) + (setq files-to-attach + (delq nil + (mapcar + ;; don't attach directories + (lambda (f) (if (file-directory-p f) nil f)) + (nreverse (dired-map-over-marks (dired-get-filename) nil)))))) + (if (not files-to-attach) + (progn + (message "No files marked, aborting.") + (kill-buffer-and-window)) + (if-let ((mail-target-buffer (bound-and-true-p dired-mail-buffer))) + (progn (kill-buffer-and-window) + (switch-to-buffer mail-target-buffer)) + (if (and (+mu4e-current-buffers) + (y-or-n-p "Attach files to existing mail composition buffer? ")) + (progn (setf mail-target-buffer + (completing-read "Message: " (+mu4e-current-buffers))) + (kill-buffer-and-window) + (switch-to-buffer mail-target-buffer)) + (kill-buffer-and-window) + (mu4e-compose 'new))) + (mapcar + (pcase major-mode + ('mu4e-compose-mode #'mail-add-attachment) + ('org-msg-edit-mode #'org-msg-attach-attach)) + files-to-attach))) + (_ + (split-window-sensibly) + (with-current-buffer (call-interactively #'find-file) + (dired-mu4e-attach-ctrl-c-ctrl-c 1))))) + +(define-minor-mode dired-mu4e-attach-ctrl-c-ctrl-c + "Adds C-c C-c as a keybinding to attach files to a message." + :lighter "attach" + :keymap (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-c") '+mu4e/attach-files) + map) + (setq header-line-format + (when dired-mu4e-attach-ctrl-c-ctrl-c + (substitute-command-keys + "Mu4e attach active. `\\[+mu4e/attach-files]' to attach the marked files.")))) + +(defun +mu4e-current-buffers () + "Return a list of active message buffers." + (let (buffers) + (save-current-buffer + (dolist (buffer (buffer-list t)) + (set-buffer buffer) + (when (or (and (derived-mode-p 'message-mode) + (null message-sent-message-via)) + (eq major-mode 'org-msg-edit-mode)) + (push (buffer-name buffer) buffers)))) + (nreverse buffers))) + +;;; Hooks (defun +mu4e-init-h () (add-hook 'kill-buffer-hook #'+mu4e-kill-mu4e-h nil t)) @@ -87,3 +327,22 @@ default/fallback account." (+mu4e--old-wconf (set-window-configuration +mu4e--old-wconf) (setq +mu4e--old-wconf nil)))) + +;;;###autoload +(defun +mu4e-set-from-address-h () + "If the user defines multiple `+mu4e-personal-addresses' for email aliases +within a context, set `user-mail-address' to an alias found in the 'To' or +'From' headers of the parent message if present, or prompt the user for a +preferred alias" + (when-let ((addresses (if (or mu4e-contexts +mu4e-personal-addresses) + (and (> (length +mu4e-personal-addresses) 1) + +mu4e-personal-addresses) + (mu4e-personal-addresses)))) + (setq user-mail-address + (if mu4e-compose-parent-message + (let ((to (mapcar #'cdr (mu4e-message-field mu4e-compose-parent-message :to))) + (from (mapcar #'cdr (mu4e-message-field mu4e-compose-parent-message :from)))) + (or (car (seq-intersection to addresses)) + (car (seq-intersection from addresses)) + (completing-read "From: " addresses))) + (completing-read "From: " addresses))))) diff --git a/modules/email/mu4e/autoload/mu-lock.el b/modules/email/mu4e/autoload/mu-lock.el new file mode 100644 index 000000000..f0b0bc673 --- /dev/null +++ b/modules/email/mu4e/autoload/mu-lock.el @@ -0,0 +1,91 @@ +;;; email/mu4e/autoload/mu-lock.el -*- lexical-binding: t; -*- + +(defvar +mu4e-lock-file (expand-file-name "mu4e_lock" temporary-file-directory) + "Location of the lock file which stores the PID of the process currenty running mu4e") +(defvar +mu4e-lock-request-file (expand-file-name "mu4e_lock_request" temporary-file-directory) + "Location of the lock file for which creating indicated that another process wants the lock to be released") + +(defvar +mu4e-lock-greedy nil + "Whether to 'grab' the `+mu4e-lock-file' if nobody else has it, i.e. start Mu4e") +(defvar +mu4e-lock-relaxed t + "Whether if someone else wants the lock (signaled via `+mu4e-lock-request-file'), we should stop Mu4e and let go of it") + +(defun +mu4e-lock-pid-info () + "Get info on the PID refered to in `+mu4e-lock-file' in the form (pid . process-attributes) + If the file or process do not exist, the lock file is deleted an nil returned." + (when (file-exists-p +mu4e-lock-file) + (let* ((pid (string-to-number + (with-temp-buffer + (setq coding-system-for-read 'utf-8) + (insert-file-contents +mu4e-lock-file) + (buffer-string)))) + (process (process-attributes pid))) + (if (and process (string-match-p "emacs" (alist-get 'args process))) + (cons pid process) + (delete-file +mu4e-lock-file) nil)))) + +(defun +mu4e-lock-available (&optional strict) + "If the `+mu4e-lock-file' is available (unset or owned by this emacs) return t. +If STRICT only accept an unset lock file." + (not (when-let* ((lock-info (+mu4e-lock-pid-info)) + (pid (car lock-info))) + (when (or strict (/= (emacs-pid) pid)) t)))) + +;;;###autoload +(defun +mu4e-lock-file-delete-maybe () + "Check `+mu4e-lock-file', and delete it if this process is responsible for it." + (when (+mu4e-lock-available) + (delete-file +mu4e-lock-file) + (file-notify-rm-watch +mu4e-lock--request-watcher))) + +;;;###autoload +(defun +mu4e-lock-start (orig-fun &optional callback) + "Check `+mu4e-lock-file', and if another process is responsible for it, abort starting. +Else, write to this process' PID to the lock file" + (unless (+mu4e-lock-available) + (call-process "touch" nil nil nil +mu4e-lock-request-file) + (message "Lock file exists, requesting that it be given up") + (sleep-for 0.1) + (delete-file +mu4e-lock-request-file)) + (if (not (+mu4e-lock-available)) + (user-error "Unfortunately another Emacs is already doing stuff with Mu4e, and you can only have one at a time") + (write-region (number-to-string (emacs-pid)) nil +mu4e-lock-file) + (delete-file +mu4e-lock-request-file) + (call-process "touch" nil nil nil +mu4e-lock-request-file) + (funcall orig-fun callback) + (setq +mu4e-lock--request-watcher + (file-notify-add-watch +mu4e-lock-request-file + '(change) + #'+mu4e-lock-request)))) + +(defvar +mu4e-lock--file-watcher nil) +(defvar +mu4e-lock--file-just-deleted nil) +(defvar +mu4e-lock--request-watcher nil) + +(defun +mu4e-lock-add-watcher () + (setq +mu4e-lock--file-just-deleted nil) + (file-notify-rm-watch +mu4e-lock--file-watcher) + (setq +mu4e-lock--file-watcher + (file-notify-add-watch +mu4e-lock-file + '(change) + #'+mu4e-lock-file-updated))) + +(defun +mu4e-lock-request (event) + "Handle another process requesting the Mu4e lock." + (when (equal (nth 1 event) 'created) + (when +mu4e-lock-relaxed + (mu4e~stop) + (file-notify-rm-watch +mu4e-lock--file-watcher) + (message "Someone else wants to use Mu4e, releasing lock") + (delete-file +mu4e-lock-file) + (run-at-time 0.2 nil #'+mu4e-lock-add-watcher)) + (delete-file +mu4e-lock-request-file))) + +(defun +mu4e-lock-file-updated (event) + (if +mu4e-lock--file-just-deleted + (+mu4e-lock-add-watcher) + (when (equal (nth 1 event) 'deleted) + (setq +mu4e-lock--file-just-deleted t) + (when (and +mu4e-lock-greedy (+mu4e-lock-available t)) + (message "Noticed Mu4e lock was available, grabbed it") + (run-at-time 0.2 nil #'mu4e~start))))) diff --git a/modules/email/mu4e/config.el b/modules/email/mu4e/config.el index bf7e46cd4..81b0dcbb6 100644 --- a/modules/email/mu4e/config.el +++ b/modules/email/mu4e/config.el @@ -3,6 +3,9 @@ (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 @@ -27,13 +30,13 @@ (setq mu4e-get-mail-command "offlineimap -o -q"))) (setq mu4e-update-interval nil - mu4e-compose-format-flowed t ; visual-line-mode + auto-fill upon sending mu4e-view-show-addresses t mu4e-sent-messages-behavior 'sent mu4e-hide-index-messages t ;; try to show images mu4e-view-show-images t mu4e-view-image-max-width 800 + mu4e-view-use-gnus t ; the way of the future: https://github.com/djcb/mu/pull/1442#issuecomment-591695814 ;; configuration for sending mail message-send-mail-function #'smtpmail-send-it smtpmail-stream-type 'starttls @@ -48,44 +51,106 @@ ((featurep! :completion helm) #'completing-read) ((featurep! :completion vertico) #'completing-read) (t #'ido-completing-read)) + mu4e-attachment-dir + (if (executable-find "xdg-user-dir") + ;; remove trailing newline + (substring (shell-command-to-string "xdg-user-dir DOWNLOAD") 0 -1) + (expand-file-name (or (getenv "XDG_DOWNLOAD_DIR") + "Downloads") + "~")) ;; no need to ask mu4e-confirm-quit nil + mu4e-headers-thread-single-orphan-prefix '("─>" . "─▶") + mu4e-headers-thread-orphan-prefix '("┬>" . "┬▶ ") + mu4e-headers-thread-last-child-prefix '("└>" . "╰▶") + mu4e-headers-thread-child-prefix '("├>" . "├▶") + mu4e-headers-thread-connection-prefix '("│" . "│ ") ;; remove 'lists' column mu4e-headers-fields - '((:account . 12) - (:human-date . 12) - (:flags . 4) - (:from . 25) + '((:account-stripe . 1) + (:human-date . 8) + (:flags . 6) ; 3 icon flags + (:from-or-to . 25) (:subject))) ;; set mail user agent - (setq mail-user-agent 'mu4e-user-agent) + (setq mail-user-agent 'mu4e-user-agent + message-mail-user-agent 'mu4e-user-agent) - ;; Use fancy icons - (setq mu4e-use-fancy-chars t - mu4e-headers-draft-mark '("D" . "") - mu4e-headers-flagged-mark '("F" . "") - mu4e-headers-new-mark '("N" . "") - mu4e-headers-passed-mark '("P" . "") - mu4e-headers-replied-mark '("R" . "") - mu4e-headers-seen-mark '("S" . "") - mu4e-headers-trashed-mark '("T" . "") - mu4e-headers-attach-mark '("a" . "") - mu4e-headers-encrypted-mark '("x" . "") - mu4e-headers-signed-mark '("s" . "") - mu4e-headers-unread-mark '("u" . "")) + ;; 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))))) - ;; Add a column to display what email account the email belongs to. - (add-to-list 'mu4e-header-info-custom - '(:account - :name "Account" - :shortname "Account" - :help "Which account this email belongs to" - :function - (lambda (msg) - (let ((maildir (mu4e-message-field msg :maildir))) - (format "%s" (substring maildir 1 (string-match-p "/" maildir 1))))))) + (plist-put (cdr (assoc :flags mu4e-header-info)) :shortname " Flags") ; default=Flgs + (add-to-list 'mu4e-bookmarks + '(:name "Flagged messages" :query "flag:flagged" :key ?f) t) + ;; TODO avoid assuming that all-the-icons is present + (defvar +mu4e-header-colorized-faces + '(all-the-icons-green + all-the-icons-lblue + all-the-icons-purple-alt + all-the-icons-blue-alt + all-the-icons-purple + all-the-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-headers-rerun-search)) @@ -94,25 +159,266 @@ ;; 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-with-xwidget))) + + ;; 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 frame with if it's less than `+mu4e-min-header-frame-width'." + (when (< (frame-width) +mu4e-min-header-frame-width) + (set-frame-width (selected-frame) +mu4e-min-header-frame-width))) + (add-hook 'mu4e-headers-mode-hook #'+mu4e-widen-frame-maybe) (when (fboundp 'imagemagick-register-types) (imagemagick-register-types)) + (map! :map mu4e-main-mode-map + :ne "h" #'+workspace/other) + + (map! :map mu4e-headers-mode-map + :vne "l" #'+mu4e/capture-msg-to-agenda) + (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" #'mail-add-attachment)) + :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 (featurep! :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)) + + (advice-add #'mu4e~key-val :filter-return #'+mu4e~main-keyval-str-prettier-a) + (advice-add #'mu4e~main-action-str :override #'+mu4e~main-action-str-prettier-a) + (when (featurep! :editor evil) + ;; As +mu4e~main-action-str-prettier replaces [k]ey with key q]uit should become quit + (setq evil-collection-mu4e-end-region-misc "quit")) + + ;; process lock control + (when IS-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 (featurep! +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 - :hook (mu4e-compose-pre . org-msg-mode) + :after mu4e + :when (featurep! +org) :config - (setq org-msg-startup "inlineimages" + (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 '(html text))) + org-msg-default-alternatives '((new . (utf-8 html)) + (reply-to-text . (utf-8)) + (reply-to-html . (utf-8 html))) + org-msg-convert-citation t) + (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) + + (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 (orig-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 orig-fn)) + + (defadvice! +mu4e-draft-open-signature-a (orig-fn compose-type &optional msg) + "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))) + (funcall orig-fn compose-type msg))) + + (map! :map org-msg-edit-mode-map + "TAB" #'org-msg-tab) ; only 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 . "84ch"))) + (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 . ,(doom-color 'fg)) + (background-color . ,(doom-color 'bg)) + (margin . "4px 0px 8px 0px") + (padding . "8px 12px") + (width . "max-content") + (min-width . "80ch") + (border-radius . "5px") + (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") + (padding . "3px 5px") (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 . "90ch"))) + (b nil ((font-weight . "500") (color . ,theme-color))) + (div nil (,@font (line-height . "12pt"))))))) ;; @@ -120,36 +426,71 @@ (when (featurep! +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 'delete + (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))) - ;; don't need to run cleanup after indexing for gmail - mu4e-index-cleanup nil - - ;; because gmail uses labels as folders we can use lazy check since - ;; messages don't really "move" - mu4e-index-lazy-check t) + (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'. + ;; However, the real magic happens in `+mu4e-gmail-fix-flags-h'. ;; ;; Gmail will handle the rest. (defun +mu4e--mark-seen (docid _msg target) (mu4e~proc-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 'trash mu4e-marks) + (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~proc-remove docid)))) + (alist-get 'trash mu4e-marks) (list :char '("d" . "▼") :prompt "dtrash" :dyn-target (lambda (_target msg) (mu4e-get-trash-folder msg)) - :action #'+mu4e--mark-seen) + :action (lambda (docid msg target) + (if (+mu4e-msg-gmail-p msg) + (+mu4e--mark-seen docid msg target) + (mu4e~proc-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 #'+mu4e--mark-seen)) + :action (lambda (docid msg target) + (if (+mu4e-msg-gmail-p msg) + (+mu4e--mark-seen docid msg target) + (mu4e~proc-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 @@ -157,8 +498,70 @@ ;; are ineffectual otherwise. (add-hook! 'mu4e-mark-execute-pre-hook (defun +mu4e-gmail-fix-flags-h (mark msg) - (pcase mark - (`trash (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"))))))) + (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 IS-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 (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%s • %s" + (cond + ((plist-get mail :in-reply-to) "⮩ ") + ((string-match-p "\\`Fwd:" + (plist-get mail :subject)) " ⮯ ") + (t "  ")) + (truncate-string-to-width (caar (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))) diff --git a/modules/email/mu4e/doctor.el b/modules/email/mu4e/doctor.el new file mode 100644 index 000000000..a9a4a9586 --- /dev/null +++ b/modules/email/mu4e/doctor.el @@ -0,0 +1,15 @@ +;;; email/mu4e/doctor.el -*- lexical-binding: t; -*- + +(unless (executable-find "mu") + (warn! "Couldn't find mu command. Mu4e requires this to work.")) + +(unless (or (executable-find "mbsync") + (executable-find "offlineimap")) + (warn! "Couldn't find mbsync or offlineimap command. \ +You may not have a way of fetching mail.")) + +(when (and (featurep! +org) + (not IS-WINDOWS)) + (unless (executable-find "identify") + (warn! "Couldn't find the identify command from imagemagick. \ +LaTeX fragment re-scaling with org-msg will not work."))) diff --git a/modules/email/mu4e/packages.el b/modules/email/mu4e/packages.el index bcb8a4a2a..883596a8b 100644 --- a/modules/email/mu4e/packages.el +++ b/modules/email/mu4e/packages.el @@ -1,4 +1,7 @@ ;; -*- no-byte-compile: t; -*- ;;; email/mu4e/packages.el -(package! org-msg :pin "b0765b2d0bc06cdd1fd78836ef958eb81e603dd1") +(when (featurep! +org) + (package! org-msg :pin "4c92c627b6cfb234fd257b714a5dbfc72d7af8d2")) + +(package! mu4e-alert :pin "91f0657c5b245a9de57aa38391221fb5d141d9bd")