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