Merge pull request #3971 from tecosaur/m4e4u

Mu4e Module Revamp
This commit is contained in:
Henrik Lissner 2021-07-31 00:24:45 -04:00 committed by GitHub
commit 7f8cba9ba0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1089 additions and 115 deletions

View file

@ -84,7 +84,7 @@ Modules that reconfigure or augment packages or features built into Emacs.
+ [[file:../modules/emacs/vc/README.org][vc]] - TODO + [[file:../modules/emacs/vc/README.org][vc]] - TODO
* :email * :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 + [[file:../modules/email/notmuch/README.org][notmuch]] - TODO
+ wanderlust =+gmail= - TODO + wanderlust =+gmail= - TODO

View file

@ -171,7 +171,7 @@
;;zig ; C, but simpler ;;zig ; C, but simpler
:email :email
;;(mu4e +gmail) ;;(mu4e +org +gmail)
;;notmuch ;;notmuch
;;(wanderlust +gmail) ;;(wanderlust +gmail)

View file

@ -3,21 +3,20 @@
#+SINCE: v2.0 #+SINCE: v2.0
#+STARTUP: inlineimages #+STARTUP: inlineimages
* Table of Contents :TOC: * Table of Contents :TOC:noexport:
- [[#description][Description]] - [[#description][Description]]
- [[#maintainers][Maintainers]]
- [[#module-flags][Module Flags]] - [[#module-flags][Module Flags]]
- [[#plugins][Plugins]] - [[#plugins][Plugins]]
- [[#prerequisites][Prerequisites]] - [[#prerequisites][Prerequisites]]
- [[#macos][MacOS]]
- [[#arch-linux][Arch Linux]]
- [[#nixos][NixOS]] - [[#nixos][NixOS]]
- [[#opensuse][openSUSE]]
- [[#debianubuntu][Debian/Ubuntu]]
- [[#features][Features]] - [[#features][Features]]
- [[#configuration][Configuration]] - [[#configuration][Configuration]]
- [[#offlineimap][offlineimap]] - [[#offlineimap][offlineimap]]
- [[#mbsync][mbsync]] - [[#mbsync][mbsync]]
- [[#mu-and-mu4e][mu and mu4e]] - [[#mu-and-mu4e][mu and mu4e]]
- [[#orgmsg][OrgMsg]]
- [[#mu4e-alert][mu4e-alert]]
- [[#troubleshooting][Troubleshooting]] - [[#troubleshooting][Troubleshooting]]
- [[#no-such-file-or-directory-mu4e][=No such file or directory, mu4e=]] - [[#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]] - [[#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). via IMAP) and ~mu~ (to index my mail into a format ~mu4e~ can understand).
#+end_quote #+end_quote
** Maintainers
+ [[https://github.com/tecosaur][@tecosaur]]
** Module Flags ** 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 ** 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 * Prerequisites
This module requires: 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. + ~mu~, to index your downloaded messages and to provide the ~mu4e~ package.
** MacOS #+name: Install Matrix
#+BEGIN_SRC sh | Platform | Install command | Base packages |
brew install mu |---------------+------------------------+---------------------|
# And one of the following | MacOS | ~brew install <pkgs>~ | =mu= |
brew install isync # mbsync | Arch | ~pacman -S <pkgs>~ | (AUR, ~yay -S~) =mu= |
brew install offlineimap | openSUSE | ~zypper install <pkgs>~ | =maildir-utils=, =mu4e= |
#+END_SRC | Fedora | ~dnf install <pkgs>~ | =maildir-utils= |
| Debian/Ubuntu | ~apt-get install <pkgs>~ | =maildir-utils=, =mu4e= |
** Arch Linux The install either the =isync= (=mbsync=) or =offlineimap= package.
Run one of the following commands.
#+BEGIN_SRC sh To send mail, mu4e uses [[https://www.gnu.org/software/emacs/manual/html_mono/smtpmail.html][smtpmail]] (an Emacs library) by default.
sudo pacman -S isync # mbsync You can also run a local SMTP server like =sendmail= or =postfix=, or use an SMTP
# OR forwarder such as =msmtp= (recommended).
sudo pacman -S offlineimap
#+END_SRC
Install ~mu~, which is not available in the main repositories but in the AUR, by If you use =msmtp=, you'll likely want to add the following to your
using for example the AUR helper ~yay~. =config.el=:
#+begin_src emacs-lisp
#+BEGIN_SRC sh (setq sendmail-program "/usr/bin/msmtp"
yay -S mu send-mail-function #'smtpmail-send-it
#+END_SRC 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 ** NixOS
#+BEGIN_SRC nix #+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]] [[https://github.com/Emiller88/dotfiles/blob/5eaabedf1b141c80a8d32e1b496055231476f65e/modules/shell/mail.nix][An example of setting up mbsync and mu with home-manager]]
** openSUSE * Features
Remove ~#~ in ~#sync_program=offlineimap~ to choose ~offlineimap~ instead of + Tidied mu4e headers view, with flags from =all-the-icons=
~mbsync~. + Consistent coloring of reply depths (across compose and gnus modes)
+ Prettified =mu4e:main= view
#+BEGIN_SRC sh :dir /sudo:: + Cooperative locking of the =mu= process. Another Emacs instance may request
sync_program=isync # mbsync access, or grab the lock when it's available.
#sync_program=offlineimap + =org-msg= integration with =+org=, which can be toggled per-message, with revamped style and
an accent color
sudo zypper install maildir-utils $sync_program + Gmail integrations with the =+gmail= flag
#+END_SRC + Email notifications with =mu4e-alert=, and (on Linux) a customised notification style
** 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
* Configuration * Configuration
** offlineimap ** 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 The steps needed to set up =mu4e= with =mbsync= are very similar to the ones for
[[#offlineimap][offlineimap]]. [[#offlineimap][offlineimap]].
Start with writing a ~\~/.mbsyncrc~. An example for GMAIL can be found on 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 [[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. 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 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. 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 ** mu and mu4e
You should have your email downloaded already. If you have not, you need to set You should have your email downloaded already. If you have not, you need to set
=offlineimap= or =mbsync= up before you proceed. =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 initially. How to do that differs a little depending on the version of =mu= you
use. You can check your version with ~mu --version~. use. You can check your version with ~mu --version~.
@ -169,6 +173,73 @@ Then configure Emacs to use your email address:
t) t)
#+END_SRC #+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 * Troubleshooting
** =No such file or directory, mu4e= ** =No such file or directory, mu4e=
You will get =No such file or directory, mu4e= errors if you don't run ~doom You will get =No such file or directory, mu4e= errors if you don't run ~doom

View file

@ -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)))))

View file

@ -15,6 +15,7 @@ OPTIONAL:
+ `mu4e-trash-folder' + `mu4e-trash-folder'
+ `mu4e-refile-folder' + `mu4e-refile-folder'
+ `mu4e-compose-signature' + `mu4e-compose-signature'
+ `+mu4e-personal-addresses'
DEFAULT-P is a boolean. If non-nil, it marks that email account as the DEFAULT-P is a boolean. If non-nil, it marks that email account as the
default/fallback account." default/fallback account."
@ -22,29 +23,30 @@ default/fallback account."
(when (version< mu4e-mu-version "1.4") (when (version< mu4e-mu-version "1.4")
(when-let (address (cdr (assq 'user-mail-address letvars))) (when-let (address (cdr (assq 'user-mail-address letvars)))
(add-to-list 'mu4e-user-mail-address-list address))) (add-to-list 'mu4e-user-mail-address-list address)))
;; remove existing context with same label
(setq mu4e-contexts (setq mu4e-contexts
(cl-loop for context in mu4e-contexts (cl-loop for context in mu4e-contexts
unless (string= (mu4e-context-name context) label) unless (string= (mu4e-context-name context) label)
collect context)) collect context))
(let ((context (make-mu4e-context (let ((context (make-mu4e-context
:name label :name label
:enter-func (lambda () (mu4e-message "Switched to %s" label)) :enter-func
:leave-func #'mu4e-clear-caches (lambda () (mu4e-message "Switched to %s" label))
:leave-func
(lambda () (progn (setq +mu4e-personal-addresses nil)
(mu4e-clear-caches)))
:match-func :match-func
(lambda (msg) (lambda (msg)
(when msg (when msg
(string-prefix-p (format "/%s" label) (string-prefix-p (format "/%s" label)
(mu4e-message-field msg :maildir)))) (mu4e-message-field msg :maildir) t)))
:vars letvars))) :vars letvars)))
(push context mu4e-contexts) (add-to-list 'mu4e-contexts context (not default-p))
(when default-p
(setq-default mu4e-context-current context))
context))) context)))
(defvar +mu4e-workspace-name "*mu4e*" (defvar +mu4e-workspace-name "*mu4e*"
"TODO") "Name of the workspace created by `=mu4e', dedicated to mu4e.")
(defvar +mu4e--old-wconf nil) (defvar +mu4e--old-wconf nil)
(add-hook 'mu4e-main-mode-hook #'+mu4e-init-h) (add-hook 'mu4e-main-mode-hook #'+mu4e-init-h)
@ -55,7 +57,13 @@ default/fallback account."
(interactive) (interactive)
(require 'mu4e) (require 'mu4e)
(if (featurep! :ui workspaces) (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)) (setq +mu4e--old-wconf (current-window-configuration))
(delete-other-windows) (delete-other-windows)
(switch-to-buffer (doom-fallback-buffer))) (switch-to-buffer (doom-fallback-buffer)))
@ -71,9 +79,241 @@ default/fallback account."
;; TODO Interactively select email account ;; TODO Interactively select email account
(call-interactively #'mu4e-compose-new)) (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)))))
;; (cl-defun +mu4e-normalised-icon (name &key set color height v-adjust)
;; Hooks "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 () (defun +mu4e-init-h ()
(add-hook 'kill-buffer-hook #'+mu4e-kill-mu4e-h nil t)) (add-hook 'kill-buffer-hook #'+mu4e-kill-mu4e-h nil t))
@ -87,3 +327,22 @@ default/fallback account."
(+mu4e--old-wconf (+mu4e--old-wconf
(set-window-configuration +mu4e--old-wconf) (set-window-configuration +mu4e--old-wconf)
(setq +mu4e--old-wconf nil)))) (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)))))

View file

@ -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)))))

View file

@ -3,6 +3,9 @@
(defvar +mu4e-backend 'mbsync (defvar +mu4e-backend 'mbsync
"Which backend to use. Can either be offlineimap, mbsync or nil (manual).") "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 ;;; Packages
@ -27,13 +30,13 @@
(setq mu4e-get-mail-command "offlineimap -o -q"))) (setq mu4e-get-mail-command "offlineimap -o -q")))
(setq mu4e-update-interval nil (setq mu4e-update-interval nil
mu4e-compose-format-flowed t ; visual-line-mode + auto-fill upon sending
mu4e-view-show-addresses t mu4e-view-show-addresses t
mu4e-sent-messages-behavior 'sent mu4e-sent-messages-behavior 'sent
mu4e-hide-index-messages t mu4e-hide-index-messages t
;; try to show images ;; try to show images
mu4e-view-show-images t mu4e-view-show-images t
mu4e-view-image-max-width 800 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 ;; configuration for sending mail
message-send-mail-function #'smtpmail-send-it message-send-mail-function #'smtpmail-send-it
smtpmail-stream-type 'starttls smtpmail-stream-type 'starttls
@ -48,44 +51,106 @@
((featurep! :completion helm) #'completing-read) ((featurep! :completion helm) #'completing-read)
((featurep! :completion vertico) #'completing-read) ((featurep! :completion vertico) #'completing-read)
(t #'ido-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 ;; no need to ask
mu4e-confirm-quit nil 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 ;; remove 'lists' column
mu4e-headers-fields mu4e-headers-fields
'((:account . 12) '((:account-stripe . 1)
(:human-date . 12) (:human-date . 8)
(:flags . 4) (:flags . 6) ; 3 icon flags
(:from . 25) (:from-or-to . 25)
(:subject))) (:subject)))
;; set mail user agent ;; 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 ;; Set the icons only when a graphical frame has been created
(setq mu4e-use-fancy-chars t (if (display-graphic-p)
mu4e-headers-draft-mark '("D" . "") (+mu4e-initialise-icons)
mu4e-headers-flagged-mark '("F" . "") ;; When it's the server, wait till the first graphical frame
mu4e-headers-new-mark '("N" . "") (add-hook!
mu4e-headers-passed-mark '("P" . "") 'server-after-make-frame-hook
mu4e-headers-replied-mark '("R" . "") (defun +mu4e-initialise-icons-hook ()
mu4e-headers-seen-mark '("S" . "") (when (display-graphic-p)
mu4e-headers-trashed-mark '("T" . "") (+mu4e-initialise-icons)
mu4e-headers-attach-mark '("a" . "") (remove-hook 'server-after-make-frame-hook
mu4e-headers-encrypted-mark '("x" . "") #'+mu4e-initialise-icons-hook)))))
mu4e-headers-signed-mark '("s" . "")
mu4e-headers-unread-mark '("u" . ""))
;; Add a column to display what email account the email belongs to. (plist-put (cdr (assoc :flags mu4e-header-info)) :shortname " Flags") ; default=Flgs
(add-to-list 'mu4e-header-info-custom (add-to-list 'mu4e-bookmarks
'(:account '(:name "Flagged messages" :query "flag:flagged" :key ?f) t)
:name "Account"
;; 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" :shortname "Account"
:help "Which account this email belongs to" :help "which account/maildir this email belongs to"
:function :function
(lambda (msg) (lambda (msg)
(let ((maildir (mu4e-message-field msg :maildir))) (let ((maildir (replace-regexp-in-string
(format "%s" (substring maildir 1 (string-match-p "/" maildir 1))))))) "\\`/?\\([^/]+\\)/.*\\'" "\\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 _) (defadvice! +mu4e--refresh-current-view-a (&rest _)
:after #'mu4e-mark-execute-all (mu4e-headers-rerun-search)) :after #'mu4e-mark-execute-all (mu4e-headers-rerun-search))
@ -94,25 +159,266 @@
;; Html mails might be better rendered in a browser ;; Html mails might be better rendered in a browser
(add-to-list 'mu4e-view-actions '("View in browser" . mu4e-action-view-in-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) (when (fboundp 'imagemagick-register-types)
(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! :localleader
:map mu4e-compose-mode-map :map mu4e-compose-mode-map
:desc "send and exit" "s" #'message-send-and-exit :desc "send and exit" "s" #'message-send-and-exit
:desc "kill buffer" "d" #'message-kill-buffer :desc "kill buffer" "d" #'message-kill-buffer
:desc "save draft" "S" #'message-dont-send :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 (use-package! org-msg
:hook (mu4e-compose-pre . org-msg-mode) :after mu4e
:when (featurep! +org)
:config :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-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 <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 . "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) (when (featurep! +gmail)
(after! mu4e (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 ;; 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 (defun +mu4e-msg-gmail-p (msg)
mu4e-index-cleanup nil (let ((root-maildir
(replace-regexp-in-string "/.*" ""
;; because gmail uses labels as folders we can use lazy check since (substring (mu4e-message-field msg :maildir) 1))))
;; messages don't really "move" (or (string-match-p "gmail" root-maildir)
mu4e-index-lazy-check t) (member root-maildir (mapcar #'cdr +mu4e-gmail-accounts)))))
;; In my workflow, emails won't be moved at all. Only their flags/labels are ;; 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. ;; 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. ;; Gmail will handle the rest.
(defun +mu4e--mark-seen (docid _msg target) (defun +mu4e--mark-seen (docid _msg target)
(mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N")) (mu4e~proc-move docid (mu4e~mark-check-target target) "+S-u-N"))
(defvar +mu4e--last-invalid-gmail-action 0)
(delq! 'delete mu4e-marks #'assq) (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" . "") (list :char '("d" . "")
:prompt "dtrash" :prompt "dtrash"
:dyn-target (lambda (_target msg) (mu4e-get-trash-folder msg)) :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. ;; Refile will be my "archive" function.
(alist-get 'refile mu4e-marks) (alist-get 'refile mu4e-marks)
(list :char '("r" . "") (list :char '("r" . "")
:prompt "rrefile" :prompt "rrefile"
:dyn-target (lambda (_target msg) (mu4e-get-refile-folder msg)) :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. ;; This hook correctly modifies gmail flags on emails when they are marked.
;; Without it, refiling (archiving), trashing, and flagging (starring) email ;; Without it, refiling (archiving), trashing, and flagging (starring) email
@ -157,8 +498,70 @@
;; are ineffectual otherwise. ;; are ineffectual otherwise.
(add-hook! 'mu4e-mark-execute-pre-hook (add-hook! 'mu4e-mark-execute-pre-hook
(defun +mu4e-gmail-fix-flags-h (mark msg) (defun +mu4e-gmail-fix-flags-h (mark msg)
(when (+mu4e-msg-gmail-p msg)
(pcase mark (pcase mark
(`trash (mu4e-action-retag-message msg "-\\Inbox,+\\Trash,-\\Draft")) (`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")) (`refile (mu4e-action-retag-message msg "-\\Inbox"))
(`flag (mu4e-action-retag-message msg "+\\Starred")) (`flag (mu4e-action-retag-message msg "+\\Starred"))
(`unflag (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<b>%s</b> • %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)))

View file

@ -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.")))

View file

@ -1,4 +1,7 @@
;; -*- no-byte-compile: t; -*- ;; -*- no-byte-compile: t; -*-
;;; email/mu4e/packages.el ;;; email/mu4e/packages.el
(package! org-msg :pin "b0765b2d0bc06cdd1fd78836ef958eb81e603dd1") (when (featurep! +org)
(package! org-msg :pin "4c92c627b6cfb234fd257b714a5dbfc72d7af8d2"))
(package! mu4e-alert :pin "91f0657c5b245a9de57aa38391221fb5d141d9bd")