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