]> git.notmuchmail.org Git - notmuch/blob - emacs/notmuch-show.el
8db5435fb291b22abf1317ebed0c7e79c0cfd0aa
[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-thread-id nil)
784 (make-variable-buffer-local 'notmuch-show-thread-id)
785 (defvar notmuch-show-parent-buffer nil)
786 (make-variable-buffer-local 'notmuch-show-parent-buffer)
787 (defvar notmuch-show-query-context nil)
788 (make-variable-buffer-local 'notmuch-show-query-context)
789 (defvar notmuch-show-buffer-name nil)
790 (make-variable-buffer-local 'notmuch-show-buffer-name)
791
792 ;;;###autoload
793 (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name crypto-switch)
794   "Run \"notmuch show\" with the given thread ID and display results.
795
796 The optional PARENT-BUFFER is the notmuch-search buffer from
797 which this notmuch-show command was executed, (so that the
798 next thread from that buffer can be show when done with this
799 one).
800
801 The optional QUERY-CONTEXT is a notmuch search term. Only
802 messages from the thread matching this search term are shown if
803 non-nil.
804
805 The optional BUFFER-NAME provides the name of the buffer in
806 which the message thread is shown. If it is nil (which occurs
807 when the command is called interactively) the argument to the
808 function is used. "
809   (interactive "sNotmuch show: ")
810   (let* ((buffer-name (generate-new-buffer-name
811                        (or buffer-name
812                            (concat "*notmuch-" thread-id "*"))))
813          (buffer (get-buffer-create buffer-name))
814          (process-crypto (if crypto-switch
815                              (not notmuch-crypto-process-mime)
816                            notmuch-crypto-process-mime))
817          (inhibit-read-only t))
818     (switch-to-buffer buffer)
819     (notmuch-show-mode)
820
821     (setq notmuch-show-thread-id thread-id)
822     (setq notmuch-show-parent-buffer parent-buffer)
823     (setq notmuch-show-query-context query-context)
824     (setq notmuch-show-buffer-name buffer-name)
825     (setq notmuch-show-process-crypto process-crypto)
826
827     (erase-buffer)
828     (goto-char (point-min))
829     (save-excursion
830       (let* ((basic-args (list thread-id))
831              (args (if query-context
832                        (append (list "\'") basic-args (list "and (" query-context ")\'"))
833                      (append (list "\'") basic-args (list "\'")))))
834         (notmuch-show-insert-forest (notmuch-query-get-threads args))
835         ;; If the query context reduced the results to nothing, run
836         ;; the basic query.
837         (when (and (eq (buffer-size) 0)
838                    query-context)
839           (notmuch-show-insert-forest
840            (notmuch-query-get-threads basic-args))))
841
842       ;; Enable buttonisation of URLs and email addresses in the
843       ;; buffer.
844       (goto-address-mode t)
845       ;; Act on visual lines rather than logical lines.
846       (visual-line-mode t)
847
848       (run-hooks 'notmuch-show-hook))
849
850     ;; Move straight to the first open message
851     (if (not (notmuch-show-message-visible-p))
852         (notmuch-show-next-open-message))
853
854     ;; Set the header line to the subject of the first open message.
855     (setq header-line-format (notmuch-show-strip-re (notmuch-show-get-subject)))
856
857     (notmuch-show-mark-read)))
858
859 (defun notmuch-show-refresh-view (&optional crypto-switch)
860   "Refresh the current view (with crypto switch if prefix given).
861
862 Kills the current buffer and reruns notmuch show with the same
863 thread id.  If a prefix is given, the current thread is
864 redisplayed with the crypto switch activated, which switch the
865 logic of the notmuch-crypto-process-mime customization variable."
866   (interactive "P")
867   (let ((thread-id notmuch-show-thread-id)
868         (parent-buffer notmuch-show-parent-buffer)
869         (query-context notmuch-show-query-context)
870         (buffer-name notmuch-show-buffer-name))
871     (notmuch-kill-this-buffer)
872     (notmuch-show thread-id parent-buffer query-context buffer-name crypto-switch)))
873
874 (defvar notmuch-show-stash-map
875   (let ((map (make-sparse-keymap)))
876     (define-key map "c" 'notmuch-show-stash-cc)
877     (define-key map "d" 'notmuch-show-stash-date)
878     (define-key map "F" 'notmuch-show-stash-filename)
879     (define-key map "f" 'notmuch-show-stash-from)
880     (define-key map "i" 'notmuch-show-stash-message-id)
881     (define-key map "s" 'notmuch-show-stash-subject)
882     (define-key map "T" 'notmuch-show-stash-tags)
883     (define-key map "t" 'notmuch-show-stash-to)
884     map)
885   "Submap for stash commands")
886 (fset 'notmuch-show-stash-map notmuch-show-stash-map)
887
888 (defvar notmuch-show-mode-map
889       (let ((map (make-sparse-keymap)))
890         (define-key map "?" 'notmuch-help)
891         (define-key map "q" 'notmuch-kill-this-buffer)
892         (define-key map (kbd "<C-tab>") 'widget-backward)
893         (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
894         (define-key map (kbd "<backtab>") 'notmuch-show-previous-button)
895         (define-key map (kbd "TAB") 'notmuch-show-next-button)
896         (define-key map "s" 'notmuch-search)
897         (define-key map "m" 'notmuch-mua-new-mail)
898         (define-key map "f" 'notmuch-show-forward-message)
899         (define-key map "r" 'notmuch-show-reply)
900         (define-key map "|" 'notmuch-show-pipe-message)
901         (define-key map "w" 'notmuch-show-save-attachments)
902         (define-key map "V" 'notmuch-show-view-raw-message)
903         (define-key map "v" 'notmuch-show-view-all-mime-parts)
904         (define-key map "c" 'notmuch-show-stash-map)
905         (define-key map "=" 'notmuch-show-refresh-view)
906         (define-key map "h" 'notmuch-show-toggle-headers)
907         (define-key map "-" 'notmuch-show-remove-tag)
908         (define-key map "+" 'notmuch-show-add-tag)
909         (define-key map "x" 'notmuch-show-archive-thread-then-exit)
910         (define-key map "a" 'notmuch-show-archive-thread)
911         (define-key map "N" 'notmuch-show-next-message)
912         (define-key map "P" 'notmuch-show-previous-message)
913         (define-key map "n" 'notmuch-show-next-open-message)
914         (define-key map "p" 'notmuch-show-previous-open-message)
915         (define-key map (kbd "DEL") 'notmuch-show-rewind)
916         (define-key map " " 'notmuch-show-advance-and-archive)
917         (define-key map (kbd "M-RET") 'notmuch-show-open-or-close-all)
918         (define-key map (kbd "RET") 'notmuch-show-toggle-message)
919         map)
920       "Keymap for \"notmuch show\" buffers.")
921 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
922
923 (defun notmuch-show-mode ()
924   "Major mode for viewing a thread with notmuch.
925
926 This buffer contains the results of the \"notmuch show\" command
927 for displaying a single thread of email from your email archives.
928
929 By default, various components of email messages, (citations,
930 signatures, already-read messages), are hidden. You can make
931 these parts visible by clicking with the mouse button or by
932 pressing RET after positioning the cursor on a hidden part, (for
933 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
934
935 Reading the thread sequentially is well-supported by pressing
936 \\[notmuch-show-advance-and-archive]. This will scroll the current message (if necessary), advance
937 to the next message, or advance to the next thread (if already on
938 the last message of a thread).
939
940 Other commands are available to read or manipulate the thread
941 more selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages
942 without removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread
943 without scrolling through with \\[notmuch-show-advance-and-archive]).
944
945 You can add or remove arbitrary tags from the current message with
946 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
947
948 All currently available key bindings:
949
950 \\{notmuch-show-mode-map}"
951   (interactive)
952   (kill-all-local-variables)
953   (use-local-map notmuch-show-mode-map)
954   (setq major-mode 'notmuch-show-mode
955         mode-name "notmuch-show")
956   (setq buffer-read-only t))
957
958 (defun notmuch-show-move-to-message-top ()
959   (goto-char (notmuch-show-message-top)))
960
961 (defun notmuch-show-move-to-message-bottom ()
962   (goto-char (notmuch-show-message-bottom)))
963
964 (defun notmuch-show-message-adjust ()
965   (recenter 0))
966
967 ;; Movement related functions.
968
969 ;; There's some strangeness here where a text property applied to a
970 ;; region a->b is not found when point is at b. We walk backwards
971 ;; until finding the property.
972 (defun notmuch-show-message-extent ()
973   (let (r)
974     (save-excursion
975       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
976         (backward-char)))
977     r))
978
979 (defun notmuch-show-message-top ()
980   (car (notmuch-show-message-extent)))
981
982 (defun notmuch-show-message-bottom ()
983   (cdr (notmuch-show-message-extent)))
984
985 (defun notmuch-show-goto-message-next ()
986   (let ((start (point)))
987     (notmuch-show-move-to-message-bottom)
988     (if (not (eobp))
989         t
990       (goto-char start)
991       nil)))
992
993 (defun notmuch-show-goto-message-previous ()
994   (notmuch-show-move-to-message-top)
995   (if (bobp)
996       nil
997     (backward-char)
998     (notmuch-show-move-to-message-top)
999     t))
1000
1001 (defun notmuch-show-move-past-invisible-forward ()
1002   (while (point-invisible-p)
1003     (forward-char)))
1004
1005 (defun notmuch-show-move-past-invisible-backward ()
1006   (while (point-invisible-p)
1007     (backward-char)))
1008
1009 ;; Functions relating to the visibility of messages and their
1010 ;; components.
1011
1012 (defun notmuch-show-element-visible (props visible-p spec-property)
1013   (let ((spec (plist-get props spec-property)))
1014     (if visible-p
1015         (remove-from-invisibility-spec spec)
1016       (add-to-invisibility-spec spec))))
1017
1018 (defun notmuch-show-message-visible (props visible-p)
1019   (notmuch-show-element-visible props visible-p :message-invis-spec)
1020   (notmuch-show-set-prop :message-visible visible-p props))
1021
1022 (defun notmuch-show-headers-visible (props visible-p)
1023   (notmuch-show-element-visible props visible-p :headers-invis-spec)
1024   (notmuch-show-set-prop :headers-visible visible-p props))
1025
1026 ;; Functions for setting and getting attributes of the current
1027 ;; message.
1028
1029 (defun notmuch-show-set-message-properties (props)
1030   (save-excursion
1031     (notmuch-show-move-to-message-top)
1032     (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
1033
1034 (defun notmuch-show-get-message-properties ()
1035   (save-excursion
1036     (notmuch-show-move-to-message-top)
1037     (get-text-property (point) :notmuch-message-properties)))
1038
1039 (defun notmuch-show-set-prop (prop val &optional props)
1040   (let ((inhibit-read-only t)
1041         (props (or props
1042                    (notmuch-show-get-message-properties))))
1043     (plist-put props prop val)
1044     (notmuch-show-set-message-properties props)))
1045
1046 (defun notmuch-show-get-prop (prop &optional props)
1047   (let ((props (or props
1048                    (notmuch-show-get-message-properties))))
1049     (plist-get props prop)))
1050
1051 (defun notmuch-show-get-message-id ()
1052   "Return the message id of the current message."
1053   (concat "id:\"" (notmuch-show-get-prop :id) "\""))
1054
1055 ;; dme: Would it make sense to use a macro for many of these?
1056
1057 (defun notmuch-show-get-filename ()
1058   "Return the filename of the current message."
1059   (notmuch-show-get-prop :filename))
1060
1061 (defun notmuch-show-get-header (header)
1062   "Return the named header of the current message, if any."
1063   (plist-get (notmuch-show-get-prop :headers) header))
1064
1065 (defun notmuch-show-get-cc ()
1066   (notmuch-show-get-header :Cc))
1067
1068 (defun notmuch-show-get-date ()
1069   (notmuch-show-get-header :Date))
1070
1071 (defun notmuch-show-get-from ()
1072   (notmuch-show-get-header :From))
1073
1074 (defun notmuch-show-get-subject ()
1075   (notmuch-show-get-header :Subject))
1076
1077 (defun notmuch-show-get-to ()
1078   (notmuch-show-get-header :To))
1079
1080 (defun notmuch-show-set-tags (tags)
1081   "Set the tags of the current message."
1082   (notmuch-show-set-prop :tags tags)
1083   (notmuch-show-update-tags tags))
1084
1085 (defun notmuch-show-get-tags ()
1086   "Return the tags of the current message."
1087   (notmuch-show-get-prop :tags))
1088
1089 (defun notmuch-show-message-visible-p ()
1090   "Is the current message visible?"
1091   (notmuch-show-get-prop :message-visible))
1092
1093 (defun notmuch-show-headers-visible-p ()
1094   "Are the headers of the current message visible?"
1095   (notmuch-show-get-prop :headers-visible))
1096
1097 (defun notmuch-show-mark-read ()
1098   "Mark the current message as read."
1099   (notmuch-show-remove-tag "unread"))
1100
1101 ;; Functions for getting attributes of several messages in the current
1102 ;; thread.
1103
1104 (defun notmuch-show-get-message-ids-for-open-messages ()
1105   "Return a list of all message IDs for open messages in the current thread."
1106   (save-excursion
1107     (let (message-ids done)
1108       (goto-char (point-min))
1109       (while (not done)
1110         (if (notmuch-show-message-visible-p)
1111             (setq message-ids (append message-ids (list (notmuch-show-get-message-id)))))
1112         (setq done (not (notmuch-show-goto-message-next)))
1113         )
1114       message-ids
1115       )))
1116
1117 ;; Commands typically bound to keys.
1118
1119 (defun notmuch-show-advance-and-archive ()
1120   "Advance through thread and archive.
1121
1122 This command is intended to be one of the simplest ways to
1123 process a thread of email. It does the following:
1124
1125 If the current message in the thread is not yet fully visible,
1126 scroll by a near screenful to read more of the message.
1127
1128 Otherwise, (the end of the current message is already within the
1129 current window), advance to the next open message.
1130
1131 Finally, if there is no further message to advance to, and this
1132 last message is already read, then archive the entire current
1133 thread, (remove the \"inbox\" tag from each message). Also kill
1134 this buffer, and display the next thread from the search from
1135 which this thread was originally shown."
1136   (interactive)
1137   (let ((end-of-this-message (notmuch-show-message-bottom)))
1138     (cond
1139      ;; Ideally we would test `end-of-this-message' against the result
1140      ;; of `window-end', but that doesn't account for the fact that
1141      ;; the end of the message might be hidden, so we have to actually
1142      ;; go to the end, walk back over invisible text and then see if
1143      ;; point is visible.
1144      ((save-excursion
1145         (goto-char (- end-of-this-message 1))
1146         (notmuch-show-move-past-invisible-backward)
1147         (> (point) (window-end)))
1148       ;; The bottom of this message is not visible - scroll.
1149       (scroll-up nil))
1150
1151      ((not (= end-of-this-message (point-max)))
1152       ;; This is not the last message - move to the next visible one.
1153       (notmuch-show-next-open-message))
1154
1155      (t
1156       ;; This is the last message - archive the thread.
1157       (notmuch-show-archive-thread)))))
1158
1159 (defun notmuch-show-rewind ()
1160   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
1161
1162 Specifically, if the beginning of the previous email is fewer
1163 than `window-height' lines from the current point, move to it
1164 just like `notmuch-show-previous-message'.
1165
1166 Otherwise, just scroll down a screenful of the current message.
1167
1168 This command does not modify any message tags, (it does not undo
1169 any effects from previous calls to
1170 `notmuch-show-advance-and-archive'."
1171   (interactive)
1172   (let ((start-of-message (notmuch-show-message-top))
1173         (start-of-window (window-start)))
1174     (cond
1175       ;; Either this message is properly aligned with the start of the
1176       ;; window or the start of this message is not visible on the
1177       ;; screen - scroll.
1178      ((or (= start-of-message start-of-window)
1179           (< start-of-message start-of-window))
1180       (scroll-down)
1181       ;; If a small number of lines from the previous message are
1182       ;; visible, realign so that the top of the current message is at
1183       ;; the top of the screen.
1184       (if (<= (count-screen-lines (window-start) start-of-message)
1185               next-screen-context-lines)
1186           (progn
1187             (goto-char (notmuch-show-message-top))
1188             (notmuch-show-message-adjust)))
1189       ;; Move to the top left of the window.
1190       (goto-char (window-start)))
1191      (t
1192       ;; Move to the previous message.
1193       (notmuch-show-previous-message)))))
1194
1195 (defun notmuch-show-reply (&optional prompt-for-sender)
1196   "Reply to the current message."
1197   (interactive "P")
1198   (notmuch-mua-new-reply (notmuch-show-get-message-id) prompt-for-sender))
1199
1200 (defun notmuch-show-forward-message (&optional prompt-for-sender)
1201   "Forward the current message."
1202   (interactive "P")
1203   (with-current-notmuch-show-message
1204    (notmuch-mua-new-forward-message prompt-for-sender)))
1205
1206 (defun notmuch-show-next-message ()
1207   "Show the next message."
1208   (interactive)
1209   (if (notmuch-show-goto-message-next)
1210       (progn
1211         (notmuch-show-mark-read)
1212         (notmuch-show-message-adjust))
1213     (goto-char (point-max))))
1214
1215 (defun notmuch-show-previous-message ()
1216   "Show the previous message."
1217   (interactive)
1218   (notmuch-show-goto-message-previous)
1219   (notmuch-show-mark-read)
1220   (notmuch-show-message-adjust))
1221
1222 (defun notmuch-show-next-open-message ()
1223   "Show the next message."
1224   (interactive)
1225   (let (r)
1226     (while (and (setq r (notmuch-show-goto-message-next))
1227                 (not (notmuch-show-message-visible-p))))
1228     (if r
1229         (progn
1230           (notmuch-show-mark-read)
1231           (notmuch-show-message-adjust))
1232       (goto-char (point-max)))))
1233
1234 (defun notmuch-show-previous-open-message ()
1235   "Show the previous message."
1236   (interactive)
1237   (while (and (notmuch-show-goto-message-previous)
1238               (not (notmuch-show-message-visible-p))))
1239   (notmuch-show-mark-read)
1240   (notmuch-show-message-adjust))
1241
1242 (defun notmuch-show-view-raw-message ()
1243   "View the file holding the current message."
1244   (interactive)
1245   (let* ((id (notmuch-show-get-message-id))
1246          (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
1247     (call-process notmuch-command nil buf nil "show" "--format=raw" id)
1248     (switch-to-buffer buf)
1249     (goto-char (point-min))
1250     (set-buffer-modified-p nil)
1251     (view-buffer buf 'kill-buffer-if-not-modified)))
1252
1253 (defun notmuch-show-pipe-message (entire-thread command)
1254   "Pipe the contents of the current message (or thread) to the given command.
1255
1256 The given command will be executed with the raw contents of the
1257 current email message as stdin. Anything printed by the command
1258 to stdout or stderr will appear in the *notmuch-pipe* buffer.
1259
1260 When invoked with a prefix argument, the command will receive all
1261 open messages in the current thread (formatted as an mbox) rather
1262 than only the current message."
1263   (interactive "P\nsPipe message to command: ")
1264   (let (shell-command)
1265     (if entire-thread
1266         (setq shell-command 
1267               (concat notmuch-command " show --format=mbox "
1268                       (shell-quote-argument
1269                        (mapconcat 'identity (notmuch-show-get-message-ids-for-open-messages) " OR "))
1270                       " | " command))
1271       (setq shell-command
1272             (concat notmuch-command " show --format=raw "
1273                     (shell-quote-argument (notmuch-show-get-message-id)) " | " command)))
1274     (let ((buf (get-buffer-create (concat "*notmuch-pipe*"))))
1275       (with-current-buffer buf
1276         (setq buffer-read-only nil)
1277         (erase-buffer)
1278         (let ((exit-code (call-process-shell-command shell-command nil buf)))
1279           (goto-char (point-max))
1280           (set-buffer-modified-p nil)
1281           (setq buffer-read-only t)
1282           (unless (zerop exit-code)
1283             (switch-to-buffer-other-window buf)
1284             (message (format "Command '%s' exited abnormally with code %d"
1285                              shell-command exit-code))))))))
1286
1287 (defun notmuch-show-add-tags-worker (current-tags add-tags)
1288   "Add to `current-tags' with any tags from `add-tags' not
1289 currently present and return the result."
1290   (let ((result-tags (copy-sequence current-tags)))
1291     (mapc (lambda (add-tag)
1292             (unless (member add-tag current-tags)
1293               (setq result-tags (push add-tag result-tags))))
1294             add-tags)
1295     (sort result-tags 'string<)))
1296
1297 (defun notmuch-show-del-tags-worker (current-tags del-tags)
1298   "Remove any tags in `del-tags' from `current-tags' and return
1299 the result."
1300   (let ((result-tags (copy-sequence current-tags)))
1301     (mapc (lambda (del-tag)
1302             (setq result-tags (delete del-tag result-tags)))
1303           del-tags)
1304     result-tags))
1305
1306 (defun notmuch-show-add-tag (&rest toadd)
1307   "Add a tag to the current message."
1308   (interactive
1309    (list (notmuch-select-tag-with-completion "Tag to add: ")))
1310
1311   (let* ((current-tags (notmuch-show-get-tags))
1312          (new-tags (notmuch-show-add-tags-worker current-tags toadd)))
1313
1314     (unless (equal current-tags new-tags)
1315       (apply 'notmuch-tag (notmuch-show-get-message-id)
1316              (mapcar (lambda (s) (concat "+" s)) toadd))
1317       (notmuch-show-set-tags new-tags))))
1318
1319 (defun notmuch-show-remove-tag (&rest toremove)
1320   "Remove a tag from the current message."
1321   (interactive
1322    (list (notmuch-select-tag-with-completion
1323           "Tag to remove: " (notmuch-show-get-message-id))))
1324
1325   (let* ((current-tags (notmuch-show-get-tags))
1326          (new-tags (notmuch-show-del-tags-worker current-tags toremove)))
1327
1328     (unless (equal current-tags new-tags)
1329       (apply 'notmuch-tag (notmuch-show-get-message-id)
1330              (mapcar (lambda (s) (concat "-" s)) toremove))
1331       (notmuch-show-set-tags new-tags))))
1332
1333 (defun notmuch-show-toggle-headers ()
1334   "Toggle the visibility of the current message headers."
1335   (interactive)
1336   (let ((props (notmuch-show-get-message-properties)))
1337     (notmuch-show-headers-visible
1338      props
1339      (not (plist-get props :headers-visible))))
1340   (force-window-update))
1341
1342 (defun notmuch-show-toggle-message ()
1343   "Toggle the visibility of the current message."
1344   (interactive)
1345   (let ((props (notmuch-show-get-message-properties)))
1346     (notmuch-show-message-visible
1347      props
1348      (not (plist-get props :message-visible))))
1349   (force-window-update))
1350
1351 (defun notmuch-show-open-or-close-all ()
1352   "Set the visibility all of the messages in the current thread.
1353 By default make all of the messages visible. With a prefix
1354 argument, hide all of the messages."
1355   (interactive)
1356   (save-excursion
1357     (goto-char (point-min))
1358     (loop do (notmuch-show-message-visible (notmuch-show-get-message-properties)
1359                                            (not current-prefix-arg))
1360           until (not (notmuch-show-goto-message-next))))
1361   (force-window-update))
1362
1363 (defun notmuch-show-next-button ()
1364   "Advance point to the next button in the buffer."
1365   (interactive)
1366   (forward-button 1))
1367
1368 (defun notmuch-show-previous-button ()
1369   "Move point back to the previous button in the buffer."
1370   (interactive)
1371   (backward-button 1))
1372
1373 (defun notmuch-show-archive-thread-internal (show-next)
1374   ;; Remove the tag from the current set of messages.
1375   (goto-char (point-min))
1376   (loop do (notmuch-show-remove-tag "inbox")
1377         until (not (notmuch-show-goto-message-next)))
1378   ;; Move to the next item in the search results, if any.
1379   (let ((parent-buffer notmuch-show-parent-buffer))
1380     (notmuch-kill-this-buffer)
1381     (if parent-buffer
1382         (progn
1383           (switch-to-buffer parent-buffer)
1384           (forward-line)
1385           (if show-next
1386               (notmuch-search-show-thread))))))
1387
1388 (defun notmuch-show-archive-thread ()
1389   "Archive each message in thread, then show next thread from search.
1390
1391 Archive each message currently shown by removing the \"inbox\"
1392 tag from each. Then kill this buffer and show the next thread
1393 from the search from which this thread was originally shown.
1394
1395 Note: This command is safe from any race condition of new messages
1396 being delivered to the same thread. It does not archive the
1397 entire thread, but only the messages shown in the current
1398 buffer."
1399   (interactive)
1400   (notmuch-show-archive-thread-internal t))
1401
1402 (defun notmuch-show-archive-thread-then-exit ()
1403   "Archive each message in thread, then exit back to search results."
1404   (interactive)
1405   (notmuch-show-archive-thread-internal nil))
1406
1407 (defun notmuch-show-stash-cc ()
1408   "Copy CC field of current message to kill-ring."
1409   (interactive)
1410   (notmuch-common-do-stash (notmuch-show-get-cc)))
1411
1412 (defun notmuch-show-stash-date ()
1413   "Copy date of current message to kill-ring."
1414   (interactive)
1415   (notmuch-common-do-stash (notmuch-show-get-date)))
1416
1417 (defun notmuch-show-stash-filename ()
1418   "Copy filename of current message to kill-ring."
1419   (interactive)
1420   (notmuch-common-do-stash (notmuch-show-get-filename)))
1421
1422 (defun notmuch-show-stash-from ()
1423   "Copy From address of current message to kill-ring."
1424   (interactive)
1425   (notmuch-common-do-stash (notmuch-show-get-from)))
1426
1427 (defun notmuch-show-stash-message-id ()
1428   "Copy message ID of current message to kill-ring."
1429   (interactive)
1430   (notmuch-common-do-stash (notmuch-show-get-message-id)))
1431
1432 (defun notmuch-show-stash-subject ()
1433   "Copy Subject field of current message to kill-ring."
1434   (interactive)
1435   (notmuch-common-do-stash (notmuch-show-get-subject)))
1436
1437 (defun notmuch-show-stash-tags ()
1438   "Copy tags of current message to kill-ring as a comma separated list."
1439   (interactive)
1440   (notmuch-common-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
1441
1442 (defun notmuch-show-stash-to ()
1443   "Copy To address of current message to kill-ring."
1444   (interactive)
1445   (notmuch-common-do-stash (notmuch-show-get-to)))
1446
1447 ;; Commands typically bound to buttons.
1448
1449 (defun notmuch-show-part-button-action (button)
1450   (let ((nth (button-get button :notmuch-part)))
1451     (if nth
1452         (notmuch-show-save-part (notmuch-show-get-message-id) nth
1453                                 (button-get button :notmuch-filename))
1454       (message "Not a valid part (is it a fake part?)."))))
1455
1456 ;;
1457
1458 (provide 'notmuch-show)