aa9cceedd0e5c9dbcb96e1505d54f23cf80e409e
[notmuch] / emacs / notmuch-show.el
1 ;; notmuch-show.el --- displaying notmuch forests.
2 ;;
3 ;; Copyright © Carl Worth
4 ;; Copyright © David Edmondson
5 ;;
6 ;; This file is part of Notmuch.
7 ;;
8 ;; Notmuch is free software: you can redistribute it and/or modify it
9 ;; under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
12 ;;
13 ;; Notmuch is distributed in the hope that it will be useful, but
14 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 ;; General Public License for more details.
17 ;;
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
20 ;;
21 ;; Authors: Carl Worth <cworth@cworth.org>
22 ;;          David Edmondson <dme@dme.org>
23
24 (eval-when-compile (require 'cl))
25 (require 'mm-view)
26 (require 'message)
27 (require 'mm-decode)
28 (require 'mailcap)
29 (require 'icalendar)
30 (require 'goto-addr)
31
32 (require 'notmuch-lib)
33 (require 'notmuch-query)
34 (require 'notmuch-wash)
35 (require 'notmuch-mua)
36 (require 'notmuch-crypto)
37 (require 'notmuch-print)
38
39 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
40 (declare-function notmuch-fontify-headers "notmuch" nil)
41 (declare-function notmuch-read-tag-changes "notmuch" (&optional initial-input &rest search-terms))
42 (declare-function notmuch-search-next-thread "notmuch" nil)
43 (declare-function notmuch-search-show-thread "notmuch" nil)
44 (declare-function notmuch-update-tags "notmuch" (current-tags tag-changes))
45
46 (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
47   "Headers that should be shown in a message, in this order.
48
49 For an open message, all of these headers will be made visible
50 according to `notmuch-message-headers-visible' or can be toggled
51 with `notmuch-show-toggle-headers'. For a closed message, only
52 the first header in the list will be visible."
53   :type '(repeat string)
54   :group 'notmuch-show)
55
56 (defcustom notmuch-message-headers-visible t
57   "Should the headers be visible by default?
58
59 If this value is non-nil, then all of the headers defined in
60 `notmuch-message-headers' will be visible by default in the display
61 of each message. Otherwise, these headers will be hidden and
62 `notmuch-show-toggle-headers' can be used to make the visible for
63 any given message."
64   :type 'boolean
65   :group 'notmuch-show)
66
67 (defcustom notmuch-show-relative-dates t
68   "Display relative dates in the message summary line."
69   :type 'boolean
70   :group 'notmuch-show)
71
72 (defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
73   "A list of functions called to decorate the headers listed in
74 `notmuch-message-headers'.")
75
76 (defcustom notmuch-show-hook '(notmuch-show-turn-on-visual-line-mode)
77   "Functions called after populating a `notmuch-show' buffer."
78   :type 'hook
79   :options '(notmuch-show-turn-on-visual-line-mode)
80   :group 'notmuch-show
81   :group 'notmuch-hooks)
82
83 (defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-wrap-long-lines
84                                                  notmuch-wash-tidy-citations
85                                                  notmuch-wash-elide-blank-lines
86                                                  notmuch-wash-excerpt-citations)
87   "Functions used to improve the display of text/plain parts."
88   :type 'hook
89   :options '(notmuch-wash-convert-inline-patch-to-part
90              notmuch-wash-wrap-long-lines
91              notmuch-wash-tidy-citations
92              notmuch-wash-elide-blank-lines
93              notmuch-wash-excerpt-citations)
94   :group 'notmuch-show
95   :group 'notmuch-hooks)
96
97 ;; Mostly useful for debugging.
98 (defcustom notmuch-show-all-multipart/alternative-parts t
99   "Should all parts of multipart/alternative parts be shown?"
100   :type 'boolean
101   :group 'notmuch-show)
102
103 (defcustom notmuch-show-indent-messages-width 1
104   "Width of message indentation in threads.
105
106 Messages are shown indented according to their depth in a thread.
107 This variable determines the width of this indentation measured
108 in number of blanks.  Defaults to `1', choose `0' to disable
109 indentation."
110   :type 'integer
111   :group 'notmuch-show)
112
113 (defcustom notmuch-show-indent-multipart nil
114   "Should the sub-parts of a multipart/* part be indented?"
115   ;; dme: Not sure which is a good default.
116   :type 'boolean
117   :group 'notmuch-show)
118
119 (defcustom notmuch-show-part-button-default-action 'notmuch-show-save-part
120   "Default part header button action (on ENTER or mouse click)."
121   :group 'notmuch-show
122   :type '(choice (const :tag "Save part"
123                         notmuch-show-save-part)
124                  (const :tag "View part"
125                         notmuch-show-view-part)
126                  (const :tag "View interactively"
127                         notmuch-show-interactively-view-part)))
128
129 (defcustom notmuch-show-only-matching-messages nil
130   "Only matching messages are shown by default."
131   :type 'boolean
132   :group 'notmuch-show)
133
134 (defvar notmuch-show-thread-id nil)
135 (make-variable-buffer-local 'notmuch-show-thread-id)
136 (put 'notmuch-show-thread-id 'permanent-local t)
137
138 (defvar notmuch-show-parent-buffer nil)
139 (make-variable-buffer-local 'notmuch-show-parent-buffer)
140 (put 'notmuch-show-parent-buffer 'permanent-local t)
141
142 (defvar notmuch-show-query-context nil)
143 (make-variable-buffer-local 'notmuch-show-query-context)
144 (put 'notmuch-show-query-context 'permanent-local t)
145
146 (defvar notmuch-show-process-crypto nil)
147 (make-variable-buffer-local 'notmuch-show-process-crypto)
148 (put 'notmuch-show-process-crypto 'permanent-local t)
149
150 (defvar notmuch-show-elide-non-matching-messages nil)
151 (make-variable-buffer-local 'notmuch-show-elide-non-matching-messages)
152 (put 'notmuch-show-elide-non-matching-messages 'permanent-local t)
153
154 (defvar notmuch-show-indent-content t)
155 (make-variable-buffer-local 'notmuch-show-indent-content)
156 (put 'notmuch-show-indent-content 'permanent-local t)
157
158 (defmacro with-current-notmuch-show-message (&rest body)
159   "Evaluate body with current buffer set to the text of current message"
160   `(save-excursion
161      (let ((id (notmuch-show-get-message-id)))
162        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
163          (with-current-buffer buf
164             (call-process notmuch-command nil t nil "show" "--format=raw" id)
165            ,@body)
166          (kill-buffer buf)))))
167
168 (defun notmuch-show-turn-on-visual-line-mode ()
169   "Enable Visual Line mode."
170   (visual-line-mode t))
171
172 (defun notmuch-show-view-all-mime-parts ()
173   "Use external viewers to view all attachments from the current message."
174   (interactive)
175   (with-current-notmuch-show-message
176    ;; We override the mm-inline-media-tests to indicate which message
177    ;; parts are already sufficiently handled by the original
178    ;; presentation of the message in notmuch-show mode. These parts
179    ;; will be inserted directly into the temporary buffer of
180    ;; with-current-notmuch-show-message and silently discarded.
181    ;;
182    ;; Any MIME part not explicitly mentioned here will be handled by an
183    ;; external viewer as configured in the various mailcap files.
184    (let ((mm-inline-media-tests '(
185                                   ("text/.*" ignore identity)
186                                   ("application/pgp-signature" ignore identity)
187                                   ("multipart/alternative" ignore identity)
188                                   ("multipart/mixed" ignore identity)
189                                   ("multipart/related" ignore identity)
190                                  )))
191      (mm-display-parts (mm-dissect-buffer)))))
192
193 (defun notmuch-foreach-mime-part (function mm-handle)
194   (cond ((stringp (car mm-handle))
195          (dolist (part (cdr mm-handle))
196            (notmuch-foreach-mime-part function part)))
197         ((bufferp (car mm-handle))
198          (funcall function mm-handle))
199         (t (dolist (part mm-handle)
200              (notmuch-foreach-mime-part function part)))))
201
202 (defun notmuch-count-attachments (mm-handle)
203   (let ((count 0))
204     (notmuch-foreach-mime-part
205      (lambda (p)
206        (let ((disposition (mm-handle-disposition p)))
207          (and (listp disposition)
208               (or (equal (car disposition) "attachment")
209                   (and (equal (car disposition) "inline")
210                        (assq 'filename disposition)))
211               (incf count))))
212      mm-handle)
213     count))
214
215 (defun notmuch-save-attachments (mm-handle &optional queryp)
216   (notmuch-foreach-mime-part
217    (lambda (p)
218      (let ((disposition (mm-handle-disposition p)))
219        (and (listp disposition)
220             (or (equal (car disposition) "attachment")
221                 (and (equal (car disposition) "inline")
222                      (assq 'filename disposition)))
223             (or (not queryp)
224                 (y-or-n-p
225                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
226             (mm-save-part p))))
227    mm-handle))
228
229 (defun notmuch-show-save-attachments ()
230   "Save all attachments from the current message."
231   (interactive)
232   (with-current-notmuch-show-message
233    (let ((mm-handle (mm-dissect-buffer)))
234      (notmuch-save-attachments
235       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
236   (message "Done"))
237
238 (defun notmuch-show-with-message-as-text (fn)
239   "Apply FN to a text representation of the current message.
240
241 FN is called with one argument, the message properties. It should
242 operation on the contents of the current buffer."
243
244   ;; Remake the header to ensure that all information is available.
245   (let* ((to (notmuch-show-get-to))
246          (cc (notmuch-show-get-cc))
247          (from (notmuch-show-get-from))
248          (subject (notmuch-show-get-subject))
249          (date (notmuch-show-get-date))
250          (tags (notmuch-show-get-tags))
251          (depth (notmuch-show-get-depth))
252
253          (header (concat
254                   "Subject: " subject "\n"
255                   "To: " to "\n"
256                   (if (not (string= cc ""))
257                       (concat "Cc: " cc "\n")
258                     "")
259                   "From: " from "\n"
260                   "Date: " date "\n"
261                   (if tags
262                       (concat "Tags: "
263                               (mapconcat #'identity tags ", ") "\n")
264                     "")))
265          (all (buffer-substring (notmuch-show-message-top)
266                                 (notmuch-show-message-bottom)))
267
268          (props (notmuch-show-get-message-properties))
269          (indenting notmuch-show-indent-content))
270     (with-temp-buffer
271       (insert all)
272       (if indenting
273           (indent-rigidly (point-min) (point-max) (- depth)))
274       ;; Remove the original header.
275       (goto-char (point-min))
276       (re-search-forward "^$" (point-max) nil)
277       (delete-region (point-min) (point))
278       (insert header)
279       (funcall fn props))))
280
281 (defun notmuch-show-print-message ()
282   "Print the current message."
283   (interactive)
284   (notmuch-show-with-message-as-text 'notmuch-print-message))
285
286 (defun notmuch-show-fontify-header ()
287   (let ((face (cond
288                ((looking-at "[Tt]o:")
289                 'message-header-to)
290                ((looking-at "[Bb]?[Cc][Cc]:")
291                 'message-header-cc)
292                ((looking-at "[Ss]ubject:")
293                 'message-header-subject)
294                ((looking-at "[Ff]rom:")
295                 'message-header-from)
296                (t
297                 'message-header-other))))
298
299     (overlay-put (make-overlay (point) (re-search-forward ":"))
300                  'face 'message-header-name)
301     (overlay-put (make-overlay (point) (re-search-forward ".*$"))
302                  'face face)))
303
304 (defun notmuch-show-colour-headers ()
305   "Apply some colouring to the current headers."
306   (goto-char (point-min))
307   (while (looking-at "^[A-Za-z][-A-Za-z0-9]*:")
308     (notmuch-show-fontify-header)
309     (forward-line)))
310
311 (defun notmuch-show-spaces-n (n)
312   "Return a string comprised of `n' spaces."
313   (make-string n ? ))
314
315 (defun notmuch-show-update-tags (tags)
316   "Update the displayed tags of the current message."
317   (save-excursion
318     (goto-char (notmuch-show-message-top))
319     (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
320         (let ((inhibit-read-only t))
321           (replace-match (concat "("
322                                  (propertize (mapconcat 'identity tags " ")
323                                              'face 'notmuch-tag-face)
324                                  ")"))))))
325
326 (defun notmuch-show-clean-address (address)
327   "Try to clean a single email ADDRESS for display.  Return
328 unchanged ADDRESS if parsing fails."
329   (condition-case nil
330     (let (p-name p-address)
331       ;; It would be convenient to use `mail-header-parse-address',
332       ;; but that expects un-decoded mailbox parts, whereas our
333       ;; mailbox parts are already decoded (and hence may contain
334       ;; UTF-8). Given that notmuch should handle most of the awkward
335       ;; cases, some simple string deconstruction should be sufficient
336       ;; here.
337       (cond
338        ;; "User <user@dom.ain>" style.
339        ((string-match "\\(.*\\) <\\(.*\\)>" address)
340         (setq p-name (match-string 1 address)
341               p-address (match-string 2 address)))
342
343        ;; "<user@dom.ain>" style.
344        ((string-match "<\\(.*\\)>" address)
345         (setq p-address (match-string 1 address)))
346
347        ;; Everything else.
348        (t
349         (setq p-address address)))
350
351       (when p-name
352         ;; Remove elements of the mailbox part that are not relevant for
353         ;; display, even if they are required during transport:
354         ;;
355         ;; Backslashes.
356         (setq p-name (replace-regexp-in-string "\\\\" "" p-name))
357
358         ;; Outer single and double quotes, which might be nested.
359         (loop
360          with start-of-loop
361          do (setq start-of-loop p-name)
362
363          when (string-match "^\"\\(.*\\)\"$" p-name)
364          do (setq p-name (match-string 1 p-name))
365
366          when (string-match "^'\\(.*\\)'$" p-name)
367          do (setq p-name (match-string 1 p-name))
368
369          until (string= start-of-loop p-name)))
370
371       ;; If the address is 'foo@bar.com <foo@bar.com>' then show just
372       ;; 'foo@bar.com'.
373       (when (string= p-name p-address)
374         (setq p-name nil))
375
376       ;; If no name results, return just the address.
377       (if (not p-name)
378           p-address
379         ;; Otherwise format the name and address together.
380         (concat p-name " <" p-address ">")))
381     (error address)))
382
383 (defun notmuch-show-insert-headerline (headers date tags depth)
384   "Insert a notmuch style headerline based on HEADERS for a
385 message at DEPTH in the current thread."
386   (let ((start (point)))
387     (insert (notmuch-show-spaces-n (* notmuch-show-indent-messages-width depth))
388             (notmuch-show-clean-address (plist-get headers :From))
389             " ("
390             date
391             ") ("
392             (propertize (mapconcat 'identity tags " ")
393                         'face 'notmuch-tag-face)
394             ")\n")
395     (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face)))
396
397 (defun notmuch-show-insert-header (header header-value)
398   "Insert a single header."
399   (insert header ": " header-value "\n"))
400
401 (defun notmuch-show-insert-headers (headers)
402   "Insert the headers of the current message."
403   (let ((start (point)))
404     (mapc (lambda (header)
405             (let* ((header-symbol (intern (concat ":" header)))
406                    (header-value (plist-get headers header-symbol)))
407               (if (and header-value
408                        (not (string-equal "" header-value)))
409                   (notmuch-show-insert-header header header-value))))
410           notmuch-message-headers)
411     (save-excursion
412       (save-restriction
413         (narrow-to-region start (point-max))
414         (run-hooks 'notmuch-show-markup-headers-hook)))))
415
416 (define-button-type 'notmuch-show-part-button-type
417   'action 'notmuch-show-part-button-default
418   'keymap 'notmuch-show-part-button-map
419   'follow-link t
420   'face 'message-mml)
421
422 (defvar notmuch-show-part-button-map
423   (let ((map (make-sparse-keymap)))
424     (set-keymap-parent map button-map)
425     (define-key map "s" 'notmuch-show-part-button-save)
426     (define-key map "v" 'notmuch-show-part-button-view)
427     (define-key map "o" 'notmuch-show-part-button-interactively-view)
428     map)
429   "Submap for button commands")
430 (fset 'notmuch-show-part-button-map notmuch-show-part-button-map)
431
432 (defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
433   (let ((button))
434     (setq button
435           (insert-button
436            (concat "[ "
437                    (if name (concat name ": ") "")
438                    declared-type
439                    (if (not (string-equal declared-type content-type))
440                        (concat " (as " content-type ")")
441                      "")
442                    (or comment "")
443                    " ]")
444            :type 'notmuch-show-part-button-type
445            :notmuch-part nth
446            :notmuch-filename name
447            :notmuch-content-type content-type))
448     (insert "\n")
449     ;; return button
450     button))
451
452 ;; Functions handling particular MIME parts.
453
454 (defmacro notmuch-with-temp-part-buffer (message-id nth &rest body)
455   (declare (indent 2))
456   (let ((process-crypto (make-symbol "process-crypto")))
457     `(let ((,process-crypto notmuch-show-process-crypto))
458        (with-temp-buffer
459          (setq notmuch-show-process-crypto ,process-crypto)
460          ;; Always acquires the part via `notmuch part', even if it is
461          ;; available in the JSON output.
462          (insert (notmuch-show-get-bodypart-internal ,message-id ,nth))
463          ,@body))))
464
465 (defun notmuch-show-save-part (message-id nth &optional filename content-type)
466   (notmuch-with-temp-part-buffer message-id nth
467     (let ((file (read-file-name
468                  "Filename to save as: "
469                  (or mailcap-download-directory "~/")
470                  nil nil
471                  filename)))
472       ;; Don't re-compress .gz & al.  Arguably we should make
473       ;; `file-name-handler-alist' nil, but that would chop
474       ;; ange-ftp, which is reasonable to use here.
475       (mm-write-region (point-min) (point-max) file nil nil nil 'no-conversion t))))
476
477 (defun notmuch-show-view-part (message-id nth &optional filename content-type )
478   (notmuch-with-temp-part-buffer message-id nth
479     ;; set mm-inlined-types to nil to force an external viewer
480     (let ((handle (mm-make-handle (current-buffer) (list content-type)))
481           (mm-inlined-types nil))
482       ;; We override mm-save-part as notmuch-show-save-part is better
483       ;; since it offers the filename. We need to lexically bind
484       ;; everything we need for notmuch-show-save-part to prevent
485       ;; potential dynamic shadowing.
486       (lexical-let ((message-id message-id)
487                     (nth nth)
488                     (filename filename)
489                     (content-type content-type))
490         (flet ((mm-save-part (&rest args) (notmuch-show-save-part
491                                            message-id nth filename content-type)))
492           (mm-display-part handle))))))
493
494 (defun notmuch-show-interactively-view-part (message-id nth &optional filename content-type)
495   (notmuch-with-temp-part-buffer message-id nth
496     (let ((handle (mm-make-handle (current-buffer) (list content-type))))
497       (mm-interactively-view-part handle))))
498
499 (defun notmuch-show-mm-display-part-inline (msg part nth content-type)
500   "Use the mm-decode/mm-view functions to display a part in the
501 current buffer, if possible."
502   (let ((display-buffer (current-buffer)))
503     (with-temp-buffer
504       (let* ((charset (plist-get part :content-charset))
505              (handle (mm-make-handle (current-buffer) `(,content-type (charset . ,charset)))))
506         ;; If the user wants the part inlined, insert the content and
507         ;; test whether we are able to inline it (which includes both
508         ;; capability and suitability tests).
509         (when (mm-inlined-p handle)
510           (insert (notmuch-show-get-bodypart-content msg part nth))
511           (when (mm-inlinable-p handle)
512             (set-buffer display-buffer)
513             (mm-display-part handle)
514             t))))))
515
516 (defvar notmuch-show-multipart/alternative-discouraged
517   '(
518     ;; Avoid HTML parts.
519     "text/html"
520     ;; multipart/related usually contain a text/html part and some associated graphics.
521     "multipart/related"
522     ))
523
524 (defun notmuch-show-multipart/*-to-list (part)
525   (mapcar (lambda (inner-part) (plist-get inner-part :content-type))
526           (plist-get part :content)))
527
528 (defun notmuch-show-multipart/alternative-choose (types)
529   ;; Based on `mm-preferred-alternative-precedence'.
530   (let ((seq types))
531     (dolist (pref (reverse notmuch-show-multipart/alternative-discouraged))
532       (dolist (elem (copy-sequence seq))
533         (when (string-match pref elem)
534           (setq seq (nconc (delete elem seq) (list elem))))))
535     seq))
536
537 (defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth declared-type)
538   (notmuch-show-insert-part-header nth declared-type content-type nil)
539   (let ((chosen-type (car (notmuch-show-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
540         (inner-parts (plist-get part :content))
541         (start (point)))
542     ;; This inserts all parts of the chosen type rather than just one,
543     ;; but it's not clear that this is the wrong thing to do - which
544     ;; should be chosen if there are more than one that match?
545     (mapc (lambda (inner-part)
546             (let ((inner-type (plist-get inner-part :content-type)))
547               (if (or notmuch-show-all-multipart/alternative-parts
548                       (string= chosen-type inner-type))
549                   (notmuch-show-insert-bodypart msg inner-part depth)
550                 (notmuch-show-insert-part-header (plist-get inner-part :id) inner-type inner-type nil " (not shown)"))))
551           inner-parts)
552
553     (when notmuch-show-indent-multipart
554       (indent-rigidly start (point) 1)))
555   t)
556
557 (defun notmuch-show-setup-w3m ()
558   "Instruct w3m how to retrieve content from a \"related\" part of a message."
559   (interactive)
560   (if (boundp 'w3m-cid-retrieve-function-alist)
561     (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)
562       (push (cons 'notmuch-show-mode 'notmuch-show-w3m-cid-retrieve)
563             w3m-cid-retrieve-function-alist)))
564   (setq mm-inline-text-html-with-images t))
565
566 (defvar w3m-current-buffer) ;; From `w3m.el'.
567 (defvar notmuch-show-w3m-cid-store nil)
568 (make-variable-buffer-local 'notmuch-show-w3m-cid-store)
569
570 (defun notmuch-show-w3m-cid-store-internal (content-id
571                                             message-id
572                                             part-number
573                                             content-type
574                                             content)
575   (push (list content-id
576               message-id
577               part-number
578               content-type
579               content)
580         notmuch-show-w3m-cid-store))
581
582 (defun notmuch-show-w3m-cid-store (msg part)
583   (let ((content-id (plist-get part :content-id)))
584     (when content-id
585       (notmuch-show-w3m-cid-store-internal (concat "cid:" content-id)
586                                            (plist-get msg :id)
587                                            (plist-get part :id)
588                                            (plist-get part :content-type)
589                                            nil))))
590
591 (defun notmuch-show-w3m-cid-retrieve (url &rest args)
592   (let ((matching-part (with-current-buffer w3m-current-buffer
593                          (assoc url notmuch-show-w3m-cid-store))))
594     (if matching-part
595         (let ((message-id (nth 1 matching-part))
596               (part-number (nth 2 matching-part))
597               (content-type (nth 3 matching-part))
598               (content (nth 4 matching-part)))
599           ;; If we don't already have the content, get it and cache
600           ;; it, as some messages reference the same cid: part many
601           ;; times (hundreds!), which results in many calls to
602           ;; `notmuch part'.
603           (unless content
604             (setq content (notmuch-show-get-bodypart-internal (concat "id:" message-id)
605                                                               part-number))
606             (with-current-buffer w3m-current-buffer
607               (notmuch-show-w3m-cid-store-internal url
608                                                    message-id
609                                                    part-number
610                                                    content-type
611                                                    content)))
612           (insert content)
613           content-type)
614       nil)))
615
616 (defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth declared-type)
617   (notmuch-show-insert-part-header nth declared-type content-type nil)
618   (let ((inner-parts (plist-get part :content))
619         (start (point)))
620
621     ;; We assume that the first part is text/html and the remainder
622     ;; things that it references.
623
624     ;; Stash the non-primary parts.
625     (mapc (lambda (part)
626             (notmuch-show-w3m-cid-store msg part))
627           (cdr inner-parts))
628
629     ;; Render the primary part.
630     (notmuch-show-insert-bodypart msg (car inner-parts) depth)
631
632     (when notmuch-show-indent-multipart
633       (indent-rigidly start (point) 1)))
634   t)
635
636 (defun notmuch-show-insert-part-multipart/signed (msg part content-type nth depth declared-type)
637   (let ((button (notmuch-show-insert-part-header nth declared-type content-type nil)))
638     (button-put button 'face 'notmuch-crypto-part-header)
639     ;; add signature status button if sigstatus provided
640     (if (plist-member part :sigstatus)
641         (let* ((from (notmuch-show-get-header :From msg))
642                (sigstatus (car (plist-get part :sigstatus))))
643           (notmuch-crypto-insert-sigstatus-button sigstatus from))
644       ;; if we're not adding sigstatus, tell the user how they can get it
645       (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts.")))
646
647   (let ((inner-parts (plist-get part :content))
648         (start (point)))
649     ;; Show all of the parts.
650     (mapc (lambda (inner-part)
651             (notmuch-show-insert-bodypart msg inner-part depth))
652           inner-parts)
653
654     (when notmuch-show-indent-multipart
655       (indent-rigidly start (point) 1)))
656   t)
657
658 (defun notmuch-show-insert-part-multipart/encrypted (msg part content-type nth depth declared-type)
659   (let ((button (notmuch-show-insert-part-header nth declared-type content-type nil)))
660     (button-put button 'face 'notmuch-crypto-part-header)
661     ;; add encryption status button if encstatus specified
662     (if (plist-member part :encstatus)
663         (let ((encstatus (car (plist-get part :encstatus))))
664           (notmuch-crypto-insert-encstatus-button encstatus)
665           ;; add signature status button if sigstatus specified
666           (if (plist-member part :sigstatus)
667               (let* ((from (notmuch-show-get-header :From msg))
668                      (sigstatus (car (plist-get part :sigstatus))))
669                 (notmuch-crypto-insert-sigstatus-button sigstatus from))))
670       ;; if we're not adding encstatus, tell the user how they can get it
671       (button-put button 'help-echo "Set notmuch-crypto-process-mime to process cryptographic MIME parts.")))
672
673   (let ((inner-parts (plist-get part :content))
674         (start (point)))
675     ;; Show all of the parts.
676     (mapc (lambda (inner-part)
677             (notmuch-show-insert-bodypart msg inner-part depth))
678           inner-parts)
679
680     (when notmuch-show-indent-multipart
681       (indent-rigidly start (point) 1)))
682   t)
683
684 (defun notmuch-show-insert-part-multipart/* (msg part content-type nth depth declared-type)
685   (notmuch-show-insert-part-header nth declared-type content-type nil)
686   (let ((inner-parts (plist-get part :content))
687         (start (point)))
688     ;; Show all of the parts.
689     (mapc (lambda (inner-part)
690             (notmuch-show-insert-bodypart msg inner-part depth))
691           inner-parts)
692
693     (when notmuch-show-indent-multipart
694       (indent-rigidly start (point) 1)))
695   t)
696
697 (defun notmuch-show-insert-part-message/rfc822 (msg part content-type nth depth declared-type)
698   (notmuch-show-insert-part-header nth declared-type content-type nil)
699   (let* ((message (car (plist-get part :content)))
700          (body (car (plist-get message :body)))
701          (start (point)))
702
703     ;; Override `notmuch-message-headers' to force `From' to be
704     ;; displayed.
705     (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
706       (notmuch-show-insert-headers (plist-get message :headers)))
707
708     ;; Blank line after headers to be compatible with the normal
709     ;; message display.
710     (insert "\n")
711
712     ;; Show the body
713     (notmuch-show-insert-bodypart msg body depth)
714
715     (when notmuch-show-indent-multipart
716       (indent-rigidly start (point) 1)))
717   t)
718
719 (defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type)
720   (let ((start (point)))
721     ;; If this text/plain part is not the first part in the message,
722     ;; insert a header to make this clear.
723     (if (> nth 1)
724         (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
725     (insert (notmuch-show-get-bodypart-content msg part nth))
726     (save-excursion
727       (save-restriction
728         (narrow-to-region start (point-max))
729         (run-hook-with-args 'notmuch-show-insert-text/plain-hook msg depth))))
730   t)
731
732 (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth declared-type)
733   (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
734   (insert (with-temp-buffer
735             (insert (notmuch-show-get-bodypart-content msg part nth))
736             (goto-char (point-min))
737             (let ((file (make-temp-file "notmuch-ical"))
738                   result)
739               (icalendar--convert-ical-to-diary
740                (icalendar--read-element nil nil)
741                file t)
742               (set-buffer (get-file-buffer file))
743               (setq result (buffer-substring (point-min) (point-max)))
744               (set-buffer-modified-p nil)
745               (kill-buffer (current-buffer))
746               (delete-file file)
747               result)))
748   t)
749
750 (defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type)
751   ;; If we can deduce a MIME type from the filename of the attachment,
752   ;; do so and pass it on to the handler for that type.
753   (if (plist-get part :filename)
754       (let ((extension (file-name-extension (plist-get part :filename)))
755             mime-type)
756         (if extension
757             (progn
758               (mailcap-parse-mimetypes)
759               (setq mime-type (mailcap-extension-to-mime extension))
760               (if (and mime-type
761                        (not (string-equal mime-type "application/octet-stream")))
762                   (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type)
763                 nil))
764           nil))))
765
766 ;; Handler for wash generated inline patch fake parts.
767 (defun notmuch-show-insert-part-inline-patch-fake-part (msg part content-type nth depth declared-type)
768   (notmuch-show-insert-part-*/* msg part "text/x-diff" nth depth "inline patch"))
769
770 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
771   ;; This handler _must_ succeed - it is the handler of last resort.
772   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
773   (notmuch-show-mm-display-part-inline msg part nth content-type)
774   t)
775
776 ;; Functions for determining how to handle MIME parts.
777
778 (defun notmuch-show-split-content-type (content-type)
779   (split-string content-type "/"))
780
781 (defun notmuch-show-handlers-for (content-type)
782   "Return a list of content handlers for a part of type CONTENT-TYPE."
783   (let (result)
784     (mapc (lambda (func)
785             (if (functionp func)
786                 (push func result)))
787           ;; Reverse order of prefrence.
788           (list (intern (concat "notmuch-show-insert-part-*/*"))
789                 (intern (concat
790                          "notmuch-show-insert-part-"
791                          (car (notmuch-show-split-content-type content-type))
792                          "/*"))
793                 (intern (concat "notmuch-show-insert-part-" content-type))))
794     result))
795
796 ;; Helper for parts which are generally not included in the default
797 ;; JSON output.
798 (defun notmuch-show-get-bodypart-internal (message-id part-number)
799   (let ((args '("show" "--format=raw"))
800         (part-arg (format "--part=%s" part-number)))
801     (setq args (append args (list part-arg)))
802     (if notmuch-show-process-crypto
803         (setq args (append args '("--decrypt"))))
804     (setq args (append args (list message-id)))
805     (with-temp-buffer
806       (let ((coding-system-for-read 'no-conversion))
807         (progn
808           (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
809           (buffer-string))))))
810
811 (defun notmuch-show-get-bodypart-content (msg part nth)
812   (or (plist-get part :content)
813       (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
814
815 ;; \f
816
817 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
818   (let ((handlers (notmuch-show-handlers-for content-type)))
819     ;; Run the content handlers until one of them returns a non-nil
820     ;; value.
821     (while (and handlers
822                 (not (funcall (car handlers) msg part content-type nth depth declared-type)))
823       (setq handlers (cdr handlers))))
824   t)
825
826 (defun notmuch-show-insert-bodypart (msg part depth)
827   "Insert the body part PART at depth DEPTH in the current thread."
828   (let ((content-type (downcase (plist-get part :content-type)))
829         (nth (plist-get part :id)))
830     (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
831   ;; Some of the body part handlers leave point somewhere up in the
832   ;; part, so we make sure that we're down at the end.
833   (goto-char (point-max))
834   ;; Ensure that the part ends with a carriage return.
835   (unless (bolp)
836     (insert "\n")))
837
838 (defun notmuch-show-insert-body (msg body depth)
839   "Insert the body BODY at depth DEPTH in the current thread."
840   (mapc (lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
841
842 (defun notmuch-show-make-symbol (type)
843   (make-symbol (concat "notmuch-show-" type)))
844
845 (defun notmuch-show-strip-re (string)
846   (replace-regexp-in-string "\\([Rr]e: *\\)+" "" string))
847
848 (defvar notmuch-show-previous-subject "")
849 (make-variable-buffer-local 'notmuch-show-previous-subject)
850
851 (defun notmuch-show-insert-msg (msg depth)
852   "Insert the message MSG at depth DEPTH in the current thread."
853   (let* ((headers (plist-get msg :headers))
854          ;; Indentation causes the buffer offset of the start/end
855          ;; points to move, so we must use markers.
856          message-start message-end
857          content-start content-end
858          headers-start headers-end
859          body-start body-end
860          (headers-invis-spec (notmuch-show-make-symbol "header"))
861          (message-invis-spec (notmuch-show-make-symbol "message"))
862          (bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
863
864     ;; Set `buffer-invisibility-spec' to `nil' (a list), otherwise
865     ;; removing items from `buffer-invisibility-spec' (which is what
866     ;; `notmuch-show-headers-visible' and
867     ;; `notmuch-show-message-visible' do) is a no-op and has no
868     ;; effect. This caused threads with only matching messages to have
869     ;; those messages hidden initially because
870     ;; `buffer-invisibility-spec' stayed `t'.
871     ;;
872     ;; This needs to be set here (rather than just above the call to
873     ;; `notmuch-show-headers-visible') because some of the part
874     ;; rendering or body washing functions
875     ;; (e.g. `notmuch-wash-text/plain-citations') manipulate
876     ;; `buffer-invisibility-spec').
877     (when (eq buffer-invisibility-spec t)
878       (setq buffer-invisibility-spec nil))
879
880     (setq message-start (point-marker))
881
882     (notmuch-show-insert-headerline headers
883                                     (or (if notmuch-show-relative-dates
884                                             (plist-get msg :date_relative)
885                                           nil)
886                                         (plist-get headers :Date))
887                                     (plist-get msg :tags) depth)
888
889     (setq content-start (point-marker))
890
891     (plist-put msg :headers-invis-spec headers-invis-spec)
892     (plist-put msg :message-invis-spec message-invis-spec)
893
894     ;; Set `headers-start' to point after the 'Subject:' header to be
895     ;; compatible with the existing implementation. This just sets it
896     ;; to after the first header.
897     (notmuch-show-insert-headers headers)
898     (save-excursion
899       (goto-char content-start)
900       ;; If the subject of this message is the same as that of the
901       ;; previous message, don't display it when this message is
902       ;; collapsed.
903       (when (not (string= notmuch-show-previous-subject
904                           bare-subject))
905         (forward-line 1))
906       (setq headers-start (point-marker)))
907     (setq headers-end (point-marker))
908
909     (setq notmuch-show-previous-subject bare-subject)
910
911     (setq body-start (point-marker))
912     ;; A blank line between the headers and the body.
913     (insert "\n")
914     (notmuch-show-insert-body msg (plist-get msg :body)
915                               (if notmuch-show-indent-content depth 0))
916     ;; Ensure that the body ends with a newline.
917     (unless (bolp)
918       (insert "\n"))
919     (setq body-end (point-marker))
920     (setq content-end (point-marker))
921
922     ;; Indent according to the depth in the thread.
923     (if notmuch-show-indent-content
924         (indent-rigidly content-start content-end (* notmuch-show-indent-messages-width depth)))
925
926     (setq message-end (point-max-marker))
927
928     ;; Save the extents of this message over the whole text of the
929     ;; message.
930     (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
931
932     (let ((headers-overlay (make-overlay headers-start headers-end))
933           (invis-specs (list headers-invis-spec message-invis-spec)))
934       (overlay-put headers-overlay 'invisible invis-specs)
935       (overlay-put headers-overlay 'priority 10))
936     (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
937
938     (plist-put msg :depth depth)
939
940     ;; Save the properties for this message. Currently this saves the
941     ;; entire message (augmented it with other stuff), which seems
942     ;; like overkill. We might save a reduced subset (for example, not
943     ;; the content).
944     (notmuch-show-set-message-properties msg)
945
946     ;; Set header visibility.
947     (notmuch-show-headers-visible msg notmuch-message-headers-visible)
948
949     ;; Message visibility depends on whether it matched the search
950     ;; criteria.
951     (notmuch-show-message-visible msg (plist-get msg :match))))
952
953 (defun notmuch-show-toggle-process-crypto ()
954   "Toggle the processing of cryptographic MIME parts."
955   (interactive)
956   (setq notmuch-show-process-crypto (not notmuch-show-process-crypto))
957   (message (if notmuch-show-process-crypto
958                "Processing cryptographic MIME parts."
959              "Not processing cryptographic MIME parts."))
960   (notmuch-show-refresh-view t))
961
962 (defun notmuch-show-toggle-elide-non-matching ()
963   "Toggle the display of non-matching messages."
964   (interactive)
965   (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages))
966   (message (if notmuch-show-elide-non-matching-messages
967                "Showing matching messages only."
968              "Showing all messages."))
969   (notmuch-show-refresh-view t))
970
971 (defun notmuch-show-toggle-thread-indentation ()
972   "Toggle the indentation of threads."
973   (interactive)
974   (setq notmuch-show-indent-content (not notmuch-show-indent-content))
975   (message (if notmuch-show-indent-content
976                "Content is indented."
977              "Content is not indented."))
978   (notmuch-show-refresh-view t))
979
980 (defun notmuch-show-insert-tree (tree depth)
981   "Insert the message tree TREE at depth DEPTH in the current thread."
982   (let ((msg (car tree))
983         (replies (cadr tree)))
984     (if (or (not notmuch-show-elide-non-matching-messages)
985             (plist-get msg :match))
986         (notmuch-show-insert-msg msg depth))
987     (notmuch-show-insert-thread replies (1+ depth))))
988
989 (defun notmuch-show-insert-thread (thread depth)
990   "Insert the thread THREAD at depth DEPTH in the current forest."
991   (mapc (lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
992
993 (defun notmuch-show-insert-forest (forest)
994   "Insert the forest of threads FOREST."
995   (mapc (lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
996
997 (defun notmuch-show-buttonise-links (start end)
998   "Buttonise URLs and mail addresses between START and END.
999
1000 This also turns id:\"<message id>\"-parts into buttons for
1001 a corresponding notmuch search."
1002   (goto-address-fontify-region start end)
1003   (save-excursion
1004     (goto-char start)
1005     (while (re-search-forward "id:\\(\"?\\)[^[:space:]\"]+\\1" end t)
1006       ;; remove the overlay created by goto-address-mode
1007       (remove-overlays (match-beginning 0) (match-end 0) 'goto-address t)
1008       (make-text-button (match-beginning 0) (match-end 0)
1009                         'action `(lambda (arg)
1010                                    (notmuch-show ,(match-string-no-properties 0)))
1011                         'follow-link t
1012                         'help-echo "Mouse-1, RET: search for this message"
1013                         'face goto-address-mail-face))))
1014
1015 ;;;###autoload
1016 (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name)
1017   "Run \"notmuch show\" with the given thread ID and display results.
1018
1019 The optional PARENT-BUFFER is the notmuch-search buffer from
1020 which this notmuch-show command was executed, (so that the
1021 next thread from that buffer can be show when done with this
1022 one).
1023
1024 The optional QUERY-CONTEXT is a notmuch search term. Only
1025 messages from the thread matching this search term are shown if
1026 non-nil.
1027
1028 The optional BUFFER-NAME provides the name of the buffer in
1029 which the message thread is shown. If it is nil (which occurs
1030 when the command is called interactively) the argument to the
1031 function is used."
1032   (interactive "sNotmuch show: ")
1033   (let ((buffer-name (generate-new-buffer-name
1034                       (or buffer-name
1035                           (concat "*notmuch-" thread-id "*")))))
1036     (switch-to-buffer (get-buffer-create buffer-name))
1037     ;; Set the default value for `notmuch-show-process-crypto' in this
1038     ;; buffer.
1039     (setq notmuch-show-process-crypto notmuch-crypto-process-mime)
1040     ;; Set the default value for
1041     ;; `notmuch-show-elide-non-matching-messages' in this buffer. If
1042     ;; there is a prefix argument, invert the default.
1043     (setq notmuch-show-elide-non-matching-messages notmuch-show-only-matching-messages)
1044     (if current-prefix-arg
1045         (setq notmuch-show-elide-non-matching-messages (not notmuch-show-elide-non-matching-messages)))
1046
1047     (setq notmuch-show-thread-id thread-id
1048           notmuch-show-parent-buffer parent-buffer
1049           notmuch-show-query-context query-context)
1050     (notmuch-show-worker)))
1051
1052 (defun notmuch-show-worker ()
1053   (let ((inhibit-read-only t))
1054
1055     (notmuch-show-mode)
1056     ;; Don't track undo information for this buffer
1057     (set 'buffer-undo-list t)
1058
1059     (erase-buffer)
1060     (goto-char (point-min))
1061     (save-excursion
1062       (let* ((basic-args (list notmuch-show-thread-id))
1063              (args (if notmuch-show-query-context
1064                        (append (list "\'") basic-args
1065                                (list "and (" notmuch-show-query-context ")\'"))
1066                      (append (list "\'") basic-args (list "\'")))))
1067         (notmuch-show-insert-forest (notmuch-query-get-threads args))
1068         ;; If the query context reduced the results to nothing, run
1069         ;; the basic query.
1070         (when (and (eq (buffer-size) 0)
1071                    notmuch-show-query-context)
1072           (notmuch-show-insert-forest
1073            (notmuch-query-get-threads basic-args))))
1074
1075       (jit-lock-register #'notmuch-show-buttonise-links)
1076
1077       (run-hooks 'notmuch-show-hook))
1078
1079     ;; Move straight to the first open message
1080     (unless (notmuch-show-message-visible-p)
1081       (notmuch-show-next-open-message))
1082
1083     ;; Set the header line to the subject of the first open message.
1084     (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-pretty-subject)))
1085
1086     (notmuch-show-mark-read)))
1087
1088 (defun notmuch-show-capture-state ()
1089   "Capture the state of the current buffer.
1090
1091 This includes:
1092  - the list of open messages,
1093  - the current message."
1094   (list (notmuch-show-get-message-id) (notmuch-show-get-message-ids-for-open-messages)))
1095
1096 (defun notmuch-show-apply-state (state)
1097   "Apply STATE to the current buffer.
1098
1099 This includes:
1100  - opening the messages previously opened,
1101  - closing all other messages,
1102  - moving to the correct current message."
1103   (let ((current (car state))
1104         (open (cadr state)))
1105
1106     ;; Open those that were open.
1107     (goto-char (point-min))
1108     (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
1109                                            (member (notmuch-show-get-message-id) open))
1110           until (not (notmuch-show-goto-message-next)))
1111
1112     ;; Go to the previously open message.
1113     (goto-char (point-min))
1114     (unless (loop if (string= current (notmuch-show-get-message-id))
1115                   return t
1116                   until (not (notmuch-show-goto-message-next)))
1117       (goto-char (point-min))
1118       (message "Previously current message not found."))
1119     (notmuch-show-message-adjust)))
1120
1121 (defun notmuch-show-refresh-view (&optional retain-state)
1122   "Refresh the current view.
1123
1124 Refreshes the current view, observing changes in display
1125 preferences. If RETAIN-STATE is non-nil then the state of the
1126 buffer is stored and re-applied after the refresh."
1127   (interactive "P")
1128   (let ((inhibit-read-only t)
1129         state)
1130     (if retain-state
1131         (setq state (notmuch-show-capture-state)))
1132     (erase-buffer)
1133     (notmuch-show-worker)
1134     (if state
1135         (notmuch-show-apply-state state))))
1136
1137 (defvar notmuch-show-stash-map
1138   (let ((map (make-sparse-keymap)))
1139     (define-key map "c" 'notmuch-show-stash-cc)
1140     (define-key map "d" 'notmuch-show-stash-date)
1141     (define-key map "F" 'notmuch-show-stash-filename)
1142     (define-key map "f" 'notmuch-show-stash-from)
1143     (define-key map "i" 'notmuch-show-stash-message-id)
1144     (define-key map "I" 'notmuch-show-stash-message-id-stripped)
1145     (define-key map "s" 'notmuch-show-stash-subject)
1146     (define-key map "T" 'notmuch-show-stash-tags)
1147     (define-key map "t" 'notmuch-show-stash-to)
1148     map)
1149   "Submap for stash commands")
1150 (fset 'notmuch-show-stash-map notmuch-show-stash-map)
1151
1152 (defvar notmuch-show-mode-map
1153       (let ((map (make-sparse-keymap)))
1154         (define-key map "?" 'notmuch-help)
1155         (define-key map "q" 'notmuch-kill-this-buffer)
1156         (define-key map (kbd "<C-tab>") 'widget-backward)
1157         (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
1158         (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
1159         (define-key map (kbd "TAB") 'notmuch-show-next-button)
1160         (define-key map "s" 'notmuch-search)
1161         (define-key map "m" 'notmuch-mua-new-mail)
1162         (define-key map "f" 'notmuch-show-forward-message)
1163         (define-key map "r" 'notmuch-show-reply-sender)
1164         (define-key map "R" 'notmuch-show-reply)
1165         (define-key map "|" 'notmuch-show-pipe-message)
1166         (define-key map "w" 'notmuch-show-save-attachments)
1167         (define-key map "V" 'notmuch-show-view-raw-message)
1168         (define-key map "v" 'notmuch-show-view-all-mime-parts)
1169         (define-key map "c" 'notmuch-show-stash-map)
1170         (define-key map "=" 'notmuch-show-refresh-view)
1171         (define-key map "h" 'notmuch-show-toggle-headers)
1172         (define-key map "*" 'notmuch-show-tag-all)
1173         (define-key map "-" 'notmuch-show-remove-tag)
1174         (define-key map "+" 'notmuch-show-add-tag)
1175         (define-key map "X" 'notmuch-show-archive-thread-then-exit)
1176         (define-key map "x" 'notmuch-show-archive-message-then-next-or-exit)
1177         (define-key map "A" 'notmuch-show-archive-thread-then-next)
1178         (define-key map "a" 'notmuch-show-archive-message-then-next-or-next-thread)
1179         (define-key map "N" 'notmuch-show-next-message)
1180         (define-key map "P" 'notmuch-show-previous-message)
1181         (define-key map "n" 'notmuch-show-next-open-message)
1182         (define-key map "p" 'notmuch-show-previous-open-message)
1183         (define-key map (kbd "DEL") 'notmuch-show-rewind)
1184         (define-key map " " 'notmuch-show-advance-and-archive)
1185         (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
1186         (define-key map (kbd "RET") 'notmuch-show-toggle-message)
1187         (define-key map "#" 'notmuch-show-print-message)
1188         (define-key map "!" 'notmuch-show-toggle-elide-non-matching)
1189         (define-key map "$" 'notmuch-show-toggle-process-crypto)
1190         (define-key map "<" 'notmuch-show-toggle-thread-indentation)
1191         (define-key map "t" 'toggle-truncate-lines)
1192         map)
1193       "Keymap for \"notmuch show\" buffers.")
1194 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
1195
1196 (defun notmuch-show-mode ()
1197   "Major mode for viewing a thread with notmuch.
1198
1199 This buffer contains the results of the \"notmuch show\" command
1200 for displaying a single thread of email from your email archives.
1201
1202 By default, various components of email messages, (citations,
1203 signatures, already-read messages), are hidden. You can make
1204 these parts visible by clicking with the mouse button or by
1205 pressing RET after positioning the cursor on a hidden part, (for
1206 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
1207
1208 Reading the thread sequentially is well-supported by pressing
1209 \\[notmuch-show-advance-and-archive]. This will scroll the current message (if necessary), advance
1210 to the next message, or advance to the next thread (if already on
1211 the last message of a thread).
1212
1213 Other commands are available to read or manipulate the thread
1214 more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages
1215 without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread
1216 without scrolling through with \\[notmuch-show-advance-and-archive]).
1217
1218 You can add or remove arbitrary tags from the current message with
1219 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
1220
1221 All currently available key bindings:
1222
1223 \\{notmuch-show-mode-map}"
1224   (interactive)
1225   (kill-all-local-variables)
1226   (use-local-map notmuch-show-mode-map)
1227   (setq major-mode 'notmuch-show-mode
1228         mode-name "notmuch-show")
1229   (setq buffer-read-only t
1230         truncate-lines t))
1231
1232 (defun notmuch-show-move-to-message-top ()
1233   (goto-char (notmuch-show-message-top)))
1234
1235 (defun notmuch-show-move-to-message-bottom ()
1236   (goto-char (notmuch-show-message-bottom)))
1237
1238 (defun notmuch-show-message-adjust ()
1239   (recenter 0))
1240
1241 ;; Movement related functions.
1242
1243 ;; There's some strangeness here where a text property applied to a
1244 ;; region a->b is not found when point is at b. We walk backwards
1245 ;; until finding the property.
1246 (defun notmuch-show-message-extent ()
1247   (let (r)
1248     (save-excursion
1249       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
1250         (backward-char)))
1251     r))
1252
1253 (defun notmuch-show-message-top ()
1254   (car (notmuch-show-message-extent)))
1255
1256 (defun notmuch-show-message-bottom ()
1257   (cdr (notmuch-show-message-extent)))
1258
1259 (defun notmuch-show-goto-message-next ()
1260   (let ((start (point)))
1261     (notmuch-show-move-to-message-bottom)
1262     (if (not (eobp))
1263         t
1264       (goto-char start)
1265       nil)))
1266
1267 (defun notmuch-show-goto-message-previous ()
1268   (notmuch-show-move-to-message-top)
1269   (if (bobp)
1270       nil
1271     (backward-char)
1272     (notmuch-show-move-to-message-top)
1273     t))
1274
1275 (defun notmuch-show-mapc (function)
1276   "Iterate through all messages in the current thread with
1277 `notmuch-show-goto-message-next' and call FUNCTION for side
1278 effects."
1279   (save-excursion
1280     (goto-char (point-min))
1281     (loop do (funcall function)
1282           while (notmuch-show-goto-message-next))))
1283
1284 ;; Functions relating to the visibility of messages and their
1285 ;; components.
1286
1287 (defun notmuch-show-element-visible (props visible-p spec-property)
1288   (let ((spec (plist-get props spec-property)))
1289     (if visible-p
1290         (remove-from-invisibility-spec spec)
1291       (add-to-invisibility-spec spec))))
1292
1293 (defun notmuch-show-message-visible (props visible-p)
1294   (notmuch-show-element-visible props visible-p :message-invis-spec)
1295   (notmuch-show-set-prop :message-visible visible-p props))
1296
1297 (defun notmuch-show-headers-visible (props visible-p)
1298   (notmuch-show-element-visible props visible-p :headers-invis-spec)
1299   (notmuch-show-set-prop :headers-visible visible-p props))
1300
1301 ;; Functions for setting and getting attributes of the current
1302 ;; message.
1303
1304 (defun notmuch-show-set-message-properties (props)
1305   (save-excursion
1306     (notmuch-show-move-to-message-top)
1307     (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
1308
1309 (defun notmuch-show-get-message-properties ()
1310   "Return the properties of the current message as a plist.
1311
1312 Some useful entries are:
1313 :headers - Property list containing the headers :Date, :Subject, :From, etc.
1314 :body - Body of the message
1315 :tags - Tags for this message"
1316   (save-excursion
1317     (notmuch-show-move-to-message-top)
1318     (get-text-property (point) :notmuch-message-properties)))
1319
1320 (defun notmuch-show-set-prop (prop val &optional props)
1321   (let ((inhibit-read-only t)
1322         (props (or props
1323                    (notmuch-show-get-message-properties))))
1324     (plist-put props prop val)
1325     (notmuch-show-set-message-properties props)))
1326
1327 (defun notmuch-show-get-prop (prop &optional props)
1328   (let ((props (or props
1329                    (notmuch-show-get-message-properties))))
1330     (plist-get props prop)))
1331
1332 (defun notmuch-show-get-message-id ()
1333   "Return the message id of the current message."
1334   (concat "id:\"" (notmuch-show-get-prop :id) "\""))
1335
1336 (defun notmuch-show-get-messages-ids ()
1337   "Return all message ids of messages in the current thread."
1338   (let ((message-ids))
1339     (notmuch-show-mapc
1340      (lambda () (push (notmuch-show-get-message-id) message-ids)))
1341     message-ids))
1342
1343 (defun notmuch-show-get-messages-ids-search ()
1344   "Return a search string for all message ids of messages in the
1345 current thread."
1346   (mapconcat 'identity (notmuch-show-get-messages-ids) " or "))
1347
1348 ;; dme: Would it make sense to use a macro for many of these?
1349
1350 (defun notmuch-show-get-filename ()
1351   "Return the filename of the current message."
1352   (notmuch-show-get-prop :filename))
1353
1354 (defun notmuch-show-get-header (header &optional props)
1355   "Return the named header of the current message, if any."
1356   (plist-get (notmuch-show-get-prop :headers props) header))
1357
1358 (defun notmuch-show-get-cc ()
1359   (notmuch-show-get-header :Cc))
1360
1361 (defun notmuch-show-get-date ()
1362   (notmuch-show-get-header :Date))
1363
1364 (defun notmuch-show-get-from ()
1365   (notmuch-show-get-header :From))
1366
1367 (defun notmuch-show-get-subject ()
1368   (notmuch-show-get-header :Subject))
1369
1370 (defun notmuch-show-get-to ()
1371   (notmuch-show-get-header :To))
1372
1373 (defun notmuch-show-get-depth ()
1374   (notmuch-show-get-prop :depth))
1375
1376 (defun notmuch-show-get-pretty-subject ()
1377   (notmuch-prettify-subject (notmuch-show-get-subject)))
1378
1379 (defun notmuch-show-set-tags (tags)
1380   "Set the tags of the current message."
1381   (notmuch-show-set-prop :tags tags)
1382   (notmuch-show-update-tags tags))
1383
1384 (defun notmuch-show-get-tags ()
1385   "Return the tags of the current message."
1386   (notmuch-show-get-prop :tags))
1387
1388 (defun notmuch-show-message-visible-p ()
1389   "Is the current message visible?"
1390   (notmuch-show-get-prop :message-visible))
1391
1392 (defun notmuch-show-headers-visible-p ()
1393   "Are the headers of the current message visible?"
1394   (notmuch-show-get-prop :headers-visible))
1395
1396 (defun notmuch-show-mark-read ()
1397   "Mark the current message as read."
1398   (notmuch-show-tag-message "-unread"))
1399
1400 ;; Functions for getting attributes of several messages in the current
1401 ;; thread.
1402
1403 (defun notmuch-show-get-message-ids-for-open-messages ()
1404   "Return a list of all message IDs for open messages in the current thread."
1405   (save-excursion
1406     (let (message-ids done)
1407       (goto-char (point-min))
1408       (while (not done)
1409         (if (notmuch-show-message-visible-p)
1410             (setq message-ids (append message-ids (list (notmuch-show-get-message-id)))))
1411         (setq done (not (notmuch-show-goto-message-next)))
1412         )
1413       message-ids
1414       )))
1415
1416 ;; Commands typically bound to keys.
1417
1418 (defun notmuch-show-advance ()
1419   "Advance through thread.
1420
1421 If the current message in the thread is not yet fully visible,
1422 scroll by a near screenful to read more of the message.
1423
1424 Otherwise, (the end of the current message is already within the
1425 current window), advance to the next open message."
1426   (interactive)
1427   (let* ((end-of-this-message (notmuch-show-message-bottom))
1428          (visible-end-of-this-message (1- end-of-this-message))
1429          (ret nil))
1430     (while (invisible-p visible-end-of-this-message)
1431       (setq visible-end-of-this-message
1432             (max (point-min)
1433                  (1- (previous-single-char-property-change
1434                       visible-end-of-this-message 'invisible)))))
1435     (cond
1436      ;; Ideally we would test `end-of-this-message' against the result
1437      ;; of `window-end', but that doesn't account for the fact that
1438      ;; the end of the message might be hidden.
1439      ((and visible-end-of-this-message
1440            (> visible-end-of-this-message (window-end)))
1441       ;; The bottom of this message is not visible - scroll.
1442       (scroll-up nil))
1443
1444      ((not (= end-of-this-message (point-max)))
1445       ;; This is not the last message - move to the next visible one.
1446       (notmuch-show-next-open-message))
1447
1448      (t
1449       ;; This is the last message - change the return value
1450       (setq ret t)))
1451     ret))
1452
1453 (defun notmuch-show-advance-and-archive ()
1454   "Advance through thread and archive.
1455
1456 This command is intended to be one of the simplest ways to
1457 process a thread of email. It works exactly like
1458 notmuch-show-advance, in that it scrolls through messages in a
1459 show buffer, except that when it gets to the end of the buffer it
1460 archives the entire current thread, (remove the \"inbox\" tag
1461 from each message), kills the buffer, and displays the next
1462 thread from the search from which this thread was originally
1463 shown."
1464   (interactive)
1465   (if (notmuch-show-advance)
1466       (notmuch-show-archive-thread-then-next)))
1467
1468 (defun notmuch-show-rewind ()
1469   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
1470
1471 Specifically, if the beginning of the previous email is fewer
1472 than `window-height' lines from the current point, move to it
1473 just like `notmuch-show-previous-message'.
1474
1475 Otherwise, just scroll down a screenful of the current message.
1476
1477 This command does not modify any message tags, (it does not undo
1478 any effects from previous calls to
1479 `notmuch-show-advance-and-archive'."
1480   (interactive)
1481   (let ((start-of-message (notmuch-show-message-top))
1482         (start-of-window (window-start)))
1483     (cond
1484       ;; Either this message is properly aligned with the start of the
1485       ;; window or the start of this message is not visible on the
1486       ;; screen - scroll.
1487      ((or (= start-of-message start-of-window)
1488           (< start-of-message start-of-window))
1489       (scroll-down)
1490       ;; If a small number of lines from the previous message are
1491       ;; visible, realign so that the top of the current message is at
1492       ;; the top of the screen.
1493       (when (<= (count-screen-lines (window-start) start-of-message)
1494                 next-screen-context-lines)
1495         (goto-char (notmuch-show-message-top))
1496         (notmuch-show-message-adjust))
1497       ;; Move to the top left of the window.
1498       (goto-char (window-start)))
1499      (t
1500       ;; Move to the previous message.
1501       (notmuch-show-previous-message)))))
1502
1503 (defun notmuch-show-reply (&optional prompt-for-sender)
1504   "Reply to the sender and all recipients of the current message."
1505   (interactive "P")
1506   (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender t))
1507
1508 (defun notmuch-show-reply-sender (&optional prompt-for-sender)
1509   "Reply to the sender of the current message."
1510   (interactive "P")
1511   (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender nil))
1512
1513 (defun notmuch-show-forward-message (&optional prompt-for-sender)
1514   "Forward the current message."
1515   (interactive "P")
1516   (with-current-notmuch-show-message
1517    (notmuch-mua-new-forward-message prompt-for-sender)))
1518
1519 (defun notmuch-show-next-message (&optional pop-at-end)
1520   "Show the next message.
1521
1522 If a prefix argument is given and this is the last message in the
1523 thread, navigate to the next thread in the parent search buffer."
1524   (interactive "P")
1525   (if (notmuch-show-goto-message-next)
1526       (progn
1527         (notmuch-show-mark-read)
1528         (notmuch-show-message-adjust))
1529     (if pop-at-end
1530         (notmuch-show-next-thread)
1531       (goto-char (point-max)))))
1532
1533 (defun notmuch-show-previous-message ()
1534   "Show the previous message."
1535   (interactive)
1536   (notmuch-show-goto-message-previous)
1537   (notmuch-show-mark-read)
1538   (notmuch-show-message-adjust))
1539
1540 (defun notmuch-show-next-open-message (&optional pop-at-end)
1541   "Show the next open message.
1542
1543 If a prefix argument is given and this is the last open message
1544 in the thread, navigate to the next thread in the parent search
1545 buffer. Return t if there was a next open message in the thread
1546 to show, nil otherwise."
1547   (interactive "P")
1548   (let (r)
1549     (while (and (setq r (notmuch-show-goto-message-next))
1550                 (not (notmuch-show-message-visible-p))))
1551     (if r
1552         (progn
1553           (notmuch-show-mark-read)
1554           (notmuch-show-message-adjust))
1555       (if pop-at-end
1556           (notmuch-show-next-thread)
1557         (goto-char (point-max))))
1558     r))
1559
1560 (defun notmuch-show-previous-open-message ()
1561   "Show the previous open message."
1562   (interactive)
1563   (while (and (notmuch-show-goto-message-previous)
1564               (not (notmuch-show-message-visible-p))))
1565   (notmuch-show-mark-read)
1566   (notmuch-show-message-adjust))
1567
1568 (defun notmuch-show-view-raw-message ()
1569   "View the file holding the current message."
1570   (interactive)
1571   (let* ((id (notmuch-show-get-message-id))
1572          (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
1573     (call-process notmuch-command nil buf nil "show" "--format=raw" id)
1574     (switch-to-buffer buf)
1575     (goto-char (point-min))
1576     (set-buffer-modified-p nil)
1577     (view-buffer buf 'kill-buffer-if-not-modified)))
1578
1579 (defun notmuch-show-pipe-message (entire-thread command)
1580   "Pipe the contents of the current message (or thread) to the given command.
1581
1582 The given command will be executed with the raw contents of the
1583 current email message as stdin. Anything printed by the command
1584 to stdout or stderr will appear in the *notmuch-pipe* buffer.
1585
1586 When invoked with a prefix argument, the command will receive all
1587 open messages in the current thread (formatted as an mbox) rather
1588 than only the current message."
1589   (interactive "P\nsPipe message to command: ")
1590   (let (shell-command)
1591     (if entire-thread
1592         (setq shell-command
1593               (concat notmuch-command " show --format=mbox "
1594                       (shell-quote-argument
1595                        (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
1596                       " | " command))
1597       (setq shell-command
1598             (concat notmuch-command " show --format=raw "
1599                     (shell-quote-argument (notmuch-show-get-message-id)) " | " command)))
1600     (let ((buf (get-buffer-create (concat "*notmuch-pipe*"))))
1601       (with-current-buffer buf
1602         (setq buffer-read-only nil)
1603         (erase-buffer)
1604         (let ((exit-code (call-process-shell-command shell-command nil buf)))
1605           (goto-char (point-max))
1606           (set-buffer-modified-p nil)
1607           (setq buffer-read-only t)
1608           (unless (zerop exit-code)
1609             (switch-to-buffer-other-window buf)
1610             (message (format "Command '%s' exited abnormally with code %d"
1611                              shell-command exit-code))))))))
1612
1613 (defun notmuch-show-tag-message (&rest tag-changes)
1614   "Change tags for the current message.
1615
1616 TAG-CHANGES is a list of tag operations for `notmuch-tag'."
1617   (let* ((current-tags (notmuch-show-get-tags))
1618          (new-tags (notmuch-update-tags current-tags tag-changes)))
1619     (unless (equal current-tags new-tags)
1620       (apply 'notmuch-tag (notmuch-show-get-message-id) tag-changes)
1621       (notmuch-show-set-tags new-tags))))
1622
1623 (defun notmuch-show-tag (&optional initial-input)
1624   "Change tags for the current message, read input from the minibuffer."
1625   (interactive)
1626   (let ((tag-changes (notmuch-read-tag-changes
1627                       initial-input (notmuch-show-get-message-id))))
1628     (apply 'notmuch-show-tag-message tag-changes)))
1629
1630 (defun notmuch-show-tag-all (&rest tag-changes)
1631   "Change tags for all messages in the current thread.
1632
1633 TAG-CHANGES is a list of tag operations for `notmuch-tag'."
1634   (interactive (notmuch-read-tag-changes nil notmuch-show-thread-id))
1635   (apply 'notmuch-tag (notmuch-show-get-messages-ids-search) tag-changes)
1636   (notmuch-show-mapc
1637    (lambda ()
1638      (let* ((current-tags (notmuch-show-get-tags))
1639             (new-tags (notmuch-update-tags current-tags tag-changes)))
1640        (unless (equal current-tags new-tags)
1641          (notmuch-show-set-tags new-tags))))))
1642
1643 (defun notmuch-show-add-tag ()
1644   "Same as `notmuch-show-tag' but sets initial input to '+'."
1645   (interactive)
1646   (notmuch-show-tag "+"))
1647
1648 (defun notmuch-show-remove-tag ()
1649   "Same as `notmuch-show-tag' but sets initial input to '-'."
1650   (interactive)
1651   (notmuch-show-tag "-"))
1652
1653 (defun notmuch-show-toggle-headers ()
1654   "Toggle the visibility of the current message headers."
1655   (interactive)
1656   (let ((props (notmuch-show-get-message-properties)))
1657     (notmuch-show-headers-visible
1658      props
1659      (not (plist-get props :headers-visible))))
1660   (force-window-update))
1661
1662 (defun notmuch-show-toggle-message ()
1663   "Toggle the visibility of the current message."
1664   (interactive)
1665   (let ((props (notmuch-show-get-message-properties)))
1666     (notmuch-show-message-visible
1667      props
1668      (not (plist-get props :message-visible))))
1669   (force-window-update))
1670
1671 (defun notmuch-show-open-or-close-all ()
1672   "Set the visibility all of the messages in the current thread.
1673 By default make all of the messages visible. With a prefix
1674 argument, hide all of the messages."
1675   (interactive)
1676   (save-excursion
1677     (goto-char (point-min))
1678     (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
1679                                            (not current-prefix-arg))
1680           until (not (notmuch-show-goto-message-next))))
1681   (force-window-update))
1682
1683 (defun notmuch-show-next-button ()
1684   "Advance point to the next button in the buffer."
1685   (interactive)
1686   (forward-button 1))
1687
1688 (defun notmuch-show-previous-button ()
1689   "Move point back to the previous button in the buffer."
1690   (interactive)
1691   (backward-button 1))
1692
1693 (defun notmuch-show-next-thread (&optional show-next)
1694   "Move to the next item in the search results, if any."
1695   (interactive "P")
1696   (let ((parent-buffer notmuch-show-parent-buffer))
1697     (notmuch-kill-this-buffer)
1698     (when (buffer-live-p parent-buffer)
1699       (switch-to-buffer parent-buffer)
1700       (notmuch-search-next-thread)
1701       (if show-next
1702           (notmuch-search-show-thread)))))
1703
1704 (defun notmuch-show-archive-thread (&optional unarchive)
1705   "Archive each message in thread.
1706
1707 If a prefix argument is given, the messages will be
1708 \"unarchived\" (ie. the \"inbox\" tag will be added instead of
1709 removed).
1710
1711 Archive each message currently shown by removing the \"inbox\"
1712 tag from each. Then kill this buffer and show the next thread
1713 from the search from which this thread was originally shown.
1714
1715 Note: This command is safe from any race condition of new messages
1716 being delivered to the same thread. It does not archive the
1717 entire thread, but only the messages shown in the current
1718 buffer."
1719   (interactive "P")
1720   (let ((op (if unarchive "+" "-")))
1721     (notmuch-show-tag-all (concat op "inbox"))))
1722
1723 (defun notmuch-show-archive-thread-then-next ()
1724   "Archive each message in thread, then show next thread from search."
1725   (interactive)
1726   (notmuch-show-archive-thread)
1727   (notmuch-show-next-thread t))
1728
1729 (defun notmuch-show-archive-thread-then-exit ()
1730   "Archive each message in thread, then exit back to search results."
1731   (interactive)
1732   (notmuch-show-archive-thread)
1733   (notmuch-show-next-thread))
1734
1735 (defun notmuch-show-archive-message (&optional unarchive)
1736   "Archive the current message.
1737
1738 If a prefix argument is given, the message will be
1739 \"unarchived\" (ie. the \"inbox\" tag will be added instead of
1740 removed)."
1741   (interactive "P")
1742   (let ((op (if unarchive "+" "-")))
1743     (notmuch-show-tag-message (concat op "inbox"))))
1744
1745 (defun notmuch-show-archive-message-then-next-or-exit ()
1746   "Archive the current message, then show the next open message in the current thread.
1747
1748 If at the last open message in the current thread, then exit back
1749 to search results."
1750   (interactive)
1751   (notmuch-show-archive-message)
1752   (notmuch-show-next-open-message t))
1753
1754 (defun notmuch-show-archive-message-then-next-or-next-thread ()
1755   "Archive the current message, then show the next open message in the current thread.
1756
1757 If at the last open message in the current thread, then show next
1758 thread from search."
1759   (interactive)
1760   (notmuch-show-archive-message)
1761   (unless (notmuch-show-next-open-message)
1762     (notmuch-show-next-thread t)))
1763
1764 (defun notmuch-show-stash-cc ()
1765   "Copy CC field of current message to kill-ring."
1766   (interactive)
1767   (notmuch-common-do-stash (notmuch-show-get-cc)))
1768
1769 (defun notmuch-show-stash-date ()
1770   "Copy date of current message to kill-ring."
1771   (interactive)
1772   (notmuch-common-do-stash (notmuch-show-get-date)))
1773
1774 (defun notmuch-show-stash-filename ()
1775   "Copy filename of current message to kill-ring."
1776   (interactive)
1777   (notmuch-common-do-stash (notmuch-show-get-filename)))
1778
1779 (defun notmuch-show-stash-from ()
1780   "Copy From address of current message to kill-ring."
1781   (interactive)
1782   (notmuch-common-do-stash (notmuch-show-get-from)))
1783
1784 (defun notmuch-show-stash-message-id ()
1785   "Copy message ID of current message to kill-ring."
1786   (interactive)
1787   (notmuch-common-do-stash (notmuch-show-get-message-id)))
1788
1789 (defun notmuch-show-stash-message-id-stripped ()
1790   "Copy message ID of current message (sans `id:' prefix) to kill-ring."
1791   (interactive)
1792   (notmuch-common-do-stash (substring (notmuch-show-get-message-id) 4 -1)))
1793
1794 (defun notmuch-show-stash-subject ()
1795   "Copy Subject field of current message to kill-ring."
1796   (interactive)
1797   (notmuch-common-do-stash (notmuch-show-get-subject)))
1798
1799 (defun notmuch-show-stash-tags ()
1800   "Copy tags of current message to kill-ring as a comma separated list."
1801   (interactive)
1802   (notmuch-common-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
1803
1804 (defun notmuch-show-stash-to ()
1805   "Copy To address of current message to kill-ring."
1806   (interactive)
1807   (notmuch-common-do-stash (notmuch-show-get-to)))
1808
1809 ;; Commands typically bound to buttons.
1810
1811 (defun notmuch-show-part-button-default (&optional button)
1812   (interactive)
1813   (notmuch-show-part-button-internal button notmuch-show-part-button-default-action))
1814
1815 (defun notmuch-show-part-button-save (&optional button)
1816   (interactive)
1817   (notmuch-show-part-button-internal button #'notmuch-show-save-part))
1818
1819 (defun notmuch-show-part-button-view (&optional button)
1820   (interactive)
1821   (notmuch-show-part-button-internal button #'notmuch-show-view-part))
1822
1823 (defun notmuch-show-part-button-interactively-view (&optional button)
1824   (interactive)
1825   (notmuch-show-part-button-internal button #'notmuch-show-interactively-view-part))
1826
1827 (defun notmuch-show-part-button-internal (button handler)
1828   (let ((button (or button (button-at (point)))))
1829     (if button
1830         (let ((nth (button-get button :notmuch-part)))
1831           (if nth
1832               (funcall handler (notmuch-show-get-message-id) nth
1833                        (button-get button :notmuch-filename)
1834                        (button-get button :notmuch-content-type)))))))
1835
1836 ;;
1837
1838 (provide 'notmuch-show)