]> git.notmuchmail.org Git - notmuch/blob - emacs/notmuch-show.el
aa6ddd1a0ac6130969ee65204b84191c76622880
[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
31 (require 'notmuch-lib)
32 (require 'notmuch-query)
33 (require 'notmuch-wash)
34 (require 'notmuch-mua)
35 (require 'notmuch-crypto)
36
37 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
38 (declare-function notmuch-fontify-headers "notmuch" nil)
39 (declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
40 (declare-function notmuch-search-show-thread "notmuch" nil)
41
42 (defcustom notmuch-message-headers '("Subject" "To" "Cc" "Date")
43   "Headers that should be shown in a message, in this order.
44
45 For an open message, all of these headers will be made visible
46 according to `notmuch-message-headers-visible' or can be toggled
47 with `notmuch-show-toggle-headers'. For a closed message, only
48 the first header in the list will be visible."
49   :group 'notmuch
50   :type '(repeat string))
51
52 (defcustom notmuch-message-headers-visible t
53   "Should the headers be visible by default?
54
55 If this value is non-nil, then all of the headers defined in
56 `notmuch-message-headers' will be visible by default in the display
57 of each message. Otherwise, these headers will be hidden and
58 `notmuch-show-toggle-headers' can be used to make the visible for
59 any given message."
60   :group 'notmuch
61   :type 'boolean)
62
63 (defcustom notmuch-show-relative-dates t
64   "Display relative dates in the message summary line."
65   :group 'notmuch
66   :type 'boolean)
67
68 (defcustom notmuch-show-elide-same-subject nil
69   "Do not show the subject of a collapsed message if it is the
70 same as that of the previous message."
71   :group 'notmuch
72   :type 'boolean)
73
74 (defcustom notmuch-show-always-show-subject t
75   "Should a collapsed message show the `Subject:' line?"
76   :group 'notmuch
77   :type 'boolean)
78
79 (defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
80   "A list of functions called to decorate the headers listed in
81 `notmuch-message-headers'.")
82
83 (defcustom notmuch-show-hook nil
84   "Functions called after populating a `notmuch-show' buffer."
85   :group 'notmuch
86   :type 'hook)
87
88 (defcustom notmuch-show-insert-text/plain-hook '(notmuch-wash-excerpt-citations)
89   "Functions used to improve the display of text/plain parts."
90   :group 'notmuch
91   :type 'hook
92   :options '(notmuch-wash-convert-inline-patch-to-part
93              notmuch-wash-wrap-long-lines
94              notmuch-wash-tidy-citations
95              notmuch-wash-elide-blank-lines
96              notmuch-wash-excerpt-citations))
97
98 ;; Mostly useful for debugging.
99 (defcustom notmuch-show-all-multipart/alternative-parts nil
100   "Should all parts of multipart/alternative parts be shown?"
101   :group 'notmuch
102   :type 'boolean)
103
104 (defcustom notmuch-show-indent-multipart nil
105   "Should the sub-parts of a multipart/* part be indented?"
106   ;; dme: Not sure which is a good default.
107   :group 'notmuch
108   :type 'boolean)
109
110 (defmacro with-current-notmuch-show-message (&rest body)
111   "Evaluate body with current buffer set to the text of current message"
112   `(save-excursion
113      (let ((id (notmuch-show-get-message-id)))
114        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" id "*"))))
115          (with-current-buffer buf
116             (call-process notmuch-command nil t nil "show" "--format=raw" id)
117            ,@body)
118          (kill-buffer buf)))))
119
120 (defun notmuch-show-view-all-mime-parts ()
121   "Use external viewers to view all attachments from the current message."
122   (interactive)
123   (with-current-notmuch-show-message
124    ; We ovverride the mm-inline-media-tests to indicate which message
125    ; parts are already sufficiently handled by the original
126    ; presentation of the message in notmuch-show mode. These parts
127    ; will be inserted directly into the temporary buffer of
128    ; with-current-notmuch-show-message and silently discarded.
129    ;
130    ; Any MIME part not explicitly mentioned here will be handled by an
131    ; external viewer as configured in the various mailcap files.
132    (let ((mm-inline-media-tests '(
133                                   ("text/.*" ignore identity)
134                                   ("application/pgp-signature" ignore identity)
135                                   ("multipart/alternative" ignore identity)
136                                   ("multipart/mixed" ignore identity)
137                                   ("multipart/related" ignore identity)
138                                  )))
139      (mm-display-parts (mm-dissect-buffer)))))
140
141 (defun notmuch-foreach-mime-part (function mm-handle)
142   (cond ((stringp (car mm-handle))
143          (dolist (part (cdr mm-handle))
144            (notmuch-foreach-mime-part function part)))
145         ((bufferp (car mm-handle))
146          (funcall function mm-handle))
147         (t (dolist (part mm-handle)
148              (notmuch-foreach-mime-part function part)))))
149
150 (defun notmuch-count-attachments (mm-handle)
151   (let ((count 0))
152     (notmuch-foreach-mime-part
153      (lambda (p)
154        (let ((disposition (mm-handle-disposition p)))
155          (and (listp disposition)
156               (or (equal (car disposition) "attachment")
157                   (and (equal (car disposition) "inline")
158                        (assq 'filename disposition)))
159               (incf count))))
160      mm-handle)
161     count))
162
163 (defun notmuch-save-attachments (mm-handle &optional queryp)
164   (notmuch-foreach-mime-part
165    (lambda (p)
166      (let ((disposition (mm-handle-disposition p)))
167        (and (listp disposition)
168             (or (equal (car disposition) "attachment")
169                 (and (equal (car disposition) "inline")
170                      (assq 'filename disposition)))
171             (or (not queryp)
172                 (y-or-n-p
173                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
174             (mm-save-part p))))
175    mm-handle))
176
177 (defun notmuch-show-save-attachments ()
178   "Save all attachments from the current message."
179   (interactive)
180   (with-current-notmuch-show-message
181    (let ((mm-handle (mm-dissect-buffer)))
182      (notmuch-save-attachments
183       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
184   (message "Done"))
185
186 (defun notmuch-show-fontify-header ()
187   (let ((face (cond
188                ((looking-at "[Tt]o:")
189                 'message-header-to)
190                ((looking-at "[Bb]?[Cc][Cc]:")
191                 'message-header-cc)
192                ((looking-at "[Ss]ubject:")
193                 'message-header-subject)
194                ((looking-at "[Ff]rom:")
195                 'message-header-from)
196                (t
197                 'message-header-other))))
198
199     (overlay-put (make-overlay (point) (re-search-forward ":"))
200                  'face 'message-header-name)
201     (overlay-put (make-overlay (point) (re-search-forward ".*$"))
202                  'face face)))
203
204 (defun notmuch-show-colour-headers ()
205   "Apply some colouring to the current headers."
206   (goto-char (point-min))
207   (while (looking-at "^[A-Za-z][-A-Za-z0-9]*:")
208     (notmuch-show-fontify-header)
209     (forward-line)))
210
211 (defun notmuch-show-spaces-n (n)
212   "Return a string comprised of `n' spaces."
213   (make-string n ? ))
214
215 (defun notmuch-show-update-tags (tags)
216   "Update the displayed tags of the current message."
217   (save-excursion
218     (goto-char (notmuch-show-message-top))
219     (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
220         (let ((inhibit-read-only t))
221           (replace-match (concat "("
222                                  (propertize (mapconcat 'identity tags " ")
223                                              'face 'notmuch-tag-face)
224                                  ")"))))))
225
226 (defun notmuch-show-clean-address (address)
227   "Clean a single email address for display."
228   (let* ((parsed (mail-header-parse-address address))
229          (address (car parsed))
230          (name (cdr parsed)))
231     ;; Remove double quotes. They might be required during transport,
232     ;; but we don't need to see them.
233     (when name
234       (setq name (replace-regexp-in-string "\"" "" name)))
235     ;; If the address is 'foo@bar.com <foo@bar.com>' then show just
236     ;; 'foo@bar.com'.
237     (when (string= name address)
238       (setq name nil))
239
240     (if (not name)
241         address
242       (concat name " <" address ">"))))
243
244 (defun notmuch-show-insert-headerline (headers date tags depth)
245   "Insert a notmuch style headerline based on HEADERS for a
246 message at DEPTH in the current thread."
247   (let ((start (point)))
248     (insert (notmuch-show-spaces-n depth)
249             (notmuch-show-clean-address (plist-get headers :From))
250             " ("
251             date
252             ") ("
253             (propertize (mapconcat 'identity tags " ")
254                         'face 'notmuch-tag-face)
255             ")\n")
256     (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face)))
257
258 (defun notmuch-show-insert-header (header header-value)
259   "Insert a single header."
260   (insert header ": " header-value "\n"))
261
262 (defun notmuch-show-insert-headers (headers)
263   "Insert the headers of the current message."
264   (let ((start (point)))
265     (mapc '(lambda (header)
266              (let* ((header-symbol (intern (concat ":" header)))
267                     (header-value (plist-get headers header-symbol)))
268                (if (and header-value
269                         (not (string-equal "" header-value)))
270                    (notmuch-show-insert-header header header-value))))
271           notmuch-message-headers)
272     (save-excursion
273       (save-restriction
274         (narrow-to-region start (point-max))
275         (run-hooks 'notmuch-show-markup-headers-hook)))))
276
277 (define-button-type 'notmuch-show-part-button-type
278   'action 'notmuch-show-part-button-action
279   'follow-link t
280   'face 'message-mml)
281
282 (defun notmuch-show-insert-part-header (nth content-type declared-type &optional name comment)
283   (insert-button
284    (concat "[ "
285            (if name (concat name ": ") "")
286            declared-type
287            (if (not (string-equal declared-type content-type))
288                (concat " (as " content-type ")")
289              "")
290            (or comment "")
291            " ]\n")
292    :type 'notmuch-show-part-button-type
293    :notmuch-part nth
294    :notmuch-filename name))
295
296 ;; Functions handling particular MIME parts.
297
298 (defun notmuch-show-save-part (message-id nth &optional filename)
299   (let ((process-crypto notmuch-show-process-crypto))
300     (with-temp-buffer
301       (setq notmuch-show-process-crypto process-crypto)
302       ;; Always acquires the part via `notmuch part', even if it is
303       ;; available in the JSON output.
304       (insert (notmuch-show-get-bodypart-internal message-id nth))
305       (let ((file (read-file-name
306                    "Filename to save as: "
307                    (or mailcap-download-directory "~/")
308                    nil nil
309                    filename))
310             (require-final-newline nil)
311             (coding-system-for-write 'no-conversion))
312         (write-region (point-min) (point-max) file)))))
313
314 (defun notmuch-show-mm-display-part-inline (msg part content-type content)
315   "Use the mm-decode/mm-view functions to display a part in the
316 current buffer, if possible."
317   (let ((display-buffer (current-buffer)))
318     (with-temp-buffer
319       (insert content)
320       (let ((handle (mm-make-handle (current-buffer) (list content-type))))
321         (set-buffer display-buffer)
322         (if (and (mm-inlinable-p handle)
323                  (mm-inlined-p handle))
324             (progn
325               (mm-display-part handle)
326               t)
327           nil)))))
328
329 (defvar notmuch-show-multipart/alternative-discouraged
330   '(
331     ;; Avoid HTML parts.
332     "text/html"
333     ;; multipart/related usually contain a text/html part and some associated graphics.
334     "multipart/related"
335     ))
336
337 (defun notmuch-show-multipart/*-to-list (part)
338   (mapcar '(lambda (inner-part) (plist-get inner-part :content-type))
339           (plist-get part :content)))
340
341 (defun notmuch-show-multipart/alternative-choose (types)
342   ;; Based on `mm-preferred-alternative-precedence'.
343   (let ((seq types))
344     (dolist (pref (reverse notmuch-show-multipart/alternative-discouraged))
345       (dolist (elem (copy-sequence seq))
346         (when (string-match pref elem)
347           (setq seq (nconc (delete elem seq) (list elem))))))
348     seq))
349
350 (defun notmuch-show-insert-part-multipart/alternative (msg part content-type nth depth declared-type)
351   (notmuch-show-insert-part-header nth declared-type content-type nil)
352   (let ((chosen-type (car (notmuch-show-multipart/alternative-choose (notmuch-show-multipart/*-to-list part))))
353         (inner-parts (plist-get part :content))
354         (start (point)))
355     ;; This inserts all parts of the chosen type rather than just one,
356     ;; but it's not clear that this is the wrong thing to do - which
357     ;; should be chosen if there are more than one that match?
358     (mapc (lambda (inner-part)
359             (let ((inner-type (plist-get inner-part :content-type)))
360               (if (or notmuch-show-all-multipart/alternative-parts
361                       (string= chosen-type inner-type))
362                   (notmuch-show-insert-bodypart msg inner-part depth)
363                 (notmuch-show-insert-part-header (plist-get inner-part :id) inner-type inner-type nil " (not shown)"))))
364           inner-parts)
365
366     (when notmuch-show-indent-multipart
367       (indent-rigidly start (point) 1)))
368   t)
369
370 (defun notmuch-show-setup-w3m ()
371   "Instruct w3m how to retrieve content from a \"related\" part of a message."
372   (interactive)
373   (if (boundp 'w3m-cid-retrieve-function-alist)
374     (unless (assq 'notmuch-show-mode w3m-cid-retrieve-function-alist)
375       (push (cons 'notmuch-show-mode 'notmuch-show-w3m-cid-retrieve)
376             w3m-cid-retrieve-function-alist)))
377   (setq mm-inline-text-html-with-images t))
378
379 (defvar w3m-current-buffer) ;; From `w3m.el'.
380 (defvar notmuch-show-w3m-cid-store nil)
381 (make-variable-buffer-local 'notmuch-show-w3m-cid-store)
382
383 (defun notmuch-show-w3m-cid-store-internal (content-id
384                                             message-id
385                                             part-number
386                                             content-type
387                                             content)
388   (push (list content-id
389               message-id
390               part-number
391               content-type
392               content)
393         notmuch-show-w3m-cid-store))
394
395 (defun notmuch-show-w3m-cid-store (msg part)
396   (let ((content-id (plist-get part :content-id)))
397     (when content-id
398       (notmuch-show-w3m-cid-store-internal (concat "cid:" content-id)
399                                            (plist-get msg :id)
400                                            (plist-get part :id)
401                                            (plist-get part :content-type)
402                                            nil))))
403
404 (defun notmuch-show-w3m-cid-retrieve (url &rest args)
405   (let ((matching-part (with-current-buffer w3m-current-buffer
406                          (assoc url notmuch-show-w3m-cid-store))))
407     (if matching-part
408         (let ((message-id (nth 1 matching-part))
409               (part-number (nth 2 matching-part))
410               (content-type (nth 3 matching-part))
411               (content (nth 4 matching-part)))
412           ;; If we don't already have the content, get it and cache
413           ;; it, as some messages reference the same cid: part many
414           ;; times (hundreds!), which results in many calls to
415           ;; `notmuch part'.
416           (unless content
417             (setq content (notmuch-show-get-bodypart-internal (concat "id:" message-id)
418                                                               part-number))
419             (with-current-buffer w3m-current-buffer
420               (notmuch-show-w3m-cid-store-internal url
421                                                    message-id
422                                                    part-number
423                                                    content-type
424                                                    content)))
425           (insert content)
426           content-type)
427       nil)))
428
429 (defun notmuch-show-insert-part-multipart/related (msg part content-type nth depth declared-type)
430   (notmuch-show-insert-part-header nth declared-type content-type nil)
431   (let ((inner-parts (plist-get part :content))
432         (start (point)))
433
434     ;; We assume that the first part is text/html and the remainder
435     ;; things that it references.
436
437     ;; Stash the non-primary parts.
438     (mapc (lambda (part)
439             (notmuch-show-w3m-cid-store msg part))
440           (cdr inner-parts))
441
442     ;; Render the primary part.
443     (notmuch-show-insert-bodypart msg (car inner-parts) depth)
444
445     (when notmuch-show-indent-multipart
446       (indent-rigidly start (point) 1)))
447   t)
448
449 (defun notmuch-show-insert-part-multipart/* (msg part content-type nth depth declared-type)
450   (notmuch-show-insert-part-header nth declared-type content-type nil)
451   (let ((inner-parts (plist-get part :content))
452         (start (point)))
453     ;; Show all of the parts.
454     (mapc (lambda (inner-part)
455             (notmuch-show-insert-bodypart msg inner-part depth))
456           inner-parts)
457
458     (when notmuch-show-indent-multipart
459       (indent-rigidly start (point) 1)))
460   t)
461
462 (defun notmuch-show-insert-part-message/rfc822 (msg part content-type nth depth declared-type)
463   (let* ((message-part (plist-get part :content))
464          (inner-parts (plist-get message-part :content)))
465     (notmuch-show-insert-part-header nth declared-type content-type nil)
466     ;; Override `notmuch-message-headers' to force `From' to be
467     ;; displayed.
468     (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
469       (notmuch-show-insert-headers (plist-get part :headers)))
470     ;; Blank line after headers to be compatible with the normal
471     ;; message display.
472     (insert "\n")
473
474     ;; Show all of the parts.
475     (mapc (lambda (inner-part)
476             (notmuch-show-insert-bodypart msg inner-part depth))
477           inner-parts))
478   t)
479
480 (defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type)
481   (let ((start (point)))
482     ;; If this text/plain part is not the first part in the message,
483     ;; insert a header to make this clear.
484     (if (> nth 1)
485         (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
486     (insert (notmuch-show-get-bodypart-content msg part nth))
487     (save-excursion
488       (save-restriction
489         (narrow-to-region start (point-max))
490         (run-hook-with-args 'notmuch-show-insert-text/plain-hook depth))))
491   t)
492
493 (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth declared-type)
494   (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
495   (insert (with-temp-buffer
496             (insert (notmuch-show-get-bodypart-content msg part nth))
497             (goto-char (point-min))
498             (let ((file (make-temp-file "notmuch-ical"))
499                   result)
500               (icalendar--convert-ical-to-diary
501                (icalendar--read-element nil nil)
502                file t)
503               (set-buffer (get-file-buffer file))
504               (setq result (buffer-substring (point-min) (point-max)))
505               (set-buffer-modified-p nil)
506               (kill-buffer (current-buffer))
507               (delete-file file)
508               result)))
509   t)
510
511 (defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type)
512   ;; If we can deduce a MIME type from the filename of the attachment,
513   ;; do so and pass it on to the handler for that type.
514   (if (plist-get part :filename)
515       (let ((extension (file-name-extension (plist-get part :filename)))
516             mime-type)
517         (if extension
518             (progn
519               (mailcap-parse-mimetypes)
520               (setq mime-type (mailcap-extension-to-mime extension))
521               (if (and mime-type
522                        (not (string-equal mime-type "application/octet-stream")))
523                   (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type)
524                 nil))
525           nil))))
526
527 (defun notmuch-show-insert-part-application/* (msg part content-type nth depth declared-type
528 )
529   ;; do not render random "application" parts
530   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename)))
531
532 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
533   ;; This handler _must_ succeed - it is the handler of last resort.
534   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
535   (let ((content (notmuch-show-get-bodypart-content msg part nth)))
536     (if content
537         (notmuch-show-mm-display-part-inline msg part content-type content)))
538   t)
539
540 ;; Functions for determining how to handle MIME parts.
541
542 (defun notmuch-show-split-content-type (content-type)
543   (split-string content-type "/"))
544
545 (defun notmuch-show-handlers-for (content-type)
546   "Return a list of content handlers for a part of type CONTENT-TYPE."
547   (let (result)
548     (mapc (lambda (func)
549             (if (functionp func)
550                 (push func result)))
551           ;; Reverse order of prefrence.
552           (list (intern (concat "notmuch-show-insert-part-*/*"))
553                 (intern (concat
554                          "notmuch-show-insert-part-"
555                          (car (notmuch-show-split-content-type content-type))
556                          "/*"))
557                 (intern (concat "notmuch-show-insert-part-" content-type))))
558     result))
559
560 ;; Helper for parts which are generally not included in the default
561 ;; JSON output.
562 ;; Uses the buffer-local variable notmuch-show-process-crypto to
563 ;; determine if parts should be decrypted first.
564 (defun notmuch-show-get-bodypart-internal (message-id part-number)
565   (let ((args '("show" "--format=raw"))
566         (part-arg (format "--part=%s" part-number)))
567     (setq args (append args (list part-arg)))
568     (if notmuch-show-process-crypto
569         (setq args (append args '("--decrypt"))))
570     (setq args (append args (list message-id)))
571     (with-temp-buffer
572       (let ((coding-system-for-read 'no-conversion))
573         (progn
574           (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
575           (buffer-string))))))
576
577 (defun notmuch-show-get-bodypart-content (msg part nth)
578   (or (plist-get part :content)
579       (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
580
581 ;; \f
582
583 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
584   (let ((handlers (notmuch-show-handlers-for content-type)))
585     ;; Run the content handlers until one of them returns a non-nil
586     ;; value.
587     (while (and handlers
588                 (not (funcall (car handlers) msg part content-type nth depth declared-type)))
589       (setq handlers (cdr handlers))))
590   t)
591
592 (defun notmuch-show-insert-bodypart (msg part depth)
593   "Insert the body part PART at depth DEPTH in the current thread."
594   (let ((content-type (downcase (plist-get part :content-type)))
595         (nth (plist-get part :id)))
596     ;; add encryption status button if encstatus specified
597     (if (plist-member part :encstatus)
598         (let* ((encstatus (car (plist-get part :encstatus))))
599           (notmuch-crypto-insert-encstatus-button encstatus)))
600     ;; add signature status button if sigstatus specified
601     (if (plist-member part :sigstatus)
602         (let* ((headers (plist-get msg :headers))
603                (from (plist-get headers :From))
604                (sigstatus (car (plist-get part :sigstatus))))
605           (notmuch-crypto-insert-sigstatus-button sigstatus from)))
606     (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
607   ;; Some of the body part handlers leave point somewhere up in the
608   ;; part, so we make sure that we're down at the end.
609   (goto-char (point-max))
610   ;; Ensure that the part ends with a carriage return.
611   (if (not (bolp))
612       (insert "\n")))
613
614 (defun notmuch-show-insert-body (msg body depth)
615   "Insert the body BODY at depth DEPTH in the current thread."
616   (mapc '(lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
617
618 (defun notmuch-show-make-symbol (type)
619   (make-symbol (concat "notmuch-show-" type)))
620
621 (defun notmuch-show-strip-re (string)
622   (replace-regexp-in-string "\\([Rr]e: *\\)+" "" string))
623
624 (defvar notmuch-show-previous-subject "")
625 (make-variable-buffer-local 'notmuch-show-previous-subject)
626
627 (defun notmuch-show-insert-msg (msg depth)
628   "Insert the message MSG at depth DEPTH in the current thread."
629   (let* ((headers (plist-get msg :headers))
630          ;; Indentation causes the buffer offset of the start/end
631          ;; points to move, so we must use markers.
632          message-start message-end
633          content-start content-end
634          headers-start headers-end
635          body-start body-end
636          (headers-invis-spec (notmuch-show-make-symbol "header"))
637          (message-invis-spec (notmuch-show-make-symbol "message"))
638          (bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
639
640     ;; Set `buffer-invisibility-spec' to `nil' (a list), otherwise
641     ;; removing items from `buffer-invisibility-spec' (which is what
642     ;; `notmuch-show-headers-visible' and
643     ;; `notmuch-show-message-visible' do) is a no-op and has no
644     ;; effect. This caused threads with only matching messages to have
645     ;; those messages hidden initially because
646     ;; `buffer-invisibility-spec' stayed `t'.
647     ;;
648     ;; This needs to be set here (rather than just above the call to
649     ;; `notmuch-show-headers-visible') because some of the part
650     ;; rendering or body washing functions
651     ;; (e.g. `notmuch-wash-text/plain-citations') manipulate
652     ;; `buffer-invisibility-spec').
653     (when (eq buffer-invisibility-spec t)
654       (setq buffer-invisibility-spec nil))
655
656     (setq message-start (point-marker))
657
658     (notmuch-show-insert-headerline headers
659                                     (or (if notmuch-show-relative-dates
660                                             (plist-get msg :date_relative)
661                                           nil)
662                                         (plist-get headers :Date))
663                                     (plist-get msg :tags) depth)
664
665     (setq content-start (point-marker))
666
667     ;; Set `headers-start' to point after the 'Subject:' header to be
668     ;; compatible with the existing implementation. This just sets it
669     ;; to after the first header.
670     (notmuch-show-insert-headers headers)
671     ;; Headers should include a blank line (backwards compatibility).
672     (insert "\n")
673     (save-excursion
674       (goto-char content-start)
675       ;; If the subject of this message is the same as that of the
676       ;; previous message, don't display it when this message is
677       ;; collapsed.
678       (when (and notmuch-show-elide-same-subject
679                  (not (string= notmuch-show-previous-subject
680                                bare-subject)))
681         (forward-line 1))
682       (setq headers-start (point-marker)))
683     (setq headers-end (point-marker))
684
685     (setq notmuch-show-previous-subject bare-subject)
686
687     (setq body-start (point-marker))
688     (notmuch-show-insert-body msg (plist-get msg :body) depth)
689     ;; Ensure that the body ends with a newline.
690     (if (not (bolp))
691         (insert "\n"))
692     (setq body-end (point-marker))
693     (setq content-end (point-marker))
694
695     ;; Indent according to the depth in the thread.
696     (indent-rigidly content-start content-end depth)
697
698     (setq message-end (point-max-marker))
699
700     ;; Save the extents of this message over the whole text of the
701     ;; message.
702     (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
703
704     (plist-put msg :headers-invis-spec headers-invis-spec)
705     (overlay-put (make-overlay headers-start headers-end) 'invisible headers-invis-spec)
706
707     (plist-put msg :message-invis-spec message-invis-spec)
708     (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
709
710     ;; Save the properties for this message. Currently this saves the
711     ;; entire message (augmented it with other stuff), which seems
712     ;; like overkill. We might save a reduced subset (for example, not
713     ;; the content).
714     (notmuch-show-set-message-properties msg)
715
716     ;; Set header visibility.
717     (notmuch-show-headers-visible msg notmuch-message-headers-visible)
718
719     ;; Message visibility depends on whether it matched the search
720     ;; criteria.
721     (notmuch-show-message-visible msg (plist-get msg :match))))
722
723 (defun notmuch-show-insert-tree (tree depth)
724   "Insert the message tree TREE at depth DEPTH in the current thread."
725   (let ((msg (car tree))
726         (replies (cadr tree)))
727     (notmuch-show-insert-msg msg depth)
728     (notmuch-show-insert-thread replies (1+ depth))))
729
730 (defun notmuch-show-insert-thread (thread depth)
731   "Insert the thread THREAD at depth DEPTH in the current forest."
732   (mapc '(lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
733
734 (defun notmuch-show-insert-forest (forest)
735   "Insert the forest of threads FOREST."
736   (mapc '(lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
737
738 (defvar notmuch-show-parent-buffer nil)
739 (make-variable-buffer-local 'notmuch-show-parent-buffer)
740
741 ;;;###autoload
742 (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name crypto-switch)
743   "Run \"notmuch show\" with the given thread ID and display results.
744
745 The optional PARENT-BUFFER is the notmuch-search buffer from
746 which this notmuch-show command was executed, (so that the
747 next thread from that buffer can be show when done with this
748 one).
749
750 The optional QUERY-CONTEXT is a notmuch search term. Only
751 messages from the thread matching this search term are shown if
752 non-nil.
753
754 The optional BUFFER-NAME provides the neame of the buffer in
755 which the message thread is shown. If it is nil (which occurs
756 when the command is called interactively) the argument to the
757 function is used. "
758   (interactive "sNotmuch show: ")
759   (let ((buffer (get-buffer-create (generate-new-buffer-name
760                                     (or buffer-name
761                                         (concat "*notmuch-" thread-id "*")))))
762         (process-crypto (if crypto-switch
763                             (not notmuch-crypto-process-mime)
764                           notmuch-crypto-process-mime))
765         (inhibit-read-only t))
766     (switch-to-buffer buffer)
767     (notmuch-show-mode)
768     (setq notmuch-show-parent-buffer parent-buffer)
769     (setq notmuch-show-process-crypto process-crypto)
770     (erase-buffer)
771     (goto-char (point-min))
772     (save-excursion
773       (let* ((basic-args (list thread-id))
774              (args (if query-context
775                        (append (list "\'") basic-args (list "and (" query-context ")\'"))
776                      (append (list "\'") basic-args (list "\'")))))
777         (notmuch-show-insert-forest (notmuch-query-get-threads args))
778         ;; If the query context reduced the results to nothing, run
779         ;; the basic query.
780         (when (and (eq (buffer-size) 0)
781                    query-context)
782           (notmuch-show-insert-forest
783            (notmuch-query-get-threads basic-args))))
784
785       ;; Enable buttonisation of URLs and email addresses in the
786       ;; buffer.
787       (goto-address-mode t)
788       ;; Act on visual lines rather than logical lines.
789       (visual-line-mode t)
790
791       (run-hooks 'notmuch-show-hook))
792
793     ;; Move straight to the first open message
794     (if (not (notmuch-show-message-visible-p))
795         (notmuch-show-next-open-message))
796
797     ;; Set the header line to the subject of the first open message.
798     (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
799
800     (notmuch-show-mark-read)))
801
802 (defvar notmuch-show-stash-map
803   (let ((map (make-sparse-keymap)))
804     (define-key map "c" 'notmuch-show-stash-cc)
805     (define-key map "d" 'notmuch-show-stash-date)
806     (define-key map "F" 'notmuch-show-stash-filename)
807     (define-key map "f" 'notmuch-show-stash-from)
808     (define-key map "i" 'notmuch-show-stash-message-id)
809     (define-key map "s" 'notmuch-show-stash-subject)
810     (define-key map "T" 'notmuch-show-stash-tags)
811     (define-key map "t" 'notmuch-show-stash-to)
812     map)
813   "Submap for stash commands")
814 (fset 'notmuch-show-stash-map notmuch-show-stash-map)
815
816 (defvar notmuch-show-mode-map
817       (let ((map (make-sparse-keymap)))
818         (define-key map "?" 'notmuch-help)
819         (define-key map "q" 'notmuch-kill-this-buffer)
820         (define-key map (kbd "<C-tab>") 'widget-backward)
821         (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
822         (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
823         (define-key map (kbd "TAB") 'notmuch-show-next-button)
824         (define-key map "s" 'notmuch-search)
825         (define-key map "m" 'notmuch-mua-new-mail)
826         (define-key map "f" 'notmuch-show-forward-message)
827         (define-key map "r" 'notmuch-show-reply)
828         (define-key map "|" 'notmuch-show-pipe-message)
829         (define-key map "w" 'notmuch-show-save-attachments)
830         (define-key map "V" 'notmuch-show-view-raw-message)
831         (define-key map "v" 'notmuch-show-view-all-mime-parts)
832         (define-key map "c" 'notmuch-show-stash-map)
833         (define-key map "h" 'notmuch-show-toggle-headers)
834         (define-key map "-" 'notmuch-show-remove-tag)
835         (define-key map "+" 'notmuch-show-add-tag)
836         (define-key map "x" 'notmuch-show-archive-thread-then-exit)
837         (define-key map "a" 'notmuch-show-archive-thread)
838         (define-key map "N" 'notmuch-show-next-message)
839         (define-key map "P" 'notmuch-show-previous-message)
840         (define-key map "n" 'notmuch-show-next-open-message)
841         (define-key map "p" 'notmuch-show-previous-open-message)
842         (define-key map (kbd "DEL") 'notmuch-show-rewind)
843         (define-key map " " 'notmuch-show-advance-and-archive)
844         (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
845         (define-key map (kbd "RET") 'notmuch-show-toggle-message)
846         map)
847       "Keymap for \"notmuch show\" buffers.")
848 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
849
850 (defun notmuch-show-mode ()
851   "Major mode for viewing a thread with notmuch.
852
853 This buffer contains the results of the \"notmuch show\" command
854 for displaying a single thread of email from your email archives.
855
856 By default, various components of email messages, (citations,
857 signatures, already-read messages), are hidden. You can make
858 these parts visible by clicking with the mouse button or by
859 pressing RET after positioning the cursor on a hidden part, (for
860 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
861
862 Reading the thread sequentially is well-supported by pressing
863 \\[notmuch-show-advance-and-archive]. This will scroll the current message (if necessary), advance
864 to the next message, or advance to the next thread (if already on
865 the last message of a thread).
866
867 Other commands are available to read or manipulate the thread
868 more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages
869 without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread
870 without scrolling through with \\[notmuch-show-advance-and-archive]).
871
872 You can add or remove arbitary tags from the current message with
873 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
874
875 All currently available key bindings:
876
877 \\{notmuch-show-mode-map}"
878   (interactive)
879   (kill-all-local-variables)
880   (use-local-map notmuch-show-mode-map)
881   (setq major-mode 'notmuch-show-mode
882         mode-name "notmuch-show")
883   (setq buffer-read-only t))
884
885 (defun notmuch-show-move-to-message-top ()
886   (goto-char (notmuch-show-message-top)))
887
888 (defun notmuch-show-move-to-message-bottom ()
889   (goto-char (notmuch-show-message-bottom)))
890
891 (defun notmuch-show-message-adjust ()
892   (recenter 0))
893
894 ;; Movement related functions.
895
896 ;; There's some strangeness here where a text property applied to a
897 ;; region a->b is not found when point is at b. We walk backwards
898 ;; until finding the property.
899 (defun notmuch-show-message-extent ()
900   (let (r)
901     (save-excursion
902       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
903         (backward-char)))
904     r))
905
906 (defun notmuch-show-message-top ()
907   (car (notmuch-show-message-extent)))
908
909 (defun notmuch-show-message-bottom ()
910   (cdr (notmuch-show-message-extent)))
911
912 (defun notmuch-show-goto-message-next ()
913   (let ((start (point)))
914     (notmuch-show-move-to-message-bottom)
915     (if (not (eobp))
916         t
917       (goto-char start)
918       nil)))
919
920 (defun notmuch-show-goto-message-previous ()
921   (notmuch-show-move-to-message-top)
922   (if (bobp)
923       nil
924     (backward-char)
925     (notmuch-show-move-to-message-top)
926     t))
927
928 (defun notmuch-show-move-past-invisible-forward ()
929   (while (point-invisible-p)
930     (forward-char)))
931
932 (defun notmuch-show-move-past-invisible-backward ()
933   (while (point-invisible-p)
934     (backward-char)))
935
936 ;; Functions relating to the visibility of messages and their
937 ;; components.
938
939 (defun notmuch-show-element-visible (props visible-p spec-property)
940   (let ((spec (plist-get props spec-property)))
941     (if visible-p
942         (remove-from-invisibility-spec spec)
943       (add-to-invisibility-spec spec))))
944
945 (defun notmuch-show-message-visible (props visible-p)
946   (if visible-p
947       ;; When making the message visible, the headers may or not be
948       ;; visible. So we check that property separately.
949       (let ((headers-visible (plist-get props :headers-visible)))
950         (notmuch-show-element-visible props headers-visible :headers-invis-spec)
951         (notmuch-show-element-visible props t :message-invis-spec))
952     (notmuch-show-element-visible props nil :headers-invis-spec)
953     (notmuch-show-element-visible props nil :message-invis-spec))
954
955   (notmuch-show-set-prop :message-visible visible-p props))
956
957 (defun notmuch-show-headers-visible (props visible-p)
958   (if (plist-get props :message-visible)
959       (notmuch-show-element-visible props visible-p :headers-invis-spec))
960   (notmuch-show-set-prop :headers-visible visible-p props))
961
962 ;; Functions for setting and getting attributes of the current
963 ;; message.
964
965 (defun notmuch-show-set-message-properties (props)
966   (save-excursion
967     (notmuch-show-move-to-message-top)
968     (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
969
970 (defun notmuch-show-get-message-properties ()
971   (save-excursion
972     (notmuch-show-move-to-message-top)
973     (get-text-property (point) :notmuch-message-properties)))
974
975 (defun notmuch-show-set-prop (prop val &optional props)
976   (let ((inhibit-read-only t)
977         (props (or props
978                    (notmuch-show-get-message-properties))))
979     (plist-put props prop val)
980     (notmuch-show-set-message-properties props)))
981
982 (defun notmuch-show-get-prop (prop &optional props)
983   (let ((props (or props
984                    (notmuch-show-get-message-properties))))
985     (plist-get props prop)))
986
987 (defun notmuch-show-get-message-id ()
988   "Return the message id of the current message."
989   (concat "id:\"" (notmuch-show-get-prop :id) "\""))
990
991 ;; dme: Would it make sense to use a macro for many of these?
992
993 (defun notmuch-show-get-filename ()
994   "Return the filename of the current message."
995   (notmuch-show-get-prop :filename))
996
997 (defun notmuch-show-get-header (header)
998   "Return the named header of the current message, if any."
999   (plist-get (notmuch-show-get-prop :headers) header))
1000
1001 (defun notmuch-show-get-cc ()
1002   (notmuch-show-get-header :Cc))
1003
1004 (defun notmuch-show-get-date ()
1005   (notmuch-show-get-header :Date))
1006
1007 (defun notmuch-show-get-from ()
1008   (notmuch-show-get-header :From))
1009
1010 (defun notmuch-show-get-subject ()
1011   (notmuch-show-get-header :Subject))
1012
1013 (defun notmuch-show-get-to ()
1014   (notmuch-show-get-header :To))
1015
1016 (defun notmuch-show-set-tags (tags)
1017   "Set the tags of the current message."
1018   (notmuch-show-set-prop :tags tags)
1019   (notmuch-show-update-tags tags))
1020
1021 (defun notmuch-show-get-tags ()
1022   "Return the tags of the current message."
1023   (notmuch-show-get-prop :tags))
1024
1025 (defun notmuch-show-message-visible-p ()
1026   "Is the current message visible?"
1027   (notmuch-show-get-prop :message-visible))
1028
1029 (defun notmuch-show-headers-visible-p ()
1030   "Are the headers of the current message visible?"
1031   (notmuch-show-get-prop :headers-visible))
1032
1033 (defun notmuch-show-mark-read ()
1034   "Mark the current message as read."
1035   (notmuch-show-remove-tag "unread"))
1036
1037 ;; Functions for getting attributes of several messages in the current
1038 ;; thread.
1039
1040 (defun notmuch-show-get-message-ids-for-open-messages ()
1041   "Return a list of all message IDs for open messages in the current thread."
1042   (save-excursion
1043     (let (message-ids done)
1044       (goto-char (point-min))
1045       (while (not done)
1046         (if (notmuch-show-message-visible-p)
1047             (setq message-ids (append message-ids (list (notmuch-show-get-message-id)))))
1048         (setq done (not (notmuch-show-goto-message-next)))
1049         )
1050       message-ids
1051       )))
1052
1053 ;; Commands typically bound to keys.
1054
1055 (defun notmuch-show-advance-and-archive ()
1056   "Advance through thread and archive.
1057
1058 This command is intended to be one of the simplest ways to
1059 process a thread of email. It does the following:
1060
1061 If the current message in the thread is not yet fully visible,
1062 scroll by a near screenful to read more of the message.
1063
1064 Otherwise, (the end of the current message is already within the
1065 current window), advance to the next open message.
1066
1067 Finally, if there is no further message to advance to, and this
1068 last message is already read, then archive the entire current
1069 thread, (remove the \"inbox\" tag from each message). Also kill
1070 this buffer, and display the next thread from the search from
1071 which this thread was originally shown."
1072   (interactive)
1073   (let ((end-of-this-message (notmuch-show-message-bottom)))
1074     (cond
1075      ;; Ideally we would test `end-of-this-message' against the result
1076      ;; of `window-end', but that doesn't account for the fact that
1077      ;; the end of the message might be hidden, so we have to actually
1078      ;; go to the end, walk back over invisible text and then see if
1079      ;; point is visible.
1080      ((save-excursion
1081         (goto-char (- end-of-this-message 1))
1082         (notmuch-show-move-past-invisible-backward)
1083         (> (point) (window-end)))
1084       ;; The bottom of this message is not visible - scroll.
1085       (scroll-up nil))
1086
1087      ((not (= end-of-this-message (point-max)))
1088       ;; This is not the last message - move to the next visible one.
1089       (notmuch-show-next-open-message))
1090
1091      (t
1092       ;; This is the last message - archive the thread.
1093       (notmuch-show-archive-thread)))))
1094
1095 (defun notmuch-show-rewind ()
1096   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
1097
1098 Specifically, if the beginning of the previous email is fewer
1099 than `window-height' lines from the current point, move to it
1100 just like `notmuch-show-previous-message'.
1101
1102 Otherwise, just scroll down a screenful of the current message.
1103
1104 This command does not modify any message tags, (it does not undo
1105 any effects from previous calls to
1106 `notmuch-show-advance-and-archive'."
1107   (interactive)
1108   (let ((start-of-message (notmuch-show-message-top))
1109         (start-of-window (window-start)))
1110     (cond
1111       ;; Either this message is properly aligned with the start of the
1112       ;; window or the start of this message is not visible on the
1113       ;; screen - scroll.
1114      ((or (= start-of-message start-of-window)
1115           (< start-of-message start-of-window))
1116       (scroll-down)
1117       ;; If a small number of lines from the previous message are
1118       ;; visible, realign so that the top of the current message is at
1119       ;; the top of the screen.
1120       (if (<= (count-screen-lines (window-start) start-of-message)
1121               next-screen-context-lines)
1122           (progn
1123             (goto-char (notmuch-show-message-top))
1124             (notmuch-show-message-adjust)))
1125       ;; Move to the top left of the window.
1126       (goto-char (window-start)))
1127      (t
1128       ;; Move to the previous message.
1129       (notmuch-show-previous-message)))))
1130
1131 (defun notmuch-show-reply (&optional prompt-for-sender)
1132   "Reply to the current message."
1133   (interactive "P")
1134   (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender))
1135
1136 (defun notmuch-show-forward-message (&optional prompt-for-sender)
1137   "Forward the current message."
1138   (interactive "P")
1139   (with-current-notmuch-show-message
1140    (notmuch-mua-new-forward-message prompt-for-sender)))
1141
1142 (defun notmuch-show-next-message ()
1143   "Show the next message."
1144   (interactive)
1145   (if (notmuch-show-goto-message-next)
1146       (progn
1147         (notmuch-show-mark-read)
1148         (notmuch-show-message-adjust))
1149     (goto-char (point-max))))
1150
1151 (defun notmuch-show-previous-message ()
1152   "Show the previous message."
1153   (interactive)
1154   (notmuch-show-goto-message-previous)
1155   (notmuch-show-mark-read)
1156   (notmuch-show-message-adjust))
1157
1158 (defun notmuch-show-next-open-message ()
1159   "Show the next message."
1160   (interactive)
1161   (let (r)
1162     (while (and (setq r (notmuch-show-goto-message-next))
1163                 (not (notmuch-show-message-visible-p))))
1164     (if r
1165         (progn
1166           (notmuch-show-mark-read)
1167           (notmuch-show-message-adjust))
1168       (goto-char (point-max)))))
1169
1170 (defun notmuch-show-previous-open-message ()
1171   "Show the previous message."
1172   (interactive)
1173   (while (and (notmuch-show-goto-message-previous)
1174               (not (notmuch-show-message-visible-p))))
1175   (notmuch-show-mark-read)
1176   (notmuch-show-message-adjust))
1177
1178 (defun notmuch-show-view-raw-message ()
1179   "View the file holding the current message."
1180   (interactive)
1181   (let* ((id (notmuch-show-get-message-id))
1182          (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
1183     (call-process notmuch-command nil buf nil "show" "--format=raw" id)
1184     (switch-to-buffer buf)
1185     (goto-char (point-min))
1186     (set-buffer-modified-p nil)
1187     (view-buffer buf 'kill-buffer-if-not-modified)))
1188
1189 (defun notmuch-show-pipe-message (entire-thread command)
1190   "Pipe the contents of the current message (or thread) to the given command.
1191
1192 The given command will be executed with the raw contents of the
1193 current email message as stdin. Anything printed by the command
1194 to stdout or stderr will appear in the *notmuch-pipe* buffer.
1195
1196 When invoked with a prefix argument, the command will receive all
1197 open messages in the current thread (formatted as an mbox) rather
1198 than only the current message."
1199   (interactive "P\nsPipe message to command: ")
1200   (let (shell-command)
1201     (if entire-thread
1202         (setq shell-command 
1203               (concat notmuch-command " show --format=mbox "
1204                       (shell-quote-argument
1205                        (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
1206                       " | " command))
1207       (setq shell-command
1208             (concat notmuch-command " show --format=raw "
1209                     (shell-quote-argument (notmuch-show-get-message-id)) " | " command)))
1210     (let ((buf (get-buffer-create (concat "*notmuch-pipe*"))))
1211       (with-current-buffer buf
1212         (setq buffer-read-only nil)
1213         (erase-buffer)
1214         (let ((exit-code (call-process-shell-command shell-command nil buf)))
1215           (goto-char (point-max))
1216           (set-buffer-modified-p nil)
1217           (setq buffer-read-only t)
1218           (unless (zerop exit-code)
1219             (switch-to-buffer-other-window buf)
1220             (message (format "Command '%s' exited abnormally with code %d"
1221                              shell-command exit-code))))))))
1222
1223 (defun notmuch-show-add-tags-worker (current-tags add-tags)
1224   "Add to `current-tags' with any tags from `add-tags' not
1225 currently present and return the result."
1226   (let ((result-tags (copy-sequence current-tags)))
1227     (mapc (lambda (add-tag)
1228             (unless (member add-tag current-tags)
1229               (setq result-tags (push add-tag result-tags))))
1230             add-tags)
1231     (sort result-tags 'string<)))
1232
1233 (defun notmuch-show-del-tags-worker (current-tags del-tags)
1234   "Remove any tags in `del-tags' from `current-tags' and return
1235 the result."
1236   (let ((result-tags (copy-sequence current-tags)))
1237     (mapc (lambda (del-tag)
1238             (setq result-tags (delete del-tag result-tags)))
1239           del-tags)
1240     result-tags))
1241
1242 (defun notmuch-show-add-tag (&rest toadd)
1243   "Add a tag to the current message."
1244   (interactive
1245    (list (notmuch-select-tag-with-completion "Tag to add: ")))
1246
1247   (let* ((current-tags (notmuch-show-get-tags))
1248          (new-tags (notmuch-show-add-tags-worker current-tags toadd)))
1249
1250     (unless (equal current-tags new-tags)
1251       (apply 'notmuch-tag (notmuch-show-get-message-id)
1252              (mapcar (lambda (s) (concat "+" s)) toadd))
1253       (notmuch-show-set-tags new-tags))))
1254
1255 (defun notmuch-show-remove-tag (&rest toremove)
1256   "Remove a tag from the current message."
1257   (interactive
1258    (list (notmuch-select-tag-with-completion
1259           "Tag to remove: " (notmuch-show-get-message-id))))
1260
1261   (let* ((current-tags (notmuch-show-get-tags))
1262          (new-tags (notmuch-show-del-tags-worker current-tags toremove)))
1263
1264     (unless (equal current-tags new-tags)
1265       (apply 'notmuch-tag (notmuch-show-get-message-id)
1266              (mapcar (lambda (s) (concat "-" s)) toremove))
1267       (notmuch-show-set-tags new-tags))))
1268
1269 (defun notmuch-show-toggle-headers ()
1270   "Toggle the visibility of the current message headers."
1271   (interactive)
1272   (let ((props (notmuch-show-get-message-properties)))
1273     (notmuch-show-headers-visible
1274      props
1275      (not (plist-get props :headers-visible))))
1276   (force-window-update))
1277
1278 (defun notmuch-show-toggle-message ()
1279   "Toggle the visibility of the current message."
1280   (interactive)
1281   (let ((props (notmuch-show-get-message-properties)))
1282     (notmuch-show-message-visible
1283      props
1284      (not (plist-get props :message-visible))))
1285   (force-window-update))
1286
1287 (defun notmuch-show-open-or-close-all ()
1288   "Set the visibility all of the messages in the current thread.
1289 By default make all of the messages visible. With a prefix
1290 argument, hide all of the messages."
1291   (interactive)
1292   (save-excursion
1293     (goto-char (point-min))
1294     (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
1295                                            (not current-prefix-arg))
1296           until (not (notmuch-show-goto-message-next))))
1297   (force-window-update))
1298
1299 (defun notmuch-show-next-button ()
1300   "Advance point to the next button in the buffer."
1301   (interactive)
1302   (forward-button 1))
1303
1304 (defun notmuch-show-previous-button ()
1305   "Move point back to the previous button in the buffer."
1306   (interactive)
1307   (backward-button 1))
1308
1309 (defun notmuch-show-archive-thread-internal (show-next)
1310   ;; Remove the tag from the current set of messages.
1311   (goto-char (point-min))
1312   (loop do (notmuch-show-remove-tag "inbox")
1313         until (not (notmuch-show-goto-message-next)))
1314   ;; Move to the next item in the search results, if any.
1315   (let ((parent-buffer notmuch-show-parent-buffer))
1316     (notmuch-kill-this-buffer)
1317     (if parent-buffer
1318         (progn
1319           (switch-to-buffer parent-buffer)
1320           (forward-line)
1321           (if show-next
1322               (notmuch-search-show-thread))))))
1323
1324 (defun notmuch-show-archive-thread ()
1325   "Archive each message in thread, then show next thread from search.
1326
1327 Archive each message currently shown by removing the \"inbox\"
1328 tag from each. Then kill this buffer and show the next thread
1329 from the search from which this thread was originally shown.
1330
1331 Note: This command is safe from any race condition of new messages
1332 being delivered to the same thread. It does not archive the
1333 entire thread, but only the messages shown in the current
1334 buffer."
1335   (interactive)
1336   (notmuch-show-archive-thread-internal t))
1337
1338 (defun notmuch-show-archive-thread-then-exit ()
1339   "Archive each message in thread, then exit back to search results."
1340   (interactive)
1341   (notmuch-show-archive-thread-internal nil))
1342
1343 (defun notmuch-show-stash-cc ()
1344   "Copy CC field of current message to kill-ring."
1345   (interactive)
1346   (notmuch-common-do-stash (notmuch-show-get-cc)))
1347
1348 (defun notmuch-show-stash-date ()
1349   "Copy date of current message to kill-ring."
1350   (interactive)
1351   (notmuch-common-do-stash (notmuch-show-get-date)))
1352
1353 (defun notmuch-show-stash-filename ()
1354   "Copy filename of current message to kill-ring."
1355   (interactive)
1356   (notmuch-common-do-stash (notmuch-show-get-filename)))
1357
1358 (defun notmuch-show-stash-from ()
1359   "Copy From address of current message to kill-ring."
1360   (interactive)
1361   (notmuch-common-do-stash (notmuch-show-get-from)))
1362
1363 (defun notmuch-show-stash-message-id ()
1364   "Copy message ID of current message to kill-ring."
1365   (interactive)
1366   (notmuch-common-do-stash (notmuch-show-get-message-id)))
1367
1368 (defun notmuch-show-stash-subject ()
1369   "Copy Subject field of current message to kill-ring."
1370   (interactive)
1371   (notmuch-common-do-stash (notmuch-show-get-subject)))
1372
1373 (defun notmuch-show-stash-tags ()
1374   "Copy tags of current message to kill-ring as a comma separated list."
1375   (interactive)
1376   (notmuch-common-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
1377
1378 (defun notmuch-show-stash-to ()
1379   "Copy To address of current message to kill-ring."
1380   (interactive)
1381   (notmuch-common-do-stash (notmuch-show-get-to)))
1382
1383 ;; Commands typically bound to buttons.
1384
1385 (defun notmuch-show-part-button-action (button)
1386   (let ((nth (button-get button :notmuch-part)))
1387     (if nth
1388         (notmuch-show-save-part (notmuch-show-get-message-id) nth
1389                                 (button-get button :notmuch-filename))
1390       (message "Not a valid part (is it a fake part?)."))))
1391
1392 ;;
1393
1394 (provide 'notmuch-show)