Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
687 lines (624 sloc) 25.1 KB
;;; my-email.el --- Email set-up
;;; Commentary:
;; After with notmuch, mu4e and Gnus I went with mu4e mainly due to
;; speed and apparent simplicity of customisation.
;;; Code:
(require 'use-package)
(require 'my-vars)
(require 'my-libs)
(require 'my-hydra)
(require 'dash)
(use-package smtpmail
:commands smtpmail-send-queued-mail
(setq smtpmail-queue-mail nil ;; start in non-queuing mode
smtpmail-queue-dir "~/Maildir/queue/cur"
smtpmail-default-smtp-server "localhost"
smtpmail-smtp-server "localhost"
smtpmail-smtp-service 25))
;; Signature
(defun my-sig-function ()
"Generate a signature."
(concat "Alex Bennée"))
;; Simple mail-mode and message-mode hooks.
;; Ostensibly they both do the same thing however message-mode (and
;; the derived mu4e-compose-mode) assume they are sending from within
;; emacs. So I'll use the convention that I'll use mail-mode for
;; edit-server spawned mails and message-mode for the rest
(defun my-common-mail-tweaks ()
"Enable common mail tweaks for sending messages."
(defun my-mail-mode-tweaks()
"Customise mail-mode stuff"
(when (and
(string-match "/tmp/mutt" buffer-file-name)
(string-match "gitsend" buffer-file-name)))
(define-key (current-local-map) (kbd "C-c C-c") 'server-edit)
(define-key (current-local-map) (kbd "C-c C-s") 'server-edit)))
(use-package sendmail
;; Enable mail-mode for mutt spawned files
:mode (("/tmp/mutt-*" . mail-mode)
("0000-cover-letter.patch" . mail-mode)
(".*/\.git/\.gitsendemail.MSG.*" . mail-mode))
:config (add-hook 'mail-mode-hook 'my-mail-mode-tweaks))
(use-package message
:commands message-mode
:config (add-hook 'message-mode-hook 'my-common-mail-tweaks))
(use-package bbdb
:config (progn
(setq bbdb-add-aka t
bbdb-layout 'one-line
bbdb-mua-auto-update-p 'query)
(bbdb-initialize 'mu4e 'rmail 'gnus 'message)))
;; Finally the mu4e configuration
;; This is my main work horse for day to day email.
(defun my-rig-mu4e-for-idle-running ()
"Setup more comprehensive indexing when Emacs is idle."
(setq mu4e-index-lazy-check nil ; more comprehensive
mu4e-index-cleanup t)) ; check messages in store still there
(defun my-rig-mu4e-for-active-running ()
"Setup faster indexing for when I'm actively using Emacs mail."
(setq mu4e-index-lazy-check t ; faster sync
mu4e-index-cleanup nil) ; skip checking the whole store
(when (not (-contains? (--map (aref it 5) timer-idle-list) 'my-rig-mu4e-for-idle-running))
(run-with-idle-timer 600 nil 'my-rig-mu4e-for-idle-running)))
;; Switch function
(defun my-switch-to-mu4e (&optional prefix)
"Smart dwim switch to mu4e."
(interactive "P")
(if prefix
(let ((candidate
;; unsent emails
(car (--filter
(with-current-buffer it
(eq major-mode 'mu4e-compose-mode)
(not message-sent-message-via)))
;; current search
(get-buffer "*mu4e-headers*")
;; current view
(get-buffer "*mu4e-view*"))))
(if candidate
(switch-to-buffer candidate)
;; ivy powered switch function
(defun my-ivy-switch-to-mu4e (&optional prefix)
"Ivy based switch to mu4e.
Instead of the heuristics of `my-switch-to-mu4e' we build a list of
all mu4e buffers and allow ivy selection of them.
(interactive "P")
(if (or prefix (not (get-buffer " *mu4e-main*")))
(let (collection)
;; Go backwards in priority (as add to list prepends by default)
;; The main menu
(add-to-list 'collection
(propertize (format "mu4e menu")
'buffer (get-buffer " *mu4e-main*")))
;; What are we reading
(let ((view (get-buffer "*mu4e-view*")))
(when view
(let ((subject (with-current-buffer view
(mu4e-message-field-at-point :subject))))
(add-to-list 'collection
(propertize (format "reading:%s" subject)
'buffer view)))))
;; What are we searching
(let ((headers (get-buffer "*mu4e-headers*")))
(when headers
(let ((search (with-current-buffer headers
(add-to-list 'collection
(propertize (format "mu4e headers:%s" search)
'buffer headers)))))
;; What are we composing
(--each (buffer-list)
(with-current-buffer it
(when (and (eq major-mode 'mu4e-compose-mode)
(not message-sent-message-via))
(add-to-list 'collection
(format "composing:%s"
(or (message-fetch-field "subject")
"No subject"))
'buffer it)))))
(get-text-property 0 'buffer
(if (< 1 (length collection))
(ivy-read "mu4e:"
(car collection))))
;; Jump to current thread
(defun my-switch-to-thread ()
"Switch to headers view of current thread."
(let* ((msg (mu4e-message-at-point))
(id (or (mu4e-message-field-raw msg :in-reply-to)
(mu4e-message-field-raw msg :message-id))))
(when (> (length id) 0)
(mu4e-headers-search (format "i:%s" (s-replace-all '(("<" . "")
(">" . ""))
;; Set default directory when viewing messages
(defvar my-mailing-list-dir-mapping
'( ("" . "~/lsrc/qemu/qemu.git/")
("" . "~/lsrc/qemu/qemu.git/")
("" . "~/lsrc/kvm/linux.git/")
("" . "~/lsrc/kvm/linux.git/")
("" . "~/lsrc/kvm/linux.git/") )
"Mapping from mailing lists to source tree.")
(defvar my-maildir-mapping
'( ("linaro/virtualization/qemu" . "~/lsrc/qemu/qemu.git/")
("linaro/virtualization/qemu-arm" . "~/lsrc/qemu/qemu.git/")
("linaro/virtualization/qemu-multithread" . "~/lsrc/qemu/qemu.git/")
("linaro/kernel" . "~/lsrc/kvm/linux.git/") )
"Mapping from maildirs to source tree.")
(defvar my-mail-address-mapping
' (
("" . "~/lsrc/qemu/qemu.git/")
("" . "~/lsrc/qemu/qemu.git/")
("" . "~/lsrc/kvm/linux.git/") )
"Mapping from target address to source tree.
Useful for replies and drafts")
(defun my-get-code-dir-from-email ()
"Return the associated code directory depending on email."
(let ((msg (mu4e-message-at-point t)))
(if (not msg)
(let ((list (mu4e-message-field msg :mailing-list))
(maildir (mu4e-message-field msg :maildir))
(addresses (-map 'cdr (append (mu4e-message-field msg :to)
(mu4e-message-field msg :cc)))))
(assoc-default list my-mailing-list-dir-mapping)
(assoc-default maildir my-maildir-mapping 'string-match)
(assoc-default (-first
#'(lambda (mail)
(assoc-default mail my-mail-address-mapping))
addresses) my-mail-address-mapping)
(defun my-set-view-directory ()
"Switch the `default-directory' depending mail contents."
(when (mu4e-message-at-point t)
(setq default-directory (my-get-code-dir-from-email))))
(defun my-search-code-from-email ()
"Search code depending on email."
(my-project-find (my-get-code-dir-from-email)))
(defun my-strip-msg-to-sig ()
"Delete everything from point to my signature."
(let ((start (point)))
(goto-char (point-max))
(when (re-search-backward "--")
(kill-region start (- (match-beginning 0) 1))))))
(use-package mu4e-compose
:commands mu4e-compose-mode
:defines mu4e-compose-mode-map
:config (progn
;; key-bindings
(when (keymapp mu4e-compose-mode-map)
(define-key mu4e-compose-mode-map (kbd "C-w")
(define-key mu4e-compose-mode-map (kbd "<f5>")
(add-hook 'mu4e-compose-mode-hook 'my-set-view-directory)
(add-hook 'mu4e-compose-pre-hook 'my-choose-mail-address)))
(use-package mu4e-headers
:commands mu4e-headers-mode
:defines mu4e-headers-mode-map
:config (progn
;; My mode bindings
(define-key mu4e-headers-mode-map (kbd "C-c C-l") 'org-store-link)
(define-key mu4e-headers-mode-map (kbd "C-c t")
(add-hook 'mu4e-headers-mode-hook
'(lambda () (yas-minor-mode -1)))
(add-hook 'mu4e-headers-mode-hook 'my-set-view-directory)))
(defhydra my-mu4e-view-toggle (:hint nil :color blue :timeout 5)
(concat "_c_itation function:%`mu4e-compose-cite-function ")
;; toggle citation mode
("c" (lambda()
(if (eq mu4e-compose-cite-function
(setq mu4e-compose-cite-function 'message-cite-original)
(setq mu4e-compose-cite-function 'message-cite-original-without-signature))))
("t" my-hydra-toggle/body "main toggles"))
(use-package mu4e-view
:commands mu4e-view
:bind (:map mu4e-view-mode-map
("C-c C-l". org-store-link)
("C-c t" . my-switch-to-thread)
("C-x t" . my-mu4e-view-toggle/body))
:defines mu4e-view-mode-map
:config (add-hook 'mu4e-view-mode-hook 'my-set-view-directory))
;; spam learning: ionice -c 3 sa-learn --progress --spam ~/Maildir/.Spam/cur/*
;; loosely hacked from mu4e-control.el HEAD
(defvar my-mu4e-register-spam-cmd
"sa-learn --spam %s"
"Command for invoking spam processor to register message as spam.")
(defvar my-mu4e-register-ham-cmd
"sa-learn --ham %s"
"Command for invoking spam processor to register message as ham.")
(defun my-mu4e-register-spam-action (msg)
"Mark `MSG' as spam."
(let* ((path (shell-quote-argument
(mu4e-message-field msg :path)))
(command (format my-mu4e-register-spam-cmd path)))
;; (async-shell-command command nil))
(start-process "LSPAM" nil "sa-learn" "--spam" path))
(mu4e-mark-at-point 'delete nil)
(defun my-mu4e-register-ham-action (msg)
"Mark `MSG' as ham."
(let* ((path (shell-quote-argument
(mu4e-message-field msg :path)))
(command (format my-mu4e-register-ham-cmd path)))
;; (async-shell-command command))
(start-process "LHAM" nil "sa-learn" "--ham" path))
(mu4e-mark-at-point 'unmark nil)
;; Check if patch merged into a given tree
;; Subject: [Qemu-devel] [PATCH 1/2] tcg: Allow constant pool entries in the prologue
(defvar my-extract-patch-title
(rx (:
"[PATCH" (one-or-more print) "]"
(zero-or-more space)
(group (one-or-more print))
"Regex to extract patch title from email subject.")
(defun my-mu4e-action-check-if-merged (msg)
"Check if `MSG' is in your tree."
(let ((subj (mu4e-message-field-at-point :subject)))
(when (string-match my-extract-patch-title subj)
(let ((title (match-string-no-properties 1 subj))
(default-directory (read-directory-name "Project:")))
(let ((result (magit-git-string
"log" "origin/master" "--no-merges" "--oneline"
"--grep" title)))
(when (and result
(yes-or-no-p (format "Visit:%s?" result)))
(magit-show-commit (car (split-string result)))))))))
(use-package mu4e
:commands mu4e
;; Bindings
:bind ("C-c m" . my-ivy-switch-to-mu4e)
(require 'mu4e-vars)
;; config options
;; generic mail options
(I-am-at-work "")
(t ""))
user-full-name "Alex Bennée"
mail-signature '(insert (concat "\n--\n" (my-sig-function)))
mail-user-agent 'mu4e-user-agent
;; message functions
message-signature 'my-sig-function
message-send-mail-function 'smtpmail-send-it
;; mu4e functions
;; mail fetch
(I-am-at-work "mbsync linaro-sync")
(t "true"))
mu4e-update-interval 600
mu4e-hide-index-messages t
mu4e-change-filenames-when-moving t ; keep mbsync happy
mu4e-index-lazy-check t ; faster sync
mu4e-index-cleanup nil ; should toggle this
;; navigate options
mu4e-use-fancy-chars t
mu4e-headers-skip-duplicates t
mu4e-headers-include-related t
;; compose options
mu4e-compose-signature 'my-sig-function
;; this ensures completion-at-point functionality is setup
;; which eventually percolates to company-capf.
mu4e-compose-complete-addresses t
mu4e-compose-complete-only-personal t
(I-am-at-work '(""))
(t '("")))
mu4e-compose-complete-only-after "2013-11-01"
;; view options
mu4e-view-show-images t
mu4e-view-show-addresses t
mu4e-view-fill-headers nil
mu4e-html2text-command "html2text -utf8 -width 72"
'(:from :to :cc :subject :flags :date :tags :attachments
'( ("/linaro/Inbox" . ?i)
("/linaro/mythreads" . ?m)
("/linaro/archived" . ?A)
("/linaro/team" . ?t)
("/linaro/kernel/lkml" . ?l)
("/linaro/virtualization/qemu" . ?q)
("/linaro/virtualization/qemu-arm" . ?a)
("/linaro/virtualization/qemu-multithread" . ?M)
("/linaro/virtualization/kvm-arm" . ?k)
("/sent" . ?s) ))
'( ("/" . ?i)
("/.Spam" . ?s)
("/.Oldmail" . ?o) ))))
;; Header markers
(defvar my-mu4e-patches nil
"List of mu4e-messages snagged by the (Patches) actions.")
(defvar my-mu4e-applied-patches nil
"List of mu4e-messages successfully applied by the (Patches)
(make-variable-buffer-local 'my-mu4e-patches)
(make-variable-buffer-local 'my-mu4e-applied-patches)
(defun my-mu4e-get-patch-number (msg)
"Return patch number from a message."
(let ((subject (mu4e-message-field msg :subject)))
(rx (: (group-n 1 (one-or-more (any "0-9"))) (any "/")
(one-or-more (any "0-9"))))
(match-string 1 subject))))
(defun my-mu4e-remaining-patches ()
"Return a sorted list of patches left to apply"
(my-mu4e-get-patch-number it)
(my-mu4e-get-patch-number other))
(-difference my-mu4e-patches
(defun my-mu4e-apply-marked-mbox-patches (&optional arg)
"Apply patches in order. With PREFIX include signoff"
(interactive "P")
(let ((applied-or-skipped
(let ((docid (plist-get it :docid)))
(if (mu4e-mark-docid-marked-p docid)
(if (= 0 (mu4e-action-git-apply-mbox it arg))
(when (mu4e~headers-goto-docid docid)
(mu4e-mark-set 'unmark) t)
; failed to apply, stop
(switch-to-buffer "*Shell Command Output*")
; not marked, skip
(setq my-mu4e-applied-patches
(-union my-mu4e-applied-patches applied-or-skipped))
(message (format "Applied %d (%d)/%d patches"
(length applied-or-skipped)
(length my-mu4e-applied-patches)
(length my-mu4e-patches)))))
;; The following two functions are custom marker functions
;; Match function
(defun my-mu4e-patch-match (msg parent-id)
"Match any patches related to the parent-id. Add them
to `my-mu4e-patches' for later processing."
(and (string-match parent-id
(mu4e-message-field-raw msg :in-reply-to)
(: bol "["
(minimal-match (zero-or-more (not (any "/"))))
(or (: (any "0-9") (zero-or-one (any "1-9")))
(: (any "1-9") (zero-or-one (any "0-9"))))
(mu4e-message-field-raw msg :subject)))
(add-to-list 'my-mu4e-patches msg)))
;; Param function
(defun my-mu4e-patch-setup ()
"Reset the patch list and extract parent-id for `my-mu4e-patch-match'"
(setq my-mu4e-patches nil
my-mu4e-applied-patches nil)
(let ((msg (mu4e-message-at-point)))
(mu4e-message-field-raw msg :message-id)))
(when (boundp 'mu4e-marks)
:char ("#" . "#")
:prompt "Patch")))
'("Patches" my-mu4e-patch-match my-mu4e-patch-setup))
;; Header actions
(setq mu4e-headers-actions
'(("gapply git patches" . mu4e-action-git-apply-patch)
("mgit am patch" . mu4e-action-git-apply-mbox)
("rrun checkpatch script" . my-mu4e-action-run-check-patch)
("sMark SPAM" . my-mu4e-register-spam-action)
("hMark HAM" . my-mu4e-register-ham-action)
("MCheck if merged" . my-mu4e-action-check-if-merged)))))
;; Message actions
(setq mu4e-view-actions
'(("gapply git patches" . mu4e-action-git-apply-patch)
("mgit am patch" . mu4e-action-git-apply-mbox)
("crun checkpatch script" . my-mu4e-action-run-check-patch)
("MCheck if merged" . my-mu4e-action-check-if-merged)))))
;; Bookmarks
(setq mu4e-bookmarks
;; Personal bookmarks
("recip:alex.bennee flag:unread "
"Unread posts addressed to me" ?M)
("recip:alex.bennee flag:list flag:unread "
"Unread list email addressed to me" ?m)
("recip:alex.bennee AND \( \(reviewed ADJ by\) OR \(signed ADJ off ADJ by\) \)"
"Mail addressed to me with git tags" ?g)
("\(from:alex.bennee OR\)"
"Mail sent by me" ?s)
("recip:alex.bennee s:Re NOT flag:seen"
"Mail sent by me (unread replied)" ?S)
("\(from:alex.bennee OR\) AND s:PATCH NOT s:Re"
"My patches" ?p)
("s:PULL \(b:Bennée OR b:Bennee\)" "Pull Reqs with my name" ?P)
("flag:flagged" "Flagged and starred posts" ?f)
("flag:flagged NOT flag:seen" "Unread flagged and starred posts" ?F)
(" AND"
"From my boss" ?B)
"In the last hour" ?h)
(" AND flag:unread"
"In the last hour (unread)" ?H)
;; Virt related
(" and flag:unread"
"Latest QEMU posts" ?q)
("(( AND (s:aarch64 OR s:arm OR s:A64)) OR"
"QEMU ARM posts" ?a)
(" OR maildir:/linaro/virtualization/qemu-multithread"
"Multi-threaded QEMU posts" ?T)
(" OR ( AND subject:android)"
"Android related emails" ?A)
(" and flag:unread"
"Latest ARM KVM posts" ?k)
(" and flag:unread"
"Linaro Virtualization List" ?v)
("maildir:\"/linaro/virtualization/*\" AND flag:list AND flag:unread"
"All unread Virtualization email" ?V)
;; Linaro Specific
(" AND flag:unread"
"Latest Conf emails" ?c)
(" AND flag:unread"
"Latest Linaro-Dev emails" ?d)
(" AND flag:unread"
"Latest Linaro-Tech emails" ?t)
("\( OR\) AND flag:list AND flag:unread"
"Unread work mailing lists (" ?l)
(" and flag:unread"
"Latest unread Linaro posts from Linaro emails" ?L)
;; Emacs
(" and flag:unread"
"Latest unread Emacs developer posts" ?E)
(" and flag:unread"
"Latest unread Emacs user posts" ?e)
(" and flag:unread"
"Latest unread org-mode posts" ?o)))
;; Personal bookmarks
(" AND flag:unread "
"Unread posts addressed to me" ?M)
("flag:unread AND NOT"
"Unread posts addressed to aliases" ?a)
("flag:list AND flag:unread "
"Unread list/notification email" ?m)
("\(from:alex.bennee OR\)"
"Mail sent by me" ?s)
("from:eileen OR from:nigel"
"From parents" ?P)
("" "Bug Mail" ?B)))))))
(use-package helm-mu
:commands helm-mu
:if (and (string-match "zen" (system-name))
(locate-library "helm-mu"))
:config (progn
(setq helm-mu-contacts-personal t)
(define-key mu4e-headers-mode-map (kbd "C-s") 'helm-mu)))
(when (locate-library "mu4e")
(use-package mu4e-alert
:ensure t
:config (progn
(setq mu4e-alert-interesting-mail-query
"recip:alex.bennee flag:unread AND NOT flag:trashed")
;; Magic handling for multiple email addrsses
(defvar my-email-address-alist
'( ("Maildir/developer" . "")
("Maildir/linaro" . "")
("Maildir/personal" . "") )
"List of mappings from Maildir->email address.")
(defun my-choose-mail-address ()
"Pick new `user-mail-address' based on the parent email.
Sadly this is not a local variable as at the time of the
hook we are not yet in the compose buffer."
(let ((email
(when mu4e-compose-parent-message
(plist-get mu4e-compose-parent-message :path)
my-email-address-alist 'string-match))))
(setq user-mail-address
(if email
(I-am-at-work "")
(t ""))))))
;; Utility functions for email
(defun my-snip-region (beg end)
"Kill the region BEG to END and replace with <snip> tag."
(interactive (list (point) (mark)))
(kill-region beg end)
(when (string-prefix-p ">" (car kill-ring))
(insert "<snip>\n")))
;; Reset the cache of the directory list
(defun my-reset-mu4e-caches ()
"Reset some of mu4e's caches to avoid restarting."
(setq mu4e~maildir-list nil))
;; Checkpatch in emails
(defun my-mu4e-action-run-check-patch (msg)
"Call checkpatch against `MSG'."
(checkpatch-run-against-patch-file (mu4e-message-field msg :path)))
;; WIP: Pull requests
(defun my-insert-pull-request ()
"Insert basic pull request into buffer."
(with-current-buffer (current-buffer)
"git request-pull %s HEAD"
"Commit start:" '("HEAD~" "origin/master") 'nil))))))
(provide 'my-email)
;;; my-email.el ends here