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