X-Git-Url: https://git.notmuchmail.org/git?a=blobdiff_plain;f=emacs%2Fnotmuch-show.el;h=b0f2d28bd6f02057f6e524d81fdcde5d6b46f864;hb=42781f18214a4adf277c4bf52dd191ccf74e1b3b;hp=1ac80cac9cba3559a002da14c78b4a6835047ff3;hpb=47b9314eeecc7ae6c97a5933c8fe028fb6d2b410;p=notmuch
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
index 1ac80cac..b0f2d28b 100644
--- a/emacs/notmuch-show.el
+++ b/emacs/notmuch-show.el
@@ -1,4 +1,4 @@
-;; notmuch-show.el --- displaying notmuch forests.
+;;; notmuch-show.el --- displaying notmuch forests
;;
;; Copyright © Carl Worth
;; Copyright © David Edmondson
@@ -16,12 +16,17 @@
;; 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
;; David Edmondson
-(eval-when-compile (require 'cl))
+;;; Code:
+
+(eval-when-compile
+ (require 'cl-lib)
+ (require 'pcase))
+
(require 'mm-view)
(require 'message)
(require 'mm-decode)
@@ -36,6 +41,7 @@
(require 'notmuch-mua)
(require 'notmuch-crypto)
(require 'notmuch-print)
+(require 'notmuch-draft)
(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
(declare-function notmuch-search-next-thread "notmuch" nil)
@@ -45,7 +51,13 @@
(declare-function notmuch-count-attachments "notmuch" (mm-handle))
(declare-function notmuch-save-attachments "notmuch" (mm-handle &optional queryp))
(declare-function notmuch-tree "notmuch-tree"
+ (&optional query query-context target buffer-name
+ open-target unthreaded))
+(declare-function notmuch-tree-get-message-properties "notmuch-tree" nil)
+(declare-function notmuch-unthreaded
(&optional query query-context target buffer-name open-target))
+(declare-function notmuch-read-query "notmuch" (prompt))
+(declare-function notmuch-draft-resume "notmuch-draft" (id))
(defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
"Headers that should be shown in a message, in this order.
@@ -84,10 +96,11 @@ visible for any given message."
:group 'notmuch-show
:group 'notmuch-hooks)
-(defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-wrap-long-lines
- notmuch-wash-tidy-citations
- notmuch-wash-elide-blank-lines
- notmuch-wash-excerpt-citations)
+(defcustom notmuch-show-insert-text/plain-hook
+ '(notmuch-wash-wrap-long-lines
+ notmuch-wash-tidy-citations
+ notmuch-wash-elide-blank-lines
+ notmuch-wash-excerpt-citations)
"Functions used to improve the display of text/plain parts."
:type 'hook
:options '(notmuch-wash-convert-inline-patch-to-part
@@ -98,6 +111,13 @@ visible for any given message."
:group 'notmuch-show
:group 'notmuch-hooks)
+(defcustom notmuch-show-max-text-part-size 100000
+ "Maximum size of a text part to be shown by default in characters.
+
+Set to 0 to show the part regardless of size."
+ :type 'integer
+ :group 'notmuch-show)
+
;; Mostly useful for debugging.
(defcustom notmuch-show-all-multipart/alternative-parts nil
"Should all parts of multipart/alternative parts be shown?"
@@ -135,32 +155,33 @@ indentation."
:type 'boolean
:group 'notmuch-show)
+;; By default, block all external images to prevent privacy leaks and
+;; potential attacks.
+(defcustom notmuch-show-text/html-blocked-images "."
+ "Remote images that have URLs matching this regexp will be blocked."
+ :type '(choice (const nil) regexp)
+ :group 'notmuch-show)
+
(defvar notmuch-show-thread-id nil)
(make-variable-buffer-local 'notmuch-show-thread-id)
-(put 'notmuch-show-thread-id 'permanent-local t)
(defvar notmuch-show-parent-buffer nil)
(make-variable-buffer-local 'notmuch-show-parent-buffer)
-(put 'notmuch-show-parent-buffer 'permanent-local t)
(defvar notmuch-show-query-context nil)
(make-variable-buffer-local 'notmuch-show-query-context)
-(put 'notmuch-show-query-context 'permanent-local t)
(defvar notmuch-show-process-crypto nil)
(make-variable-buffer-local 'notmuch-show-process-crypto)
-(put 'notmuch-show-process-crypto 'permanent-local t)
(defvar notmuch-show-elide-non-matching-messages nil)
(make-variable-buffer-local 'notmuch-show-elide-non-matching-messages)
-(put 'notmuch-show-elide-non-matching-messages 'permanent-local t)
(defvar notmuch-show-indent-content t)
(make-variable-buffer-local 'notmuch-show-indent-content)
-(put 'notmuch-show-indent-content 'permanent-local t)
(defvar notmuch-show-attachment-debug nil
- "If t log stdout and stderr from attachment handlers
+ "If t log stdout and stderr from attachment handlers.
When set to nil (the default) stdout and stderr from attachment
handlers is discarded. When set to t the stdout and stderr from
@@ -169,10 +190,10 @@ each attachment handler is logged in buffers with names beginning
24.3 to work.")
(defcustom notmuch-show-stash-mlarchive-link-alist
- '(("Gmane" . "http://mid.gmane.org/")
- ("MARC" . "http://marc.info/?i=")
- ("Mail Archive, The" . "http://mail-archive.com/search?l=mid&q=")
- ("LKML" . "http://lkml.kernel.org/r/")
+ '(("Gmane" . "https://mid.gmane.org/")
+ ("MARC" . "https://marc.info/?i=")
+ ("Mail Archive, The" . "https://mid.mail-archive.com/")
+ ("LKML" . "https://lkml.kernel.org/r/")
;; FIXME: can these services be searched by `Message-Id' ?
;; ("MarkMail" . "http://markmail.org/")
;; ("Nabble" . "http://nabble.com/")
@@ -180,10 +201,21 @@ each attachment handler is logged in buffers with names beginning
)
"List of Mailing List Archives to use when stashing links.
-These URIs are concatenated with the current message's
-Message-Id in `notmuch-show-stash-mlarchive-link'."
+This list is used for generating a Mailing List Archive reference
+URI with the current message's Message-Id in
+`notmuch-show-stash-mlarchive-link'.
+
+If the cdr of the alist element is not a function, the cdr is
+expected to contain a URI that is concatenated with the current
+message's Message-Id to create a ML archive reference URI.
+
+If the cdr is a function, the function is called with the
+Message-Id as the argument, and the function is expected to
+return the ML archive reference URI."
:type '(alist :key-type (string :tag "Name")
- :value-type (string :tag "URL"))
+ :value-type (choice
+ (string :tag "URL")
+ (function :tag "Function returning the URL")))
:group 'notmuch-show)
(defcustom notmuch-show-stash-mlarchive-link-default "Gmane"
@@ -211,17 +243,40 @@ For example, if you wanted to remove an \"unread\" tag and add a
:type '(repeat string)
:group 'notmuch-show)
+(defcustom notmuch-show-mark-read-function #'notmuch-show-seen-current-message
+ "Function to control which messages are marked read.
+
+The function should take two arguments START and END which will
+be the start and end of the visible portion of the buffer and
+should mark the appropriate messages read by applying
+`notmuch-show-mark-read'. This function will be called after
+every user interaction with notmuch."
+ :type 'function
+ :group 'notmuch-show)
+
+(defcustom notmuch-show-imenu-indent nil
+ "Should Imenu display messages indented.
+
+By default, Imenu (see Info node `(emacs) Imenu') in a
+notmuch-show buffer displays all messages straight. This is
+because the default Emacs frontend for Imenu makes it difficult
+to select an Imenu entry with spaces in front. Other imenu
+frontends such as counsel-imenu does not have this limitation.
+In these cases, Imenu entries can be indented to reflect the
+position of the message in the thread."
+ :type 'boolean
+ :group 'notmuch-show)
(defmacro with-current-notmuch-show-message (&rest body)
- "Evaluate body with current buffer set to the text of current message"
+ "Evaluate body with current buffer set to the text of current message."
`(save-excursion
(let ((id (notmuch-show-get-message-id)))
(let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
- (with-current-buffer buf
+ (with-current-buffer buf
(let ((coding-system-for-read 'no-conversion))
- (call-process notmuch-command nil t nil "show" "--format=raw" id)
- ,@body)
- (kill-buffer buf))))))
+ (call-process notmuch-command nil t nil "show" "--format=raw" id))
+ ,@body)
+ (kill-buffer buf)))))
(defun notmuch-show-turn-on-visual-line-mode ()
"Enable Visual Line mode."
@@ -242,13 +297,12 @@ For example, if you wanted to remove an \"unread\" tag and add a
;;
;; Any MIME part not explicitly mentioned here will be handled by an
;; external viewer as configured in the various mailcap files.
- (let ((mm-inline-media-tests '(
- ("text/.*" ignore identity)
- ("application/pgp-signature" ignore identity)
- ("multipart/alternative" ignore identity)
- ("multipart/mixed" ignore identity)
- ("multipart/related" ignore identity)
- )))
+ (let ((mm-inline-media-tests
+ '(("text/.*" ignore identity)
+ ("application/pgp-signature" ignore identity)
+ ("multipart/alternative" ignore identity)
+ ("multipart/mixed" ignore identity)
+ ("multipart/related" ignore identity))))
(mm-display-parts (mm-dissect-buffer)))))
(defun notmuch-show-save-attachments ()
@@ -265,7 +319,6 @@ For example, if you wanted to remove an \"unread\" tag and add a
FN is called with one argument, the message properties. It should
operation on the contents of the current buffer."
-
;; Remake the header to ensure that all information is available.
(let* ((to (notmuch-show-get-to))
(cc (notmuch-show-get-cc))
@@ -274,7 +327,6 @@ operation on the contents of the current buffer."
(date (notmuch-show-get-date))
(tags (notmuch-show-get-tags))
(depth (notmuch-show-get-depth))
-
(header (concat
"Subject: " subject "\n"
"To: " to "\n"
@@ -294,8 +346,10 @@ operation on the contents of the current buffer."
(indenting notmuch-show-indent-content))
(with-temp-buffer
(insert all)
- (if indenting
- (indent-rigidly (point-min) (point-max) (- depth)))
+ (when indenting
+ (indent-rigidly (point-min)
+ (point-max)
+ (- (* notmuch-show-indent-messages-width depth))))
;; Remove the original header.
(goto-char (point-min))
(re-search-forward "^$" (point-max) nil)
@@ -316,11 +370,8 @@ operation on the contents of the current buffer."
'message-header-cc)
((looking-at "[Ss]ubject:")
'message-header-subject)
- ((looking-at "[Ff]rom:")
- 'message-header-from)
(t
'message-header-other))))
-
(overlay-put (make-overlay (point) (re-search-forward ":"))
'face 'message-header-name)
(overlay-put (make-overlay (point) (re-search-forward ".*$"))
@@ -341,69 +392,62 @@ operation on the contents of the current buffer."
"Update the displayed tags of the current message."
(save-excursion
(goto-char (notmuch-show-message-top))
- (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
- (let ((inhibit-read-only t))
- (replace-match (concat "("
- (notmuch-tag-format-tags tags)
- ")"))))))
+ (when (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
+ (let ((inhibit-read-only t))
+ (replace-match (concat "("
+ (notmuch-tag-format-tags
+ tags
+ (notmuch-show-get-prop :orig-tags))
+ ")"))))))
(defun notmuch-clean-address (address)
"Try to clean a single email ADDRESS for display. Return a cons
cell of (AUTHOR_EMAIL AUTHOR_NAME). Return (ADDRESS nil) if
parsing fails."
(condition-case nil
- (let (p-name p-address)
- ;; It would be convenient to use `mail-header-parse-address',
- ;; but that expects un-decoded mailbox parts, whereas our
- ;; mailbox parts are already decoded (and hence may contain
- ;; UTF-8). Given that notmuch should handle most of the awkward
- ;; cases, some simple string deconstruction should be sufficient
- ;; here.
- (cond
- ;; "User " style.
- ((string-match "\\(.*\\) <\\(.*\\)>" address)
- (setq p-name (match-string 1 address)
- p-address (match-string 2 address)))
-
- ;; "" style.
- ((string-match "<\\(.*\\)>" address)
- (setq p-address (match-string 1 address)))
-
- ;; Everything else.
- (t
- (setq p-address address)))
-
- (when p-name
- ;; Remove elements of the mailbox part that are not relevant for
- ;; display, even if they are required during transport:
- ;;
- ;; Backslashes.
- (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
-
- ;; Outer single and double quotes, which might be nested.
- (loop
- with start-of-loop
- do (setq start-of-loop p-name)
-
- when (string-match "^\"\\(.*\\)\"$" p-name)
- do (setq p-name (match-string 1 p-name))
-
- when (string-match "^'\\(.*\\)'$" p-name)
- do (setq p-name (match-string 1 p-name))
-
- until (string= start-of-loop p-name)))
-
- ;; If the address is 'foo@bar.com ' then show just
- ;; 'foo@bar.com'.
- (when (string= p-name p-address)
- (setq p-name nil))
-
- (cons p-address p-name))
+ (let (p-name p-address)
+ ;; It would be convenient to use `mail-header-parse-address',
+ ;; but that expects un-decoded mailbox parts, whereas our
+ ;; mailbox parts are already decoded (and hence may contain
+ ;; UTF-8). Given that notmuch should handle most of the awkward
+ ;; cases, some simple string deconstruction should be sufficient
+ ;; here.
+ (cond
+ ;; "User " style.
+ ((string-match "\\(.*\\) <\\(.*\\)>" address)
+ (setq p-name (match-string 1 address))
+ (setq p-address (match-string 2 address)))
+
+ ;; "" style.
+ ((string-match "<\\(.*\\)>" address)
+ (setq p-address (match-string 1 address)))
+ ;; Everything else.
+ (t
+ (setq p-address address)))
+ (when p-name
+ ;; Remove elements of the mailbox part that are not relevant for
+ ;; display, even if they are required during transport:
+ ;;
+ ;; Backslashes.
+ (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
+ ;; Outer single and double quotes, which might be nested.
+ (cl-loop with start-of-loop
+ do (setq start-of-loop p-name)
+ when (string-match "^\"\\(.*\\)\"$" p-name)
+ do (setq p-name (match-string 1 p-name))
+ when (string-match "^'\\(.*\\)'$" p-name)
+ do (setq p-name (match-string 1 p-name))
+ until (string= start-of-loop p-name)))
+ ;; If the address is 'foo@bar.com ' then show just
+ ;; 'foo@bar.com'.
+ (when (string= p-name p-address)
+ (setq p-name nil))
+ (cons p-address p-name))
(error (cons address nil))))
(defun notmuch-show-clean-address (address)
- "Try to clean a single email ADDRESS for display. Return
-unchanged ADDRESS if parsing fails."
+ "Try to clean a single email ADDRESS for display.
+Return unchanged ADDRESS if parsing fails."
(let* ((clean-address (notmuch-clean-address address))
(p-address (car clean-address))
(p-name (cdr clean-address)))
@@ -416,16 +460,23 @@ unchanged ADDRESS if parsing fails."
(defun notmuch-show-insert-headerline (headers date tags depth)
"Insert a notmuch style headerline based on HEADERS for a
message at DEPTH in the current thread."
- (let ((start (point)))
+ (let ((start (point))
+ (from (notmuch-sanitize
+ (notmuch-show-clean-address (plist-get headers :From)))))
+ (when (string-match "\\cR" from)
+ ;; If the From header has a right-to-left character add
+ ;; invisible U+200E LEFT-TO-RIGHT MARK character which forces
+ ;; the header paragraph as left-to-right text.
+ (insert (propertize (string ?\x200e) 'invisible t)))
(insert (notmuch-show-spaces-n (* notmuch-show-indent-messages-width depth))
- (notmuch-sanitize
- (notmuch-show-clean-address (plist-get headers :From)))
+ from
" ("
date
") ("
- (notmuch-tag-format-tags tags)
+ (notmuch-tag-format-tags tags tags)
")\n")
- (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face)))
+ (overlay-put (make-overlay start (point))
+ 'face 'notmuch-message-summary-face)))
(defun notmuch-show-insert-header (header header-value)
"Insert a single header."
@@ -437,9 +488,9 @@ message at DEPTH in the current thread."
(mapc (lambda (header)
(let* ((header-symbol (intern (concat ":" header)))
(header-value (plist-get headers header-symbol)))
- (if (and header-value
- (not (string-equal "" header-value)))
- (notmuch-show-insert-header header header-value))))
+ (when (and header-value
+ (not (string-equal "" header-value)))
+ (notmuch-show-insert-header header header-value))))
notmuch-message-headers)
(save-excursion
(save-restriction
@@ -452,56 +503,121 @@ message at DEPTH in the current thread."
'face 'message-mml
:supertype 'notmuch-button-type)
-(defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
- (let ((button)
- (base-label (concat (when name (concat name ": "))
+(defun notmuch-show-insert-part-header (nth content-type declared-type
+ &optional name comment)
+ (let ((base-label (concat (and name (concat name ": "))
declared-type
- (unless (string-equal declared-type content-type)
- (concat " (as " content-type ")"))
+ (and (not (string-equal declared-type content-type))
+ (concat " (as " content-type ")"))
comment)))
-
- (setq button
- (insert-button
- (concat "[ " base-label " ]")
- :base-label base-label
- :type 'notmuch-show-part-button-type
- :notmuch-part-hidden nil))
- (insert "\n")
- ;; return button
- button))
+ (prog1 (insert-button
+ (concat "[ " base-label " ]")
+ :base-label base-label
+ :type 'notmuch-show-part-button-type
+ :notmuch-part-hidden nil)
+ (insert "\n"))))
(defun notmuch-show-toggle-part-invisibility (&optional button)
(interactive)
- (let* ((button (or button (button-at (point))))
- (overlay (button-get button 'overlay))
- (lazy-part (button-get button :notmuch-lazy-part)))
- ;; We have a part to toggle if there is an overlay or if there is a lazy part.
- ;; If neither is present we cannot toggle the part so we just return nil.
- (when (or overlay lazy-part)
- (let* ((show (button-get button :notmuch-part-hidden))
- (new-start (button-start button))
- (button-label (button-get button :base-label))
- (old-point (point))
- (properties (text-properties-at (button-start button)))
- (inhibit-read-only t))
- ;; Toggle the button itself.
- (button-put button :notmuch-part-hidden (not show))
- (goto-char new-start)
- (insert "[ " button-label (if show " ]" " (hidden) ]"))
- (set-text-properties new-start (point) properties)
- (let ((old-end (button-end button)))
- (move-overlay button new-start (point))
- (delete-region (point) old-end))
- (goto-char (min old-point (1- (button-end button))))
- ;; Return nil if there is a lazy-part, it is empty, and we are
- ;; trying to show it. In all other cases return t.
- (if lazy-part
- (when show
- (button-put button :notmuch-lazy-part nil)
- (notmuch-show-lazy-part lazy-part button))
- ;; else there must be an overlay.
- (overlay-put overlay 'invisible (not show))
- t)))))
+ (let ((button (or button (button-at (point)))))
+ (when button
+ (let ((overlay (button-get button 'overlay))
+ (lazy-part (button-get button :notmuch-lazy-part)))
+ ;; We have a part to toggle if there is an overlay or if there
+ ;; is a lazy part. If neither is present we cannot toggle the
+ ;; part so we just return nil.
+ (when (or overlay lazy-part)
+ (let* ((show (button-get button :notmuch-part-hidden))
+ (new-start (button-start button))
+ (button-label (button-get button :base-label))
+ (old-point (point))
+ (properties (text-properties-at (button-start button)))
+ (inhibit-read-only t))
+ ;; Toggle the button itself.
+ (button-put button :notmuch-part-hidden (not show))
+ (goto-char new-start)
+ (insert "[ " button-label (if show " ]" " (hidden) ]"))
+ (set-text-properties new-start (point) properties)
+ (let ((old-end (button-end button)))
+ (move-overlay button new-start (point))
+ (delete-region (point) old-end))
+ (goto-char (min old-point (1- (button-end button))))
+ ;; Return nil if there is a lazy-part, it is empty, and we are
+ ;; trying to show it. In all other cases return t.
+ (if lazy-part
+ (when show
+ (button-put button :notmuch-lazy-part nil)
+ (notmuch-show-lazy-part lazy-part button))
+ ;; else there must be an overlay.
+ (overlay-put overlay 'invisible (not show))
+ t)))))))
+
+;; Part content ID handling
+
+(defvar notmuch-show--cids nil
+ "Alist from raw content ID to (MSG PART).")
+(make-variable-buffer-local 'notmuch-show--cids)
+
+(defun notmuch-show--register-cids (msg part)
+ "Register content-IDs in PART and all of PART's sub-parts."
+ (let ((content-id (plist-get part :content-id)))
+ (when content-id
+ ;; Note that content-IDs are globally unique, except when they
+ ;; aren't: RFC 2046 section 5.1.4 permits children of a
+ ;; multipart/alternative to have the same content-ID, in which
+ ;; case the MUA is supposed to pick the best one it can render.
+ ;; We simply add the content-ID to the beginning of our alist;
+ ;; so if this happens, we'll take the last (and "best")
+ ;; alternative (even if we can't render it).
+ (push (list content-id msg part) notmuch-show--cids)))
+ ;; Recurse on sub-parts
+ (let ((ctype (notmuch-split-content-type
+ (downcase (plist-get part :content-type)))))
+ (cond ((equal (car ctype) "multipart")
+ (mapc (apply-partially #'notmuch-show--register-cids msg)
+ (plist-get part :content)))
+ ((equal ctype '("message" "rfc822"))
+ (notmuch-show--register-cids
+ msg
+ (car (plist-get (car (plist-get part :content)) :body)))))))
+
+(defun notmuch-show--get-cid-content (cid)
+ "Return a list (CID-content content-type) or nil.
+
+This will only find parts from messages that have been inserted
+into the current buffer. CID must be a raw content ID, without
+enclosing angle brackets, a cid: prefix, or URL encoding. This
+will return nil if the CID is unknown or cannot be retrieved."
+ (let ((descriptor (cdr (assoc cid notmuch-show--cids))))
+ (when descriptor
+ (let* ((msg (car descriptor))
+ (part (cadr descriptor))
+ ;; Request caching for this content, as some messages
+ ;; reference the same cid: part many times (hundreds!).
+ (content (notmuch-get-bodypart-binary
+ msg part notmuch-show-process-crypto 'cache))
+ (content-type (plist-get part :content-type)))
+ (list content content-type)))))
+
+(defun notmuch-show-setup-w3m ()
+ "Instruct w3m how to retrieve content from a \"related\" part of a message."
+ (interactive)
+ (when (and (boundp 'w3m-cid-retrieve-function-alist)
+ (not (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)))
+ (push (cons 'notmuch-show-mode #'notmuch-show--cid-w3m-retrieve)
+ w3m-cid-retrieve-function-alist))
+ (setq mm-html-inhibit-images nil))
+
+(defvar w3m-current-buffer) ;; From `w3m.el'.
+(defun notmuch-show--cid-w3m-retrieve (url &rest args)
+ ;; url includes the cid: prefix and is URL encoded (see RFC 2392).
+ (let* ((cid (url-unhex-string (substring url 4)))
+ (content-and-type
+ (with-current-buffer w3m-current-buffer
+ (notmuch-show--get-cid-content cid))))
+ (when content-and-type
+ (insert (car content-and-type))
+ (cadr content-and-type))))
;; MIME part renderers
@@ -510,7 +626,8 @@ message at DEPTH in the current thread."
(plist-get part :content)))
(defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth button)
- (let ((chosen-type (car (notmuch-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
+ (let ((chosen-type (car (notmuch-multipart/alternative-choose
+ msg (notmuch-show-multipart/*-to-list part))))
(inner-parts (plist-get part :content))
(start (point)))
;; This inserts all parts of the chosen type rather than just one,
@@ -518,8 +635,8 @@ message at DEPTH in the current thread."
;; should be chosen if there are more than one that match?
(mapc (lambda (inner-part)
(let* ((inner-type (plist-get inner-part :content-type))
- (hide (not (or notmuch-show-all-multipart/alternative-parts
- (string= chosen-type inner-type)))))
+ (hide (not (or notmuch-show-all-multipart/alternative-parts
+ (string= chosen-type inner-type)))))
(notmuch-show-insert-bodypart msg inner-part depth hide)))
inner-parts)
@@ -527,134 +644,56 @@ message at DEPTH in the current thread."
(indent-rigidly start (point) 1)))
t)
-(defun notmuch-show-setup-w3m ()
- "Instruct w3m how to retrieve content from a \"related\" part of a message."
- (interactive)
- (if (boundp 'w3m-cid-retrieve-function-alist)
- (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)
- (push (cons 'notmuch-show-mode 'notmuch-show-w3m-cid-retrieve)
- w3m-cid-retrieve-function-alist)))
- (setq mm-inline-text-html-with-images t))
-
-(defvar w3m-current-buffer) ;; From `w3m.el'.
-(defvar notmuch-show-w3m-cid-store nil)
-(make-variable-buffer-local 'notmuch-show-w3m-cid-store)
-
-(defun notmuch-show-w3m-cid-store-internal (content-id
- message-id
- part-number
- content-type
- content)
- (push (list content-id
- message-id
- part-number
- content-type
- content)
- notmuch-show-w3m-cid-store))
-
-(defun notmuch-show-w3m-cid-store (msg part)
- (let ((content-id (plist-get part :content-id)))
- (when content-id
- (notmuch-show-w3m-cid-store-internal (concat "cid:" content-id)
- (plist-get msg :id)
- (plist-get part :id)
- (plist-get part :content-type)
- nil))))
-
-(defun notmuch-show-w3m-cid-retrieve (url &rest args)
- (let ((matching-part (with-current-buffer w3m-current-buffer
- (assoc url notmuch-show-w3m-cid-store))))
- (if matching-part
- (let ((message-id (nth 1 matching-part))
- (part-number (nth 2 matching-part))
- (content-type (nth 3 matching-part))
- (content (nth 4 matching-part)))
- ;; If we don't already have the content, get it and cache
- ;; it, as some messages reference the same cid: part many
- ;; times (hundreds!), which results in many calls to
- ;; `notmuch part'.
- (unless content
- (setq content (notmuch-get-bodypart-internal (notmuch-id-to-query message-id)
- part-number notmuch-show-process-crypto))
- (with-current-buffer w3m-current-buffer
- (notmuch-show-w3m-cid-store-internal url
- message-id
- part-number
- content-type
- content)))
- (insert content)
- content-type)
- nil)))
-
(defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth button)
(let ((inner-parts (plist-get part :content))
(start (point)))
-
- ;; We assume that the first part is text/html and the remainder
- ;; things that it references.
-
- ;; Stash the non-primary parts.
- (mapc (lambda (part)
- (notmuch-show-w3m-cid-store msg part))
- (cdr inner-parts))
-
- ;; Render the primary part.
+ ;; Render the primary part. FIXME: Support RFC 2387 Start header.
(notmuch-show-insert-bodypart msg (car inner-parts) depth)
;; Add hidden buttons for the rest
(mapc (lambda (inner-part)
(notmuch-show-insert-bodypart msg inner-part depth t))
(cdr inner-parts))
-
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
t)
(defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth button)
- (button-put button 'face 'notmuch-crypto-part-header)
- ;; add signature status button if sigstatus provided
- (if (plist-member part :sigstatus)
- (let* ((from (notmuch-show-get-header :From msg))
- (sigstatus (car (plist-get part :sigstatus))))
- (notmuch-crypto-insert-sigstatus-button sigstatus from))
- ;; if we're not adding sigstatus, tell the user how they can get it
- (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts."))
-
+ (when button
+ (button-put button 'face 'notmuch-crypto-part-header))
+ ;; Insert a button detailing the signature status.
+ (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+ (notmuch-show-get-header :From msg))
(let ((inner-parts (plist-get part :content))
(start (point)))
;; Show all of the parts.
(mapc (lambda (inner-part)
(notmuch-show-insert-bodypart msg inner-part depth))
inner-parts)
-
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
t)
(defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth button)
- (button-put button 'face 'notmuch-crypto-part-header)
- ;; add encryption status button if encstatus specified
- (if (plist-member part :encstatus)
- (let ((encstatus (car (plist-get part :encstatus))))
- (notmuch-crypto-insert-encstatus-button encstatus)
- ;; add signature status button if sigstatus specified
- (if (plist-member part :sigstatus)
- (let* ((from (notmuch-show-get-header :From msg))
- (sigstatus (car (plist-get part :sigstatus))))
- (notmuch-crypto-insert-sigstatus-button sigstatus from))))
- ;; if we're not adding encstatus, tell the user how they can get it
- (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts."))
-
+ (when button
+ (button-put button 'face 'notmuch-crypto-part-header))
+ ;; Insert a button detailing the encryption status.
+ (notmuch-crypto-insert-encstatus-button (car (plist-get part :encstatus)))
+ ;; Insert a button detailing the signature status.
+ (notmuch-crypto-insert-sigstatus-button (car (plist-get part :sigstatus))
+ (notmuch-show-get-header :From msg))
(let ((inner-parts (plist-get part :content))
(start (point)))
;; Show all of the parts.
(mapc (lambda (inner-part)
(notmuch-show-insert-bodypart msg inner-part depth))
inner-parts)
-
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
t)
+(defun notmuch-show-insert-part-application/pgp-encrypted (msg part content-type nth depth button)
+ t)
+
(defun notmuch-show-insert-part-multipart/* (msg part content-type nth depth button)
(let ((inner-parts (plist-get part :content))
(start (point)))
@@ -662,7 +701,6 @@ message at DEPTH in the current thread."
(mapc (lambda (inner-part)
(notmuch-show-insert-bodypart msg inner-part depth))
inner-parts)
-
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
t)
@@ -671,19 +709,15 @@ message at DEPTH in the current thread."
(let* ((message (car (plist-get part :content)))
(body (car (plist-get message :body)))
(start (point)))
-
;; Override `notmuch-message-headers' to force `From' to be
;; displayed.
(let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
(notmuch-show-insert-headers (plist-get message :headers)))
-
;; Blank line after headers to be compatible with the normal
;; message display.
(insert "\n")
-
;; Show the body
(notmuch-show-insert-bodypart msg body depth)
-
(when notmuch-show-indent-multipart
(indent-rigidly start (point) 1)))
t)
@@ -695,7 +729,7 @@ message at DEPTH in the current thread."
(let ((start (if button
(button-start button)
(point))))
- (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
+ (insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))
(save-excursion
(save-restriction
(narrow-to-region start (point-max))
@@ -704,9 +738,9 @@ message at DEPTH in the current thread."
(defun notmuch-show-insert-part-text/calendar (msg part content-type nth depth button)
(insert (with-temp-buffer
- (insert (notmuch-get-bodypart-content msg part nth notmuch-show-process-crypto))
- ;; notmuch-get-bodypart-content provides "raw", non-converted
- ;; data. Replace CRLF with LF before icalendar can use it.
+ (insert (notmuch-get-bodypart-text msg part notmuch-show-process-crypto))
+ ;; notmuch-get-bodypart-text does no newline conversion.
+ ;; Replace CRLF with LF before icalendar can use it.
(goto-char (point-min))
(while (re-search-forward "\r\n" nil t)
(replace-match "\n" nil nil))
@@ -715,7 +749,8 @@ message at DEPTH in the current thread."
(unwind-protect
(progn
(unless (icalendar-import-buffer file t)
- (error "Icalendar import error. See *icalendar-errors* for more information"))
+ (error "Icalendar import error. %s"
+ "See *icalendar-errors* for more information"))
(set-buffer (get-file-buffer file))
(setq result (buffer-substring (point-min) (point-max)))
(set-buffer-modified-p nil)
@@ -728,35 +763,80 @@ message at DEPTH in the current thread."
(defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth button)
(notmuch-show-insert-part-text/calendar msg part content-type nth depth button))
+(when (version< emacs-version "25.3")
+ ;; https://bugs.gnu.org/28350
+ ;;
+ ;; For newer emacs, we fall back to notmuch-show-insert-part-*/*
+ ;; (see notmuch-show-handlers-for)
+ (defun notmuch-show-insert-part-text/enriched
+ (msg part content-type nth depth button)
+ ;; By requiring enriched below, we ensure that the function
+ ;; enriched-decode-display-prop is defined before it will be
+ ;; shadowed by the letf below. Otherwise the version in
+ ;; enriched.el may be loaded a bit later and used instead (for
+ ;; the first time).
+ (require 'enriched)
+ (cl-letf (((symbol-function 'enriched-decode-display-prop)
+ (lambda (start end &optional param) (list start end))))
+ (notmuch-show-insert-part-*/* msg part content-type nth depth button))))
+
(defun notmuch-show-get-mime-type-of-application/octet-stream (part)
;; If we can deduce a MIME type from the filename of the attachment,
;; we return that.
- (if (plist-get part :filename)
- (let ((extension (file-name-extension (plist-get part :filename)))
- mime-type)
- (if extension
- (progn
- (mailcap-parse-mimetypes)
- (setq mime-type (mailcap-extension-to-mime extension))
- (if (and mime-type
- (not (string-equal mime-type "application/octet-stream")))
- mime-type
- nil))
- nil))))
+ (and (plist-get part :filename)
+ (let ((extension (file-name-extension (plist-get part :filename))))
+ (and extension
+ (progn
+ (mailcap-parse-mimetypes)
+ (let ((mime-type (mailcap-extension-to-mime extension)))
+ (and mime-type
+ (not (string-equal mime-type "application/octet-stream"))
+ mime-type)))))))
(defun notmuch-show-insert-part-text/html (msg part content-type nth depth button)
- ;; text/html handler to work around bugs in renderers and our
- ;; invisibile parts code. In particular w3m sets up a keymap which
- ;; "leaks" outside the invisible region and causes strange effects
- ;; in notmuch. We set mm-inline-text-html-with-w3m-keymap to nil to
- ;; tell w3m not to set a keymap (so the normal notmuch-show-mode-map
- ;; remains).
- (let ((mm-inline-text-html-with-w3m-keymap nil))
- (notmuch-show-insert-part-*/* msg part content-type nth depth button)))
+ (if (eq mm-text-html-renderer 'shr)
+ ;; It's easier to drive shr ourselves than to work around the
+ ;; goofy things `mm-shr' does (like irreversibly taking over
+ ;; content ID handling).
+ ;; FIXME: If we block an image, offer a button to load external
+ ;; images.
+ (let ((shr-blocked-images notmuch-show-text/html-blocked-images))
+ (notmuch-show--insert-part-text/html-shr msg part))
+ ;; Otherwise, let message-mode do the heavy lifting
+ ;;
+ ;; w3m sets up a keymap which "leaks" outside the invisible region
+ ;; and causes strange effects in notmuch. We set
+ ;; mm-inline-text-html-with-w3m-keymap to nil to tell w3m not to
+ ;; set a keymap (so the normal notmuch-show-mode-map remains).
+ (let ((mm-inline-text-html-with-w3m-keymap nil)
+ ;; FIXME: If we block an image, offer a button to load external
+ ;; images.
+ (gnus-blocked-images notmuch-show-text/html-blocked-images))
+ (notmuch-show-insert-part-*/* msg part content-type nth depth button))))
+
+;; These functions are used by notmuch-show--insert-part-text/html-shr
+(declare-function libxml-parse-html-region "xml.c")
+(declare-function shr-insert-document "shr")
+
+(defun notmuch-show--insert-part-text/html-shr (msg part)
+ ;; Make sure shr is loaded before we start let-binding its globals
+ (require 'shr)
+ (let ((dom (let ((process-crypto notmuch-show-process-crypto))
+ (with-temp-buffer
+ (insert (notmuch-get-bodypart-text msg part process-crypto))
+ (libxml-parse-html-region (point-min) (point-max)))))
+ (shr-content-function
+ (lambda (url)
+ ;; shr strips the "cid:" part of URL, but doesn't
+ ;; URL-decode it (see RFC 2392).
+ (let ((cid (url-unhex-string url)))
+ (car (notmuch-show--get-cid-content cid))))))
+ (shr-insert-document dom)
+ t))
(defun notmuch-show-insert-part-*/* (msg part content-type nth depth button)
;; This handler _must_ succeed - it is the handler of last resort.
- (notmuch-mm-display-part-inline msg part nth content-type notmuch-show-process-crypto)
+ (notmuch-mm-display-part-inline msg part content-type notmuch-show-process-crypto)
t)
;; Functions for determining how to handle MIME parts.
@@ -765,8 +845,8 @@ message at DEPTH in the current thread."
"Return a list of content handlers for a part of type CONTENT-TYPE."
(let (result)
(mapc (lambda (func)
- (if (functionp func)
- (push func result)))
+ (when (functionp func)
+ (push func result)))
;; Reverse order of prefrence.
(list (intern (concat "notmuch-show-insert-part-*/*"))
(intern (concat
@@ -779,22 +859,20 @@ message at DEPTH in the current thread."
;;
(defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth button)
- (let ((handlers (notmuch-show-handlers-for content-type)))
- ;; Run the content handlers until one of them returns a non-nil
- ;; value.
- (while (and handlers
- (not (condition-case err
- (funcall (car handlers) msg part content-type nth depth button)
- (error (progn
- (insert "!!! Bodypart insert error: ")
- (insert (error-message-string err))
- (insert " !!!\n") nil)))))
- (setq handlers (cdr handlers))))
- t)
+ ;; Run the handlers until one of them succeeds.
+ (cl-loop for handler in (notmuch-show-handlers-for content-type)
+ until (condition-case err
+ (funcall handler msg part content-type nth depth button)
+ ;; Specifying `debug' here lets the debugger run if
+ ;; `debug-on-error' is non-nil.
+ ((debug error)
+ (insert "!!! Bodypart handler `" (prin1-to-string handler)
+ "' threw an error:\n"
+ "!!! " (error-message-string err) "\n")
+ nil))))
(defun notmuch-show-create-part-overlays (button beg end)
- "Add an overlay to the part between BEG and END"
-
+ "Add an overlay to the part between BEG and END."
;; If there is no button (i.e., the part is text/plain and the first
;; part) or if the part has no content then we don't make the part
;; toggleable.
@@ -804,8 +882,7 @@ message at DEPTH in the current thread."
t))
(defun notmuch-show-record-part-information (part beg end)
- "Store PART as a text property from BEG to END"
-
+ "Store PART as a text property from BEG to END."
;; Record part information. Since we already inserted subparts,
;; don't override existing :notmuch-part properties.
(notmuch-map-text-property beg end :notmuch-part
@@ -816,13 +893,15 @@ message at DEPTH in the current thread."
;; watch out for sticky specs of t, which means all properties are
;; front-sticky/rear-nonsticky.
(notmuch-map-text-property beg end 'front-sticky
- (lambda (v) (if (listp v)
- (pushnew :notmuch-part v)
- v)))
+ (lambda (v)
+ (if (listp v)
+ (cl-pushnew :notmuch-part v)
+ v)))
(notmuch-map-text-property beg end 'rear-nonsticky
- (lambda (v) (if (listp v)
- (pushnew :notmuch-part v)
- v))))
+ (lambda (v)
+ (if (listp v)
+ (cl-pushnew :notmuch-part v)
+ v))))
(defun notmuch-show-lazy-part (part-args button)
;; Insert the lazy part after the button for the part. We would just
@@ -847,50 +926,86 @@ message at DEPTH in the current thread."
(narrow-to-region part-beg part-end)
(delete-region part-beg part-end)
(apply #'notmuch-show-insert-bodypart-internal part-args)
- (indent-rigidly part-beg part-end depth))
+ (indent-rigidly part-beg
+ part-end
+ (* notmuch-show-indent-messages-width depth)))
(goto-char part-end)
(delete-char 1)
- (notmuch-show-record-part-information (second part-args)
+ (notmuch-show-record-part-information (cadr part-args)
(button-start button)
part-end)
;; Create the overlay. If the lazy-part turned out to be empty/not
;; showable this returns nil.
(notmuch-show-create-part-overlays button part-beg part-end))))
+(defun notmuch-show-mime-type (part)
+ "Return the correct mime-type to use for PART."
+ (let ((content-type (downcase (plist-get part :content-type))))
+ (or (and (string= content-type "application/octet-stream")
+ (notmuch-show-get-mime-type-of-application/octet-stream part))
+ (and (string= content-type "inline patch")
+ "text/x-diff")
+ content-type)))
+
+;; The following variable can be overridden by let bindings.
+(defvar notmuch-show-insert-header-p-function 'notmuch-show-insert-header-p
+ "Specify which function decides which part headers get inserted.
+
+The function should take two parameters, PART and HIDE, and
+should return non-NIL if a header button should be inserted for
+this part.")
+
+(defun notmuch-show-insert-header-p (part hide)
+ ;; Show all part buttons except for the first part if it is text/plain.
+ (let ((mime-type (notmuch-show-mime-type part)))
+ (not (and (string= mime-type "text/plain")
+ (<= (plist-get part :id) 1)))))
+
+(defun notmuch-show-reply-insert-header-p-never (part hide)
+ nil)
+
+(defun notmuch-show-reply-insert-header-p-trimmed (part hide)
+ (let ((mime-type (notmuch-show-mime-type part)))
+ (and (not (notmuch-match-content-type mime-type "multipart/*"))
+ (not hide))))
+
+(defun notmuch-show-reply-insert-header-p-minimal (part hide)
+ (let ((mime-type (notmuch-show-mime-type part)))
+ (and (notmuch-match-content-type mime-type "text/*")
+ (not hide))))
+
(defun notmuch-show-insert-bodypart (msg part depth &optional hide)
"Insert the body part PART at depth DEPTH in the current thread.
HIDE determines whether to show or hide the part and the button
as follows: If HIDE is nil, show the part and the button. If HIDE
-is t, hide the part initially and show the button. If HIDE is
-'no-buttons, show the part but do not add any buttons (this is
-useful for quoting in replies)."
-
+is t, hide the part initially and show the button."
(let* ((content-type (downcase (plist-get part :content-type)))
- (mime-type (or (and (string= content-type "application/octet-stream")
- (notmuch-show-get-mime-type-of-application/octet-stream part))
- (and (string= content-type "inline patch")
- "text/x-diff")
- content-type))
+ (mime-type (notmuch-show-mime-type part))
(nth (plist-get part :id))
+ (long (and (notmuch-match-content-type mime-type "text/*")
+ (> notmuch-show-max-text-part-size 0)
+ (> (length (plist-get part :content))
+ notmuch-show-max-text-part-size)))
(beg (point))
- ;; Hide the part initially if HIDE is t.
- (show-part (not (equal hide t)))
- ;; We omit the part button for the first (or only) part if
- ;; this is text/plain, or HIDE is 'no-buttons.
- (button (unless (or (equal hide 'no-buttons)
- (and (string= mime-type "text/plain") (<= nth 1)))
- (notmuch-show-insert-part-header nth mime-type content-type (plist-get part :filename))))
+ ;; This default header-p function omits the part button for
+ ;; the first (or only) part if this is text/plain.
+ (button (and (funcall notmuch-show-insert-header-p-function part hide)
+ (notmuch-show-insert-part-header
+ nth mime-type content-type
+ (plist-get part :filename))))
+ ;; Hide the part initially if HIDE is t, or if it is too long
+ ;; and we have a button to allow toggling.
+ (show-part (not (or (equal hide t)
+ (and long button))))
(content-beg (point)))
-
;; Store the computed mime-type for later use (e.g. by attachment handlers).
(plist-put part :computed-type mime-type)
-
(if show-part
- (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button)
- (button-put button :notmuch-lazy-part
- (list msg part mime-type nth depth button)))
-
+ (notmuch-show-insert-bodypart-internal msg part mime-type nth depth button)
+ (when button
+ (button-put button :notmuch-lazy-part
+ (list msg part mime-type nth depth button))))
;; Some of the body part handlers leave point somewhere up in the
;; part, so we make sure that we're down at the end.
(goto-char (point-max))
@@ -907,6 +1022,10 @@ useful for quoting in replies)."
(defun notmuch-show-insert-body (msg body depth)
"Insert the body BODY at depth DEPTH in the current thread."
+ ;; Register all content IDs for this message. According to RFC
+ ;; 2392, content IDs are *global*, but it's okay if an MUA treats
+ ;; them as only global within a message.
+ (notmuch-show--register-cids msg (car body))
(mapc (lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
(defun notmuch-show-make-symbol (type)
@@ -927,18 +1046,13 @@ useful for quoting in replies)."
content-start content-end
headers-start headers-end
(bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
-
(setq message-start (point-marker))
-
(notmuch-show-insert-headerline headers
- (or (if notmuch-show-relative-dates
- (plist-get msg :date_relative)
- nil)
+ (or (and notmuch-show-relative-dates
+ (plist-get msg :date_relative))
(plist-get headers :Date))
(plist-get msg :tags) depth)
-
(setq content-start (point-marker))
-
;; Set `headers-start' to point after the 'Subject:' header to be
;; compatible with the existing implementation. This just sets it
;; to after the first header.
@@ -948,14 +1062,11 @@ useful for quoting in replies)."
;; If the subject of this message is the same as that of the
;; previous message, don't display it when this message is
;; collapsed.
- (when (not (string= notmuch-show-previous-subject
- bare-subject))
+ (unless (string= notmuch-show-previous-subject bare-subject)
(forward-line 1))
(setq headers-start (point-marker)))
(setq headers-end (point-marker))
-
(setq notmuch-show-previous-subject bare-subject)
-
;; A blank line between the headers and the body.
(insert "\n")
(notmuch-show-insert-body msg (plist-get msg :body)
@@ -964,32 +1075,28 @@ useful for quoting in replies)."
(unless (bolp)
(insert "\n"))
(setq content-end (point-marker))
-
;; Indent according to the depth in the thread.
- (if notmuch-show-indent-content
- (indent-rigidly content-start content-end (* notmuch-show-indent-messages-width depth)))
-
+ (when notmuch-show-indent-content
+ (indent-rigidly content-start
+ content-end
+ (* notmuch-show-indent-messages-width depth)))
(setq message-end (point-max-marker))
-
;; Save the extents of this message over the whole text of the
;; message.
- (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
-
+ (put-text-property message-start message-end
+ :notmuch-message-extent
+ (cons message-start message-end))
;; Create overlays used to control visibility
(plist-put msg :headers-overlay (make-overlay headers-start headers-end))
(plist-put msg :message-overlay (make-overlay headers-start content-end))
-
(plist-put msg :depth depth)
-
;; Save the properties for this message. Currently this saves the
;; entire message (augmented it with other stuff), which seems
;; like overkill. We might save a reduced subset (for example, not
;; the content).
(notmuch-show-set-message-properties msg)
-
;; Set header visibility.
(notmuch-show-headers-visible msg notmuch-message-headers-visible)
-
;; Message visibility depends on whether it matched the search
;; criteria.
(notmuch-show-message-visible msg (and (plist-get msg :match)
@@ -1007,7 +1114,8 @@ useful for quoting in replies)."
(defun notmuch-show-toggle-elide-non-matching ()
"Toggle the display of non-matching messages."
(interactive)
- (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages))
+ (setq notmuch-show-elide-non-matching-messages
+ (not notmuch-show-elide-non-matching-messages))
(message (if notmuch-show-elide-non-matching-messages
"Showing matching messages only."
"Showing all messages."))
@@ -1073,26 +1181,28 @@ This also turns id:\"\"-parts and mid: links into
buttons for a corresponding notmuch search."
(goto-address-fontify-region start end)
(save-excursion
- (let (links)
- (goto-char start)
- (while (re-search-forward notmuch-id-regexp end t)
+ (let (links
+ (beg-line (progn (goto-char start) (line-beginning-position)))
+ (end-line (progn (goto-char end) (line-end-position))))
+ (goto-char beg-line)
+ (while (re-search-forward notmuch-id-regexp end-line t)
(push (list (match-beginning 0) (match-end 0)
(match-string-no-properties 0)) links))
- (goto-char start)
- (while (re-search-forward notmuch-mid-regexp end t)
+ (goto-char beg-line)
+ (while (re-search-forward notmuch-mid-regexp end-line t)
(let* ((mid-cid (match-string-no-properties 1))
(mid (save-match-data
(string-match "^[^/]*" mid-cid)
(url-unhex-string (match-string 0 mid-cid)))))
(push (list (match-beginning 0) (match-end 0)
(notmuch-id-to-query mid)) links)))
- (dolist (link links)
+ (pcase-dolist (`(,beg ,end ,link) links)
;; Remove the overlay created by goto-address-mode
- (remove-overlays (first link) (second link) 'goto-address t)
- (make-text-button (first link) (second link)
+ (remove-overlays beg end 'goto-address t)
+ (make-text-button beg end
:type 'notmuch-button-type
'action `(lambda (arg)
- (notmuch-show ,(third link) current-prefix-arg))
+ (notmuch-show ,link current-prefix-arg))
'follow-link t
'help-echo "Mouse-1, RET: search for this message"
'face goto-address-mail-face)))))
@@ -1115,70 +1225,141 @@ non-nil.
The optional BUFFER-NAME provides the name of the buffer in
which the message thread is shown. If it is nil (which occurs
when the command is called interactively) the argument to the
-function is used."
+function is used.
+
+Returns the buffer containing the messages, or NIL if no messages
+matched."
(interactive "sNotmuch show: \nP")
(let ((buffer-name (generate-new-buffer-name
(or buffer-name
- (concat "*notmuch-" thread-id "*")))))
+ (concat "*notmuch-" thread-id "*"))))
+ ;; We override mm-inline-override-types to stop application/*
+ ;; parts from being displayed unless the user has customized
+ ;; it themselves.
+ (mm-inline-override-types
+ (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)))
(switch-to-buffer (get-buffer-create buffer-name))
- ;; Set the default value for `notmuch-show-process-crypto' in this
- ;; buffer.
- (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
- ;; Set the default value for
- ;; `notmuch-show-elide-non-matching-messages' in this buffer. If
- ;; elide-toggle is set, invert the default.
- (setq notmuch-show-elide-non-matching-messages notmuch-show-only-matching-messages)
- (if elide-toggle
- (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages)))
-
- (setq notmuch-show-thread-id thread-id
- notmuch-show-parent-buffer parent-buffer
- notmuch-show-query-context query-context)
- (notmuch-show-build-buffer)
- (notmuch-show-goto-first-wanted-message)
- (current-buffer)))
-
-(defun notmuch-show-build-buffer ()
- (let ((inhibit-read-only t))
-
+ ;; No need to track undo information for this buffer.
+ (setq buffer-undo-list t)
(notmuch-show-mode)
- ;; Don't track undo information for this buffer
- (set 'buffer-undo-list t)
-
- (erase-buffer)
- (goto-char (point-min))
- (save-excursion
- (let* ((basic-args (list notmuch-show-thread-id))
- (args (if notmuch-show-query-context
- (append (list "\'") basic-args
- (list "and (" notmuch-show-query-context ")\'"))
- (append (list "\'") basic-args (list "\'"))))
- (cli-args (cons "--exclude=false"
- (when notmuch-show-elide-non-matching-messages
- (list "--entire-thread=false")))))
-
- (notmuch-show-insert-forest (notmuch-query-get-threads (append cli-args args)))
- ;; If the query context reduced the results to nothing, run
- ;; the basic query.
- (when (and (eq (buffer-size) 0)
- notmuch-show-query-context)
- (notmuch-show-insert-forest
- (notmuch-query-get-threads (append cli-args basic-args)))))
-
- (jit-lock-register #'notmuch-show-buttonise-links)
-
+ ;; Set various buffer local variables to their appropriate initial
+ ;; state. Do this after enabling `notmuch-show-mode' so that they
+ ;; aren't wiped out.
+ (setq notmuch-show-thread-id thread-id)
+ (setq notmuch-show-parent-buffer parent-buffer)
+ (setq notmuch-show-query-context
+ (if (or (string= query-context "")
+ (string= query-context "*"))
+ nil
+ query-context))
+ (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
+ ;; If `elide-toggle', invert the default value.
+ (setq notmuch-show-elide-non-matching-messages
+ (if elide-toggle
+ (not notmuch-show-only-matching-messages)
+ notmuch-show-only-matching-messages))
+ (add-hook 'post-command-hook #'notmuch-show-command-hook nil t)
+ (jit-lock-register #'notmuch-show-buttonise-links)
+ (notmuch-tag-clear-cache)
+ (let ((inhibit-read-only t))
+ (if (notmuch-show--build-buffer)
+ ;; Messages were inserted into the buffer.
+ (current-buffer)
+ ;; No messages were inserted - presumably none matched the
+ ;; query.
+ (kill-buffer (current-buffer))
+ (ding)
+ (message "No messages matched the query!")
+ nil))))
+
+(defun notmuch-show--build-queries (thread context)
+ "Return a list of queries to try for this search.
+
+THREAD and CONTEXT are both strings, though CONTEXT may be nil.
+When CONTEXT is not nil, the first query is the conjunction of it
+and THREAD. The next query is THREAD alone, and serves as a
+fallback if the prior matches no messages."
+ (let (queries)
+ (push (list thread) queries)
+ (when context
+ (push (list thread "and (" context ")") queries))
+ queries))
+
+(defun notmuch-show--build-buffer (&optional state)
+ "Display messages matching the current buffer context.
+
+Apply the previously saved STATE if supplied, otherwise show the
+first relevant message.
+
+If no messages match the query return NIL."
+ (let* ((cli-args (cons "--exclude=false"
+ (and notmuch-show-elide-non-matching-messages
+ (list "--entire-thread=false"))))
+ (queries (notmuch-show--build-queries
+ notmuch-show-thread-id notmuch-show-query-context))
+ (forest nil)
+ ;; Must be reset every time we are going to start inserting
+ ;; messages into the buffer.
+ (notmuch-show-previous-subject ""))
+ ;; Use results from the first query that returns some.
+ (while (and (not forest) queries)
+ (setq forest (notmuch-query-get-threads
+ (append cli-args (list "'") (car queries) (list "'"))))
+ (setq queries (cdr queries)))
+ (when forest
+ (notmuch-show-insert-forest forest)
+ ;; Store the original tags for each message so that we can
+ ;; display changes.
+ (notmuch-show-mapc
+ (lambda () (notmuch-show-set-prop :orig-tags (notmuch-show-get-tags))))
;; Set the header line to the subject of the first message.
- (setq header-line-format (notmuch-sanitize (notmuch-show-strip-re (notmuch-show-get-subject))))
-
- (run-hooks 'notmuch-show-hook))))
+ (setq header-line-format
+ (replace-regexp-in-string "%" "%%"
+ (notmuch-sanitize
+ (notmuch-show-strip-re
+ (notmuch-show-get-subject)))))
+ (run-hooks 'notmuch-show-hook)
+ (if state
+ (notmuch-show-apply-state state)
+ ;; With no state to apply, just go to the first message.
+ (notmuch-show-goto-first-wanted-message)))
+ ;; Report back to the caller whether any messages matched.
+ forest))
(defun notmuch-show-capture-state ()
"Capture the state of the current buffer.
This includes:
- the list of open messages,
- - the current message."
- (list (notmuch-show-get-message-id) (notmuch-show-get-message-ids-for-open-messages)))
+ - the combination of current message id with/for each visible window."
+ (let* ((win-list (get-buffer-window-list (current-buffer) nil t))
+ (win-id-combo (mapcar (lambda (win)
+ (with-selected-window win
+ (list win (notmuch-show-get-message-id))))
+ win-list)))
+ (list win-id-combo (notmuch-show-get-message-ids-for-open-messages))))
+
+(defun notmuch-show-get-query ()
+ "Return the current query in this show buffer."
+ (if notmuch-show-query-context
+ (concat notmuch-show-thread-id
+ " and ("
+ notmuch-show-query-context
+ ")")
+ notmuch-show-thread-id))
+
+(defun notmuch-show-goto-message (msg-id)
+ "Go to message with msg-id."
+ (goto-char (point-min))
+ (unless (cl-loop if (string= msg-id (notmuch-show-get-message-id))
+ return t
+ until (not (notmuch-show-goto-message-next)))
+ (goto-char (point-min))
+ (message "Message-id not found."))
+ (notmuch-show-message-adjust))
(defun notmuch-show-apply-state (state)
"Apply STATE to the current buffer.
@@ -1186,24 +1367,19 @@ This includes:
This includes:
- opening the messages previously opened,
- closing all other messages,
- - moving to the correct current message."
- (let ((current (car state))
+ - moving to the correct current message in every displayed window."
+ (let ((win-msg-alist (car state))
(open (cadr state)))
-
;; Open those that were open.
(goto-char (point-min))
- (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
- (member (notmuch-show-get-message-id) open))
- until (not (notmuch-show-goto-message-next)))
-
- ;; Go to the previously open message.
- (goto-char (point-min))
- (unless (loop if (string= current (notmuch-show-get-message-id))
- return t
- until (not (notmuch-show-goto-message-next)))
- (goto-char (point-min))
- (message "Previously current message not found."))
- (notmuch-show-message-adjust)))
+ (cl-loop do (notmuch-show-message-visible
+ (notmuch-show-get-message-properties)
+ (member (notmuch-show-get-message-id) open))
+ until (not (notmuch-show-goto-message-next)))
+ (dolist (win-msg-pair win-msg-alist)
+ (with-selected-window (car win-msg-pair)
+ ;; Go to the previously open message in this window
+ (notmuch-show-goto-message (cadr win-msg-pair))))))
(defun notmuch-show-refresh-view (&optional reset-state)
"Refresh the current view.
@@ -1216,17 +1392,16 @@ reset based on the original query."
(let ((inhibit-read-only t)
(state (unless reset-state
(notmuch-show-capture-state))))
- ;; erase-buffer does not seem to remove overlays, which can lead
+ ;; `erase-buffer' does not seem to remove overlays, which can lead
;; to weird effects such as remaining images, so remove them
;; manually.
(remove-overlays)
(erase-buffer)
- (notmuch-show-build-buffer)
- (if state
- (notmuch-show-apply-state state)
- ;; We're resetting state, so navigate to the first open message
- ;; and mark it read, just like opening a new show buffer.
- (notmuch-show-goto-first-wanted-message))))
+ (unless (notmuch-show--build-buffer state)
+ ;; No messages were inserted.
+ (kill-buffer (current-buffer))
+ (ding)
+ (message "Refreshing the buffer resulted in no messages!"))))
(defvar notmuch-show-stash-map
(let ((map (make-sparse-keymap)))
@@ -1241,9 +1416,10 @@ reset based on the original query."
(define-key map "t" 'notmuch-show-stash-to)
(define-key map "l" 'notmuch-show-stash-mlarchive-link)
(define-key map "L" 'notmuch-show-stash-mlarchive-link-and-go)
+ (define-key map "G" 'notmuch-show-stash-git-send-email)
(define-key map "?" 'notmuch-subkeymap-help)
map)
- "Submap for stash commands")
+ "Submap for stash commands.")
(fset 'notmuch-show-stash-map notmuch-show-stash-map)
(defvar notmuch-show-part-map
@@ -1252,55 +1428,63 @@ reset based on the original query."
(define-key map "v" 'notmuch-show-view-part)
(define-key map "o" 'notmuch-show-interactively-view-part)
(define-key map "|" 'notmuch-show-pipe-part)
+ (define-key map "m" 'notmuch-show-choose-mime-of-part)
(define-key map "?" 'notmuch-subkeymap-help)
map)
- "Submap for part commands")
+ "Submap for part commands.")
(fset 'notmuch-show-part-map notmuch-show-part-map)
(defvar notmuch-show-mode-map
- (let ((map (make-sparse-keymap)))
- (set-keymap-parent map notmuch-common-keymap)
- (define-key map "Z" 'notmuch-tree-from-show-current-query)
- (define-key map (kbd "") 'widget-backward)
- (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
- (define-key map (kbd "") 'notmuch-show-previous-button)
- (define-key map (kbd "TAB") 'notmuch-show-next-button)
- (define-key map "f" 'notmuch-show-forward-message)
- (define-key map "r" 'notmuch-show-reply-sender)
- (define-key map "R" 'notmuch-show-reply)
- (define-key map "|" 'notmuch-show-pipe-message)
- (define-key map "w" 'notmuch-show-save-attachments)
- (define-key map "V" 'notmuch-show-view-raw-message)
- (define-key map "c" 'notmuch-show-stash-map)
- (define-key map "h" 'notmuch-show-toggle-visibility-headers)
- (define-key map "*" 'notmuch-show-tag-all)
- (define-key map "-" 'notmuch-show-remove-tag)
- (define-key map "+" 'notmuch-show-add-tag)
- (define-key map "X" 'notmuch-show-archive-thread-then-exit)
- (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
- (define-key map "A" 'notmuch-show-archive-thread-then-next)
- (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
- (define-key map "N" 'notmuch-show-next-message)
- (define-key map "P" 'notmuch-show-previous-message)
- (define-key map "n" 'notmuch-show-next-open-message)
- (define-key map "p" 'notmuch-show-previous-open-message)
- (define-key map (kbd "M-n") 'notmuch-show-next-thread-show)
- (define-key map (kbd "M-p") 'notmuch-show-previous-thread-show)
- (define-key map (kbd "DEL") 'notmuch-show-rewind)
- (define-key map " " 'notmuch-show-advance-and-archive)
- (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
- (define-key map (kbd "RET") 'notmuch-show-toggle-message)
- (define-key map "#" 'notmuch-show-print-message)
- (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
- (define-key map "$" 'notmuch-show-toggle-process-crypto)
- (define-key map "<" 'notmuch-show-toggle-thread-indentation)
- (define-key map "t" 'toggle-truncate-lines)
- (define-key map "." 'notmuch-show-part-map)
- map)
- "Keymap for \"notmuch show\" buffers.")
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map notmuch-common-keymap)
+ (define-key map "Z" 'notmuch-tree-from-show-current-query)
+ (define-key map "U" 'notmuch-unthreaded-from-show-current-query)
+ (define-key map (kbd "") 'widget-backward)
+ (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
+ (define-key map (kbd "") 'notmuch-show-previous-button)
+ (define-key map (kbd "TAB") 'notmuch-show-next-button)
+ (define-key map "f" 'notmuch-show-forward-message)
+ (define-key map "F" 'notmuch-show-forward-open-messages)
+ (define-key map "b" 'notmuch-show-resend-message)
+ (define-key map "l" 'notmuch-show-filter-thread)
+ (define-key map "r" 'notmuch-show-reply-sender)
+ (define-key map "R" 'notmuch-show-reply)
+ (define-key map "|" 'notmuch-show-pipe-message)
+ (define-key map "w" 'notmuch-show-save-attachments)
+ (define-key map "V" 'notmuch-show-view-raw-message)
+ (define-key map "e" 'notmuch-show-resume-message)
+ (define-key map "c" 'notmuch-show-stash-map)
+ (define-key map "h" 'notmuch-show-toggle-visibility-headers)
+ (define-key map "k" 'notmuch-tag-jump)
+ (define-key map "*" 'notmuch-show-tag-all)
+ (define-key map "-" 'notmuch-show-remove-tag)
+ (define-key map "+" 'notmuch-show-add-tag)
+ (define-key map "X" 'notmuch-show-archive-thread-then-exit)
+ (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
+ (define-key map "A" 'notmuch-show-archive-thread-then-next)
+ (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
+ (define-key map "N" 'notmuch-show-next-message)
+ (define-key map "P" 'notmuch-show-previous-message)
+ (define-key map "n" 'notmuch-show-next-open-message)
+ (define-key map "p" 'notmuch-show-previous-open-message)
+ (define-key map (kbd "M-n") 'notmuch-show-next-thread-show)
+ (define-key map (kbd "M-p") 'notmuch-show-previous-thread-show)
+ (define-key map (kbd "DEL") 'notmuch-show-rewind)
+ (define-key map " " 'notmuch-show-advance-and-archive)
+ (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
+ (define-key map (kbd "RET") 'notmuch-show-toggle-message)
+ (define-key map "#" 'notmuch-show-print-message)
+ (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
+ (define-key map "$" 'notmuch-show-toggle-process-crypto)
+ (define-key map "<" 'notmuch-show-toggle-thread-indentation)
+ (define-key map "t" 'toggle-truncate-lines)
+ (define-key map "." 'notmuch-show-part-map)
+ (define-key map "B" 'notmuch-show-browse-urls)
+ map)
+ "Keymap for \"notmuch show\" buffers.")
(fset 'notmuch-show-mode-map notmuch-show-mode-map)
-(defun notmuch-show-mode ()
+(define-derived-mode notmuch-show-mode fundamental-mode "notmuch-show"
"Major mode for viewing a thread with notmuch.
This buffer contains the results of the \"notmuch show\" command
@@ -1328,22 +1512,28 @@ You can add or remove arbitrary tags from the current message with
All currently available key bindings:
\\{notmuch-show-mode-map}"
- (interactive)
- (kill-all-local-variables)
(setq notmuch-buffer-refresh-function #'notmuch-show-refresh-view)
- (use-local-map notmuch-show-mode-map)
- (setq major-mode 'notmuch-show-mode
- mode-name "notmuch-show")
- (setq buffer-read-only t
- truncate-lines t))
+ (setq buffer-read-only t)
+ (setq truncate-lines t)
+ (setq imenu-prev-index-position-function
+ #'notmuch-show-imenu-prev-index-position-function)
+ (setq imenu-extract-index-name-function
+ #'notmuch-show-imenu-extract-index-name-function))
(defun notmuch-tree-from-show-current-query ()
- "Call notmuch tree with the current query"
+ "Call notmuch tree with the current query."
(interactive)
(notmuch-tree notmuch-show-thread-id
notmuch-show-query-context
(notmuch-show-get-message-id)))
+(defun notmuch-unthreaded-from-show-current-query ()
+ "Call notmuch unthreaded with the current query."
+ (interactive)
+ (notmuch-unthreaded notmuch-show-thread-id
+ notmuch-show-query-context
+ (notmuch-show-get-message-id)))
+
(defun notmuch-show-move-to-message-top ()
(goto-char (notmuch-show-message-top)))
@@ -1359,6 +1549,8 @@ All currently available key bindings:
;; region a->b is not found when point is at b. We walk backwards
;; until finding the property.
(defun notmuch-show-message-extent ()
+ "Return a cons cell containing the start and end buffer offset
+of the current message."
(let (r)
(save-excursion
(while (not (setq r (get-text-property (point) :notmuch-message-extent)))
@@ -1393,8 +1585,8 @@ All currently available key bindings:
effects."
(save-excursion
(goto-char (point-min))
- (loop do (funcall function)
- while (notmuch-show-goto-message-next))))
+ (cl-loop do (funcall function)
+ while (notmuch-show-goto-message-next))))
;; Functions relating to the visibility of messages and their
;; components.
@@ -1413,7 +1605,8 @@ effects."
(defun notmuch-show-set-message-properties (props)
(save-excursion
(notmuch-show-move-to-message-top)
- (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
+ (put-text-property (point) (+ (point) 1)
+ :notmuch-message-properties props)))
(defun notmuch-show-get-message-properties ()
"Return the properties of the current message as a plist.
@@ -1442,8 +1635,18 @@ an error if there is no part containing point."
(notmuch-show-set-message-properties props)))
(defun notmuch-show-get-prop (prop &optional props)
+ "Get property PROP from current message in show or tree mode.
+
+It gets property PROP from PROPS or, if PROPS is nil, the current
+message in either tree or show. This means that several utility
+functions in notmuch-show can be used directly by notmuch-tree as
+they just need the correct message properties."
(let ((props (or props
- (notmuch-show-get-message-properties))))
+ (cond ((eq major-mode 'notmuch-show-mode)
+ (notmuch-show-get-message-properties))
+ ((eq major-mode 'notmuch-tree-mode)
+ (notmuch-tree-get-message-properties))
+ (t nil)))))
(plist-get props prop)))
(defun notmuch-show-get-message-id (&optional bare)
@@ -1469,9 +1672,10 @@ current thread."
;; dme: Would it make sense to use a macro for many of these?
+;; XXX TODO figure out what to do about multiple filenames
(defun notmuch-show-get-filename ()
"Return the filename of the current message."
- (notmuch-show-get-prop :filename))
+ (car (notmuch-show-get-prop :filename)))
(defun notmuch-show-get-header (header &optional props)
"Return the named header of the current message, if any."
@@ -1483,6 +1687,9 @@ current thread."
(defun notmuch-show-get-date ()
(notmuch-show-get-header :Date))
+(defun notmuch-show-get-timestamp ()
+ (notmuch-show-get-prop :timestamp))
+
(defun notmuch-show-get-from ()
(notmuch-show-get-header :From))
@@ -1527,6 +1734,45 @@ marked as unread, i.e. the tag changes in
(apply 'notmuch-show-tag-message
(notmuch-tag-change-list notmuch-show-mark-read-tags unread))))
+(defun notmuch-show-seen-current-message (start end)
+ "Mark the current message read if it is open.
+
+We only mark it read once: if it is changed back then that is a
+user decision and we should not override it."
+ (when (and (notmuch-show-message-visible-p)
+ (not (notmuch-show-get-prop :seen)))
+ (notmuch-show-mark-read)
+ (notmuch-show-set-prop :seen t)))
+
+(defvar notmuch-show--seen-has-errored nil)
+(make-variable-buffer-local 'notmuch-show--seen-has-errored)
+
+(defun notmuch-show-command-hook ()
+ (when (eq major-mode 'notmuch-show-mode)
+ ;; We need to redisplay to get window-start and window-end correct.
+ (redisplay)
+ (save-excursion
+ (condition-case err
+ (funcall notmuch-show-mark-read-function (window-start) (window-end))
+ ((debug error)
+ (unless notmuch-show--seen-has-errored
+ (setq notmuch-show--seen-has-errored 't)
+ (setq header-line-format
+ (concat header-line-format
+ (propertize
+ " [some mark read tag changes may have failed]"
+ 'face font-lock-warning-face)))))))))
+
+(defun notmuch-show-filter-thread (query)
+ "Filter or LIMIT the current thread based on a new query string.
+
+Reshows the current thread with matches defined by the new query-string."
+ (interactive (list (notmuch-read-query "Filter thread: ")))
+ (let ((msg-id (notmuch-show-get-message-id)))
+ (setq notmuch-show-query-context (if (string= query "") nil query))
+ (notmuch-show-refresh-view t)
+ (notmuch-show-goto-message msg-id)))
+
;; Functions for getting attributes of several messages in the current
;; thread.
@@ -1536,12 +1782,11 @@ marked as unread, i.e. the tag changes in
(let (message-ids done)
(goto-char (point-min))
(while (not done)
- (if (notmuch-show-message-visible-p)
- (setq message-ids (append message-ids (list (notmuch-show-get-message-id)))))
- (setq done (not (notmuch-show-goto-message-next)))
- )
- message-ids
- )))
+ (when (notmuch-show-message-visible-p)
+ (setq message-ids
+ (append message-ids (list (notmuch-show-get-message-id)))))
+ (setq done (not (notmuch-show-goto-message-next))))
+ message-ids)))
;; Commands typically bound to keys.
@@ -1570,16 +1815,13 @@ current window), advance to the next open message."
(> visible-end-of-this-message (window-end)))
;; The bottom of this message is not visible - scroll.
(scroll-up nil))
-
((not (= end-of-this-message (point-max)))
;; This is not the last message - move to the next visible one.
(notmuch-show-next-open-message))
-
((not (= (point) (point-max)))
;; This is the last message, but the cursor is not at the end of
;; the buffer. Move it there.
(goto-char (point-max)))
-
(t
;; This is the last message - change the return value
(setq ret t)))
@@ -1597,11 +1839,12 @@ archives the entire current thread, (apply changes in
thread from the search from which this thread was originally
shown."
(interactive)
- (if (notmuch-show-advance)
- (notmuch-show-archive-thread-then-next)))
+ (when (notmuch-show-advance)
+ (notmuch-show-archive-thread-then-next)))
(defun notmuch-show-rewind ()
- "Backup through the thread (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
+ "Backup through the thread (reverse scrolling compared to \
+\\[notmuch-show-advance-and-archive]).
Specifically, if the beginning of the previous email is fewer
than `window-height' lines from the current point, move to it
@@ -1616,9 +1859,9 @@ any effects from previous calls to
(let ((start-of-message (notmuch-show-message-top))
(start-of-window (window-start)))
(cond
- ;; Either this message is properly aligned with the start of the
- ;; window or the start of this message is not visible on the
- ;; screen - scroll.
+ ;; Either this message is properly aligned with the start of the
+ ;; window or the start of this message is not visible on the
+ ;; screen - scroll.
((or (= start-of-message start-of-window)
(< start-of-message start-of-window))
(scroll-down)
@@ -1652,8 +1895,26 @@ any effects from previous calls to
(defun notmuch-show-forward-message (&optional prompt-for-sender)
"Forward the current message."
(interactive "P")
- (with-current-notmuch-show-message
- (notmuch-mua-new-forward-message prompt-for-sender)))
+ (notmuch-mua-new-forward-messages (list (notmuch-show-get-message-id))
+ prompt-for-sender))
+
+(put 'notmuch-show-forward-open-messages 'notmuch-prefix-doc
+ "... and prompt for sender")
+(defun notmuch-show-forward-open-messages (&optional prompt-for-sender)
+ "Forward the currently open messages."
+ (interactive "P")
+ (let ((open-messages (notmuch-show-get-message-ids-for-open-messages)))
+ (unless open-messages
+ (error "No open messages to forward."))
+ (notmuch-mua-new-forward-messages open-messages prompt-for-sender)))
+
+(defun notmuch-show-resend-message (addresses)
+ "Resend the current message."
+ (interactive (list (notmuch-address-from-minibuffer "Resend to: ")))
+ (when (y-or-n-p (concat "Confirm resend to " addresses " "))
+ (notmuch-show-view-raw-message)
+ (message-resend addresses)
+ (notmuch-bury-or-kill-this-buffer)))
(defun notmuch-show-next-message (&optional pop-at-end)
"Show the next message.
@@ -1662,9 +1923,7 @@ If a prefix argument is given and this is the last message in the
thread, navigate to the next thread in the parent search buffer."
(interactive "P")
(if (notmuch-show-goto-message-next)
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(if pop-at-end
(notmuch-show-next-thread)
(goto-char (point-max)))))
@@ -1675,7 +1934,6 @@ thread, navigate to the next thread in the parent search buffer."
(if (= (point) (notmuch-show-message-top))
(notmuch-show-goto-message-previous)
(notmuch-show-move-to-message-top))
- (notmuch-show-mark-read)
(notmuch-show-message-adjust))
(defun notmuch-show-next-open-message (&optional pop-at-end)
@@ -1690,9 +1948,7 @@ to show, nil otherwise."
(while (and (setq r (notmuch-show-goto-message-next))
(not (notmuch-show-message-visible-p))))
(if r
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(if pop-at-end
(notmuch-show-next-thread)
(goto-char (point-max))))
@@ -1705,9 +1961,7 @@ to show, nil otherwise."
(while (and (setq r (notmuch-show-goto-message-next))
(not (notmuch-show-get-prop :match))))
(if r
- (progn
- (notmuch-show-mark-read)
- (notmuch-show-message-adjust))
+ (notmuch-show-message-adjust)
(goto-char (point-max)))))
(defun notmuch-show-open-if-matched ()
@@ -1716,10 +1970,9 @@ to show, nil otherwise."
(notmuch-show-message-visible props (plist-get props :match))))
(defun notmuch-show-goto-first-wanted-message ()
- "Move to the first open message and mark it read"
+ "Move to the first open message and mark it read."
(goto-char (point-min))
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
+ (unless (notmuch-show-message-visible-p)
(notmuch-show-next-open-message))
(when (eobp)
;; There are no matched non-excluded messages so open all matched
@@ -1727,8 +1980,7 @@ to show, nil otherwise."
(notmuch-show-mapc 'notmuch-show-open-if-matched)
(force-window-update)
(goto-char (point-min))
- (if (notmuch-show-message-visible-p)
- (notmuch-show-mark-read)
+ (unless (notmuch-show-message-visible-p)
(notmuch-show-next-open-message))))
(defun notmuch-show-previous-open-message ()
@@ -1738,20 +1990,28 @@ to show, nil otherwise."
(notmuch-show-goto-message-previous)
(notmuch-show-move-to-message-top))
(not (notmuch-show-message-visible-p))))
- (notmuch-show-mark-read)
(notmuch-show-message-adjust))
(defun notmuch-show-view-raw-message ()
- "View the file holding the current message."
+ "View the original source of the current message."
(interactive)
(let* ((id (notmuch-show-get-message-id))
- (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
- (call-process notmuch-command nil buf nil "show" "--format=raw" id)
+ (buf (get-buffer-create (concat "*notmuch-raw-" id "*")))
+ (inhibit-read-only t))
(switch-to-buffer buf)
+ (erase-buffer)
+ (let ((coding-system-for-read 'no-conversion))
+ (call-process notmuch-command nil t nil "show" "--format=raw" id))
(goto-char (point-min))
(set-buffer-modified-p nil)
+ (setq buffer-read-only t)
(view-buffer buf 'kill-buffer-if-not-modified)))
+(defun notmuch-show-resume-message ()
+ "Resume EDITING the current draft message."
+ (interactive)
+ (notmuch-draft-resume (notmuch-show-get-message-id)))
+
(put 'notmuch-show-pipe-message 'notmuch-doc
"Pipe the contents of the current message to a command.")
(put 'notmuch-show-pipe-message 'notmuch-prefix-doc
@@ -1776,15 +2036,22 @@ message."
(setq shell-command
(concat notmuch-command " show --format=mbox --exclude=false "
(shell-quote-argument
- (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
+ (mapconcat 'identity
+ (notmuch-show-get-message-ids-for-open-messages)
+ " OR "))
" | " command))
(setq shell-command
(concat notmuch-command " show --format=raw "
- (shell-quote-argument (notmuch-show-get-message-id)) " | " command)))
- (let ((buf (get-buffer-create (concat "*notmuch-pipe*"))))
+ (shell-quote-argument (notmuch-show-get-message-id))
+ " | " command)))
+ (let ((cwd default-directory)
+ (buf (get-buffer-create (concat "*notmuch-pipe*"))))
(with-current-buffer buf
(setq buffer-read-only nil)
(erase-buffer)
+ ;; Use the originating buffer's working directory instead of
+ ;; that of the pipe buffer.
+ (cd cwd)
(let ((exit-code (call-process-shell-command shell-command nil buf)))
(goto-char (point-max))
(set-buffer-modified-p nil)
@@ -1877,9 +2144,10 @@ argument, hide all of the messages."
(interactive)
(save-excursion
(goto-char (point-min))
- (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
- (not current-prefix-arg))
- until (not (notmuch-show-goto-message-next))))
+ (cl-loop do (notmuch-show-message-visible
+ (notmuch-show-get-message-properties)
+ (not current-prefix-arg))
+ until (not (notmuch-show-goto-message-next))))
(force-window-update))
(defun notmuch-show-next-button ()
@@ -1901,7 +2169,7 @@ buffer. If PREVIOUS is non-nil, move to the previous item in the
search results instead."
(interactive "P")
(let ((parent-buffer notmuch-show-parent-buffer))
- (notmuch-kill-this-buffer)
+ (notmuch-bury-or-kill-this-buffer)
(when (buffer-live-p parent-buffer)
(switch-to-buffer parent-buffer)
(and (if previous
@@ -1966,7 +2234,7 @@ message will be \"unarchived\", i.e. the tag changes in
(notmuch-tag-change-list notmuch-archive-tags unarchive))))
(defun notmuch-show-archive-message-then-next-or-exit ()
- "Archive the current message, then show the next open message in the current thread.
+ "Archive current message, then show next open message in current thread.
If at the last open message in the current thread, then exit back
to search results."
@@ -1975,7 +2243,7 @@ to search results."
(notmuch-show-next-open-message t))
(defun notmuch-show-archive-message-then-next-or-next-thread ()
- "Archive the current message, then show the next open message in the current thread.
+ "Archive current message, then show next open message in current or next thread.
If at the last open message in the current thread, then show next
thread from search."
@@ -1989,10 +2257,17 @@ thread from search."
(interactive)
(notmuch-common-do-stash (notmuch-show-get-cc)))
-(defun notmuch-show-stash-date ()
- "Copy date of current message to kill-ring."
- (interactive)
- (notmuch-common-do-stash (notmuch-show-get-date)))
+(put 'notmuch-show-stash-date 'notmuch-prefix-doc
+ "Copy timestamp of current message to kill-ring.")
+(defun notmuch-show-stash-date (&optional stash-timestamp)
+ "Copy date of current message to kill-ring.
+
+If invoked with a prefix argument, copy timestamp of current
+message to kill-ring."
+ (interactive "P")
+ (if stash-timestamp
+ (notmuch-common-do-stash (format "%d" (notmuch-show-get-timestamp)))
+ (notmuch-common-do-stash (notmuch-show-get-date))))
(defun notmuch-show-stash-filename ()
"Copy filename of current message to kill-ring."
@@ -2045,16 +2320,19 @@ This presumes that the message is available at the selected Mailing List Archive
If optional argument MLA is non-nil, use the provided key instead of prompting
the user (see `notmuch-show-stash-mlarchive-link-alist')."
(interactive)
- (notmuch-common-do-stash
- (concat (cdr (assoc
- (or mla
- (let ((completion-ignore-case t))
- (completing-read
- "Mailing List Archive: "
- notmuch-show-stash-mlarchive-link-alist
- nil t nil nil notmuch-show-stash-mlarchive-link-default)))
- notmuch-show-stash-mlarchive-link-alist))
- (notmuch-show-get-message-id t))))
+ (let ((url (cdr (assoc
+ (or mla
+ (let ((completion-ignore-case t))
+ (completing-read
+ "Mailing List Archive: "
+ notmuch-show-stash-mlarchive-link-alist
+ nil t nil nil
+ notmuch-show-stash-mlarchive-link-default)))
+ notmuch-show-stash-mlarchive-link-alist))))
+ (notmuch-common-do-stash
+ (if (functionp url)
+ (funcall url (notmuch-show-get-message-id t))
+ (concat url (notmuch-show-get-message-id t))))))
(defun notmuch-show-stash-mlarchive-link-and-go (&optional mla)
"Copy an ML Archive URI for the current message to the kill-ring and visit it.
@@ -2067,39 +2345,76 @@ the user (see `notmuch-show-stash-mlarchive-link-alist')."
(notmuch-show-stash-mlarchive-link mla)
(browse-url (current-kill 0 t)))
+(defun notmuch-show-stash-git-helper (addresses prefix)
+ "Escape, trim, quote, and add PREFIX to each address in list of ADDRESSES, and return the result as a single string."
+ (mapconcat (lambda (x)
+ (concat prefix "\""
+ ;; escape double-quotes
+ (replace-regexp-in-string
+ "\"" "\\\\\""
+ ;; trim leading and trailing spaces
+ (replace-regexp-in-string
+ "\\(^ *\\| *$\\)" ""
+ x)) "\""))
+ addresses " "))
+
+(put 'notmuch-show-stash-git-send-email 'notmuch-prefix-doc
+ "Copy From/To/Cc of current message to kill-ring in a form suitable for pasting to git send-email command line.")
+
+(defun notmuch-show-stash-git-send-email (&optional no-in-reply-to)
+ "Copy From/To/Cc/Message-Id of current message to kill-ring in a form suitable for pasting to git send-email command line.
+
+If invoked with a prefix argument (or NO-IN-REPLY-TO is non-nil),
+omit --in-reply-to=."
+ (interactive "P")
+ (notmuch-common-do-stash
+ (mapconcat 'identity
+ (remove ""
+ (list
+ (notmuch-show-stash-git-helper
+ (message-tokenize-header (notmuch-show-get-from)) "--to=")
+ (notmuch-show-stash-git-helper
+ (message-tokenize-header (notmuch-show-get-to)) "--to=")
+ (notmuch-show-stash-git-helper
+ (message-tokenize-header (notmuch-show-get-cc)) "--cc=")
+ (unless no-in-reply-to
+ (notmuch-show-stash-git-helper
+ (list (notmuch-show-get-message-id t)) "--in-reply-to="))))
+ " ")))
+
;; Interactive part functions and their helpers
-(defun notmuch-show-generate-part-buffer (message-id nth)
+(defun notmuch-show-generate-part-buffer (msg part)
"Return a temporary buffer containing the specified part's content."
(let ((buf (generate-new-buffer " *notmuch-part*"))
(process-crypto notmuch-show-process-crypto))
(with-current-buffer buf
- (setq notmuch-show-process-crypto process-crypto)
- ;; Always acquires the part via `notmuch part', even if it is
- ;; available in the SEXP output.
- (insert (notmuch-get-bodypart-internal message-id nth notmuch-show-process-crypto)))
+ ;; This is always used in the content of mm handles, which
+ ;; expect undecoded, binary part content.
+ (insert (notmuch-get-bodypart-binary msg part process-crypto)))
buf))
-(defun notmuch-show-current-part-handle ()
+(defun notmuch-show-current-part-handle (&optional mime-type)
"Return an mm-handle for the part containing point.
This creates a temporary buffer for the part's content; the
-caller is responsible for killing this buffer as appropriate."
- (let* ((part (notmuch-show-get-part-properties))
- (message-id (notmuch-show-get-message-id))
- (nth (plist-get part :id))
- (buf (notmuch-show-generate-part-buffer message-id nth))
- (computed-type (plist-get part :computed-type))
+caller is responsible for killing this buffer as appropriate. If
+MIME-TYPE is given then set the handle's mime-type to MIME-TYPE."
+ (let* ((msg (notmuch-show-get-message-properties))
+ (part (notmuch-show-get-part-properties))
+ (buf (notmuch-show-generate-part-buffer msg part))
+ (computed-type (or mime-type (plist-get part :computed-type)))
(filename (plist-get part :filename))
- (disposition (if filename `(attachment (filename . ,filename)))))
+ (disposition (and filename `(attachment (filename . ,filename)))))
(mm-make-handle buf (list computed-type) nil nil disposition)))
-(defun notmuch-show-apply-to-current-part-handle (fn)
+(defun notmuch-show-apply-to-current-part-handle (fn &optional mime-type)
"Apply FN to an mm-handle for the part containing point.
This ensures that the temporary buffer created for the mm-handle
-is destroyed when FN returns."
- (let ((handle (notmuch-show-current-part-handle)))
+is destroyed when FN returns. If MIME-TYPE is given then force
+part to be treated as if it had that mime-type."
+ (let ((handle (notmuch-show-current-part-handle mime-type)))
;; emacs 24.3+ puts stdout/stderr into the calling buffer so we
;; call it from a temp-buffer, unless
;; notmuch-show-attachment-debug is non-nil in which case we put
@@ -2143,5 +2458,78 @@ is destroyed when FN returns."
(interactive)
(notmuch-show-apply-to-current-part-handle #'mm-pipe-part))
+(defun notmuch-show--mm-display-part (handle)
+ "Use mm-display-part to display HANDLE in a new buffer.
+
+If the part is displayed in an external application then close
+the new buffer."
+ (let ((buf (get-buffer-create (generate-new-buffer-name
+ (concat " *notmuch-internal-part*")))))
+ (switch-to-buffer buf)
+ (if (eq (mm-display-part handle) 'external)
+ (kill-buffer buf)
+ (goto-char (point-min))
+ (set-buffer-modified-p nil)
+ (view-buffer buf 'kill-buffer-if-not-modified))))
+
+(defun notmuch-show-choose-mime-of-part (mime-type)
+ "Choose the mime type to use for displaying part."
+ (interactive
+ (list (completing-read "Mime type to use (default text/plain): "
+ (mailcap-mime-types) nil nil nil nil "text/plain")))
+ (notmuch-show-apply-to-current-part-handle #'notmuch-show--mm-display-part
+ mime-type))
+
+(defun notmuch-show-imenu-prev-index-position-function ()
+ "Move point to previous message in notmuch-show buffer.
+This function is used as a value for
+`imenu-prev-index-position-function'."
+ (if (bobp)
+ nil
+ (notmuch-show-previous-message)
+ t))
+
+(defun notmuch-show-imenu-extract-index-name-function ()
+ "Return imenu name for line at point.
+This function is used as a value for
+`imenu-extract-index-name-function'. Point should be at the
+beginning of the line."
+ (back-to-indentation)
+ (buffer-substring-no-properties (if notmuch-show-imenu-indent
+ (line-beginning-position)
+ (point))
+ (line-end-position)))
+
+(defmacro notmuch-show--with-currently-shown-message (&rest body)
+ "Evaluate BODY with display restricted to the currently shown
+message."
+ `(save-excursion
+ (save-restriction
+ (let ((extent (notmuch-show-message-extent)))
+ (narrow-to-region (car extent) (cdr extent))
+ ,@body))))
+
+(defun notmuch-show--gather-urls ()
+ "Gather any URLs in the current message."
+ (notmuch-show--with-currently-shown-message
+ (let (urls)
+ (goto-char (point-min))
+ (while (re-search-forward goto-address-url-regexp (point-max) t)
+ (push (match-string-no-properties 0) urls))
+ (reverse urls))))
+
+(defun notmuch-show-browse-urls (&optional kill)
+ "Offer to browse any URLs in the current message.
+With a prefix argument, copy the URL to the kill ring rather than
+browsing."
+ (interactive "P")
+ (let ((urls (notmuch-show--gather-urls))
+ (prompt (if kill "Copy URL to kill ring: " "Browse URL: "))
+ (fn (if kill #'kill-new #'browse-url)))
+ (if urls
+ (funcall fn (completing-read prompt urls nil nil nil nil (car urls)))
+ (message "No URLs found."))))
(provide 'notmuch-show)
+
+;;; notmuch-show.el ends here