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