-;; 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)
-(defvar notmuch-command "notmuch"
- "Command to run the notmuch binary.")
+(unless (require 'notmuch-version nil t)
+ (defconst notmuch-emacs-version "unknown"
+ "Placeholder variable when notmuch-version.el[c] is not available."))
+
+(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.
: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-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"))
(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.
(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 "G" 'notmuch-poll-and-refresh-this-buffer)
+ (define-key map "j" 'notmuch-jump-search)
map)
"Keymap shared by all notmuch modes.")
(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)))
(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)))
(defun notmuch-poll ()
"Run \"notmuch new\" or an external script to import mail.
(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-kill-this-buffer ()
- "Kill the current buffer."
+(defun notmuch-bury-or-kill-this-buffer ()
+ "Undisplay 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."
"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 " "))))
(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)
"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."
(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)))
(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 "/"))
"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))))))
(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
(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)
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.
(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))
;; Local Variables:
;; byte-compile-warnings: (not cl-functions)
;; End:
+
+;;; notmuch-lib.el ends here