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