]> git.notmuchmail.org Git - notmuch/blob - emacs/notmuch-show.el
Merge commit '0.6'
[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   (let* ((message-part (plist-get part :content))
512          (inner-parts (plist-get message-part :content)))
513     (notmuch-show-insert-part-header nth declared-type content-type nil)
514     ;; Override `notmuch-message-headers' to force `From' to be
515     ;; displayed.
516     (let ((notmuch-message-headers '("From" "Subject" "To" "Cc" "Date")))
517       (notmuch-show-insert-headers (plist-get part :headers)))
518     ;; Blank line after headers to be compatible with the normal
519     ;; message display.
520     (insert "\n")
521
522     ;; Show all of the parts.
523     (mapc (lambda (inner-part)
524             (notmuch-show-insert-bodypart msg inner-part depth))
525           inner-parts))
526   t)
527
528 (defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type)
529   (let ((start (point)))
530     ;; If this text/plain part is not the first part in the message,
531     ;; insert a header to make this clear.
532     (if (> nth 1)
533         (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
534     (insert (notmuch-show-get-bodypart-content msg part nth))
535     (save-excursion
536       (save-restriction
537         (narrow-to-region start (point-max))
538         (run-hook-with-args 'notmuch-show-insert-text/plain-hook msg depth))))
539   t)
540
541 (defun notmuch-show-insert-part-text/x-vcalendar (msg part content-type nth depth declared-type)
542   (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename))
543   (insert (with-temp-buffer
544             (insert (notmuch-show-get-bodypart-content msg part nth))
545             (goto-char (point-min))
546             (let ((file (make-temp-file "notmuch-ical"))
547                   result)
548               (icalendar--convert-ical-to-diary
549                (icalendar--read-element nil nil)
550                file t)
551               (set-buffer (get-file-buffer file))
552               (setq result (buffer-substring (point-min) (point-max)))
553               (set-buffer-modified-p nil)
554               (kill-buffer (current-buffer))
555               (delete-file file)
556               result)))
557   t)
558
559 (defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type)
560   ;; If we can deduce a MIME type from the filename of the attachment,
561   ;; do so and pass it on to the handler for that type.
562   (if (plist-get part :filename)
563       (let ((extension (file-name-extension (plist-get part :filename)))
564             mime-type)
565         (if extension
566             (progn
567               (mailcap-parse-mimetypes)
568               (setq mime-type (mailcap-extension-to-mime extension))
569               (if (and mime-type
570                        (not (string-equal mime-type "application/octet-stream")))
571                   (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type)
572                 nil))
573           nil))))
574
575 (defun notmuch-show-insert-part-application/* (msg part content-type nth depth declared-type
576 )
577   ;; do not render random "application" parts
578   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename)))
579
580 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
581   ;; This handler _must_ succeed - it is the handler of last resort.
582   (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
583   (let ((content (notmuch-show-get-bodypart-content msg part nth)))
584     (if content
585         (notmuch-show-mm-display-part-inline msg part content-type content)))
586   t)
587
588 ;; Functions for determining how to handle MIME parts.
589
590 (defun notmuch-show-split-content-type (content-type)
591   (split-string content-type "/"))
592
593 (defun notmuch-show-handlers-for (content-type)
594   "Return a list of content handlers for a part of type CONTENT-TYPE."
595   (let (result)
596     (mapc (lambda (func)
597             (if (functionp func)
598                 (push func result)))
599           ;; Reverse order of prefrence.
600           (list (intern (concat "notmuch-show-insert-part-*/*"))
601                 (intern (concat
602                          "notmuch-show-insert-part-"
603                          (car (notmuch-show-split-content-type content-type))
604                          "/*"))
605                 (intern (concat "notmuch-show-insert-part-" content-type))))
606     result))
607
608 ;; Helper for parts which are generally not included in the default
609 ;; JSON output.
610 ;; Uses the buffer-local variable notmuch-show-process-crypto to
611 ;; determine if parts should be decrypted first.
612 (defun notmuch-show-get-bodypart-internal (message-id part-number)
613   (let ((args '("show" "--format=raw"))
614         (part-arg (format "--part=%s" part-number)))
615     (setq args (append args (list part-arg)))
616     (if notmuch-show-process-crypto
617         (setq args (append args '("--decrypt"))))
618     (setq args (append args (list message-id)))
619     (with-temp-buffer
620       (let ((coding-system-for-read 'no-conversion))
621         (progn
622           (apply 'call-process (append (list notmuch-command nil (list t nil) nil) args))
623           (buffer-string))))))
624
625 (defun notmuch-show-get-bodypart-content (msg part nth)
626   (or (plist-get part :content)
627       (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
628
629 ;; \f
630
631 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
632   (let ((handlers (notmuch-show-handlers-for content-type)))
633     ;; Run the content handlers until one of them returns a non-nil
634     ;; value.
635     (while (and handlers
636                 (not (funcall (car handlers) msg part content-type nth depth declared-type)))
637       (setq handlers (cdr handlers))))
638   t)
639
640 (defun notmuch-show-insert-bodypart (msg part depth)
641   "Insert the body part PART at depth DEPTH in the current thread."
642   (let ((content-type (downcase (plist-get part :content-type)))
643         (nth (plist-get part :id)))
644     (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
645   ;; Some of the body part handlers leave point somewhere up in the
646   ;; part, so we make sure that we're down at the end.
647   (goto-char (point-max))
648   ;; Ensure that the part ends with a carriage return.
649   (if (not (bolp))
650       (insert "\n")))
651
652 (defun notmuch-show-insert-body (msg body depth)
653   "Insert the body BODY at depth DEPTH in the current thread."
654   (mapc '(lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
655
656 (defun notmuch-show-make-symbol (type)
657   (make-symbol (concat "notmuch-show-" type)))
658
659 (defun notmuch-show-strip-re (string)
660   (replace-regexp-in-string "\\([Rr]e: *\\)+" "" string))
661
662 (defvar notmuch-show-previous-subject "")
663 (make-variable-buffer-local 'notmuch-show-previous-subject)
664
665 (defun notmuch-show-insert-msg (msg depth)
666   "Insert the message MSG at depth DEPTH in the current thread."
667   (let* ((headers (plist-get msg :headers))
668          ;; Indentation causes the buffer offset of the start/end
669          ;; points to move, so we must use markers.
670          message-start message-end
671          content-start content-end
672          headers-start headers-end
673          body-start body-end
674          (headers-invis-spec (notmuch-show-make-symbol "header"))
675          (message-invis-spec (notmuch-show-make-symbol "message"))
676          (bare-subject (notmuch-show-strip-re (plist-get headers :Subject))))
677
678     ;; Set `buffer-invisibility-spec' to `nil' (a list), otherwise
679     ;; removing items from `buffer-invisibility-spec' (which is what
680     ;; `notmuch-show-headers-visible' and
681     ;; `notmuch-show-message-visible' do) is a no-op and has no
682     ;; effect. This caused threads with only matching messages to have
683     ;; those messages hidden initially because
684     ;; `buffer-invisibility-spec' stayed `t'.
685     ;;
686     ;; This needs to be set here (rather than just above the call to
687     ;; `notmuch-show-headers-visible') because some of the part
688     ;; rendering or body washing functions
689     ;; (e.g. `notmuch-wash-text/plain-citations') manipulate
690     ;; `buffer-invisibility-spec').
691     (when (eq buffer-invisibility-spec t)
692       (setq buffer-invisibility-spec nil))
693
694     (setq message-start (point-marker))
695
696     (notmuch-show-insert-headerline headers
697                                     (or (if notmuch-show-relative-dates
698                                             (plist-get msg :date_relative)
699                                           nil)
700                                         (plist-get headers :Date))
701                                     (plist-get msg :tags) depth)
702
703     (setq content-start (point-marker))
704
705     (plist-put msg :headers-invis-spec headers-invis-spec)
706     (plist-put msg :message-invis-spec message-invis-spec)
707
708     ;; Set `headers-start' to point after the 'Subject:' header to be
709     ;; compatible with the existing implementation. This just sets it
710     ;; to after the first header.
711     (notmuch-show-insert-headers headers)
712     ;; Headers should include a blank line (backwards compatibility).
713     (insert "\n")
714     (save-excursion
715       (goto-char content-start)
716       ;; If the subject of this message is the same as that of the
717       ;; previous message, don't display it when this message is
718       ;; collapsed.
719       (when (not (string= notmuch-show-previous-subject
720                           bare-subject))
721         (forward-line 1))
722       (setq headers-start (point-marker)))
723     (setq headers-end (point-marker))
724
725     (setq notmuch-show-previous-subject bare-subject)
726
727     (setq body-start (point-marker))
728     (notmuch-show-insert-body msg (plist-get msg :body) depth)
729     ;; Ensure that the body ends with a newline.
730     (if (not (bolp))
731         (insert "\n"))
732     (setq body-end (point-marker))
733     (setq content-end (point-marker))
734
735     ;; Indent according to the depth in the thread.
736     (indent-rigidly content-start content-end depth)
737
738     (setq message-end (point-max-marker))
739
740     ;; Save the extents of this message over the whole text of the
741     ;; message.
742     (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
743
744     (let ((headers-overlay (make-overlay headers-start headers-end))
745           (invis-specs (list headers-invis-spec message-invis-spec)))
746       (overlay-put headers-overlay 'invisible invis-specs)
747       (overlay-put headers-overlay 'priority 10))
748     (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
749
750     ;; Save the properties for this message. Currently this saves the
751     ;; entire message (augmented it with other stuff), which seems
752     ;; like overkill. We might save a reduced subset (for example, not
753     ;; the content).
754     (notmuch-show-set-message-properties msg)
755
756     ;; Set header visibility.
757     (notmuch-show-headers-visible msg notmuch-message-headers-visible)
758
759     ;; Message visibility depends on whether it matched the search
760     ;; criteria.
761     (notmuch-show-message-visible msg (plist-get msg :match))))
762
763 (defun notmuch-show-insert-tree (tree depth)
764   "Insert the message tree TREE at depth DEPTH in the current thread."
765   (let ((msg (car tree))
766         (replies (cadr tree)))
767     (notmuch-show-insert-msg msg depth)
768     (notmuch-show-insert-thread replies (1+ depth))))
769
770 (defun notmuch-show-insert-thread (thread depth)
771   "Insert the thread THREAD at depth DEPTH in the current forest."
772   (mapc '(lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
773
774 (defun notmuch-show-insert-forest (forest)
775   "Insert the forest of threads FOREST."
776   (mapc '(lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
777
778 (defvar notmuch-show-parent-buffer nil)
779 (make-variable-buffer-local 'notmuch-show-parent-buffer)
780
781 ;;;###autoload
782 (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name crypto-switch)
783   "Run \"notmuch show\" with the given thread ID and display results.
784
785 The optional PARENT-BUFFER is the notmuch-search buffer from
786 which this notmuch-show command was executed, (so that the
787 next thread from that buffer can be show when done with this
788 one).
789
790 The optional QUERY-CONTEXT is a notmuch search term. Only
791 messages from the thread matching this search term are shown if
792 non-nil.
793
794 The optional BUFFER-NAME provides the name of the buffer in
795 which the message thread is shown. If it is nil (which occurs
796 when the command is called interactively) the argument to the
797 function is used. "
798   (interactive "sNotmuch show: ")
799   (let ((buffer (get-buffer-create (generate-new-buffer-name
800                                     (or buffer-name
801                                         (concat "*notmuch-" thread-id "*")))))
802         (process-crypto (if crypto-switch
803                             (not notmuch-crypto-process-mime)
804                           notmuch-crypto-process-mime))
805         (inhibit-read-only t))
806     (switch-to-buffer buffer)
807     (notmuch-show-mode)
808     (setq notmuch-show-parent-buffer parent-buffer)
809     (setq notmuch-show-process-crypto process-crypto)
810     (erase-buffer)
811     (goto-char (point-min))
812     (save-excursion
813       (let* ((basic-args (list thread-id))
814              (args (if query-context
815                        (append (list "\'") basic-args (list "and (" query-context ")\'"))
816                      (append (list "\'") basic-args (list "\'")))))
817         (notmuch-show-insert-forest (notmuch-query-get-threads args))
818         ;; If the query context reduced the results to nothing, run
819         ;; the basic query.
820         (when (and (eq (buffer-size) 0)
821                    query-context)
822           (notmuch-show-insert-forest
823            (notmuch-query-get-threads basic-args))))
824
825       ;; Enable buttonisation of URLs and email addresses in the
826       ;; buffer.
827       (goto-address-mode t)
828       ;; Act on visual lines rather than logical lines.
829       (visual-line-mode t)
830
831       (run-hooks 'notmuch-show-hook))
832
833     ;; Move straight to the first open message
834     (if (not (notmuch-show-message-visible-p))
835         (notmuch-show-next-open-message))
836
837     ;; Set the header line to the subject of the first open message.
838     (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
839
840     (notmuch-show-mark-read)))
841
842 (defvar notmuch-show-stash-map
843   (let ((map (make-sparse-keymap)))
844     (define-key map "c" 'notmuch-show-stash-cc)
845     (define-key map "d" 'notmuch-show-stash-date)
846     (define-key map "F" 'notmuch-show-stash-filename)
847     (define-key map "f" 'notmuch-show-stash-from)
848     (define-key map "i" 'notmuch-show-stash-message-id)
849     (define-key map "s" 'notmuch-show-stash-subject)
850     (define-key map "T" 'notmuch-show-stash-tags)
851     (define-key map "t" 'notmuch-show-stash-to)
852     map)
853   "Submap for stash commands")
854 (fset 'notmuch-show-stash-map notmuch-show-stash-map)
855
856 (defvar notmuch-show-mode-map
857       (let ((map (make-sparse-keymap)))
858         (define-key map "?" 'notmuch-help)
859         (define-key map "q" 'notmuch-kill-this-buffer)
860         (define-key map (kbd "<C-tab>") 'widget-backward)
861         (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
862         (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
863         (define-key map (kbd "TAB") 'notmuch-show-next-button)
864         (define-key map "s" 'notmuch-search)
865         (define-key map "m" 'notmuch-mua-new-mail)
866         (define-key map "f" 'notmuch-show-forward-message)
867         (define-key map "r" 'notmuch-show-reply)
868         (define-key map "|" 'notmuch-show-pipe-message)
869         (define-key map "w" 'notmuch-show-save-attachments)
870         (define-key map "V" 'notmuch-show-view-raw-message)
871         (define-key map "v" 'notmuch-show-view-all-mime-parts)
872         (define-key map "c" 'notmuch-show-stash-map)
873         (define-key map "h" 'notmuch-show-toggle-headers)
874         (define-key map "-" 'notmuch-show-remove-tag)
875         (define-key map "+" 'notmuch-show-add-tag)
876         (define-key map "x" 'notmuch-show-archive-thread-then-exit)
877         (define-key map "a" 'notmuch-show-archive-thread)
878         (define-key map "N" 'notmuch-show-next-message)
879         (define-key map "P" 'notmuch-show-previous-message)
880         (define-key map "n" 'notmuch-show-next-open-message)
881         (define-key map "p" 'notmuch-show-previous-open-message)
882         (define-key map (kbd "DEL") 'notmuch-show-rewind)
883         (define-key map " " 'notmuch-show-advance-and-archive)
884         (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
885         (define-key map (kbd "RET") 'notmuch-show-toggle-message)
886         map)
887       "Keymap for \"notmuch show\" buffers.")
888 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
889
890 (defun notmuch-show-mode ()
891   "Major mode for viewing a thread with notmuch.
892
893 This buffer contains the results of the \"notmuch show\" command
894 for displaying a single thread of email from your email archives.
895
896 By default, various components of email messages, (citations,
897 signatures, already-read messages), are hidden. You can make
898 these parts visible by clicking with the mouse button or by
899 pressing RET after positioning the cursor on a hidden part, (for
900 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
901
902 Reading the thread sequentially is well-supported by pressing
903 \\[notmuch-show-advance-and-archive]. This will scroll the current message (if necessary), advance
904 to the next message, or advance to the next thread (if already on
905 the last message of a thread).
906
907 Other commands are available to read or manipulate the thread
908 more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages
909 without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread
910 without scrolling through with \\[notmuch-show-advance-and-archive]).
911
912 You can add or remove arbitrary tags from the current message with
913 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
914
915 All currently available key bindings:
916
917 \\{notmuch-show-mode-map}"
918   (interactive)
919   (kill-all-local-variables)
920   (use-local-map notmuch-show-mode-map)
921   (setq major-mode 'notmuch-show-mode
922         mode-name "notmuch-show")
923   (setq buffer-read-only t))
924
925 (defun notmuch-show-move-to-message-top ()
926   (goto-char (notmuch-show-message-top)))
927
928 (defun notmuch-show-move-to-message-bottom ()
929   (goto-char (notmuch-show-message-bottom)))
930
931 (defun notmuch-show-message-adjust ()
932   (recenter 0))
933
934 ;; Movement related functions.
935
936 ;; There's some strangeness here where a text property applied to a
937 ;; region a->b is not found when point is at b. We walk backwards
938 ;; until finding the property.
939 (defun notmuch-show-message-extent ()
940   (let (r)
941     (save-excursion
942       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
943         (backward-char)))
944     r))
945
946 (defun notmuch-show-message-top ()
947   (car (notmuch-show-message-extent)))
948
949 (defun notmuch-show-message-bottom ()
950   (cdr (notmuch-show-message-extent)))
951
952 (defun notmuch-show-goto-message-next ()
953   (let ((start (point)))
954     (notmuch-show-move-to-message-bottom)
955     (if (not (eobp))
956         t
957       (goto-char start)
958       nil)))
959
960 (defun notmuch-show-goto-message-previous ()
961   (notmuch-show-move-to-message-top)
962   (if (bobp)
963       nil
964     (backward-char)
965     (notmuch-show-move-to-message-top)
966     t))
967
968 (defun notmuch-show-move-past-invisible-forward ()
969   (while (point-invisible-p)
970     (forward-char)))
971
972 (defun notmuch-show-move-past-invisible-backward ()
973   (while (point-invisible-p)
974     (backward-char)))
975
976 ;; Functions relating to the visibility of messages and their
977 ;; components.
978
979 (defun notmuch-show-element-visible (props visible-p spec-property)
980   (let ((spec (plist-get props spec-property)))
981     (if visible-p
982         (remove-from-invisibility-spec spec)
983       (add-to-invisibility-spec spec))))
984
985 (defun notmuch-show-message-visible (props visible-p)
986   (notmuch-show-element-visible props visible-p :message-invis-spec)
987   (notmuch-show-set-prop :message-visible visible-p props))
988
989 (defun notmuch-show-headers-visible (props visible-p)
990   (notmuch-show-element-visible props visible-p :headers-invis-spec)
991   (notmuch-show-set-prop :headers-visible visible-p props))
992
993 ;; Functions for setting and getting attributes of the current
994 ;; message.
995
996 (defun notmuch-show-set-message-properties (props)
997   (save-excursion
998     (notmuch-show-move-to-message-top)
999     (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
1000
1001 (defun notmuch-show-get-message-properties ()
1002   (save-excursion
1003     (notmuch-show-move-to-message-top)
1004     (get-text-property (point) :notmuch-message-properties)))
1005
1006 (defun notmuch-show-set-prop (prop val &optional props)
1007   (let ((inhibit-read-only t)
1008         (props (or props
1009                    (notmuch-show-get-message-properties))))
1010     (plist-put props prop val)
1011     (notmuch-show-set-message-properties props)))
1012
1013 (defun notmuch-show-get-prop (prop &optional props)
1014   (let ((props (or props
1015                    (notmuch-show-get-message-properties))))
1016     (plist-get props prop)))
1017
1018 (defun notmuch-show-get-message-id ()
1019   "Return the message id of the current message."
1020   (concat "id:\"" (notmuch-show-get-prop :id) "\""))
1021
1022 ;; dme: Would it make sense to use a macro for many of these?
1023
1024 (defun notmuch-show-get-filename ()
1025   "Return the filename of the current message."
1026   (notmuch-show-get-prop :filename))
1027
1028 (defun notmuch-show-get-header (header)
1029   "Return the named header of the current message, if any."
1030   (plist-get (notmuch-show-get-prop :headers) header))
1031
1032 (defun notmuch-show-get-cc ()
1033   (notmuch-show-get-header :Cc))
1034
1035 (defun notmuch-show-get-date ()
1036   (notmuch-show-get-header :Date))
1037
1038 (defun notmuch-show-get-from ()
1039   (notmuch-show-get-header :From))
1040
1041 (defun notmuch-show-get-subject ()
1042   (notmuch-show-get-header :Subject))
1043
1044 (defun notmuch-show-get-to ()
1045   (notmuch-show-get-header :To))
1046
1047 (defun notmuch-show-set-tags (tags)
1048   "Set the tags of the current message."
1049   (notmuch-show-set-prop :tags tags)
1050   (notmuch-show-update-tags tags))
1051
1052 (defun notmuch-show-get-tags ()
1053   "Return the tags of the current message."
1054   (notmuch-show-get-prop :tags))
1055
1056 (defun notmuch-show-message-visible-p ()
1057   "Is the current message visible?"
1058   (notmuch-show-get-prop :message-visible))
1059
1060 (defun notmuch-show-headers-visible-p ()
1061   "Are the headers of the current message visible?"
1062   (notmuch-show-get-prop :headers-visible))
1063
1064 (defun notmuch-show-mark-read ()
1065   "Mark the current message as read."
1066   (notmuch-show-remove-tag "unread"))
1067
1068 ;; Functions for getting attributes of several messages in the current
1069 ;; thread.
1070
1071 (defun notmuch-show-get-message-ids-for-open-messages ()
1072   "Return a list of all message IDs for open messages in the current thread."
1073   (save-excursion
1074     (let (message-ids done)
1075       (goto-char (point-min))
1076       (while (not done)
1077         (if (notmuch-show-message-visible-p)
1078             (setq message-ids (append message-ids (list (notmuch-show-get-message-id)))))
1079         (setq done (not (notmuch-show-goto-message-next)))
1080         )
1081       message-ids
1082       )))
1083
1084 ;; Commands typically bound to keys.
1085
1086 (defun notmuch-show-advance-and-archive ()
1087   "Advance through thread and archive.
1088
1089 This command is intended to be one of the simplest ways to
1090 process a thread of email. It does the following:
1091
1092 If the current message in the thread is not yet fully visible,
1093 scroll by a near screenful to read more of the message.
1094
1095 Otherwise, (the end of the current message is already within the
1096 current window), advance to the next open message.
1097
1098 Finally, if there is no further message to advance to, and this
1099 last message is already read, then archive the entire current
1100 thread, (remove the \"inbox\" tag from each message). Also kill
1101 this buffer, and display the next thread from the search from
1102 which this thread was originally shown."
1103   (interactive)
1104   (let ((end-of-this-message (notmuch-show-message-bottom)))
1105     (cond
1106      ;; Ideally we would test `end-of-this-message' against the result
1107      ;; of `window-end', but that doesn't account for the fact that
1108      ;; the end of the message might be hidden, so we have to actually
1109      ;; go to the end, walk back over invisible text and then see if
1110      ;; point is visible.
1111      ((save-excursion
1112         (goto-char (- end-of-this-message 1))
1113         (notmuch-show-move-past-invisible-backward)
1114         (> (point) (window-end)))
1115       ;; The bottom of this message is not visible - scroll.
1116       (scroll-up nil))
1117
1118      ((not (= end-of-this-message (point-max)))
1119       ;; This is not the last message - move to the next visible one.
1120       (notmuch-show-next-open-message))
1121
1122      (t
1123       ;; This is the last message - archive the thread.
1124       (notmuch-show-archive-thread)))))
1125
1126 (defun notmuch-show-rewind ()
1127   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
1128
1129 Specifically, if the beginning of the previous email is fewer
1130 than `window-height' lines from the current point, move to it
1131 just like `notmuch-show-previous-message'.
1132
1133 Otherwise, just scroll down a screenful of the current message.
1134
1135 This command does not modify any message tags, (it does not undo
1136 any effects from previous calls to
1137 `notmuch-show-advance-and-archive'."
1138   (interactive)
1139   (let ((start-of-message (notmuch-show-message-top))
1140         (start-of-window (window-start)))
1141     (cond
1142       ;; Either this message is properly aligned with the start of the
1143       ;; window or the start of this message is not visible on the
1144       ;; screen - scroll.
1145      ((or (= start-of-message start-of-window)
1146           (< start-of-message start-of-window))
1147       (scroll-down)
1148       ;; If a small number of lines from the previous message are
1149       ;; visible, realign so that the top of the current message is at
1150       ;; the top of the screen.
1151       (if (<= (count-screen-lines (window-start) start-of-message)
1152               next-screen-context-lines)
1153           (progn
1154             (goto-char (notmuch-show-message-top))
1155             (notmuch-show-message-adjust)))
1156       ;; Move to the top left of the window.
1157       (goto-char (window-start)))
1158      (t
1159       ;; Move to the previous message.
1160       (notmuch-show-previous-message)))))
1161
1162 (defun notmuch-show-reply (&optional prompt-for-sender)
1163   "Reply to the current message."
1164   (interactive "P")
1165   (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender))
1166
1167 (defun notmuch-show-forward-message (&optional prompt-for-sender)
1168   "Forward the current message."
1169   (interactive "P")
1170   (with-current-notmuch-show-message
1171    (notmuch-mua-new-forward-message prompt-for-sender)))
1172
1173 (defun notmuch-show-next-message ()
1174   "Show the next message."
1175   (interactive)
1176   (if (notmuch-show-goto-message-next)
1177       (progn
1178         (notmuch-show-mark-read)
1179         (notmuch-show-message-adjust))
1180     (goto-char (point-max))))
1181
1182 (defun notmuch-show-previous-message ()
1183   "Show the previous message."
1184   (interactive)
1185   (notmuch-show-goto-message-previous)
1186   (notmuch-show-mark-read)
1187   (notmuch-show-message-adjust))
1188
1189 (defun notmuch-show-next-open-message ()
1190   "Show the next message."
1191   (interactive)
1192   (let (r)
1193     (while (and (setq r (notmuch-show-goto-message-next))
1194                 (not (notmuch-show-message-visible-p))))
1195     (if r
1196         (progn
1197           (notmuch-show-mark-read)
1198           (notmuch-show-message-adjust))
1199       (goto-char (point-max)))))
1200
1201 (defun notmuch-show-previous-open-message ()
1202   "Show the previous message."
1203   (interactive)
1204   (while (and (notmuch-show-goto-message-previous)
1205               (not (notmuch-show-message-visible-p))))
1206   (notmuch-show-mark-read)
1207   (notmuch-show-message-adjust))
1208
1209 (defun notmuch-show-view-raw-message ()
1210   "View the file holding the current message."
1211   (interactive)
1212   (let* ((id (notmuch-show-get-message-id))
1213          (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
1214     (call-process notmuch-command nil buf nil "show" "--format=raw" id)
1215     (switch-to-buffer buf)
1216     (goto-char (point-min))
1217     (set-buffer-modified-p nil)
1218     (view-buffer buf 'kill-buffer-if-not-modified)))
1219
1220 (defun notmuch-show-pipe-message (entire-thread command)
1221   "Pipe the contents of the current message (or thread) to the given command.
1222
1223 The given command will be executed with the raw contents of the
1224 current email message as stdin. Anything printed by the command
1225 to stdout or stderr will appear in the *notmuch-pipe* buffer.
1226
1227 When invoked with a prefix argument, the command will receive all
1228 open messages in the current thread (formatted as an mbox) rather
1229 than only the current message."
1230   (interactive "P\nsPipe message to command: ")
1231   (let (shell-command)
1232     (if entire-thread
1233         (setq shell-command 
1234               (concat notmuch-command " show --format=mbox "
1235                       (shell-quote-argument
1236                        (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
1237                       " | " command))
1238       (setq shell-command
1239             (concat notmuch-command " show --format=raw "
1240                     (shell-quote-argument (notmuch-show-get-message-id)) " | " command)))
1241     (let ((buf (get-buffer-create (concat "*notmuch-pipe*"))))
1242       (with-current-buffer buf
1243         (setq buffer-read-only nil)
1244         (erase-buffer)
1245         (let ((exit-code (call-process-shell-command shell-command nil buf)))
1246           (goto-char (point-max))
1247           (set-buffer-modified-p nil)
1248           (setq buffer-read-only t)
1249           (unless (zerop exit-code)
1250             (switch-to-buffer-other-window buf)
1251             (message (format "Command '%s' exited abnormally with code %d"
1252                              shell-command exit-code))))))))
1253
1254 (defun notmuch-show-add-tags-worker (current-tags add-tags)
1255   "Add to `current-tags' with any tags from `add-tags' not
1256 currently present and return the result."
1257   (let ((result-tags (copy-sequence current-tags)))
1258     (mapc (lambda (add-tag)
1259             (unless (member add-tag current-tags)
1260               (setq result-tags (push add-tag result-tags))))
1261             add-tags)
1262     (sort result-tags 'string<)))
1263
1264 (defun notmuch-show-del-tags-worker (current-tags del-tags)
1265   "Remove any tags in `del-tags' from `current-tags' and return
1266 the result."
1267   (let ((result-tags (copy-sequence current-tags)))
1268     (mapc (lambda (del-tag)
1269             (setq result-tags (delete del-tag result-tags)))
1270           del-tags)
1271     result-tags))
1272
1273 (defun notmuch-show-add-tag (&rest toadd)
1274   "Add a tag to the current message."
1275   (interactive
1276    (list (notmuch-select-tag-with-completion "Tag to add: ")))
1277
1278   (let* ((current-tags (notmuch-show-get-tags))
1279          (new-tags (notmuch-show-add-tags-worker current-tags toadd)))
1280
1281     (unless (equal current-tags new-tags)
1282       (apply 'notmuch-tag (notmuch-show-get-message-id)
1283              (mapcar (lambda (s) (concat "+" s)) toadd))
1284       (notmuch-show-set-tags new-tags))))
1285
1286 (defun notmuch-show-remove-tag (&rest toremove)
1287   "Remove a tag from the current message."
1288   (interactive
1289    (list (notmuch-select-tag-with-completion
1290           "Tag to remove: " (notmuch-show-get-message-id))))
1291
1292   (let* ((current-tags (notmuch-show-get-tags))
1293          (new-tags (notmuch-show-del-tags-worker current-tags toremove)))
1294
1295     (unless (equal current-tags new-tags)
1296       (apply 'notmuch-tag (notmuch-show-get-message-id)
1297              (mapcar (lambda (s) (concat "-" s)) toremove))
1298       (notmuch-show-set-tags new-tags))))
1299
1300 (defun notmuch-show-toggle-headers ()
1301   "Toggle the visibility of the current message headers."
1302   (interactive)
1303   (let ((props (notmuch-show-get-message-properties)))
1304     (notmuch-show-headers-visible
1305      props
1306      (not (plist-get props :headers-visible))))
1307   (force-window-update))
1308
1309 (defun notmuch-show-toggle-message ()
1310   "Toggle the visibility of the current message."
1311   (interactive)
1312   (let ((props (notmuch-show-get-message-properties)))
1313     (notmuch-show-message-visible
1314      props
1315      (not (plist-get props :message-visible))))
1316   (force-window-update))
1317
1318 (defun notmuch-show-open-or-close-all ()
1319   "Set the visibility all of the messages in the current thread.
1320 By default make all of the messages visible. With a prefix
1321 argument, hide all of the messages."
1322   (interactive)
1323   (save-excursion
1324     (goto-char (point-min))
1325     (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
1326                                            (not current-prefix-arg))
1327           until (not (notmuch-show-goto-message-next))))
1328   (force-window-update))
1329
1330 (defun notmuch-show-next-button ()
1331   "Advance point to the next button in the buffer."
1332   (interactive)
1333   (forward-button 1))
1334
1335 (defun notmuch-show-previous-button ()
1336   "Move point back to the previous button in the buffer."
1337   (interactive)
1338   (backward-button 1))
1339
1340 (defun notmuch-show-archive-thread-internal (show-next)
1341   ;; Remove the tag from the current set of messages.
1342   (goto-char (point-min))
1343   (loop do (notmuch-show-remove-tag "inbox")
1344         until (not (notmuch-show-goto-message-next)))
1345   ;; Move to the next item in the search results, if any.
1346   (let ((parent-buffer notmuch-show-parent-buffer))
1347     (notmuch-kill-this-buffer)
1348     (if parent-buffer
1349         (progn
1350           (switch-to-buffer parent-buffer)
1351           (forward-line)
1352           (if show-next
1353               (notmuch-search-show-thread))))))
1354
1355 (defun notmuch-show-archive-thread ()
1356   "Archive each message in thread, then show next thread from search.
1357
1358 Archive each message currently shown by removing the \"inbox\"
1359 tag from each. Then kill this buffer and show the next thread
1360 from the search from which this thread was originally shown.
1361
1362 Note: This command is safe from any race condition of new messages
1363 being delivered to the same thread. It does not archive the
1364 entire thread, but only the messages shown in the current
1365 buffer."
1366   (interactive)
1367   (notmuch-show-archive-thread-internal t))
1368
1369 (defun notmuch-show-archive-thread-then-exit ()
1370   "Archive each message in thread, then exit back to search results."
1371   (interactive)
1372   (notmuch-show-archive-thread-internal nil))
1373
1374 (defun notmuch-show-stash-cc ()
1375   "Copy CC field of current message to kill-ring."
1376   (interactive)
1377   (notmuch-common-do-stash (notmuch-show-get-cc)))
1378
1379 (defun notmuch-show-stash-date ()
1380   "Copy date of current message to kill-ring."
1381   (interactive)
1382   (notmuch-common-do-stash (notmuch-show-get-date)))
1383
1384 (defun notmuch-show-stash-filename ()
1385   "Copy filename of current message to kill-ring."
1386   (interactive)
1387   (notmuch-common-do-stash (notmuch-show-get-filename)))
1388
1389 (defun notmuch-show-stash-from ()
1390   "Copy From address of current message to kill-ring."
1391   (interactive)
1392   (notmuch-common-do-stash (notmuch-show-get-from)))
1393
1394 (defun notmuch-show-stash-message-id ()
1395   "Copy message ID of current message to kill-ring."
1396   (interactive)
1397   (notmuch-common-do-stash (notmuch-show-get-message-id)))
1398
1399 (defun notmuch-show-stash-subject ()
1400   "Copy Subject field of current message to kill-ring."
1401   (interactive)
1402   (notmuch-common-do-stash (notmuch-show-get-subject)))
1403
1404 (defun notmuch-show-stash-tags ()
1405   "Copy tags of current message to kill-ring as a comma separated list."
1406   (interactive)
1407   (notmuch-common-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
1408
1409 (defun notmuch-show-stash-to ()
1410   "Copy To address of current message to kill-ring."
1411   (interactive)
1412   (notmuch-common-do-stash (notmuch-show-get-to)))
1413
1414 ;; Commands typically bound to buttons.
1415
1416 (defun notmuch-show-part-button-action (button)
1417   (let ((nth (button-get button :notmuch-part)))
1418     (if nth
1419         (notmuch-show-save-part (notmuch-show-get-message-id) nth
1420                                 (button-get button :notmuch-filename))
1421       (message "Not a valid part (is it a fake part?)."))))
1422
1423 ;;
1424
1425 (provide 'notmuch-show)