556941a507425f978b325e41ee6e20fbd3e6ca53
[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 (require 'cl)
25 (require 'mm-view)
26 (require 'message)
27 (require 'mm-decode)
28 (require 'mailcap)
29
30 (require 'notmuch-lib)
31 (require 'notmuch-query)
32 (require 'notmuch-wash)
33
34 (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
35 (declare-function notmuch-reply "notmuch" (query-string))
36 (declare-function notmuch-fontify-headers "notmuch" nil)
37 (declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
38 (declare-function notmuch-search-show-thread "notmuch" nil)
39
40 (defvar notmuch-show-headers '("Subject" "To" "Cc" "From" "Date")
41   "Headers that should be shown in a message, in this order. Note
42 that if this order is changed the headers shown when a message is
43 collapsed will change.")
44
45 (defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
46   "A list of functions called to decorate the headers listed in
47 `notmuch-show-headers'.")
48
49 (defvar notmuch-show-hook '(notmuch-show-pretty-hook)
50   "A list of functions called after populating a
51 `notmuch-show' buffer.")
52
53 (defvar notmuch-show-insert-text/plain-hook '(notmuch-wash-text/plain-citations)
54   "A list of functions called to clean up text/plain body parts.")
55
56 (defun notmuch-show-pretty-hook ()
57   (goto-address-mode 1)
58   (visual-line-mode))
59
60 (defmacro with-current-notmuch-show-message (&rest body)
61   "Evaluate body with current buffer set to the text of current message"
62   `(save-excursion
63      (let ((filename (notmuch-show-get-filename)))
64        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
65          (with-current-buffer buf
66            (insert-file-contents filename nil nil nil t)
67            ,@body)
68          (kill-buffer buf)))))
69
70 (defun notmuch-show-view-all-mime-parts ()
71   "Use external viewers to view all attachments from the current message."
72   (interactive)
73   (with-current-notmuch-show-message
74    ; We ovverride the mm-inline-media-tests to indicate which message
75    ; parts are already sufficiently handled by the original
76    ; presentation of the message in notmuch-show mode. These parts
77    ; will be inserted directly into the temporary buffer of
78    ; with-current-notmuch-show-message and silently discarded.
79    ;
80    ; Any MIME part not explicitly mentioned here will be handled by an
81    ; external viewer as configured in the various mailcap files.
82    (let ((mm-inline-media-tests '(
83                                   ("text/.*" ignore identity)
84                                   ("application/pgp-signature" ignore identity)
85                                   ("multipart/alternative" ignore identity)
86                                   ("multipart/mixed" ignore identity)
87                                   ("multipart/related" ignore identity)
88                                  )))
89      (mm-display-parts (mm-dissect-buffer)))))
90
91 (defun notmuch-foreach-mime-part (function mm-handle)
92   (cond ((stringp (car mm-handle))
93          (dolist (part (cdr mm-handle))
94            (notmuch-foreach-mime-part function part)))
95         ((bufferp (car mm-handle))
96          (funcall function mm-handle))
97         (t (dolist (part mm-handle)
98              (notmuch-foreach-mime-part function part)))))
99
100 (defun notmuch-count-attachments (mm-handle)
101   (let ((count 0))
102     (notmuch-foreach-mime-part
103      (lambda (p)
104        (let ((disposition (mm-handle-disposition p)))
105          (and (listp disposition)
106               (or (equal (car disposition) "attachment")
107                   (and (equal (car disposition) "inline")
108                        (assq 'filename disposition)))
109               (incf count))))
110      mm-handle)
111     count))
112
113 (defun notmuch-save-attachments (mm-handle &optional queryp)
114   (notmuch-foreach-mime-part
115    (lambda (p)
116      (let ((disposition (mm-handle-disposition p)))
117        (and (listp disposition)
118             (or (equal (car disposition) "attachment")
119                 (and (equal (car disposition) "inline")
120                      (assq 'filename disposition)))
121             (or (not queryp)
122                 (y-or-n-p
123                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
124             (mm-save-part p))))
125    mm-handle))
126
127 (defun notmuch-show-save-attachments ()
128   "Save all attachments from the current message."
129   (interactive)
130   (with-current-notmuch-show-message
131    (let ((mm-handle (mm-dissect-buffer)))
132      (notmuch-save-attachments
133       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
134   (message "Done"))
135
136 (defun notmuch-show-fontify-header ()
137   (let ((face (cond
138                ((looking-at "[Tt]o:")
139                 'message-header-to)
140                ((looking-at "[Bb]?[Cc][Cc]:")
141                 'message-header-cc)
142                ((looking-at "[Ss]ubject:")
143                 'message-header-subject)
144                ((looking-at "[Ff]rom:")
145                 'message-header-from)
146                (t
147                 'message-header-other))))
148
149     (overlay-put (make-overlay (point) (re-search-forward ":"))
150                  'face 'message-header-name)
151     (overlay-put (make-overlay (point) (re-search-forward ".*$"))
152                  'face face)))
153
154 (defun notmuch-show-colour-headers ()
155   "Apply some colouring to the current headers."
156   (goto-char (point-min))
157   (while (looking-at "^[A-Za-z][-A-Za-z0-9]*:")
158     (notmuch-show-fontify-header)
159     (forward-line)))
160
161 (defun notmuch-show-spaces-n (n)
162   "Return a string comprised of `n' spaces."
163   (make-string n ? ))
164
165 (defun notmuch-show-update-tags (tags)
166   "Update the displayed tags of the current message."
167   (save-excursion
168     (goto-char (notmuch-show-message-top))
169     (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
170         (let ((inhibit-read-only t))
171           (replace-match (concat "("
172                                  (mapconcat 'identity tags " ")
173                                  ")"))))))
174
175 (defun notmuch-show-insert-headerline (headers date tags depth)
176   "Insert a notmuch style headerline based on HEADERS for a
177 message at DEPTH in the current thread."
178   (let ((start (point)))
179     (insert (notmuch-show-spaces-n depth)
180             (plist-get headers :From)
181             " ("
182             date
183             ") ("
184             (mapconcat 'identity tags " ")
185             ")\n")
186     (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face)))
187
188 (defun notmuch-show-insert-header (header header-value)
189   "Insert a single header."
190   (insert header ": " header-value "\n"))
191
192 (defun notmuch-show-insert-headers (headers)
193   "Insert the headers of the current message."
194   (let ((start (point)))
195     (mapc '(lambda (header)
196              (let* ((header-symbol (intern (concat ":" header)))
197                     (header-value (plist-get headers header-symbol)))
198                (if (and header-value
199                         (not (string-equal "" header-value)))
200                    (notmuch-show-insert-header header header-value))))
201           notmuch-show-headers)
202     (save-excursion
203       (save-restriction
204         (narrow-to-region start (point-max))
205         (run-hooks 'notmuch-show-markup-headers-hook)))))
206
207 (defun notmuch-show-insert-part-header (content-type declared-type &optional name)
208   (let ((start (point)))
209     ;; XXX dme: Make this a more useful button (save the part, display
210     ;; external, etc.)
211     (insert "[ "
212             (if name (concat name ": ") "")
213             declared-type
214             (if (not (string-equal declared-type content-type))
215                 (concat " (as " content-type ")")
216               "")
217             " ]\n")
218     (overlay-put (make-overlay start (point)) 'face 'bold)))
219
220 ;; Functions handling particular MIME parts.
221
222 (defun notmuch-show-mm-display-part-inline (msg part content-type content)
223   "Use the mm-decode/mm-view functions to display a part in the
224 current buffer, if possible."
225   (let ((display-buffer (current-buffer)))
226     (with-temp-buffer
227       (insert content)
228       (let ((handle (mm-make-handle (current-buffer) (list content-type))))
229         (set-buffer display-buffer)
230         (if (and (mm-inlinable-p handle)
231                  (mm-inlined-p handle))
232             (progn
233               (mm-display-part handle)
234               t)
235           nil)))))
236
237 (defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type)
238   (let ((start (point)))
239     ;; If this text/plain part is not the first part in the message,
240     ;; insert a header to make this clear.
241     (if (> nth 1)
242         (notmuch-show-insert-part-header declared-type content-type (plist-get part :filename)))
243     (insert (notmuch-show-get-bodypart-content msg part nth))
244     (save-excursion
245       (save-restriction
246         (narrow-to-region start (point-max))
247         (run-hook-with-args 'notmuch-show-insert-text/plain-hook depth))))
248   t)
249
250 (defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type)
251   ;; If we can deduce a MIME type from the filename of the attachment,
252   ;; do so and pass it on to the handler for that type.
253   (if (plist-get part :filename)
254       (let ((extension (file-name-extension (plist-get part :filename)))
255             mime-type)
256         (if extension
257             (progn
258               (mailcap-parse-mimetypes)
259               (setq mime-type (mailcap-extension-to-mime extension))
260               (if (and mime-type
261                        (not (string-equal mime-type "application/octet-stream")))
262                   (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type)
263                 nil))
264           nil))))
265
266 (defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
267   ;; This handler _must_ succeed - it is the handler of last resort.
268   (notmuch-show-insert-part-header content-type declared-type (plist-get part :filename))
269   (let ((content (notmuch-show-get-bodypart-content msg part nth)))
270     (if content
271         (notmuch-show-mm-display-part-inline msg part content-type content)))
272   t)
273
274 ;; Functions for determining how to handle MIME parts.
275
276 (defun notmuch-show-split-content-type (content-type)
277   (split-string content-type "/"))
278
279 (defun notmuch-show-handlers-for (content-type)
280   "Return a list of content handlers for a part of type CONTENT-TYPE."
281   (let (result)
282     (mapc (lambda (func)
283             (if (functionp func)
284                 (push func result)))
285           ;; Reverse order of prefrence.
286           (list (intern (concat "notmuch-show-insert-part-*/*"))
287                 (intern (concat
288                          "notmuch-show-insert-part-"
289                          (car (notmuch-show-split-content-type content-type))
290                          "/*"))
291                 (intern (concat "notmuch-show-insert-part-" content-type))))
292     result))
293
294 ;; Helper for parts which are generally not included in the default
295 ;; JSON output.
296
297 (defun notmuch-show-get-bodypart-internal (message-id part-number)
298   (with-temp-buffer
299     (let ((coding-system-for-read 'no-conversion))
300       (call-process notmuch-command nil t nil
301                     "part" (format "--part=%s" part-number) message-id)
302       (buffer-string))))
303
304 (defun notmuch-show-get-bodypart-content (msg part nth)
305   (or (plist-get part :content)
306       (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
307
308 ;; \f
309
310 (defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
311   (let ((handlers (notmuch-show-handlers-for content-type)))
312     ;; Run the content handlers until one of them returns a non-nil
313     ;; value.
314     (while (and handlers
315                 (not (funcall (car handlers) msg part content-type nth depth declared-type)))
316       (setq handlers (cdr handlers))))
317   t)
318
319 (defun notmuch-show-insert-bodypart (msg part depth)
320   "Insert the body part PART at depth DEPTH in the current thread."
321   (let ((content-type (downcase (plist-get part :content-type)))
322         (nth (plist-get part :id)))
323     (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
324   ;; Some of the body part handlers leave point somewhere up in the
325   ;; part, so we make sure that we're down at the end.
326   (goto-char (point-max))
327   ;; Ensure that the part ends with a carriage return.
328   (if (not (bolp))
329       (insert "\n")))
330
331 (defun notmuch-show-insert-body (msg body depth)
332   "Insert the body BODY at depth DEPTH in the current thread."
333   (mapc '(lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
334
335 (defun notmuch-show-make-symbol (type)
336   (make-symbol (concat "notmuch-show-" type)))
337
338 (defun notmuch-show-insert-msg (msg depth)
339   "Insert the message MSG at depth DEPTH in the current thread."
340   (let ((headers (plist-get msg :headers))
341         ;; Indentation causes the buffer offset of the start/end
342         ;; points to move, so we must use markers.
343         message-start message-end
344         content-start content-end
345         headers-start headers-end
346         body-start body-end
347         (headers-invis-spec (notmuch-show-make-symbol "header"))
348         (message-invis-spec (notmuch-show-make-symbol "message")))
349
350     (setq message-start (point-marker))
351
352     (notmuch-show-insert-headerline headers
353                                     (or (plist-get msg :date_relative)
354                                         (plist-get headers :Date))
355                                     (plist-get msg :tags) depth)
356
357     (setq content-start (point-marker))
358
359     ;; Set `headers-start' to point after the 'Subject:' header to be
360     ;; compatible with the existing implementation. This just sets it
361     ;; to after the first header.
362     (notmuch-show-insert-headers headers)
363     ;; Headers should include a blank line (backwards compatibility).
364     (insert "\n")
365     (save-excursion
366       (goto-char content-start)
367       (forward-line 1)
368       (setq headers-start (point-marker)))
369     (setq headers-end (point-marker))
370
371     (setq body-start (point-marker))
372     (notmuch-show-insert-body msg (plist-get msg :body) depth)
373     ;; Ensure that the body ends with a newline.
374     (if (not (bolp))
375         (insert "\n"))
376     (setq body-end (point-marker))
377     (setq content-end (point-marker))
378
379     ;; Indent according to the depth in the thread.
380     (indent-rigidly content-start content-end depth)
381
382     (setq message-end (point-max-marker))
383
384     ;; Save the extents of this message over the whole text of the
385     ;; message.
386     (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
387
388     (plist-put msg :headers-invis-spec headers-invis-spec)
389     (overlay-put (make-overlay headers-start headers-end) 'invisible headers-invis-spec)
390
391     (plist-put msg :message-invis-spec message-invis-spec)
392     (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
393
394     ;; Save the properties for this message. Currently this saves the
395     ;; entire message (augmented it with other stuff), which seems
396     ;; like overkill. We might save a reduced subset (for example, not
397     ;; the content).
398     (notmuch-show-set-message-properties msg)
399
400     ;; Headers are hidden by default.
401     (notmuch-show-headers-visible msg nil)
402
403     ;; Message visibility depends on whether it matched the search
404     ;; criteria.
405     (notmuch-show-message-visible msg (plist-get msg :match))))
406
407 (defun notmuch-show-insert-tree (tree depth)
408   "Insert the message tree TREE at depth DEPTH in the current thread."
409   (let ((msg (car tree))
410         (replies (cadr tree)))
411     (notmuch-show-insert-msg msg depth)
412     (notmuch-show-insert-thread replies (1+ depth))))
413
414 (defun notmuch-show-insert-thread (thread depth)
415   "Insert the thread THREAD at depth DEPTH in the current forest."
416   (mapc '(lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
417
418 (defun notmuch-show-insert-forest (forest)
419   "Insert the forest of threads FOREST."
420   (mapc '(lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
421
422 (defvar notmuch-show-parent-buffer nil)
423
424 ;;;###autoload
425 (defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name)
426   "Run \"notmuch show\" with the given thread ID and display results.
427
428 The optional PARENT-BUFFER is the notmuch-search buffer from
429 which this notmuch-show command was executed, (so that the
430 next thread from that buffer can be show when done with this
431 one).
432
433 The optional QUERY-CONTEXT is a notmuch search term. Only
434 messages from the thread matching this search term are shown if
435 non-nil.
436
437 The optional BUFFER-NAME provides the neame of the buffer in
438 which the message thread is shown. If it is nil (which occurs
439 when the command is called interactively) the argument to the
440 function is used. "
441   (interactive "sNotmuch show: ")
442   (let ((buffer (get-buffer-create (generate-new-buffer-name
443                                     (or buffer-name
444                                         (concat "*notmuch-" thread-id "*")))))
445         (inhibit-read-only t))
446     (switch-to-buffer buffer)
447     (notmuch-show-mode)
448     (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
449     (erase-buffer)
450     (goto-char (point-min))
451     (save-excursion
452       (let* ((basic-args (list thread-id))
453              (args (if query-context
454                        (append basic-args (list "and (" query-context ")"))
455                      basic-args)))
456         (notmuch-show-insert-forest (notmuch-query-get-threads args))
457         ;; If the query context reduced the results to nothing, run
458         ;; the basic query.
459         (when (and (eq (buffer-size) 0)
460                    query-context)
461           (notmuch-show-insert-forest
462            (notmuch-query-get-threads basic-args))))
463       (run-hooks 'notmuch-show-hook))
464
465     ;; Move straight to the first open message
466     (if (not (notmuch-show-message-visible-p))
467         (notmuch-show-next-open-message))
468     (notmuch-show-mark-read)))
469
470 (defvar notmuch-show-stash-map
471   (let ((map (make-sparse-keymap)))
472     (define-key map "c" 'notmuch-show-stash-cc)
473     (define-key map "d" 'notmuch-show-stash-date)
474     (define-key map "F" 'notmuch-show-stash-filename)
475     (define-key map "f" 'notmuch-show-stash-from)
476     (define-key map "i" 'notmuch-show-stash-message-id)
477     (define-key map "s" 'notmuch-show-stash-subject)
478     (define-key map "T" 'notmuch-show-stash-tags)
479     (define-key map "t" 'notmuch-show-stash-to)
480     map)
481   "Submap for stash commands")
482 (fset 'notmuch-show-stash-map notmuch-show-stash-map)
483
484 (defvar notmuch-show-mode-map
485       (let ((map (make-sparse-keymap)))
486         (define-key map "?" 'notmuch-help)
487         (define-key map "q" 'kill-this-buffer)
488         (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
489         (define-key map (kbd "TAB") 'notmuch-show-next-button)
490         (define-key map "s" 'notmuch-search)
491         (define-key map "m" 'message-mail)
492         (define-key map "f" 'notmuch-show-forward-message)
493         (define-key map "r" 'notmuch-show-reply)
494         (define-key map "|" 'notmuch-show-pipe-message)
495         (define-key map "w" 'notmuch-show-save-attachments)
496         (define-key map "V" 'notmuch-show-view-raw-message)
497         (define-key map "v" 'notmuch-show-view-all-mime-parts)
498         (define-key map "c" 'notmuch-show-stash-map)
499         (define-key map "h" 'notmuch-show-toggle-headers)
500         (define-key map "-" 'notmuch-show-remove-tag)
501         (define-key map "+" 'notmuch-show-add-tag)
502         (define-key map "x" 'notmuch-show-archive-thread-then-exit)
503         (define-key map "a" 'notmuch-show-archive-thread)
504         (define-key map "N" 'notmuch-show-next-message)
505         (define-key map "P" 'notmuch-show-previous-message)
506         (define-key map "n" 'notmuch-show-next-open-message)
507         (define-key map "p" 'notmuch-show-previous-open-message)
508         (define-key map (kbd "DEL") 'notmuch-show-rewind)
509         (define-key map " " 'notmuch-show-advance-and-archive)
510         (define-key map (kbd "RET") 'notmuch-show-toggle-message)
511         map)
512       "Keymap for \"notmuch show\" buffers.")
513 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
514
515 ;;;###autoload
516 (defun notmuch-show-mode ()
517   "Major mode for viewing a thread with notmuch.
518
519 This buffer contains the results of the \"notmuch show\" command
520 for displaying a single thread of email from your email archives.
521
522 By default, various components of email messages, (citations,
523 signatures, already-read messages), are hidden. You can make
524 these parts visible by clicking with the mouse button or by
525 pressing RET after positioning the cursor on a hidden part, (for
526 which \\[notmuch-show-next-button] and
527 \\[notmuch-show-previous-button] are helpful).
528
529 Reading the thread sequentially is well-supported by pressing
530 \\[notmuch-show-advance-and-archive]. This will scroll the
531 current message (if necessary), advance to the next message, or
532 advance to the next thread (if already on the last message of a
533 thread).
534
535 Other commands are available to read or manipulate the thread
536 more selectively, (such as '\\[notmuch-show-next-message]' and
537 '\\[notmuch-show-previous-message]' to advance to messages
538 without removing any tags, and '\\[notmuch-show-archive-thread]'
539 to archive an entire thread without scrolling through with
540 \\[notmuch-show-advance-and-archive]).
541
542 You can add or remove arbitary tags from the current message with
543 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
544
545 All currently available key bindings:
546
547 \\{notmuch-show-mode-map}"
548   (interactive)
549   (kill-all-local-variables)
550   (use-local-map notmuch-show-mode-map)
551   (setq major-mode 'notmuch-show-mode
552         mode-name "notmuch-show")
553   (setq buffer-read-only t))
554
555 (defun notmuch-show-move-to-message-top ()
556   (goto-char (notmuch-show-message-top)))
557
558 (defun notmuch-show-move-to-message-bottom ()
559   (goto-char (notmuch-show-message-bottom)))
560
561 (defun notmuch-show-message-adjust ()
562   (recenter 0))
563
564 ;; Movement related functions.
565
566 ;; There's some strangeness here where a text property applied to a
567 ;; region a->b is not found when point is at b. We walk backwards
568 ;; until finding the property.
569 (defun notmuch-show-message-extent ()
570   (let (r)
571     (save-excursion
572       (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
573         (backward-char)))
574     r))
575
576 (defun notmuch-show-message-top ()
577   (car (notmuch-show-message-extent)))
578
579 (defun notmuch-show-message-bottom ()
580   (cdr (notmuch-show-message-extent)))
581
582 (defun notmuch-show-goto-message-next ()
583   (let ((start (point)))
584     (notmuch-show-move-to-message-bottom)
585     (if (not (eobp))
586         t
587       (goto-char start)
588       nil)))
589
590 (defun notmuch-show-goto-message-previous ()
591   (notmuch-show-move-to-message-top)
592   (if (bobp)
593       nil
594     (backward-char)
595     (notmuch-show-move-to-message-top)
596     t))
597
598 (defun notmuch-show-move-past-invisible-forward ()
599   (while (point-invisible-p)
600     (forward-char)))
601
602 (defun notmuch-show-move-past-invisible-backward ()
603   (while (point-invisible-p)
604     (backward-char)))
605
606 ;; Functions relating to the visibility of messages and their
607 ;; components.
608
609 (defun notmuch-show-element-visible (props visible-p spec-property)
610   (let ((spec (plist-get props spec-property)))
611     (if visible-p
612         (remove-from-invisibility-spec spec)
613       (add-to-invisibility-spec spec))))
614
615 (defun notmuch-show-message-visible (props visible-p)
616   (if visible-p
617       ;; When making the message visible, the headers may or not be
618       ;; visible. So we check that property separately.
619       (let ((headers-visible (plist-get props :headers-visible)))
620         (notmuch-show-element-visible props headers-visible :headers-invis-spec)
621         (notmuch-show-element-visible props t :message-invis-spec))
622     (notmuch-show-element-visible props nil :headers-invis-spec)
623     (notmuch-show-element-visible props nil :message-invis-spec))
624
625   (notmuch-show-set-prop :message-visible visible-p props))
626
627 (defun notmuch-show-headers-visible (props visible-p)
628   (if (plist-get props :message-visible)
629       (notmuch-show-element-visible props visible-p :headers-invis-spec))
630   (notmuch-show-set-prop :headers-visible visible-p props))
631
632 ;; Functions for setting and getting attributes of the current
633 ;; message.
634
635 (defun notmuch-show-set-message-properties (props)
636   (save-excursion
637     (notmuch-show-move-to-message-top)
638     (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
639
640 (defun notmuch-show-get-message-properties ()
641   (save-excursion
642     (notmuch-show-move-to-message-top)
643     (get-text-property (point) :notmuch-message-properties)))
644
645 (defun notmuch-show-set-prop (prop val &optional props)
646   (let ((inhibit-read-only t)
647         (props (or props
648                    (notmuch-show-get-message-properties))))
649     (plist-put props prop val)
650     (notmuch-show-set-message-properties props)))
651
652 (defun notmuch-show-get-prop (prop &optional props)
653   (let ((props (or props
654                    (notmuch-show-get-message-properties))))
655     (plist-get props prop)))
656
657 (defun notmuch-show-get-message-id ()
658   "Return the message id of the current message."
659   (concat "id:" (notmuch-show-get-prop :id)))
660
661 ;; dme: Would it make sense to use a macro for many of these?
662
663 (defun notmuch-show-get-filename ()
664   "Return the filename of the current message."
665   (notmuch-show-get-prop :filename))
666
667 (defun notmuch-show-get-header (header)
668   "Return the named header of the current message, if any."
669   (plist-get (notmuch-show-get-prop :headers) header))
670
671 (defun notmuch-show-get-cc ()
672   (notmuch-show-get-header :Cc))
673
674 (defun notmuch-show-get-date ()
675   (notmuch-show-get-header :Date))
676
677 (defun notmuch-show-get-from ()
678   (notmuch-show-get-header :From))
679
680 (defun notmuch-show-get-subject ()
681   (notmuch-show-get-header :Subject))
682
683 (defun notmuch-show-get-to ()
684   (notmuch-show-get-header :To))
685
686 (defun notmuch-show-set-tags (tags)
687   "Set the tags of the current message."
688   (notmuch-show-set-prop :tags tags)
689   (notmuch-show-update-tags tags))
690
691 (defun notmuch-show-get-tags ()
692   "Return the tags of the current message."
693   (notmuch-show-get-prop :tags))
694
695 (defun notmuch-show-message-visible-p ()
696   "Is the current message visible?"
697   (notmuch-show-get-prop :message-visible))
698
699 (defun notmuch-show-headers-visible-p ()
700   "Are the headers of the current message visible?"
701   (notmuch-show-get-prop :headers-visible))
702
703 (defun notmuch-show-mark-read ()
704   "Mark the current message as read."
705   (notmuch-show-remove-tag "unread"))
706
707 ;; Commands typically bound to keys.
708
709 (defun notmuch-show-advance-and-archive ()
710   "Advance through thread and archive.
711
712 This command is intended to be one of the simplest ways to
713 process a thread of email. It does the following:
714
715 If the current message in the thread is not yet fully visible,
716 scroll by a near screenful to read more of the message.
717
718 Otherwise, (the end of the current message is already within the
719 current window), advance to the next open message.
720
721 Finally, if there is no further message to advance to, and this
722 last message is already read, then archive the entire current
723 thread, (remove the \"inbox\" tag from each message). Also kill
724 this buffer, and display the next thread from the search from
725 which this thread was originally shown."
726   (interactive)
727   (let ((end-of-this-message (notmuch-show-message-bottom)))
728     (cond
729      ;; Ideally we would test `end-of-this-message' against the result
730      ;; of `window-end', but that doesn't account for the fact that
731      ;; the end of the message might be hidden, so we have to actually
732      ;; go to the end, walk back over invisible text and then see if
733      ;; point is visible.
734      ((save-excursion
735         (goto-char (- end-of-this-message 1))
736         (notmuch-show-move-past-invisible-backward)
737         (> (point) (window-end)))
738       ;; The bottom of this message is not visible - scroll.
739       (scroll-up nil))
740
741      ((not (= end-of-this-message (point-max)))
742       ;; This is not the last message - move to the next visible one.
743       (notmuch-show-next-open-message))
744
745      (t
746       ;; This is the last message - archive the thread.
747       (notmuch-show-archive-thread)))))
748
749 (defun notmuch-show-rewind ()
750   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
751
752 Specifically, if the beginning of the previous email is fewer
753 than `window-height' lines from the current point, move to it
754 just like `notmuch-show-previous-message'.
755
756 Otherwise, just scroll down a screenful of the current message.
757
758 This command does not modify any message tags, (it does not undo
759 any effects from previous calls to
760 `notmuch-show-advance-and-archive'."
761   (interactive)
762   (let ((start-of-message (notmuch-show-message-top))
763         (start-of-window (window-start)))
764     (cond
765       ;; Either this message is properly aligned with the start of the
766       ;; window or the start of this message is not visible on the
767       ;; screen - scroll.
768      ((or (= start-of-message start-of-window)
769           (< start-of-message start-of-window))
770       (scroll-down)
771       ;; If a small number of lines from the previous message are
772       ;; visible, realign so that the top of the current message is at
773       ;; the top of the screen.
774       (if (< (count-lines (window-start) (notmuch-show-message-top))
775              next-screen-context-lines)
776           (progn
777             (goto-char (notmuch-show-message-top))
778             (notmuch-show-message-adjust)))
779       ;; Move to the top left of the window.
780       (goto-char (window-start)))
781      (t
782       ;; Move to the previous message.
783       (notmuch-show-previous-message)))))
784
785 (defun notmuch-show-reply ()
786   "Reply to the current message."
787   (interactive)
788   (notmuch-reply (notmuch-show-get-message-id)))
789
790 (defun notmuch-show-forward-message ()
791   "Forward the current message."
792   (interactive)
793   (with-current-notmuch-show-message
794    (message-forward)))
795
796 (defun notmuch-show-next-message ()
797   "Show the next message."
798   (interactive)
799   (notmuch-show-goto-message-next)
800   (notmuch-show-mark-read)
801   (notmuch-show-message-adjust))
802
803 (defun notmuch-show-previous-message ()
804   "Show the previous message."
805   (interactive)
806   (notmuch-show-goto-message-previous)
807   (notmuch-show-mark-read)
808   (notmuch-show-message-adjust))
809
810 (defun notmuch-show-next-open-message ()
811   "Show the next message."
812   (interactive)
813   (while (and (notmuch-show-goto-message-next)
814               (not (notmuch-show-message-visible-p))))
815   (notmuch-show-mark-read)
816   (notmuch-show-message-adjust))
817
818 (defun notmuch-show-previous-open-message ()
819   "Show the previous message."
820   (interactive)
821   (while (and (notmuch-show-goto-message-previous)
822               (not (notmuch-show-message-visible-p))))
823   (notmuch-show-mark-read)
824   (notmuch-show-message-adjust))
825
826 (defun notmuch-show-view-raw-message ()
827   "View the file holding the current message."
828   (interactive)
829   (view-file (notmuch-show-get-filename)))
830
831 (defun notmuch-show-pipe-message (command)
832   "Pipe the contents of the current message to the given command.
833
834 The given command will be executed with the raw contents of the
835 current email message as stdin. Anything printed by the command
836 to stdout or stderr will appear in the *Messages* buffer."
837   (interactive "sPipe message to command: ")
838   (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
839          (list command " < "
840                (shell-quote-argument (notmuch-show-get-filename)))))
841
842 (defun notmuch-show-add-tag (&rest toadd)
843   "Add a tag to the current message."
844   (interactive
845    (list (notmuch-select-tag-with-completion "Tag to add: ")))
846   (apply 'notmuch-call-notmuch-process
847          (append (cons "tag"
848                        (mapcar (lambda (s) (concat "+" s)) toadd))
849                  (cons (notmuch-show-get-message-id) nil)))
850   (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
851
852 (defun notmuch-show-remove-tag (&rest toremove)
853   "Remove a tag from the current message."
854   (interactive
855    (list (notmuch-select-tag-with-completion
856           "Tag to remove: " (notmuch-show-get-message-id))))
857   (let ((tags (notmuch-show-get-tags)))
858     (if (intersection tags toremove :test 'string=)
859         (progn
860           (apply 'notmuch-call-notmuch-process
861                  (append (cons "tag"
862                                (mapcar (lambda (s) (concat "-" s)) toremove))
863                          (cons (notmuch-show-get-message-id) nil)))
864           (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
865
866 (defun notmuch-show-toggle-headers ()
867   "Toggle the visibility of the current message headers."
868   (interactive)
869   (let ((props (notmuch-show-get-message-properties)))
870     (notmuch-show-headers-visible
871      props
872      (not (plist-get props :headers-visible))))
873   (force-window-update))
874
875 (defun notmuch-show-toggle-message ()
876   "Toggle the visibility of the current message."
877   (interactive)
878   (let ((props (notmuch-show-get-message-properties)))
879     (notmuch-show-message-visible
880      props
881      (not (plist-get props :message-visible))))
882   (force-window-update))
883
884 (defun notmuch-show-next-button ()
885   "Advance point to the next button in the buffer."
886   (interactive)
887   (forward-button 1))
888
889 (defun notmuch-show-previous-button ()
890   "Move point back to the previous button in the buffer."
891   (interactive)
892   (backward-button 1))
893
894 (defun notmuch-show-archive-thread-internal (show-next)
895   ;; Remove the tag from the current set of messages.
896   (goto-char (point-min))
897   (loop do (notmuch-show-remove-tag "inbox")
898         until (not (notmuch-show-goto-message-next)))
899   ;; Move to the next item in the search results, if any.
900   (let ((parent-buffer notmuch-show-parent-buffer))
901     (kill-this-buffer)
902     (if parent-buffer
903         (progn
904           (switch-to-buffer parent-buffer)
905           (forward-line)
906           (if show-next
907               (notmuch-search-show-thread))))))
908
909 (defun notmuch-show-archive-thread ()
910   "Archive each message in thread, then show next thread from search.
911
912 Archive each message currently shown by removing the \"inbox\"
913 tag from each. Then kill this buffer and show the next thread
914 from the search from which this thread was originally shown.
915
916 Note: This command is safe from any race condition of new messages
917 being delivered to the same thread. It does not archive the
918 entire thread, but only the messages shown in the current
919 buffer."
920   (interactive)
921   (notmuch-show-archive-thread-internal t))
922
923 (defun notmuch-show-archive-thread-then-exit ()
924   "Archive each message in thread, then exit back to search results."
925   (interactive)
926   (notmuch-show-archive-thread-internal nil))
927
928 (defun notmuch-show-do-stash (text)
929   (kill-new text)
930   (message "Saved: %s" text))
931
932 (defun notmuch-show-stash-cc ()
933   "Copy CC field of current message to kill-ring."
934   (interactive)
935   (notmuch-show-do-stash (notmuch-show-get-cc)))
936
937 (defun notmuch-show-stash-date ()
938   "Copy date of current message to kill-ring."
939   (interactive)
940   (notmuch-show-do-stash (notmuch-show-get-date)))
941
942 (defun notmuch-show-stash-filename ()
943   "Copy filename of current message to kill-ring."
944   (interactive)
945   (notmuch-show-do-stash (notmuch-show-get-filename)))
946
947 (defun notmuch-show-stash-from ()
948   "Copy From address of current message to kill-ring."
949   (interactive)
950   (notmuch-show-do-stash (notmuch-show-get-from)))
951
952 (defun notmuch-show-stash-message-id ()
953   "Copy message ID of current message to kill-ring."
954   (interactive)
955   (notmuch-show-do-stash (notmuch-show-get-message-id)))
956
957 (defun notmuch-show-stash-subject ()
958   "Copy Subject field of current message to kill-ring."
959   (interactive)
960   (notmuch-show-do-stash (notmuch-show-get-subject)))
961
962 (defun notmuch-show-stash-tags ()
963   "Copy tags of current message to kill-ring as a comma separated list."
964   (interactive)
965   (notmuch-show-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
966
967 (defun notmuch-show-stash-to ()
968   "Copy To address of current message to kill-ring."
969   (interactive)
970   (notmuch-show-do-stash (notmuch-show-get-to)))
971
972 ;;
973
974 (provide 'notmuch-show)