X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=emacs%2Fnotmuch-lib.el;h=8acad267fc50eed0031be07f635ef69b95c9e92e;hp=2be409b39a1abe1fee91b514ce9f96347eb15644;hb=HEAD;hpb=c734dd75344ea0d1701969a8ecb9ff00d2bd5531 diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el index 2be409b3..bf9c4a53 100644 --- a/emacs/notmuch-lib.el +++ b/emacs/notmuch-lib.el @@ -1,4 +1,4 @@ -;; notmuch-lib.el --- common variables, functions and function declarations +;;; notmuch-lib.el --- common variables, functions and function declarations -*- lexical-binding: t -*- ;; ;; Copyright © Carl Worth ;; @@ -15,18 +15,27 @@ ;; General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License -;; along with Notmuch. If not, see . +;; along with Notmuch. If not, see . ;; ;; Authors: Carl Worth -;; 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." @@ -46,9 +55,12 @@ (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." @@ -62,10 +74,26 @@ "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. @@ -75,9 +103,24 @@ search results. Note that any filtered searches created by search." :type 'boolean :group 'notmuch-search) +(make-variable-buffer-local 'notmuch-search-oldest-first) + +(defcustom notmuch-search-hide-excluded t + "Hide mail tagged with a excluded tag. + +Excluded tags are defined in the users configuration file under +the search section. When this variable is true, any mail with +such a tag will not be shown in the search output." + :type 'boolean + :group 'notmuch-search) +(make-variable-buffer-local 'notmuch-search-hide-excluded) (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') @@ -93,26 +136,12 @@ 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")) :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. @@ -127,15 +156,27 @@ For example, if you wanted to remove an \"inbox\" tag and add an :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) + (define-key map [remap undo] 'notmuch-tag-undo) map) "Keymap shared by all notmuch modes.") @@ -154,6 +195,8 @@ For example, if you wanted to remove an \"inbox\" tag and add an (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. @@ -161,15 +204,33 @@ 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. -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 #'notmuch--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 (notmuch--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))) @@ -178,10 +239,33 @@ Otherwise the output will be returned" (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." @@ -197,7 +281,12 @@ 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))) + +;;; Commands (defun notmuch-poll () "Run \"notmuch new\" or an external script to import mail. @@ -205,27 +294,25 @@ Otherwise the output will be returned" 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 (notmuch--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. @@ -237,12 +324,11 @@ This is basically just `format-kbd-macro' but we also convert ESC to M-." "M-" (concat desc " ")))) - (defun notmuch-describe-key (actual-key binding prefix ua-keys tail) - "Prepend cons cells describing prefix-arg ACTUAL-KEY and ACTUAL-KEY to 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)))) + (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. @@ -256,10 +342,15 @@ It does not prepend if ACTUAL-KEY is already listed in TAIL." tail))) ;; Documentation for command (push (cons key-string - (or (and (symbolp binding) (get binding 'notmuch-doc)) - (notmuch-documentation-first-line binding))) + (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) + tail) (defun notmuch-describe-remaps (remap-keymap ua-keys base-keymap prefix tail) ;; Remappings are represented as a binding whose first "event" is @@ -267,13 +358,13 @@ It does not prepend if ACTUAL-KEY is already listed in TAIL." ;; 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) + (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) @@ -296,9 +387,13 @@ prefix argument. PREFIX and TAIL are used internally." (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 ua-keys base-keymap + (notmuch-prefix-key-description key) + tail)))) (binding - (setq tail (notmuch-describe-key (vector key) binding prefix ua-keys tail))))) + (setq tail + (notmuch-describe-key (vector key) + binding prefix ua-keys tail))))) keymap) tail) @@ -308,11 +403,15 @@ prefix argument. PREFIX and TAIL are used internally." (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg) (let ((desc (save-match-data - (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1))) + (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))) + (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))) @@ -325,13 +424,14 @@ This is similar to `describe-function' for the current major mode, but bindings tables are shown with documentation strings rather than command names. By default, this uses the first line of each command's documentation string. A command can override -this by setting the 'notmuch-doc property of its command symbol. +this by setting the \\='notmuch-doc property of its command symbol. A command that supports a prefix argument can explicitly document -its prefixed behavior by setting the 'notmuch-prefix-doc property +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)) @@ -342,17 +442,18 @@ of its command symbol." "Show help for a subkeymap." (interactive) (let* ((key (this-command-keys-vector)) - (prefix (make-vector (1- (length key)) nil)) - (i 0)) + (prefix (make-vector (1- (length key)) nil)) + (i 0)) (while (< i (length prefix)) (aset prefix i (aref key i)) - (setq i (1+ 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-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 @@ -360,18 +461,17 @@ of its command symbol." (insert desc))) (pop-to-buffer (help-buffer))))) -(defvar notmuch-buffer-refresh-function nil +;;; 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." @@ -379,9 +479,26 @@ 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)))))) + +;;; 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)) @@ -400,10 +517,12 @@ This includes newlines, tabs, and other funny characters." 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))) @@ -420,8 +539,6 @@ This replaces spaces, percents, and double quotes in STR with (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 @@ -433,44 +550,65 @@ This replaces spaces, percents, and double quotes in STR with (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))))) - -(defvar notmuch-multipart/alternative-discouraged - '( - ;; Avoid HTML parts. + "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)))))) + +(defcustom notmuch-multipart/alternative-discouraged + '(;; 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") + "Which mime types to hide by default for multipart messages. + +Can either be a list of mime types (as strings) or a function +mapping a plist representing the current message to such a list. +See Info node `(notmuch-emacs) notmuch-show' for a sample function." + :group 'notmuch-show + :type '(radio (repeat :tag "MIME Types" string) + (function :tag "Function"))) + +(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)))))) @@ -479,70 +617,113 @@ This replaces spaces, percents, and double quotes in STR with (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 #'notmuch--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) + (plist-put part :undisplayer (mm-handle-undisplayer 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. @@ -554,23 +735,31 @@ 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)) @@ -583,14 +772,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. @@ -603,6 +784,8 @@ returned by 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. @@ -610,7 +793,6 @@ This logs MSG and EXTRA to the *Notmuch errors* buffer and 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) @@ -623,26 +805,26 @@ signaled error. This function does not return." (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 @@ -651,10 +833,9 @@ be a number indicating the exit status code of a process or a 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) @@ -666,32 +847,64 @@ You may need to restart Emacs or upgrade your notmuch Emacs package.")) 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. + )))) + +(defmacro notmuch--apply-with-env (func &rest args) + `(let ((default-directory "~")) + (apply ,func ,@args))) + +(defun notmuch--process-lines (program &rest args) + "Wrap process-lines, binding DEFAULT-DIRECTORY to a safe +default" + (notmuch--apply-with-env #'process-lines program args)) + +(defun notmuch--make-process (&rest args) + "Wrap make-process, binding DEFAULT-DIRECTORY to a safe +default" + (notmuch--apply-with-env #'make-process args)) + +(defun notmuch--call-process-region (start end program + &optional delete buffer display + &rest args) + "Wrap call-process-region, binding DEFAULT-DIRECTORY to a safe +default" + (notmuch--apply-with-env + #'call-process-region start end program delete buffer display args)) + +(defun notmuch--call-process (program &optional infile destination display &rest args) + "Wrap call-process, binding DEFAULT-DIRECTORY to a safe default" + (notmuch--apply-with-env #'call-process program infile destination display args)) (defun notmuch-call-notmuch--helper (destination args) "Helper for synchronous notmuch invocation commands. @@ -699,18 +912,17 @@ You may need to restart Emacs or upgrade your notmuch package.")) 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) - (apply #'call-process notmuch-command nil destination nil args) + (apply #'notmuch--call-process notmuch-command nil destination nil args) (insert stdin-string) - (apply #'call-process-region (point-min) (point-max) + (apply #'notmuch--call-process-region (point-min) (point-max) notmuch-command t destination nil args)))) (defun notmuch-call-notmuch-process (&rest args) @@ -737,13 +949,16 @@ 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 (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))))) @@ -761,31 +976,29 @@ when the process exits, or nil for none. The caller must *not* 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 (notmuch--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 @@ -797,34 +1010,76 @@ status." ;; 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)))) - -;; 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) + (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) + +(defun notmuch--run-show (search-terms &optional duplicate) + "Return a list of threads of messages matching SEARCH-TERMS. + +A thread is a forest or list of trees. A tree is a two element +list where the first element is a message, and the second element +is a possibly empty forest of replies." + (let ((args '("show" "--format=sexp" "--format-version=5"))) + (when notmuch-show-process-crypto + (setq args (append args '("--decrypt=true")))) + (when duplicate + (setq args (append args (list (format "--duplicate=%d" duplicate))))) + (setq args (append args search-terms)) + (apply #'notmuch-call-notmuch-sexp args))) + +;;; 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") + +(defun notmuch--inline-override-types () + "Override mm-inline-override-types to stop application/* +parts from being displayed unless the user has customized +it themselves." + (if (equal mm-inline-override-types + (eval (car (get 'mm-inline-override-types 'standard-value)))) + (cons "application/.*" mm-inline-override-types) + mm-inline-override-types)) +;;; _ (provide 'notmuch-lib) -;; Local Variables: -;; byte-compile-warnings: (not cl-functions) -;; End: +;;; notmuch-lib.el ends here