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