-;; notmuch-lib.el --- common variables, functions and function declarations
+;;; notmuch-lib.el --- common variables, functions and function declarations -*- lexical-binding: t -*-
;;
;; Copyright © Carl Worth
;;
;; General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
-;; along with Notmuch. If not, see <http://www.gnu.org/licenses/>.
+;; along with Notmuch. If not, see <https://www.gnu.org/licenses/>.
;;
;; Authors: Carl Worth <cworth@cworth.org>
-;; This is an part of an emacs-based interface to the notmuch mail system.
+;;; Code:
+(require 'cl-lib)
+(require 'pcase)
+(require 'subr-x)
+
+(require 'mm-util)
(require 'mm-view)
(require 'mm-decode)
-(require 'cl)
-(defvar notmuch-command "notmuch"
- "Command to run the notmuch binary.")
+(require 'notmuch-compat)
+
+(unless (require 'notmuch-version nil t)
+ (defconst notmuch-emacs-version "unknown"
+ "Placeholder variable when notmuch-version.el[c] is not available."))
+
+;;; Groups
(defgroup notmuch nil
"Notmuch mail reader for Emacs."
(defgroup notmuch-send nil
"Sending messages from Notmuch."
- :group 'notmuch)
+ :group 'notmuch
+ :group 'message)
-(custom-add-to-group 'notmuch-send 'message 'custom-group)
+(defgroup notmuch-tag nil
+ "Tags and tagging in Notmuch."
+ :group 'notmuch)
(defgroup notmuch-crypto nil
"Processing and display of cryptographic MIME parts."
"Running external commands from within Notmuch."
:group 'notmuch)
+(defgroup notmuch-address nil
+ "Address completion."
+ :group 'notmuch)
+
(defgroup notmuch-faces nil
"Graphical attributes for displaying text"
:group 'notmuch)
+;;; Options
+
+(defcustom notmuch-command "notmuch"
+ "Name of the notmuch binary.
+
+This can be a relative or absolute path to the notmuch binary.
+If this is a relative path, it will be searched for in all of the
+directories given in `exec-path' (which is, by default, based on
+$PATH)."
+ :type 'string
+ :group 'notmuch-external)
+
(defcustom notmuch-search-oldest-first t
"Show the oldest mail first when searching.
search."
:type 'boolean
:group 'notmuch-search)
+(make-variable-buffer-local 'notmuch-search-oldest-first)
(defcustom notmuch-poll-script nil
- "An external script to incorporate new mail into the notmuch database.
+ "[Deprecated] Command to run to incorporate new mail into the notmuch database.
+
+This option has been deprecated in favor of \"notmuch new\"
+hooks (see man notmuch-hooks). To change the path to the notmuch
+binary, customize `notmuch-command'.
This variable controls the action invoked by
`notmuch-poll-and-refresh-this-buffer' (bound by default to 'G')
1. Invoke a program to transfer mail to the local mail store
2. Invoke \"notmuch new\" to incorporate the new mail
-3. Invoke one or more \"notmuch tag\" commands to classify the mail
-
-Note that the recommended way of achieving the same is using
-\"notmuch new\" hooks."
+3. Invoke one or more \"notmuch tag\" commands to classify the mail"
:type '(choice (const :tag "notmuch new" nil)
(const :tag "Disabled" "")
(string :tag "Custom script"))
:group 'notmuch-external)
-;;
-
-(defvar notmuch-search-history nil
- "Variable to store notmuch searches history.")
-
-(defcustom notmuch-saved-searches '(("inbox" . "tag:inbox")
- ("unread" . "tag:unread"))
- "A list of saved searches to display."
- :type '(alist :key-type string :value-type string)
- :group 'notmuch-hello)
-
(defcustom notmuch-archive-tags '("-inbox")
"List of tag changes to apply to a message or a thread when it is archived.
:group 'notmuch-search
:group 'notmuch-show)
+;;; Variables
+
+(defvar notmuch-search-history nil
+ "Variable to store notmuch searches history.")
+
(defvar notmuch-common-keymap
(let ((map (make-sparse-keymap)))
(define-key map "?" 'notmuch-help)
- (define-key map "q" 'notmuch-kill-this-buffer)
+ (define-key map "v" 'notmuch-version)
+ (define-key map "q" 'notmuch-bury-or-kill-this-buffer)
(define-key map "s" 'notmuch-search)
+ (define-key map "t" 'notmuch-search-by-tag)
(define-key map "z" 'notmuch-tree)
+ (define-key map "u" 'notmuch-unthreaded)
(define-key map "m" 'notmuch-mua-new-mail)
+ (define-key map "g" 'notmuch-refresh-this-buffer)
(define-key map "=" 'notmuch-refresh-this-buffer)
+ (define-key map (kbd "M-=") 'notmuch-refresh-all-buffers)
(define-key map "G" 'notmuch-poll-and-refresh-this-buffer)
+ (define-key map "j" 'notmuch-jump-search)
map)
"Keymap shared by all notmuch modes.")
(select-window (posn-window (event-start last-input-event)))
(button-activate button)))
+;;; CLI Utilities
+
(defun notmuch-command-to-string (&rest args)
"Synchronously invoke \"notmuch\" with the given list of arguments.
will appear in a buffer named \"*Notmuch errors*\" and an error
will be signaled.
-Otherwise the output will be returned"
+Otherwise the output will be returned."
(with-temp-buffer
- (let* ((status (apply #'call-process notmuch-command nil t nil args))
- (output (buffer-string)))
+ (let ((status (apply #'call-process notmuch-command nil t nil args))
+ (output (buffer-string)))
(notmuch-check-exit-status status (cons notmuch-command args) output)
output)))
-(defun notmuch-version ()
- "Return a string with the notmuch version number."
+(defvar notmuch--cli-sane-p nil
+ "Cache whether the CLI seems to be configured sanely.")
+
+(defun notmuch-cli-sane-p ()
+ "Return t if the cli seems to be configured sanely."
+ (unless notmuch--cli-sane-p
+ (let ((status (call-process notmuch-command nil nil nil
+ "config" "get" "user.primary_email")))
+ (setq notmuch--cli-sane-p (= status 0))))
+ notmuch--cli-sane-p)
+
+(defun notmuch-assert-cli-sane ()
+ (unless (notmuch-cli-sane-p)
+ (notmuch-logged-error
+ "notmuch cli seems misconfigured or unconfigured."
+ "Perhaps you haven't run \"notmuch setup\" yet? Try running this
+on the command line, and then retry your notmuch command")))
+
+(defun notmuch-cli-version ()
+ "Return a string with the notmuch cli command version number."
(let ((long-string
;; Trim off the trailing newline.
(substring (notmuch-command-to-string "--version") 0 -1)))
(match-string 2 long-string)
"unknown")))
+(defvar notmuch-emacs-version)
+
+(defun notmuch-version ()
+ "Display the notmuch version.
+The versions of the Emacs package and the `notmuch' executable
+should match, but if and only if they don't, then this command
+displays both values separately."
+ (interactive)
+ (let ((cli-version (notmuch-cli-version)))
+ (message "notmuch version %s"
+ (if (string= notmuch-emacs-version cli-version)
+ cli-version
+ (concat cli-version
+ " (emacs mua version " notmuch-emacs-version ")")))))
+
+;;; Notmuch Configuration
+
(defun notmuch-config-get (item)
"Return a value from the notmuch configuration."
- ;; Trim off the trailing newline
- (substring (notmuch-command-to-string "config" "get" item) 0 -1))
+ (let* ((val (notmuch-command-to-string "config" "get" item))
+ (len (length val)))
+ ;; Trim off the trailing newline (if the value is empty or not
+ ;; configured, there will be no newline).
+ (if (and (> len 0)
+ (= (aref val (- len 1)) ?\n))
+ (substring val 0 -1)
+ val)))
(defun notmuch-database-path ()
"Return the database.path value from the notmuch configuration."
(defun notmuch-user-other-email ()
"Return the user.other_email value (as a list) from the notmuch configuration."
- (split-string (notmuch-config-get "user.other_email") "\n"))
+ (split-string (notmuch-config-get "user.other_email") "\n" t))
+
+(defun notmuch-user-emails ()
+ (cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+
+;;; Commands
(defun notmuch-poll ()
"Run \"notmuch new\" or an external script to import mail.
Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
depending on the value of `notmuch-poll-script'."
(interactive)
+ (message "Polling mail...")
(if (stringp notmuch-poll-script)
- (unless (string= notmuch-poll-script "")
- (call-process notmuch-poll-script nil nil))
- (call-process notmuch-command nil nil nil "new")))
+ (unless (string-empty-p notmuch-poll-script)
+ (unless (equal (call-process notmuch-poll-script nil nil) 0)
+ (error "Notmuch: poll script `%s' failed!" notmuch-poll-script)))
+ (notmuch-call-notmuch-process "new"))
+ (message "Polling mail...done"))
+
+(defun notmuch-bury-or-kill-this-buffer ()
+ "Undisplay the current buffer.
-(defun notmuch-kill-this-buffer ()
- "Kill the current buffer."
+Bury the current buffer, unless there is only one window showing
+it, in which case it is killed."
(interactive)
- (kill-buffer (current-buffer)))
-
-(defun notmuch-documentation-first-line (symbol)
- "Return the first line of the documentation string for SYMBOL."
- (let ((doc (documentation symbol)))
- (if doc
- (with-temp-buffer
- (insert (documentation symbol t))
- (goto-char (point-min))
- (let ((beg (point)))
- (end-of-line)
- (buffer-substring beg (point))))
- "")))
+ (if (> (length (get-buffer-window-list nil nil t)) 1)
+ (bury-buffer)
+ (kill-buffer)))
+
+;;; Describe Key Bindings
(defun notmuch-prefix-key-description (key)
"Given a prefix key code, return a human-readable string representation.
This is basically just `format-kbd-macro' but we also convert ESC to M-."
- (let ((desc (format-kbd-macro (vector key))))
+ (let* ((key-vector (if (vectorp key) key (vector key)))
+ (desc (format-kbd-macro key-vector)))
(if (string= desc "ESC")
"M-"
(concat desc " "))))
-(defun notmuch-describe-keymap (keymap ua-keys &optional prefix tail)
- "Return a list of strings, each describing one binding in KEYMAP.
+(defun notmuch-describe-key (actual-key binding prefix ua-keys tail)
+ "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to TAIL.
+
+It does not prepend if ACTUAL-KEY is already listed in TAIL."
+ (let ((key-string (concat prefix (key-description actual-key))))
+ ;; We don't include documentation if the key-binding is
+ ;; over-ridden. Note, over-riding a binding automatically hides the
+ ;; prefixed version too.
+ (unless (assoc key-string tail)
+ (when (and ua-keys (symbolp binding)
+ (get binding 'notmuch-prefix-doc))
+ ;; Documentation for prefixed command
+ (let ((ua-desc (key-description ua-keys)))
+ (push (cons (concat ua-desc " " prefix (format-kbd-macro actual-key))
+ (get binding 'notmuch-prefix-doc))
+ tail)))
+ ;; Documentation for command
+ (push (cons key-string
+ (or (and (symbolp binding)
+ (get binding 'notmuch-doc))
+ (and (functionp binding)
+ (let ((doc (documentation binding)))
+ (and doc
+ (string-match "\\`.+" doc)
+ (match-string 0 doc))))))
+ tail)))
+ tail)
+
+(defun notmuch-describe-remaps (remap-keymap ua-keys base-keymap prefix tail)
+ ;; Remappings are represented as a binding whose first "event" is
+ ;; 'remap. Hence, if the keymap has any remappings, it will have a
+ ;; binding whose "key" is 'remap, and whose "binding" is itself a
+ ;; keymap that maps not from keys to commands, but from old (remapped)
+ ;; functions to the commands to use in their stead.
+ (map-keymap (lambda (command binding)
+ (mapc (lambda (actual-key)
+ (setq tail
+ (notmuch-describe-key actual-key binding
+ prefix ua-keys tail)))
+ (where-is-internal command base-keymap)))
+ remap-keymap)
+ tail)
+
+(defun notmuch-describe-keymap (keymap ua-keys base-keymap &optional prefix tail)
+ "Return a list of cons cells, each describing one binding in KEYMAP.
-Each string gives a human-readable description of the key and a
-one-line description of the bound function. See `notmuch-help'
-for an overview of how this documentation is extracted.
+Each cons cell consists of a string giving a human-readable
+description of the key, and a one-line description of the bound
+function. See `notmuch-help' for an overview of how this
+documentation is extracted.
UA-KEYS should be a key sequence bound to `universal-argument'.
It will be used to describe bindings of commands that support a
(cond ((mouse-event-p key) nil)
((keymapp binding)
(setq tail
- (notmuch-describe-keymap
- binding ua-keys (notmuch-prefix-key-description key) tail)))
+ (if (eq key 'remap)
+ (notmuch-describe-remaps
+ binding ua-keys base-keymap prefix tail)
+ (notmuch-describe-keymap
+ binding ua-keys base-keymap
+ (notmuch-prefix-key-description key)
+ tail))))
(binding
- (when (and ua-keys (symbolp binding)
- (get binding 'notmuch-prefix-doc))
- ;; Documentation for prefixed command
- (let ((ua-desc (key-description ua-keys)))
- (push (concat ua-desc " " prefix (format-kbd-macro (vector key))
- "\t" (get binding 'notmuch-prefix-doc))
- tail)))
- ;; Documentation for command
- (push (concat prefix (format-kbd-macro (vector key)) "\t"
- (or (and (symbolp binding) (get binding 'notmuch-doc))
- (notmuch-documentation-first-line binding)))
- tail))))
+ (setq tail
+ (notmuch-describe-key (vector key)
+ binding prefix ua-keys tail)))))
keymap)
tail)
"Like `substitute-command-keys' but with documentation, not function names."
(let ((beg 0))
(while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
- (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
- (keymap (symbol-value (intern keymap-name)))
- (ua-keys (where-is-internal 'universal-argument keymap t))
- (desc-list (notmuch-describe-keymap keymap ua-keys))
- (desc (mapconcat #'identity desc-list "\n")))
+ (let ((desc
+ (save-match-data
+ (let* ((keymap-name (substring doc
+ (match-beginning 1)
+ (match-end 1)))
+ (keymap (symbol-value (intern keymap-name)))
+ (ua-keys (where-is-internal 'universal-argument keymap t))
+ (desc-alist (notmuch-describe-keymap keymap ua-keys keymap))
+ (desc-list (mapcar (lambda (arg)
+ (concat (car arg) "\t" (cdr arg)))
+ desc-alist)))
+ (mapconcat #'identity desc-list "\n")))))
(setq doc (replace-match desc 1 1 doc)))
(setq beg (match-end 0)))
doc))
its prefixed behavior by setting the 'notmuch-prefix-doc property
of its command symbol."
(interactive)
- (let* ((mode major-mode)
- (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
+ (let ((doc (substitute-command-keys
+ (notmuch-substitute-command-keys
+ (documentation major-mode t)))))
(with-current-buffer (generate-new-buffer "*notmuch-help*")
(insert doc)
(goto-char (point-min))
(set-buffer-modified-p nil)
(view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
-(defvar notmuch-buffer-refresh-function nil
+(defun notmuch-subkeymap-help ()
+ "Show help for a subkeymap."
+ (interactive)
+ (let* ((key (this-command-keys-vector))
+ (prefix (make-vector (1- (length key)) nil))
+ (i 0))
+ (while (< i (length prefix))
+ (aset prefix i (aref key i))
+ (cl-incf i))
+ (let* ((subkeymap (key-binding prefix))
+ (ua-keys (where-is-internal 'universal-argument nil t))
+ (prefix-string (notmuch-prefix-key-description prefix))
+ (desc-alist (notmuch-describe-keymap
+ subkeymap ua-keys subkeymap prefix-string))
+ (desc-list (mapcar (lambda (arg) (concat (car arg) "\t" (cdr arg)))
+ desc-alist))
+ (desc (mapconcat #'identity desc-list "\n")))
+ (with-help-window (help-buffer)
+ (with-current-buffer standard-output
+ (insert "\nPress 'q' to quit this window.\n\n")
+ (insert desc)))
+ (pop-to-buffer (help-buffer)))))
+
+;;; Refreshing Buffers
+
+(defvar-local notmuch-buffer-refresh-function nil
"Function to call to refresh the current buffer.")
-(make-variable-buffer-local 'notmuch-buffer-refresh-function)
(defun notmuch-refresh-this-buffer ()
"Refresh the current buffer."
(interactive)
(when notmuch-buffer-refresh-function
- (if (commandp notmuch-buffer-refresh-function)
- ;; Pass prefix argument, etc.
- (call-interactively notmuch-buffer-refresh-function)
- (funcall notmuch-buffer-refresh-function))))
+ ;; Pass prefix argument, etc.
+ (call-interactively notmuch-buffer-refresh-function)))
(defun notmuch-poll-and-refresh-this-buffer ()
"Invoke `notmuch-poll' to import mail, then refresh the current buffer."
(notmuch-poll)
(notmuch-refresh-this-buffer))
+(defun notmuch-refresh-all-buffers ()
+ "Invoke `notmuch-refresh-this-buffer' on all notmuch major-mode buffers.
+
+The buffers are silently refreshed, i.e. they are not forced to
+be displayed."
+ (interactive)
+ (dolist (buffer (buffer-list))
+ (let ((buffer-mode (buffer-local-value 'major-mode buffer)))
+ (when (memq buffer-mode '(notmuch-show-mode
+ notmuch-tree-mode
+ notmuch-search-mode
+ notmuch-hello-mode))
+ (with-current-buffer buffer
+ (notmuch-refresh-this-buffer))))))
+
+;;; String Utilities
+
(defun notmuch-prettify-subject (subject)
- ;; This function is used by `notmuch-search-process-filter' which
- ;; requires that we not disrupt its' matching state.
+ ;; This function is used by `notmuch-search-process-filter',
+ ;; which requires that we not disrupt its matching state.
(save-match-data
(if (and subject
(string-match "^[ \t]*$" subject))
The caller is responsible for prepending the term prefix and a
colon. This performs minimal escaping in order to produce
user-friendly queries."
-
(save-match-data
(if (or (equal term "")
- (string-match "[ ()]\\|^\"" term))
+ ;; To be pessimistic, only pass through terms composed
+ ;; entirely of ASCII printing characters other than ", (,
+ ;; and ).
+ (string-match "[^!#-'*-~]" term))
;; Requires escaping
(concat "\"" (replace-regexp-in-string "\"" "\"\"" term t t) "\"")
term)))
(replace-regexp-in-string
"[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str))
-;;
-
(defun notmuch-common-do-stash (text)
"Common function to stash text in kill ring, and display in minibuffer."
(if text
(kill-new "")
(message "Nothing to stash!")))
-;;
+;;; Generic Utilities
-(defun notmuch-remove-if-not (predicate list)
- "Return a copy of LIST with all items not satisfying PREDICATE removed."
- (let (out)
- (while list
- (when (funcall predicate (car list))
- (push (car list) out))
- (setq list (cdr list)))
- (nreverse out)))
+(defun notmuch-plist-delete (plist property)
+ (let (p)
+ (while plist
+ (unless (eq property (car plist))
+ (setq p (plist-put p (car plist) (cadr plist))))
+ (setq plist (cddr plist)))
+ p))
-(defun notmuch-split-content-type (content-type)
- "Split content/type into 'content' and 'type'"
- (split-string content-type "/"))
+;;; MML Utilities
(defun notmuch-match-content-type (t1 t2)
- "Return t if t1 and t2 are matching content types, taking wildcards into account"
- (let ((st1 (notmuch-split-content-type t1))
- (st2 (notmuch-split-content-type t2)))
- (if (or (string= (cadr st1) "*")
- (string= (cadr st2) "*"))
- ;; Comparison of content types should be case insensitive.
- (string= (downcase (car st1)) (downcase (car st2)))
- (string= (downcase t1) (downcase t2)))))
+ "Return t if t1 and t2 are matching content types.
+Take wildcards into account."
+ (and (stringp t1)
+ (stringp t2)
+ (let ((st1 (split-string t1 "/"))
+ (st2 (split-string t2 "/")))
+ (if (or (string= (cadr st1) "*")
+ (string= (cadr st2) "*"))
+ ;; Comparison of content types should be case insensitive.
+ (string= (downcase (car st1))
+ (downcase (car st2)))
+ (string= (downcase t1)
+ (downcase t2))))))
(defvar notmuch-multipart/alternative-discouraged
- '(
- ;; Avoid HTML parts.
+ '(;; Avoid HTML parts.
"text/html"
- ;; multipart/related usually contain a text/html part and some associated graphics.
- "multipart/related"
- ))
-
-(defun notmuch-multipart/alternative-choose (types)
- "Return a list of preferred types from the given list of types"
+ ;; multipart/related usually contain a text/html part and some
+ ;; associated graphics.
+ "multipart/related"))
+
+(defun notmuch-multipart/alternative-determine-discouraged (msg)
+ "Return the discouraged alternatives for the specified message."
+ ;; If a function, return the result of calling it.
+ (if (functionp notmuch-multipart/alternative-discouraged)
+ (funcall notmuch-multipart/alternative-discouraged msg)
+ ;; Otherwise simply return the value of the variable, which is
+ ;; assumed to be a list of discouraged alternatives. This is the
+ ;; default behaviour.
+ notmuch-multipart/alternative-discouraged))
+
+(defun notmuch-multipart/alternative-choose (msg types)
+ "Return a list of preferred types from the given list of types
+for this message, if present."
;; Based on `mm-preferred-alternative-precedence'.
- (let ((seq types))
- (dolist (pref (reverse notmuch-multipart/alternative-discouraged))
+ (let ((discouraged (notmuch-multipart/alternative-determine-discouraged msg))
+ (seq types))
+ (dolist (pref (reverse discouraged))
(dolist (elem (copy-sequence seq))
(when (string-match pref elem)
(setq seq (nconc (delete elem seq) (list elem))))))
(defun notmuch-parts-filter-by-type (parts type)
"Given a list of message parts, return a list containing the ones matching
the given type."
- (remove-if-not
+ (cl-remove-if-not
(lambda (part) (notmuch-match-content-type (plist-get part :content-type) type))
parts))
-;; Helper for parts which are generally not included in the default
-;; SEXP output.
-(defun notmuch-get-bodypart-internal (query part-number process-crypto)
- (let ((args '("show" "--format=raw"))
- (part-arg (format "--part=%s" part-number)))
- (setq args (append args (list part-arg)))
- (if process-crypto
- (setq args (append args '("--decrypt"))))
- (setq args (append args (list query)))
- (with-temp-buffer
- (let ((coding-system-for-read 'no-conversion))
- (progn
- (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
- (buffer-string))))))
-
-(defun notmuch-get-bodypart-content (msg part nth process-crypto)
- (or (plist-get part :content)
- (notmuch-get-bodypart-internal (notmuch-id-to-query (plist-get msg :id)) nth process-crypto)))
-
-;; Workaround: The call to `mm-display-part' below triggers a bug in
-;; Emacs 24 if it attempts to use the shr renderer to display an HTML
-;; part with images in it (demonstrated in 24.1 and 24.2 on Debian and
-;; Fedora 17, though unreproducable in other configurations).
-;; `mm-shr' references the variable `gnus-inhibit-images' without
-;; first loading gnus-art, which defines it, resulting in a
-;; void-variable error. Hence, we advise `mm-shr' to ensure gnus-art
-;; is loaded.
-(if (>= emacs-major-version 24)
- (defadvice mm-shr (before load-gnus-arts activate)
- (require 'gnus-art nil t)
- (ad-disable-advice 'mm-shr 'before 'load-gnus-arts)))
-
-(defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
+(defun notmuch--get-bodypart-raw (msg part process-crypto binaryp cache)
+ (let* ((plist-elem (if binaryp :content-binary :content))
+ (data (or (plist-get part plist-elem)
+ (with-temp-buffer
+ ;; Emacs internally uses a UTF-8-like multibyte string
+ ;; representation by default (regardless of the coding
+ ;; system, which only affects how it goes from outside data
+ ;; to this internal representation). This *almost* never
+ ;; matters. Annoyingly, it does matter if we use this data
+ ;; in an image descriptor, since Emacs will use its internal
+ ;; data buffer directly and this multibyte representation
+ ;; corrupts binary image formats. Since the caller is
+ ;; asking for binary data, a unibyte string is a more
+ ;; appropriate representation anyway.
+ (when binaryp
+ (set-buffer-multibyte nil))
+ (let ((args `("show" "--format=raw"
+ ,(format "--part=%s" (plist-get part :id))
+ ,@(and process-crypto '("--decrypt=true"))
+ ,(notmuch-id-to-query (plist-get msg :id))))
+ (coding-system-for-read
+ (if binaryp
+ 'no-conversion
+ (let ((coding-system
+ (mm-charset-to-coding-system
+ (plist-get part :content-charset))))
+ ;; Sadly,
+ ;; `mm-charset-to-coding-system' seems
+ ;; to return things that are not
+ ;; considered acceptable values for
+ ;; `coding-system-for-read'.
+ (if (coding-system-p coding-system)
+ coding-system
+ ;; RFC 2047 says that the default
+ ;; charset is US-ASCII. RFC6657
+ ;; complicates this somewhat.
+ 'us-ascii)))))
+ (apply #'call-process
+ notmuch-command nil '(t nil) nil args)
+ (buffer-string))))))
+ (when (and cache data)
+ (plist-put part plist-elem data))
+ data))
+
+(defun notmuch-get-bodypart-binary (msg part process-crypto &optional cache)
+ "Return the unprocessed content of PART in MSG as a unibyte string.
+
+This returns the \"raw\" content of the given part after content
+transfer decoding, but with no further processing (see the
+discussion of --format=raw in man notmuch-show). In particular,
+this does no charset conversion.
+
+If CACHE is non-nil, the content of this part will be saved in
+MSG (if it isn't already)."
+ (notmuch--get-bodypart-raw msg part process-crypto t cache))
+
+(defun notmuch-get-bodypart-text (msg part process-crypto &optional cache)
+ "Return the text content of PART in MSG.
+
+This returns the content of the given part as a multibyte Lisp
+string after performing content transfer decoding and any
+necessary charset decoding.
+
+If CACHE is non-nil, the content of this part will be saved in
+MSG (if it isn't already)."
+ (notmuch--get-bodypart-raw msg part process-crypto nil cache))
+
+(defun notmuch-mm-display-part-inline (msg part content-type process-crypto)
"Use the mm-decode/mm-view functions to display a part in the
current buffer, if possible."
(let ((display-buffer (current-buffer)))
(with-temp-buffer
- ;; In case there is :content, the content string is already converted
- ;; into emacs internal format. `gnus-decoded' is a fake charset,
- ;; which means no further decoding (to be done by mm- functions).
- (let* ((charset (if (plist-member part :content)
+ ;; In case we already have :content, use it and tell mm-* that
+ ;; it's already been charset-decoded by using the fake
+ ;; `gnus-decoded' charset. Otherwise, we'll fetch the binary
+ ;; part content and let mm-* decode it.
+ (let* ((have-content (plist-member part :content))
+ (charset (if have-content
'gnus-decoded
(plist-get part :content-charset)))
- (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset)))))
+ (handle (mm-make-handle (current-buffer)
+ `(,content-type (charset . ,charset)))))
;; If the user wants the part inlined, insert the content and
;; test whether we are able to inline it (which includes both
;; capability and suitability tests).
(when (mm-inlined-p handle)
- (insert (notmuch-get-bodypart-content msg part nth process-crypto))
+ (if have-content
+ (insert (notmuch-get-bodypart-text msg part process-crypto))
+ (insert (notmuch-get-bodypart-binary msg part process-crypto)))
(when (mm-inlinable-p handle)
(set-buffer display-buffer)
(mm-display-part handle)
t))))))
+;;; Generic Utilities
+
;; Converts a plist of headers to an alist of headers. The input plist should
;; have symbols of the form :Header as keys, and the resulting alist will have
;; symbols of the form 'Header as keys.
(defun notmuch-headers-plist-to-alist (plist)
- (loop for (key value . rest) on plist by #'cddr
- collect (cons (intern (substring (symbol-name key) 1)) value)))
+ (cl-loop for (key value . rest) on plist by #'cddr
+ collect (cons (intern (substring (symbol-name key) 1)) value)))
(defun notmuch-face-ensure-list-form (face)
"Return FACE in face list form.
face
(list face)))
-(defun notmuch-combine-face-text-property (start end face &optional below object)
- "Combine FACE into the 'face text property between START and END.
+(defun notmuch-apply-face (object face &optional below start end)
+ "Combine FACE into the 'face text property of OBJECT between START and END.
This function combines FACE with any existing faces between START
-and END in OBJECT (which defaults to the current buffer).
-Attributes specified by FACE take precedence over existing
-attributes unless BELOW is non-nil. FACE must be a face name (a
-symbol or string), a property list of face attributes, or a list
-of these. For convenience when applied to strings, this returns
-OBJECT."
-
+and END in OBJECT. Attributes specified by FACE take precedence
+over existing attributes unless BELOW is non-nil.
+
+OBJECT may be a string, a buffer, or nil (which means the current
+buffer). If object is a string, START and END are 0-based;
+otherwise they are buffer positions (integers or markers). FACE
+must be a face name (a symbol or string), a property list of face
+attributes, or a list of these. If START and/or END are omitted,
+they default to the beginning/end of OBJECT. For convenience
+when applied to strings, this returns OBJECT."
;; A face property can have three forms: a face name (a string or
;; symbol), a property list, or a list of these two forms. In the
;; list case, the faces will be combined, with the earlier faces
;; taking precedent. Here we canonicalize everything to list form
;; to make it easy to combine.
- (let ((pos start)
+ (let ((pos (cond (start start)
+ ((stringp object) 0)
+ (t 1)))
+ (end (cond (end end)
+ ((stringp object) (length object))
+ (t (1+ (buffer-size object)))))
(face-list (notmuch-face-ensure-list-form face)))
(while (< pos end)
(let* ((cur (get-text-property pos 'face object))
(setq pos next))))
object)
-(defun notmuch-combine-face-text-property-string (string face &optional below)
- (notmuch-combine-face-text-property
- 0
- (length string)
- face
- below
- string))
-
(defun notmuch-map-text-property (start end prop func &optional object)
"Transform text property PROP using FUNC.
(put-text-property start next prop (funcall func value) object)
(setq start next))))
+;;; Running Notmuch
+
(defun notmuch-logged-error (msg &optional extra)
"Log MSG and EXTRA to *Notmuch errors* and signal MSG.
signals MSG as an error. If EXTRA is non-nil, text referring the
user to the *Notmuch errors* buffer will be appended to the
signaled error. This function does not return."
-
(with-current-buffer (get-buffer-create "*Notmuch errors*")
(goto-char (point-max))
(unless (bobp)
(insert extra)
(unless (bolp)
(newline)))))
- (error "%s" (concat msg (when extra
- " (see *Notmuch errors* for more details)"))))
+ (error "%s%s" msg (if extra " (see *Notmuch errors* for more details)" "")))
-(defun notmuch-check-async-exit-status (proc msg &optional command err-file)
+(defun notmuch-check-async-exit-status (proc msg &optional command err)
"If PROC exited abnormally, pop up an error buffer and signal an error.
This is a wrapper around `notmuch-check-exit-status' for
asynchronous process sentinels. PROC and MSG must be the
-arguments passed to the sentinel. COMMAND and ERR-FILE, if
-provided, are passed to `notmuch-check-exit-status'. If COMMAND
-is not provided, it is taken from `process-command'."
+arguments passed to the sentinel. COMMAND and ERR, if provided,
+are passed to `notmuch-check-exit-status'. If COMMAND is not
+provided, it is taken from `process-command'."
(let ((exit-status
- (case (process-status proc)
+ (cl-case (process-status proc)
((exit) (process-exit-status proc))
((signal) msg))))
(when exit-status
- (notmuch-check-exit-status exit-status (or command (process-command proc))
- nil err-file))))
+ (notmuch-check-exit-status exit-status
+ (or command (process-command proc))
+ nil err))))
-(defun notmuch-check-exit-status (exit-status command &optional output err-file)
+(defun notmuch-check-exit-status (exit-status command &optional output err)
"If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
If EXIT-STATUS is non-zero, pop up a notmuch error buffer
string describing the signal that terminated the process (such as
returned by `call-process'). COMMAND must be a list giving the
command and its arguments. OUTPUT, if provided, is a string
-giving the output of command. ERR-FILE, if provided, is the name
-of a file containing the error output of command. OUTPUT and the
-contents of ERR-FILE will be included in the error message."
-
+giving the output of command. ERR, if provided, is the error
+output of command. OUTPUT and ERR will be included in the error
+message."
(cond
((eq exit-status 0) t)
((eq exit-status 20)
Emacs requested a newer output format than supported by the notmuch CLI.
You may need to restart Emacs or upgrade your notmuch package."))
(t
- (let* ((err (when err-file
- (with-temp-buffer
- (insert-file-contents err-file)
- (unless (eobp)
- (buffer-string)))))
- (extra
- (concat
- "command: " (mapconcat #'shell-quote-argument command " ") "\n"
- (if (integerp exit-status)
- (format "exit status: %s\n" exit-status)
- (format "exit signal: %s\n" exit-status))
- (when err
- (concat "stderr:\n" err))
- (when output
- (concat "stdout:\n" output)))))
- (if err
- ;; We have an error message straight from the CLI.
- (notmuch-logged-error
- (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra)
- ;; We only have combined output from the CLI; don't inundate
- ;; the user with it. Mimic `process-lines'.
- (notmuch-logged-error (format "%s exited with status %s"
- (car command) exit-status)
- extra))
- ;; `notmuch-logged-error' does not return.
- ))))
+ (pcase-let*
+ ((`(,command . ,args) command)
+ (command (if (equal (file-name-nondirectory command)
+ notmuch-command)
+ notmuch-command
+ command))
+ (command-string
+ (mapconcat (lambda (arg)
+ (shell-quote-argument
+ (cond ((stringp arg) arg)
+ ((symbolp arg) (symbol-name arg))
+ (t "*UNKNOWN ARGUMENT*"))))
+ (cons command args)
+ " "))
+ (extra
+ (concat "command: " command-string "\n"
+ (if (integerp exit-status)
+ (format "exit status: %s\n" exit-status)
+ (format "exit signal: %s\n" exit-status))
+ (and err (concat "stderr:\n" err))
+ (and output (concat "stdout:\n" output)))))
+ (if err
+ ;; We have an error message straight from the CLI.
+ (notmuch-logged-error
+ (replace-regexp-in-string "[ \n\r\t\f]*\\'" "" err) extra)
+ ;; We only have combined output from the CLI; don't inundate
+ ;; the user with it. Mimic `process-lines'.
+ (notmuch-logged-error (format "%s exited with status %s"
+ command exit-status)
+ extra))
+ ;; `notmuch-logged-error' does not return.
+ ))))
(defun notmuch-call-notmuch--helper (destination args)
"Helper for synchronous notmuch invocation commands.
This wraps `call-process'. DESTINATION has the same meaning as
for `call-process'. ARGS is as described for
`notmuch-call-notmuch-process'."
-
(let (stdin-string)
(while (keywordp (car args))
- (case (car args)
- (:stdin-string (setq stdin-string (cadr args)
- args (cddr args)))
+ (cl-case (car args)
+ (:stdin-string (setq stdin-string (cadr args))
+ (setq args (cddr args)))
(otherwise
(error "Unknown keyword argument: %s" (car args)))))
(if (null stdin-string)
Like `notmuch-call-notmuch-process', if notmuch exits with a
non-zero status, this will report its output and signal an
error."
-
(with-temp-buffer
(let ((err-file (make-temp-file "nmerr")))
(unwind-protect
- (let ((status (notmuch-call-notmuch--helper (list t err-file) args)))
+ (let ((status (notmuch-call-notmuch--helper (list t err-file) args))
+ (err (with-temp-buffer
+ (insert-file-contents err-file)
+ (unless (eobp)
+ (buffer-string)))))
(notmuch-check-exit-status status (cons notmuch-command args)
- (buffer-string) err-file)
+ (buffer-string) err)
(goto-char (point-min))
(read (current-buffer)))
(delete-file err-file)))))
invoke `set-process-sentinel' directly on the returned process,
as that will interfere with the handling of stderr and the exit
status."
-
- ;; There is no way (as of Emacs 24.3) to capture stdout and stderr
- ;; separately for asynchronous processes, or even to redirect stderr
- ;; to a file, so we use a trivial shell wrapper to send stderr to a
- ;; temporary file and clean things up in the sentinel.
- (let* ((err-file (make-temp-file "nmerr"))
- ;; Use a pipe
- (process-connection-type nil)
- ;; Find notmuch using Emacs' `exec-path'
- (command (or (executable-find notmuch-command)
- (error "command not found: %s" notmuch-command)))
- (proc (apply #'start-process name buffer
- "/bin/sh" "-c"
- "exec 2>\"$1\"; shift; exec \"$0\" \"$@\""
- command err-file args)))
- (process-put proc 'err-file err-file)
+ (let* ((command (or (executable-find notmuch-command)
+ (error "Command not found: %s" notmuch-command)))
+ (err-buffer (generate-new-buffer " *notmuch-stderr*"))
+ (proc (make-process
+ :name name
+ :buffer buffer
+ :command (cons command args)
+ :connection-type 'pipe
+ :stderr err-buffer))
+ (err-proc (get-buffer-process err-buffer)))
+ (process-put proc 'err-buffer err-buffer)
(process-put proc 'sub-sentinel sentinel)
- (process-put proc 'real-command (cons notmuch-command args))
(set-process-sentinel proc #'notmuch-start-notmuch-sentinel)
+ (set-process-sentinel err-proc #'notmuch-start-notmuch-error-sentinel)
proc))
(defun notmuch-start-notmuch-sentinel (proc event)
- (let ((err-file (process-get proc 'err-file))
- (sub-sentinel (process-get proc 'sub-sentinel))
- (real-command (process-get proc 'real-command)))
+ "Process sentinel function used by `notmuch-start-notmuch'."
+ (let* ((err-buffer (process-get proc 'err-buffer))
+ (err (and (buffer-live-p err-buffer)
+ (not (zerop (buffer-size err-buffer)))
+ (with-current-buffer err-buffer (buffer-string))))
+ (sub-sentinel (process-get proc 'sub-sentinel)))
(condition-case err
(progn
;; Invoke the sub-sentinel, if any
;; and there's no point in telling the user that (but we
;; still check for and report stderr output below).
(when (buffer-live-p (process-buffer proc))
- (notmuch-check-async-exit-status proc event real-command err-file))
+ (notmuch-check-async-exit-status proc event nil err))
;; If that didn't signal an error, then any error output was
;; really warning output. Show warnings, if any.
(let ((warnings
- (with-temp-buffer
- (unless (= (second (insert-file-contents err-file)) 0)
- (end-of-line)
- ;; Show first line; stuff remaining lines in the
- ;; errors buffer.
- (let ((l1 (buffer-substring (point-min) (point))))
- (skip-chars-forward "\n")
- (cons l1 (unless (eobp)
- (buffer-substring (point) (point-max)))))))))
+ (and err
+ (with-current-buffer err-buffer
+ (goto-char (point-min))
+ (end-of-line)
+ ;; Show first line; stuff remaining lines in the
+ ;; errors buffer.
+ (let ((l1 (buffer-substring (point-min) (point))))
+ (skip-chars-forward "\n")
+ (cons l1 (and (not (eobp))
+ (buffer-substring (point)
+ (point-max)))))))))
(when warnings
(notmuch-logged-error (car warnings) (cdr warnings)))))
(error
;; Emacs behaves strangely if an error escapes from a sentinel,
;; so turn errors into messages.
- (message "%s" (error-message-string err))))
- (ignore-errors (delete-file err-file))))
+ (message "%s" (error-message-string err))))))
+
+(defun notmuch-start-notmuch-error-sentinel (proc _event)
+ (unless (process-live-p proc)
+ (let ((buffer (process-buffer proc)))
+ (when (buffer-live-p buffer)
+ (kill-buffer buffer)))))
+
+(defvar-local notmuch-show-process-crypto nil)
+
+;;; Generic Utilities
+
+(defun notmuch-interactive-region ()
+ "Return the bounds of the current interactive region.
+
+This returns (BEG END), where BEG and END are the bounds of the
+region if the region is active, or both `point' otherwise."
+ (if (region-active-p)
+ (list (region-beginning) (region-end))
+ (list (point) (point))))
+
+(define-obsolete-function-alias
+ 'notmuch-search-interactive-region
+ 'notmuch-interactive-region
+ "notmuch 0.29")
-;; This variable is used only buffer local, but it needs to be
-;; declared globally first to avoid compiler warnings.
-(defvar notmuch-show-process-crypto nil)
-(make-variable-buffer-local 'notmuch-show-process-crypto)
+;;; _
(provide 'notmuch-lib)
-;; Local Variables:
-;; byte-compile-warnings: (not cl-functions)
-;; End:
+;;; notmuch-lib.el ends here