]> git.notmuchmail.org Git - notmuch/blobdiff - emacs/notmuch-lib.el
Merge branch 'release'
[notmuch] / emacs / notmuch-lib.el
index e7c5c97144dc666787006cd4238ab89a99cbd588..5dc6797068c876619bc36f57cd2ffa4c9ae5e9ab 100644 (file)
@@ -1,4 +1,4 @@
-;; notmuch-lib.el --- common variables, functions and function declarations
+;;; notmuch-lib.el --- common variables, functions and function declarations
 ;;
 ;; 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 'mm-util)
 (require 'mm-view)
 (require 'mm-decode)
 (require 'cl)
+(require 'notmuch-compat)
+
+(unless (require 'notmuch-version nil t)
+  (defconst notmuch-emacs-version "unknown"
+    "Placeholder variable when notmuch-version.el[c] is not available."))
 
-(defvar notmuch-command "notmuch"
-  "Command to run the notmuch binary.")
+(autoload 'notmuch-jump-search "notmuch-jump"
+  "Jump to a saved search by shortcut key." t)
 
 (defgroup notmuch nil
   "Notmuch mail reader for Emacs."
 
 (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."
   :group 'notmuch)
   "Graphical attributes for displaying text"
   :group 'notmuch)
 
+(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.
 
@@ -77,12 +99,15 @@ search."
   :group 'notmuch-search)
 
 (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-search-poll-and-refresh-view' and
-`notmuch-hello-poll-and-update' (each have a default keybinding
-of 'G') to incorporate new mail into the notmuch database.
+`notmuch-poll-and-refresh-this-buffer' (bound by default to 'G')
+to incorporate new mail into the notmuch database.
 
 If set to nil (the default), new mail is processed by invoking
 \"notmuch new\". Otherwise, this should be set to a string that
@@ -94,10 +119,7 @@ the user's needs:
 
 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"))
@@ -108,12 +130,6 @@ Note that the recommended way of achieving the same is using
 (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.
 
@@ -131,11 +147,14 @@ For example, if you wanted to remove an \"inbox\" tag and add an
 (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 "q" 'notmuch-bury-or-kill-this-buffer)
     (define-key map "s" 'notmuch-search)
+    (define-key map "z" 'notmuch-tree)
     (define-key map "m" 'notmuch-mua-new-mail)
     (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.")
 
@@ -168,8 +187,26 @@ Otherwise the output will be returned"
       (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)))
@@ -180,8 +217,13 @@ Otherwise the output will be returned"
 
 (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."
@@ -197,7 +239,10 @@ Otherwise the output will be returned"
 
 (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)))
 
 (defun notmuch-poll ()
   "Run \"notmuch new\" or an external script to import mail.
@@ -207,13 +252,19 @@ depending on the value of `notmuch-poll-script'."
   (interactive)
   (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 (equal (call-process notmuch-poll-script nil nil) 0)
+         (error "Notmuch: poll script `%s' failed!" notmuch-poll-script)))
+    (notmuch-call-notmuch-process "new")))
+
+(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)))
+  (if (> (length (get-buffer-window-list nil nil t)) 1)
+      (bury-buffer)
+    (kill-buffer)))
 
 (defun notmuch-documentation-first-line (symbol)
   "Return the first line of the documentation string for SYMBOL."
@@ -231,17 +282,58 @@ depending on the value of `notmuch-poll-script'."
   "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.
 
-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.
+(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 (format-kbd-macro 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))
+                     (notmuch-documentation-first-line binding)))
+           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 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
@@ -251,21 +343,13 @@ prefix argument.  PREFIX and TAIL are used internally."
      (cond ((mouse-event-p key) nil)
           ((keymapp binding)
            (setq tail
-                 (notmuch-describe-keymap
-                  binding ua-keys (notmuch-prefix-key-description key) tail)))
-          (t
-           (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))))
+                 (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
+           (setq tail (notmuch-describe-key (vector key) binding prefix ua-keys tail)))))
    keymap)
   tail)
 
@@ -273,11 +357,14 @@ prefix argument.  PREFIX and TAIL are used internally."
   "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))
@@ -302,6 +389,28 @@ of its command symbol."
       (set-buffer-modified-p nil)
       (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
 
+(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))
+      (setq i (1+ 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)))))
+
 (defvar notmuch-buffer-refresh-function nil
   "Function to call to refresh the current buffer.")
 (make-variable-buffer-local 'notmuch-buffer-refresh-function)
@@ -310,10 +419,8 @@ of its command symbol."
   "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."
@@ -321,6 +428,21 @@ of its command symbol."
   (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))))))
+
 (defun notmuch-prettify-subject (subject)
   ;; This function is used by `notmuch-search-process-filter' which
   ;; requires that we not disrupt its' matching state.
@@ -345,7 +467,10 @@ 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)))
@@ -354,6 +479,14 @@ user-friendly queries."
   "Return a query that matches the message with id ID."
   (concat "id:" (notmuch-escape-boolean-term id)))
 
+(defun notmuch-hex-encode (str)
+  "Hex-encode STR (e.g., as used by batch tagging).
+
+This replaces spaces, percents, and double quotes in STR with
+%NN where NN is the hexadecimal value of the character."
+  (replace-regexp-in-string
+   "[ %\"]" (lambda (match) (format "%%%02x" (aref match 0))) str))
+
 ;;
 
 (defun notmuch-common-do-stash (text)
@@ -378,6 +511,15 @@ user-friendly queries."
       (setq list (cdr list)))
     (nreverse out)))
 
+(defun notmuch-plist-delete (plist property)
+  (let* ((xplist (cons nil plist))
+        (pred xplist))
+    (while (cdr pred)
+      (when (eq (cadr pred) property)
+       (setcdr pred (cdddr pred)))
+      (setq pred (cddr pred)))
+    (cdr xplist)))
+
 (defun notmuch-split-content-type (content-type)
   "Split content/type into 'content' and 'type'"
   (split-string content-type "/"))
@@ -400,11 +542,23 @@ user-friendly queries."
     "multipart/related"
     ))
 
-(defun notmuch-multipart/alternative-choose (types)
-  "Return a list of preferred types from the given list of types"
+(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))))))
@@ -417,24 +571,69 @@ the given type."
    (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)))
+(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))
+                                  ,@(when process-crypto '("--decrypt"))
+                                  ,(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))
 
 ;; 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
@@ -447,25 +646,29 @@ the given type."
 (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)))
+      (ad-disable-advice 'mm-shr 'before 'load-gnus-arts)
+      (ad-activate 'mm-shr)))
 
-(defun notmuch-mm-display-part-inline (msg part nth content-type process-crypto)
+(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)
-                         'gnus-decoded
+      ;; 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)))))
        ;; 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)
@@ -488,23 +691,32 @@ single element face list."
       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))
@@ -517,14 +729,6 @@ 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.
 
@@ -605,9 +809,15 @@ You may need to restart Emacs or upgrade your notmuch package."))
                    (insert-file-contents err-file)
                    (unless (eobp)
                      (buffer-string)))))
+          (command-string
+           (mapconcat (lambda (arg)
+                        (shell-quote-argument
+                         (cond ((stringp arg) arg)
+                               ((symbolp arg) (symbol-name arg))
+                               (t "*UNKNOWN ARGUMENT*"))))
+                      command " "))
           (extra
-           (concat
-            "command: " (mapconcat #'shell-quote-argument command " ") "\n"
+           (concat "command: " command-string "\n"
             (if (integerp exit-status)
                 (format "exit status: %s\n" exit-status)
               (format "exit signal: %s\n" exit-status))
@@ -627,17 +837,55 @@ You may need to restart Emacs or upgrade your notmuch package."))
        ;; `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)))
+       (otherwise
+        (error "Unknown keyword argument: %s" (car args)))))
+    (if (null stdin-string)
+       (apply #'call-process notmuch-command nil destination nil args)
+      (insert stdin-string)
+      (apply #'call-process-region (point-min) (point-max)
+            notmuch-command t destination nil args))))
+
+(defun notmuch-call-notmuch-process (&rest args)
+  "Synchronously invoke `notmuch-command' with ARGS.
+
+The caller may provide keyword arguments before ARGS.  Currently
+supported keyword arguments are:
+
+  :stdin-string STRING - Write STRING to stdin
+
+If notmuch exits with a non-zero status, output from the process
+will appear in a buffer named \"*Notmuch errors*\" and an error
+will be signaled."
+  (with-temp-buffer
+    (let ((status (notmuch-call-notmuch--helper t args)))
+      (notmuch-check-exit-status status (cons notmuch-command args)
+                                (buffer-string)))))
+
 (defun notmuch-call-notmuch-sexp (&rest args)
   "Invoke `notmuch-command' with ARGS and return the parsed S-exp output.
 
-If notmuch exits with a non-zero status, this will pop up a
-buffer containing notmuch's output and signal an error."
+This is equivalent to `notmuch-call-notmuch-process', but parses
+notmuch's output as an S-expression and returns the parsed value.
+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 (apply #'call-process
-                              notmuch-command nil (list t err-file) nil args)))
+         (let ((status (notmuch-call-notmuch--helper (list t err-file) args)))
            (notmuch-check-exit-status status (cons notmuch-command args)
                                       (buffer-string) err-file)
            (goto-char (point-min))
@@ -724,3 +972,5 @@ status."
 ;; Local Variables:
 ;; byte-compile-warnings: (not cl-functions)
 ;; End:
+
+;;; notmuch-lib.el ends here