Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
1241 lines (1104 sloc) 46.1 KB
;;; mu4e-utils.el -- part of mu4e, the mu mail user agent
;; Copyright (C) 2011-2016 Dirk-Jan C. Binnema
;; Copyright (C) 2013 Tibor Simko
;; Author: Dirk-Jan C. Binnema <>
;; Maintainer: Dirk-Jan C. Binnema <>
;; This file is not part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <>.
;;; Commentary:
;; Utility functions used in the mu4e
;;; Code:
(eval-when-compile (byte-compile-disable-warning 'cl-functions))
(require 'cl)
(eval-when-compile (require 'org nil 'noerror))
(require 'mu4e-vars)
(require 'mu4e-meta)
(require 'mu4e-lists)
(require 'doc-view)
;; keep the byte-compiler happy
(declare-function mu4e~proc-mkdir "mu4e-proc")
(declare-function mu4e~proc-ping "mu4e-proc")
(declare-function mu4e~proc-contacts "mu4e-proc")
(declare-function mu4e~proc-kill "mu4e-proc")
(declare-function mu4e~proc-index "mu4e-proc")
(declare-function mu4e~proc-add "mu4e-proc")
(declare-function mu4e~proc-mkdir "mu4e-proc")
(declare-function mu4e~proc-running-p "mu4e-proc")
(declare-function mu4e~context-autoswitch "mu4e-context")
(declare-function mu4e-context-determine "mu4e-context")
(declare-function mu4e-context-vars "mu4e-context")
(declare-function show-all "org")
;; the following is taken from org.el; we copy it here since we don't want to
;; depend on org-mode directly (it causes byte-compilation errors) TODO: a
;; cleaner solution....
(defconst mu4e~ts-regexp0
"\\( +[^]+0-9>\r\n -]+\\)?\\( +\\([0-9]\\{1,2\\}\\):"
"Regular expression matching time strings for analysis.
This one does not require the space after the date, so it can be
used on a string that terminates immediately after the date.")
(defun mu4e-parse-time-string (s &optional nodefault)
"Parse the standard Org-mode time string.
This should be a lot faster than the normal `parse-time-string'.
If time is not given, defaults to 0:00. However, with optional
NODEFAULT, hour and minute fields will be nil if not given."
(if (string-match mu4e~ts-regexp0 s)
(list 0
(if (or (match-beginning 8) (not nodefault))
(string-to-number (or (match-string 8 s) "0")))
(if (or (match-beginning 7) (not nodefault))
(string-to-number (or (match-string 7 s) "0")))
(string-to-number (match-string 4 s))
(string-to-number (match-string 3 s))
(string-to-number (match-string 2 s))
nil nil nil)
(mu4e-error "Not a standard mu4e time string: %s" s)))
(defun mu4e-user-mail-address-p (addr)
"If ADDR is one of user's e-mail addresses return t, nil otherwise.
User's addresses are set in `mu4e-user-mail-address-list')."
(when (and addr mu4e-user-mail-address-list
(find addr mu4e-user-mail-address-list :test 'string=))
(defmacro with~mu4e-context-vars (context &rest body)
"Evaluate BODY, with variables let-bound for CONTEXT (if any).
(declare (indent 2))
`(let* ((vars (and ,context (mu4e-context-vars ,context))))
(progv ;; XXX: perhaps use eval's lexical environment instead of progv?
(mapcar (lambda(cell) (car cell)) vars)
(mapcar (lambda(cell) (cdr cell)) vars)
(eval ,@body))))
;; the standard folders can be functions too
(defun mu4e~get-folder (foldervar msg)
"Within the mu-context of MSG, get message folder FOLDERVAR.
If FOLDER is a string, return it, if it is a function, evaluate
this function with MSG as parameter (which may be `nil'), and
return the result."
(unless (member foldervar
'(mu4e-sent-folder mu4e-drafts-folder
mu4e-trash-folder mu4e-refile-folder))
(mu4e-error "Folder must be one of mu4e-(sent|drafts|trash|refile)-folder"))
;; get the value with the vars for the relevants context let-bound
(with~mu4e-context-vars (mu4e-context-determine msg nil)
(let* ((folder (symbol-value foldervar))
((stringp folder) folder)
((functionp folder) (funcall folder msg))
(t (mu4e-error "unsupported type for %S" folder)))))
(or val (mu4e-error "%S evaluates to nil" foldervar)))))
(defun mu4e-get-drafts-folder (&optional msg)
"Get the sent folder. See `mu4e-drafts-folder'."
(mu4e~get-folder 'mu4e-drafts-folder msg))
(defun mu4e-get-refile-folder (&optional msg)
"Get the folder for refiling. See `mu4e-refile-folder'."
(mu4e~get-folder 'mu4e-refile-folder msg))
(defun mu4e-get-sent-folder (&optional msg)
"Get the sent folder. See `mu4e-sent-folder'."
(mu4e~get-folder 'mu4e-sent-folder msg))
(defun mu4e-get-trash-folder (&optional msg)
"Get the sent folder. See `mu4e-trash-folder'."
(mu4e~get-folder 'mu4e-trash-folder msg))
(defun mu4e-remove-file-later (filename)
"Remove FILENAME in a few seconds."
(lexical-let ((filename filename))
(run-at-time "10 sec" nil
(lambda () (ignore-errors (delete-file filename))))))
(defun mu4e-make-temp-file (ext)
"Create a temporary file with extension EXT. The file will
self-destruct in a few seconds, enough to open it in another
(let ((tmpfile (make-temp-file "mu4e-" nil (concat "." ext))))
(mu4e-remove-file-later tmpfile)
;; mu4e-attachment-dir is either a string or a function that takes a filename
;; and the mime-type as argument, either (or both) which can be nil
(defun mu4e~get-attachment-dir (&optional fname mimetype)
"Get the directory for saving attachments from
`mu4e-attachment-dir' (which can be either a string or a function,
see its docstring)."
((stringp mu4e-attachment-dir)
((functionp mu4e-attachment-dir)
(funcall mu4e-attachment-dir fname mimetype))
(mu4e-error "unsupported type for mu4e-attachment-dir" )))))
(if dir
(expand-file-name dir)
(mu4e-error (mu4e-error "mu4e-attachment-dir evaluates to nil")))))
(defun mu4e~guess-maildir (path)
"Guess the maildir for some path, or nil if cannot find it."
(let ((idx (string-match mu4e-maildir path)))
(when (and idx (zerop idx))
(concat path "/../.."))))))
(defun mu4e-create-maildir-maybe (dir)
"Offer to create maildir DIR if it does not exist yet.
Return t if the dir already existed, or an attempt has been made to
create it -- we cannot be sure creation succeeded here, since this
is done asynchronously. Otherwise, return nil. NOte, DIR has to be
an absolute path."
(if (and (file-exists-p dir) (not (file-directory-p dir)))
(mu4e-error "%s exists, but is not a directory." dir))
((file-directory-p dir) t)
((yes-or-no-p (mu4e-format "%s does not exist yet. Create now?" dir))
(mu4e~proc-mkdir dir) t)
(t nil)))
(defun mu4e-format (frm &rest args)
"Create [mu4e]-prefixed string based on format FRM and ARGS."
"[" (propertize "mu4e" 'face 'mu4e-title-face) "] "
(apply 'format frm args)))
(defun mu4e-message (frm &rest args)
"Like `message', but prefixed with mu4e.
If we're waiting for user-input or if there's some message in the
echo area, don't show anything."
(unless (or (active-minibuffer-window))
(message "%s" (apply 'mu4e-format frm args))))
(defun mu4e-index-message (frm &rest args)
"Like `mu4e-message', but specifically for
index-messages. Doesn't display anything if
`mu4e-hide-index-messages' is non-nil. "
(unless mu4e-hide-index-messages
(apply 'mu4e-message frm args)))
(defun mu4e-error (frm &rest args)
"Create [mu4e]-prefixed error based on format FRM and ARGS.
Does a local-exit and does not return, and raises a
debuggable (backtrace) error."
(mu4e-log 'error (apply 'mu4e-format frm args))
(error "%s" (apply 'mu4e-format frm args)))
;; the user-error function is only available in emacs-trunk
(unless (fboundp 'user-error)
(defalias 'user-error 'error))
(defun mu4e-warn (frm &rest args)
"Create [mu4e]-prefixed warning based on format FRM and ARGS.
Does a local-exit and does not return. In emacs versions below
24.2, the functions is the same as `mu4e-error'."
(mu4e-log 'error (apply 'mu4e-format frm args))
(user-error "%s" (apply 'mu4e-format frm args)))
(defun mu4e~read-char-choice (prompt choices)
"Read and return one of CHOICES, prompting for PROMPT.
Any input that is not one of CHOICES is ignored. This mu4e's
version of `read-char-choice', that becomes case-insentive after
trying an exact match."
(let ((choice) (chosen) (inhibit-quit nil))
(while (not chosen)
(message nil);; this seems needed...
(setq choice (read-char-exclusive prompt))
(setq chosen (or (member choice choices)
(member (downcase choice) choices)
(member (upcase choice) choices))))
(car chosen)))
(defun mu4e-read-option (prompt options)
"Ask user for an option from a list on the input area.
PROMPT describes a multiple-choice question to the user.
OPTIONS describe the options, and is a list of cells describing
particular options. Cells have the following structure:
where OPTIONSTRING is a non-empty string describing the
option. The first character of OPTIONSTRING is used as the
shortcut, and obviously all shortcuts must be different, so you
can prefix the string with an uniquifying character.
The options are provided as a list for the user to choose from;
user can then choose by typing CHAR. Example:
(mu4e-read-option \"Choose an animal: \"
'((\"Monkey\" . monkey) (\"Gnu\" . gnu) (\"xMoose\" . moose)))
User now will be presented with a list: \"Choose an animal:
[M]onkey, [G]nu, [x]Moose\".
Function will return the cdr of the list element."
(let* ((prompt (mu4e-format "%s" prompt))
(lambda (option)
;; try to detect old-style options, and warn
(when (characterp (car-safe (cdr-safe option)))
(concat "Please use the new format for options/actions; "
"see the manual")))
(let* ((kar (substring (car option) 0 1))
(val (cdr option)))
"[" (propertize kar 'face 'mu4e-highlight-face) "]"
(substring (car option) 1))))
options ", "))
(concat prompt optionsstr
" [" (propertize "C-g" 'face 'mu4e-highlight-face)
" to cancel]")
;; the allowable chars
(map 'list (lambda(elm) (string-to-char (car elm))) options)))
(lambda (option) (eq response (string-to-char (car option))))
(if chosen
(cdr chosen)
(mu4e-warn "Unknown shortcut '%c'" response))))
(defun mu4e~get-maildirs-1 (path mdir)
"Get maildirs under path, recursively, as a list of relative paths."
(let ((dirs)
(concat path mdir) nil
"^[^.]\\|\\.[^.][^.]" t))))
(dolist (dentry dentries)
(when (and (booleanp (cadr dentry)) (cadr dentry))
(if (file-accessible-directory-p
(concat mu4e-maildir "/" mdir "/" (car dentry) "/cur"))
(setq dirs (cons (concat mdir (car dentry)) dirs)))
(unless (member (car dentry) '("cur" "new" "tmp"))
(setq dirs (append dirs (mu4e~get-maildirs-1 path
(concat mdir (car dentry) "/")))))))
(defvar mu4e-cache-maildir-list nil
"Whether to cache the list of maildirs; set it to t if you find
that generating the list on the fly is too slow. If you do, you
can set `mu4e-maildir-list' to nil to force regenerating the
cache the next time `mu4e-get-maildirs' gets called.")
(defvar mu4e-maildir-list nil
"Cached list of maildirs.")
(defun mu4e-get-maildirs ()
"Get maildirs under `mu4e-maildir', recursively, as a list of
relative paths (ie., /archive, /sent etc.). Most of the work is
done in `mu4e-get-maildirs-1'. Note, these results are /cached/, so
the list of maildirs will not change until you restart mu4e."
(unless mu4e-maildir (mu4e-error "`mu4e-maildir' is not defined"))
(unless (and mu4e-maildir-list mu4e-cache-maildir-list)
(setq mu4e-maildir-list
(when (file-accessible-directory-p
(concat mu4e-maildir "/cur")) '("/"))
(mu4e~get-maildirs-1 mu4e-maildir "/"))
(lambda (s1 s2) (string< (downcase s1) (downcase s2))))))
(defun mu4e-ask-maildir (prompt)
"Ask the user for a shortcut (using PROMPT) as defined in
`mu4e-maildir-shortcuts', then return the corresponding folder
name. If the special shortcut 'o' (for _o_ther) is used, or if
`mu4e-maildir-shortcuts' is not defined, let user choose from all
maildirs under `mu4e-maildir'."
(let ((prompt (mu4e-format "%s" prompt)))
(if (not mu4e-maildir-shortcuts)
(funcall mu4e-completing-read-function prompt (mu4e-get-maildirs))
(let* ((mlist (append mu4e-maildir-shortcuts '(("ther" . ?o))))
(lambda (item)
(propertize (make-string 1 (cdr item))
'face 'mu4e-highlight-face)
(car item)))
mlist ", "))
(kar (read-char (concat prompt fnames))))
(if (member kar '(?/ ?o)) ;; user chose 'other'?
(funcall mu4e-completing-read-function prompt
(mu4e-get-maildirs) nil nil "/")
(or (car-safe
(find-if (lambda (item) (= kar (cdr item)))
(mu4e-warn "Unknown shortcut '%c'" kar)))))))
(defun mu4e-ask-maildir-check-exists (prompt)
"Like `mu4e-ask-maildir', but check for existence of the maildir,
and offer to create it if it does not exist yet."
(let* ((mdir (mu4e-ask-maildir prompt))
(fullpath (concat mu4e-maildir mdir)))
(unless (file-directory-p fullpath)
(and (yes-or-no-p
(mu4e-format "%s does not exist. Create now?" fullpath))
(mu4e~proc-mkdir fullpath)))
(defun mu4e-ask-bookmark (prompt &optional kar)
"Ask the user for a bookmark (using PROMPT) as defined in
`mu4e-bookmarks', then return the corresponding query."
(unless mu4e-bookmarks (mu4e-error "No bookmarks defined"))
(let* ((prompt (mu4e-format "%s" prompt))
(lambda (bm)
(let ((query (nth 0 bm)) (title (nth 1 bm)) (key (nth 2 bm)))
"[" (propertize (make-string 1 key)
'face 'mu4e-highlight-face)
title))) mu4e-bookmarks ", "))
(kar (read-char (concat prompt bmarks))))
(mu4e-get-bookmark-query kar)))
(defun mu4e-get-bookmark-query (kar)
"Get the corresponding bookmarked query for shortcut character
KAR, or raise an error if none is found."
(let* ((chosen-bm
(or (find-if
(lambda (bm)
(= kar (nth 2 bm)))
(mu4e-warn "Unknown shortcut '%c'" kar)))
(expr (nth 0 chosen-bm))
(query (eval expr)))
(if (stringp query)
(mu4e-warn "Expression must evaluate to query string ('%S')" expr))))
(defun mu4e-bookmark-define (query descr key)
"Define a bookmark for QUERY with description DESCR and short
character KEY in the list of `mu4e-bookmarks'. This replaces any
existing bookmark with KEY."
(setq mu4e-bookmarks (remove-if (lambda (bm) (= (nth 2 bm) key)) mu4e-bookmarks))
(add-to-list 'mu4e-bookmarks (list query descr key) t))
;;; converting flags->string and vice-versa ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun mu4e~flags-to-string-raw (flags)
"Convert a list of flags into a string as seen in Maildir
message files; flags are symbols draft, flagged, new, passed,
replied, seen, trashed and the string is the concatenation of the
uppercased first letters of these flags, as per [1]. Other flags
than the ones listed here are ignored.
Also see `mu4e-flags-to-string'.
(when flags
(let ((kar (case (car flags)
('draft ?D)
('flagged ?F)
('new ?N)
('passed ?P)
('replied ?R)
('seen ?S)
('trashed ?T)
('attach ?a)
('encrypted ?x)
('signed ?s)
('unread ?u))))
(concat (and kar (string kar))
(mu4e~flags-to-string-raw (cdr flags))))))
(defun mu4e-flags-to-string (flags)
"Remove duplicates and sort the output of `mu4e~flags-to-string-raw'."
(sort (remove-duplicates
(append (mu4e~flags-to-string-raw flags) nil)) '>)))
(defun mu4e~string-to-flags-1 (str)
"Convert a string with message flags as seen in Maildir
messages into a list of flags in; flags are symbols draft,
flagged, new, passed, replied, seen, trashed and the string is
the concatenation of the uppercased first letters of these flags,
as per [1]. Other letters than the ones listed here are ignored.
Also see `mu4e-flags-to-string'.
(when (/= 0 (length str))
(let ((flag
(case (string-to-char str)
(?D 'draft)
(?F 'flagged)
(?P 'passed)
(?R 'replied)
(?S 'seen)
(?T 'trashed))))
(append (when flag (list flag))
(mu4e~string-to-flags-1 (substring str 1))))))
(defun mu4e-string-to-flags (str)
"Convert a string with message flags as seen in Maildir messages
into a list of flags in; flags are symbols draft, flagged, new,
passed, replied, seen, trashed and the string is the concatenation
of the uppercased first letters of these flags, as per [1]. Other
letters than the ones listed here are ignored. Also see
`mu4e-flags-to-string'. \[1\]: "
;; "Remove duplicates from the output of `mu4e~string-to-flags-1'"
(remove-duplicates (mu4e~string-to-flags-1 str)))
(defun mu4e-display-size (size)
"Get a string representation of SIZE (in bytes)."
((>= size 1000000) (format "%2.1fM" (/ size 1000000.0)))
((and (>= size 1000) (< size 1000000))
(format "%2.1fK" (/ size 1000.0)))
((< size 1000) (format "%d" size))
(t (propertize "?" 'face 'mu4e-system-face))))
(defun mu4e-display-manual ()
"Display the mu4e manual page for the current mode.
Or go to the top level if there is none."
(info (case major-mode
('mu4e-main-mode "(mu4e)Main view")
('mu4e-headers-mode "(mu4e)Headers view")
('mu4e-view-mode "(mu4e)Message view")
(t "mu4e"))))
(defun mu4e-last-query ()
"Get the most recent query or nil if there is none."
(when (buffer-live-p mu4e~headers-buffer)
(with-current-buffer mu4e~headers-buffer
(defun mu4e-select-other-view ()
"When the headers view is selected, select the message view (if
that has a live window), and vice versa."
(let* ((other-buf
((eq major-mode 'mu4e-headers-mode)
((eq major-mode 'mu4e-view-mode)
(other-win (and other-buf (get-buffer-window other-buf))))
(if (window-live-p other-win)
(select-window other-win)
(mu4e-message "No window to switch to"))))
(defconst mu4e-output-buffer-name "*mu4e-output*"
"*internal* Name of the mu4e output buffer.")
(defun mu4e-process-file-through-pipe (path pipecmd)
"Process file at PATH through a pipe with PIPECMD."
(let ((buf (get-buffer-create mu4e-output-buffer-name)))
(with-current-buffer buf
(let ((inhibit-read-only t))
(call-process-shell-command pipecmd path t t)
(switch-to-buffer buf)))
(defvar mu4e~lists-hash nil
"Hashtable of mailing-list-id => shortname, based on
`mu4e~mailing-lists' and `mu4e-user-mailing-lists'.")
(defun mu4e-get-mailing-list-shortname (list-id)
"Get the shortname for a mailing-list with list-id LIST-ID. based
on `mu4e~mailing-lists', `mu4e-user-mailing-lists', and
(unless mu4e~lists-hash
(setq mu4e~lists-hash (make-hash-table :test 'equal))
(dolist (cell mu4e~mailing-lists)
(puthash (car cell) (cdr cell) mu4e~lists-hash))
(dolist (cell mu4e-user-mailing-lists)
(puthash (car cell) (cdr cell) mu4e~lists-hash)))
(gethash list-id mu4e~lists-hash)
(and (boundp 'mu4e-mailing-list-patterns)
(lambda (pattern)
(string-match pattern list-id))
(match-string 1 list-id))
;; if it's not in the db, take the part until the first dot if there is one;
;; otherwise just return the whole thing
(if (string-match "\\([^.]*\\)\\." list-id)
(match-string 1 list-id)
(defvar mu4e-index-updated-hook nil
"Hook run when the indexing process had one or more updated messages.
This can be used as a simple way to invoke some action when new
messages appear, but note that an update in the index does not
necessarily mean a new message.")
;; some handler functions for server messages
(defun mu4e-info-handler (info)
"Handler function for (:info ...) sexps received from the server
(let ((type (plist-get info :info)))
((eq type 'add) t) ;; do nothing
((eq type 'index)
(if (eq (plist-get info :status) 'running)
(mu4e-index-message "Indexing... processed %d, updated %d"
(plist-get info :processed) (plist-get info :updated))
"Indexing completed; processed %d, updated %d, cleaned-up %d"
(plist-get info :processed) (plist-get info :updated)
(plist-get info :cleaned-up))
(unless (zerop (plist-get info :updated))
(run-hooks 'mu4e-index-updated-hook)))))
((plist-get info :message)
(mu4e-index-message "%s" (plist-get info :message))))))
(defun mu4e-error-handler (errcode errmsg)
"Handler function for showing an error."
;; don't use mu4e-error here; it's running in the process filter context
(case errcode
(4 (user-error "No matches for this search query."))
(t (error "Error %d: %s" errcode errmsg))))
;;; RFC2822 handling of phrases in mail-addresses
;;; The optional display-name contains a phrase, it sits before the angle-addr
;;; as specified in RFC2822 for email-addresses in header fields.
;;; contributed by jhelberg
(defun mu4e~rfc822-phrase-type (ph)
"Return either atom, quoted-string, a corner-case or nil. This
checks for empty string first. Then quotes around the phrase
(returning 'rfc822-quoted-string). Then whether there is a quote
inside the phrase (returning 'rfc822-containing-quote).
The reverse of the RFC atext definition is then tested.
If it matches, nil is returned, if not, it is an 'rfc822-atom, which
is returned."
((= (length ph) 0) 'rfc822-empty)
((= (aref ph 0) ?\")
(if (string-match "\"\\([^\"\\\n]\\|\\\\.\\|\\\\\n\\)*\"" ph)
'rfc822-containing-quote)) ; starts with quote, but doesn't end with one
((string-match-p "[\"]" ph) 'rfc822-containing-quote)
((string-match-p "[\000-\037()\*<>@,;:\\\.]+" ph) nil)
(t 'rfc822-atom)))
(defun mu4e~rfc822-quoteit (ph)
"Quote RFC822 phrase only if necessary.
Atoms and quoted strings don't need quotes. The rest do. In
case a phrase contains a quote, it will be escaped."
(let ((type (mu4e~rfc822-phrase-type ph)))
((eq type 'rfc822-atom) ph)
((eq type 'rfc822-quoted-string) ph)
((eq type 'rfc822-containing-quote)
(format "\"%s\""
(replace-regexp-in-string "\"" "\\\\\"" ph)))
(t (format "\"%s\"" ph)))))
(defsubst mu4e~process-contact (contact)
"Process CONTACT, and either return nil when it should not be included,
or (rfc822-string . CONTACT) otherwise."
(when mu4e-contact-rewrite-function
(setq contact (funcall mu4e-contact-rewrite-function contact)))
(when contact
(let ((name (plist-get contact :name))
(mail (plist-get contact :mail))
(ignore-rx (or mu4e-compose-complete-ignore-address-regexp "$^")))
(when (and mail (not (string-match ignore-rx mail)))
(if name (format "%s <%s>" (mu4e~rfc822-quoteit name) mail) mail)
(defun mu4e~sort-contacts (contacts)
"Destructively sort contacts (only for cycling) in order of
'mostly likely contact'.t See the code for the detail"
(let* ((now (+ (float-time) 3600)) ;; allow for clock diffs
(recent (- (float-time) (* 15 24 3600))))
(sort* contacts
(lambda (c1 c2)
(let* ( (c1 (cdr c1)) (c2 (cdr c2))
(personal1 (plist-get c1 :personal))
(personal2 (plist-get c2 :personal))
;; note: freq, tstamp can only be missing if the rewrite
;; function removed them. If the rewrite function changed the
;; contact somehow, we guess it's important.
(freq1 (or (plist-get c1 :freq) 500))
(freq2 (or (plist-get c2 :freq) 500))
(tstamp1 (or (plist-get c1 :tstamp) now))
(tstamp2 (or (plist-get c2 :tstamp) now)))
;; only one is personal? if so, that one comes first
(if (not (equal personal1 personal2))
(if personal1 t nil)
;; only one is recent? that one comes first
(if (not (equal (> tstamp1 recent) (> tstamp2 recent)))
(> tstamp1 tstamp2)
;; otherwise, use the frequency
(> freq1 freq2))))))))
(defun mu4e~sort-contacts-for-completion (contacts)
"Takes CONTACTS, which is a list of RFC-822 addresses, and sort them based
on the ranking in `mu4e~contacts.'"
(sort* contacts
(lambda (c1 c2)
(let ((rank1 (gethash c1 mu4e~contacts))
(rank2 (gethash c2 mu4e~contacts)))
(< rank1 rank2)))))
;; start and stopping
(defun mu4e~fill-contacts (contact-data)
"We receive a list of contacts, which each contact of the form
(:me NAME :mail EMAIL :tstamp TIMESTAMP :freq FREQUENCY)
and fill the hash `mu4e~contacts-for-completion' with it, with
each contact mapped to an integer for their ranking.
This is used by the completion function in mu4e-compose."
(let ((contacts) (rank 0))
(dolist (contact contact-data)
(let ((contact-maybe (mu4e~process-contact contact)))
;; note, this gives cells (rfc822-address . contact)
(when contact-maybe (push contact-maybe contacts))))
(setq contacts (mu4e~sort-contacts contacts))
;; now, we have our nicely sorted list, map them to a list
;; of increasing integers. We use that map in the composer
;; to sort them there. It would have been so much easier if emacs
;; allowed us to use the sorted-list as-is, but no such luck.
(setq mu4e~contacts (make-hash-table :test 'equal :weakness nil
:size (length contacts)))
(dolist (contact contacts)
(puthash (car contact) rank mu4e~contacts)
(incf rank))
(mu4e-index-message "Contacts received: %d"
(hash-table-count mu4e~contacts))))
(defun mu4e~check-requirements ()
"Check for the settings required for running mu4e."
(unless (>= emacs-major-version 23)
(mu4e-error "Emacs >= 23.x is required for mu4e"))
(when mu4e~server-props
(let ((version (plist-get mu4e~server-props :version)))
(unless (string= version mu4e-mu-version)
(mu4e-error "mu server has version %s, but we need %s"
version mu4e-mu-version))))
(unless (and mu4e-mu-binary (file-executable-p mu4e-mu-binary))
(mu4e-error "Please set `mu4e-mu-binary' to the full path to the mu
(unless mu4e-maildir
(mu4e-error "Please set `mu4e-maildir' to the full path to your
Maildir directory."))
;; expand mu4e-maildir, mu4e-attachment-dir
(setq mu4e-maildir (expand-file-name mu4e-maildir))
(unless (mu4e-create-maildir-maybe mu4e-maildir)
(mu4e-error "%s is not a valid maildir directory" mu4e-maildir))
(dolist (var '(mu4e-sent-folder mu4e-drafts-folder
(unless (and (boundp var) (symbol-value var))
(mu4e-error "Please set %S" var))
(unless (functionp (symbol-value var)) ;; functions are okay, too
(let* ((dir (symbol-value var))
(path (concat mu4e-maildir dir)))
(unless (string= (substring dir 0 1) "/")
(mu4e-error "%S must start with a '/'" dir))
(unless (mu4e-create-maildir-maybe path)
(mu4e-error "%s (%S) does not exist" path var))))))
(defun mu4e-running-p ()
"Whether mu4e is running.
Checks whether the server process is live."
;; starting / getting mail / updating the index
(defvar mu4e~update-timer nil
"The mu4e update timer.")
(defconst mu4e~update-name "*mu4e-update*"
"Name of the process and buffer to update mail.")
(defconst mu4e~update-buffer-height 8
"Height of the mu4e message retrieval/update buffer.")
(defvar mu4e~get-mail-ask-password "mu4e get-mail: Enter password: "
"Query string for `mu4e-get-mail-command' password.")
(defvar mu4e~get-mail-password-regexp "^Remote: Enter password: $"
"Regexp to match a password query in the `mu4e-get-mail-command' output.")
(defun mu4e~request-contacts ()
"If `mu4e-compose-complete-addresses' is non-nil, get/update the
list of contacts we use for autocompletion; otherwise, do nothing."
(when mu4e-compose-complete-addresses
(setq mu4e-contacts-func 'mu4e~fill-contacts)
(when mu4e-compose-complete-only-after
(apply 'encode-time
(mu4e-parse-time-string mu4e-compose-complete-only-after)))))))
(defun mu4e~start (&optional func)
"If `mu4e-contexts' have been defined, but we don't have a
context yet, switch to the matching one, or none matches, the
If mu4e is already running, execute function FUNC (if non-nil).
Otherwise, check various requirements, then start mu4e. When
successful, call FUNC (if non-nil) afterwards."
;; if we're already running, simply go to the main view
(if (mu4e-running-p) ;; already running?
(when func (funcall func)) ;; yes! run func if defined
;; no! try to set a context, do some checks, set up pong handler and ping
;; the server maybe switch the context
(mu4e~context-autoswitch nil mu4e-context-policy)
(lexical-let ((func func))
;; set up the 'pong' handler func
(setq mu4e-pong-func
(lambda (props)
(setq mu4e~server-props props) ;; save props from the server
(let ((version (plist-get props :version))
(doccount (plist-get props :doccount)))
(when func (funcall func))
(when (and mu4e-update-interval (null mu4e~update-timer))
(setq mu4e~update-timer
0 mu4e-update-interval
(lambda () (mu4e-update-mail-and-index
(mu4e-message "Started mu4e with %d message%s in store"
doccount (if (= doccount 1) "" "s"))))))
;; wake up server
;; maybe request the list of contacts, automatically refresh after
;; reindexing
(add-hook 'mu4e-index-updated-hook 'mu4e~request-contacts))))
(defun mu4e-clear-caches ()
"Clear any cached resources."
mu4e-maildir-list nil
mu4e~contacts nil))
(defun mu4e~stop ()
"Stop the mu4e session."
(when mu4e~update-timer
(cancel-timer mu4e~update-timer)
(setq mu4e~update-timer nil))
;; kill all main/view/headers buffer
(lambda (buf)
(with-current-buffer buf
(when (member major-mode
'(mu4e-headers-mode mu4e-view-mode mu4e-main-mode))
(defvar mu4e~progress-reporter nil
"Internal, the progress reporter object.")
(defun mu4e~get-mail-process-filter (proc msg)
"Filter the output of `mu4e-get-mail-command'.
Currently the filter only checks if the command asks for a password
by matching the output against `mu4e~get-mail-password-regexp'.
The messages are inserted into the process buffer.
Also scrolls to the final line, and update the progress throbber."
(when mu4e~progress-reporter
(progress-reporter-update mu4e~progress-reporter))
(when (string-match mu4e~get-mail-password-regexp msg)
(if (process-get proc 'x-interactive)
(process-send-string proc
(concat (read-passwd mu4e~get-mail-ask-password)
;; TODO kill process?
(mu4e-error "Unrecognized password request")))
(when (process-buffer proc)
(let ((inhibit-read-only t)
(procwin (get-buffer-window (process-buffer proc))))
;; Insert at end of buffer. Leave point alone.
(with-current-buffer (process-buffer proc)
(goto-char (point-max))
(insert msg))
;; Auto-scroll unless user is interacting with the window.
(when (and (window-live-p procwin)
(not (eq (selected-window) procwin)))
(with-selected-window procwin
(goto-char (point-max)))))))
(defun mu4e-update-index ()
"Update the mu4e index."
(unless mu4e-maildir
(mu4e-error "`mu4e-maildir' is not defined"))
(mu4e~proc-index mu4e-maildir mu4e-user-mail-address-list))
(defvar mu4e~update-buffer nil
"Internal, store the buffer of the update process when
(define-derived-mode mu4e~update-mail-mode special-mode "mu4e:update"
"Major mode used for retrieving new e-mail messages in `mu4e'.")
(define-key mu4e~update-mail-mode-map (kbd "q") 'mu4e-interrupt-update-mail)
(defun mu4e~temp-window (buf height)
"Create a temporary window with HEIGHT at the bottom of the
frame to display buffer BUF."
(let ((win
(- (window-height (frame-root-window)) height))))
(set-window-buffer win buf)
(set-window-dedicated-p win t)
(defun mu4e~update-sentinel-func (proc msg)
"Sentinel function for the update process."
(when mu4e~progress-reporter
(progress-reporter-done mu4e~progress-reporter)
(setq mu4e~progress-reporter nil))
(let* ((status (process-status proc))
(code (process-exit-status proc))
(maybe-error (or (not (eq status 'exit)) (/= code 0)))
(buf (and (buffer-live-p mu4e~update-buffer) mu4e~update-buffer))
(win (and buf (get-buffer-window buf))))
(message nil)
(if maybe-error
(when mu4e-index-update-error-warning
(mu4e-message "Update process returned with non-zero exit code")
(sit-for 5))
(when mu4e-index-update-error-continue
(if (window-live-p win)
(with-selected-window win (kill-buffer-and-window))
(when (buffer-live-p buf) (kill-buffer buf)))))
;; complicated function, as it:
;; - needs to check for errors
;; - (optionally) pop-up a window
;; - (optionally) check password requests
(defun mu4e~update-mail-and-index-real (run-in-background)
"Get a new mail by running `mu4e-get-mail-command'. If
RUN-IN-BACKGROUND is non-nil (or called with prefix-argument),
run in the background; otherwise, pop up a window."
(let* ((process-connection-type t)
(proc (start-process-shell-command
"mu4e-update" " *mu4e-update*"
(buf (process-buffer proc))
(win (or run-in-background
(mu4e~temp-window buf mu4e~update-buffer-height))))
(setq mu4e~update-buffer buf)
(when (window-live-p win)
(with-selected-window win
;; ;;(switch-to-buffer buf)
;; (set-window-dedicated-p win t)
(insert "\n") ;; FIXME -- needed so output start
(setq mu4e~progress-reporter
(unless mu4e-hide-index-messages
(mu4e-format "Retrieving mail..."))))
(set-process-sentinel proc 'mu4e~update-sentinel-func)
;; if we're running in the foreground, handle password requests
(unless run-in-background
(process-put proc 'x-interactive (not run-in-background))
(set-process-filter proc 'mu4e~get-mail-process-filter))))
(defun mu4e-update-mail-and-index (run-in-background)
"Get a new mail by running `mu4e-get-mail-command'. If
run-in-background is non-nil (or called with prefix-argument), run
in the background; otherwise, pop up a window."
(interactive "P")
(unless mu4e-get-mail-command
(mu4e-error "`mu4e-get-mail-command' is not defined"))
(if (and (buffer-live-p mu4e~update-buffer)
(process-live-p (get-buffer-process mu4e~update-buffer)))
(mu4e-message "Update process is already running")
(run-hooks 'mu4e-update-pre-hook)
(mu4e~update-mail-and-index-real run-in-background))))
(defun mu4e-interrupt-update-mail ()
"Stop the update process by sending SIGINT to it."
(let* ((proc (and (buffer-live-p mu4e~update-buffer)
(get-buffer-process mu4e~update-buffer))))
(when (process-live-p proc)
(interrupt-process proc t))))
;; logging / debugging
(defvar mu4e~log-max-lines 1200
"*internal* Last <n> number of lines to keep around in the buffer.")
(defconst mu4e~log-buffer-name "*mu4e-log*"
"*internal* Name of the logging buffer.")
(defun mu4e-log (type frm &rest args)
"Write a message of TYPE with format-string FRM and ARGS in
*mu4e-log* buffer, if the variable mu4e-debug is non-nil. Type is
either 'to-server, 'from-server or 'misc. This function is meant for debugging."
(when mu4e-debug
(with-current-buffer (get-buffer-create mu4e~log-buffer-name)
(setq buffer-undo-list t)
(let* ((inhibit-read-only t)
(tstamp (propertize (format-time-string "%Y-%m-%d %T"
'face 'font-lock-string-face))
(case type
(from-server 'font-lock-type-face)
(to-server 'font-lock-function-name-face)
(misc 'font-lock-variable-name-face)
(error 'font-lock-warning-face)
(otherwise (mu4e-error "Unsupported log type"))))
(msg (propertize (apply 'format frm args) 'face msg-face)))
(goto-char (point-max))
(insert tstamp
(case type
(from-server " <- ")
(to-server " -> ")
(error " !! ")
(otherwise " "))
msg "\n")
;; if `mu4e-log-max-lines is specified and exceeded, clearest the oldest
;; lines
(when (numberp mu4e~log-max-lines)
(let ((lines (count-lines (point-min) (point-max))))
(when (> lines mu4e~log-max-lines)
(goto-char (point-max))
(forward-line (- mu4e~log-max-lines lines))
(delete-region (point-min) (point)))))))))
(defun mu4e-toggle-logging ()
"Toggle between enabling/disabling debug-mode (in debug-mode,
mu4e logs some of its internal workings to a log-buffer. See
(mu4e-log 'misc "logging disabled")
(setq mu4e-debug (not mu4e-debug))
(mu4e-message "debug logging has been %s"
(if mu4e-debug "enabled" "disabled"))
(mu4e-log 'misc "logging enabled"))
(defun mu4e-show-log ()
"Visit the mu4e debug log."
(let ((buf (get-buffer mu4e~log-buffer-name)))
(unless (buffer-live-p buf)
(mu4e-warn "No debug log available"))
(switch-to-buffer buf)))
(defun mu4e-split-ranges-to-numbers (str n)
"Convert STR containing attachment numbers into a list of numbers.
STR is a string; N is the highest possible number in the list.
This includes expanding e.g. 3-5 into 3,4,5. If the letter
\"a\" ('all')) is given, that is expanded to a list with numbers [1..n]."
(let ((str-split (split-string str))
beg end list)
(dolist (elem str-split list)
;; special number "a" converts into all attachments 1-N.
(when (equal elem "a")
(setq elem (concat "1-" (int-to-string n))))
(if (string-match "\\([0-9]+\\)-\\([0-9]+\\)" elem)
;; we have found a range A-B, which needs converting
;; into the numbers A, A+1, A+2, ... B.
(setq beg (string-to-number (match-string 1 elem))
end (string-to-number (match-string 2 elem)))
(while (<= beg end)
(add-to-list 'list beg 'append)
(setq beg (1+ beg))))
;; else just a number
(add-to-list 'list (string-to-number elem) 'append)))
;; Check that all numbers are valid.
#'(lambda (x)
((> x n)
(mu4e-warn "Attachment %d bigger than maximum (%d)" x n))
((< x 1)
(mu4e-warn "Attachment number must be greater than 0 (%d)" x))))
(defvar mu4e-imagemagick-identify "identify"
"Name/path of the Imagemagick 'identify' program.")
(defun mu4e-display-image (imgpath &optional maxwidth maxheight)
"Display image IMG at point; optionally specify MAXWIDTH and
MAXHEIGHT. Function tries to use imagemagick if available (ie.,
emacs was compiled with inmagemagick support); otherwise MAXWIDTH
and MAXHEIGHT are ignored."
(let* ((have-im (and (fboundp 'imagemagick-types)
(imagemagick-types))) ;; hmm, should check for specific type
(identify (and have-im maxwidth
(executable-find mu4e-imagemagick-identify)))
(props (and identify (shell-command-to-string
(format "%s -format '%%w' %s"
identify (shell-quote-argument imgpath)))))
(width (and props (string-to-number props)))
(img (if have-im
(if (> (or width 0) (or maxwidth 0))
(create-image imgpath 'imagemagick nil :width maxwidth)
(create-image imgpath 'imagemagick))
(create-image imgpath))))
(when img
(insert "\n")
(let ((size (image-size img))) ;; inspired by gnus..
(insert-char ?\n
(max 0 (round (- (window-height) (or maxheight (cdr size)) 1) 2)))
(insert-char ?\.
(max 0 (round (- (window-width) (or maxwidth (car size))) 2)))
(insert-image img))))))
(defun mu4e-hide-other-mu4e-buffers ()
"Bury mu4e-buffers (main, headers, view) (and delete all windows
displaying it). Do _not_ bury the current buffer, though."
(let ((curbuf (current-buffer)))
;; note: 'walk-windows' does not seem to work correctly when modifying
;; windows; therefore, the doloops here
(dolist (frame (frame-list))
(dolist (win (window-list frame nil))
(with-current-buffer (window-buffer win)
(unless (eq curbuf (current-buffer))
(when (member major-mode '(mu4e-headers-mode mu4e-view-mode))
(when (eq t (window-deletable-p win))
(delete-window win))))))) t))
(defun mu4e-get-time-date (prompt)
"Determine the emacs time value for the time/date entered by user
after PROMPT. Formats are all that are accepted by
(let ((timestr (read-string (mu4e-format "%s" prompt))))
(apply 'encode-time (mu4e-parse-time-string timestr))))
(define-derived-mode mu4e-org-mode org-mode "mu4e:org"
"Major mode for mu4e documents, derived from
(defun mu4e-info (path)
"Show a buffer with the information (an org-file) at PATH."
(unless (file-exists-p path)
(mu4e-error "Cannot find %s" path))
(lexical-let ((curbuf (current-buffer)))
(find-file path)
(setq buffer-read-only t)
(define-key mu4e-org-mode-map (kbd "q")
(lambda ()
(switch-to-buffer curbuf)))))
(defun mu4e-about ()
"Show the mu4e 'about' page."
(mu4e-info (concat mu4e-doc-dir "/")))
(defun mu4e-news ()
"Show the mu4e 'about' page."
(mu4e-info (concat mu4e-doc-dir "/")))
(defun mu4e-refresh-message (path maildir)
"Re-parse message at PATH and MAILDIR; if this works, we will
receive (:info add :path <path> :docid <docid>) as well as (:update
(mu4e~proc-add path maildir))
(defun mu4e~fontify-cited ()
"Colorize message content based on the citation level. This is
used in the view and compose modes."
(goto-char (point-min))
(when (search-forward-regexp "^\n" nil t) ;; search the first empty line
(while (re-search-forward mu4e-cited-regexp nil t)
(let* ((level (string-width (replace-regexp-in-string
" " "" (match-string 1))))
(face (unless (zerop level)
(intern-soft (format "mu4e-cited-%d-face" level)))))
(when face
(add-text-properties (line-beginning-position 1)
(line-end-position 1) `(face ,face))))))))
(defun mu4e~fontify-signature ()
"Give the message signatures a distinctive color. This is used in
the view and compose modes."
(let ((inhibit-read-only t))
;; give the footer a different color...
(goto-char (point-min))
(let ((p (search-forward "^-- *$" nil t)))
(when p
(add-text-properties p (point-max) '(face mu4e-footer-face)))))))
(defun mu4e~quote-for-modeline (str)
"Quote a string to be used literally in the modeline."
(replace-regexp-in-string "%" "%%" str t t))
(provide 'mu4e-utils)
;;; End of mu4e-utils.el
Something went wrong with that request. Please try again.