]> git.notmuchmail.org Git - notmuch/blob - notmuch.el
342ce6abe6c55bb49f709a1c716452ad4fb09561
[notmuch] / notmuch.el
1 ; notmuch.el --- run notmuch within emacs
2 ;
3 ; Copyright © Carl Worth
4 ;
5 ; This file is part of Notmuch.
6 ;
7 ; Notmuch is free software: you can redistribute it and/or modify it
8 ; under the terms of the GNU General Public License as published by
9 ; the Free Software Foundation, either version 3 of the License, or
10 ; (at your option) any later version.
11 ;
12 ; Notmuch is distributed in the hope that it will be useful, but
13 ; WITHOUT ANY WARRANTY; without even the implied warranty of
14 ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 ; General Public License for more details.
16 ;
17 ; You should have received a copy of the GNU General Public License
18 ; along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.
19 ;
20 ; Authors: Carl Worth <cworth@cworth.org>
21
22 ; This is an emacs-based interface to the notmuch mail system.
23 ;
24 ; You will first need to have the notmuch program installed and have a
25 ; notmuch database built in order to use this. See
26 ; http://notmuchmail.org for details.
27 ;
28 ; To install this software, copy it to a directory that is on the
29 ; `load-path' variable within emacs (a good candidate is
30 ; /usr/local/share/emacs/site-lisp). If you are viewing this from the
31 ; notmuch source distribution then you can simply run:
32 ;
33 ;       sudo make install-emacs
34 ;
35 ; to install it.
36 ;
37 ; Then, to actually run it, add:
38 ;
39 ;       (require 'notmuch)
40 ;
41 ; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
42 ; or run:
43 ;
44 ;       emacs -f notmuch
45 ;
46 ; Have fun, and let us know if you have any comment, questions, or
47 ; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
48 ; required, but is available from http://notmuchmail.org).
49
50 (require 'cl)
51 (require 'mm-view)
52 (require 'message)
53
54 (defvar notmuch-show-mode-map
55   (let ((map (make-sparse-keymap)))
56     (define-key map "?" 'notmuch-help)
57     (define-key map "q" 'kill-this-buffer)
58     (define-key map (kbd "C-p") 'notmuch-show-previous-line)
59     (define-key map (kbd "C-n") 'notmuch-show-next-line)
60     (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
61     (define-key map (kbd "TAB") 'notmuch-show-next-button)
62     (define-key map "s" 'notmuch-search)
63     (define-key map "m" 'message-mail)
64     (define-key map "f" 'notmuch-show-forward-current)
65     (define-key map "r" 'notmuch-show-reply)
66     (define-key map "|" 'notmuch-show-pipe-message)
67     (define-key map "w" 'notmuch-show-save-attachments)
68     (define-key map "V" 'notmuch-show-view-raw-message)
69     (define-key map "v" 'notmuch-show-view-all-mime-parts)
70     (define-key map "b" 'notmuch-show-toggle-current-body)
71     (define-key map "h" 'notmuch-show-toggle-current-header)
72     (define-key map "-" 'notmuch-show-remove-tag)
73     (define-key map "+" 'notmuch-show-add-tag)
74     (define-key map "X" 'notmuch-show-mark-read-then-archive-then-exit)
75     (define-key map "x" 'notmuch-show-archive-thread-then-exit)
76     (define-key map "A" 'notmuch-show-mark-read-then-archive-thread)
77     (define-key map "a" 'notmuch-show-archive-thread)
78     (define-key map "p" 'notmuch-show-previous-message)
79     (define-key map "N" 'notmuch-show-mark-read-then-next-open-message)
80     (define-key map "n" 'notmuch-show-next-message)
81     (define-key map (kbd "DEL") 'notmuch-show-rewind)
82     (define-key map " " 'notmuch-show-advance-marking-read-and-archiving)
83     map)
84   "Keymap for \"notmuch show\" buffers.")
85 (fset 'notmuch-show-mode-map notmuch-show-mode-map)
86
87 (defvar notmuch-show-signature-regexp "\\(-- ?\\|_+\\)$"
88   "Pattern to match a line that separates content from signature.
89
90 The regexp can (and should) include $ to match the end of the
91 line, but should not include ^ to match the beginning of the
92 line. This is because notmuch may have inserted additional space
93 for indentation at the beginning of the line. But notmuch will
94 move past the indentation when testing this pattern, (so that the
95 pattern can still test against the entire line).")
96
97 (defvar notmuch-show-signature-button-format
98   "[ %d-line signature. Click/Enter to toggle visibility. ]"
99   "String used to construct button text for hidden signatures
100
101 Can use up to one integer format parameter, i.e. %d")
102
103 (defvar notmuch-show-citation-button-format
104   "[ %d more citation lines. Click/Enter to toggle visibility. ]"
105   "String used to construct button text for hidden citations.
106
107 Can use up to one integer format parameter, i.e. %d")
108
109 (defvar notmuch-show-signature-lines-max 12
110   "Maximum length of signature that will be hidden by default.")
111
112 (defvar notmuch-show-citation-lines-prefix 4
113   "Always show at least this many lines of a citation.
114
115 If there is one more line, show that, otherwise collapse
116 remaining lines into a button.")
117
118 (defvar notmuch-command "notmuch"
119   "Command to run the notmuch binary.")
120
121 (defvar notmuch-show-message-begin-regexp    "\fmessage{")
122 (defvar notmuch-show-message-end-regexp      "\fmessage}")
123 (defvar notmuch-show-header-begin-regexp     "\fheader{")
124 (defvar notmuch-show-header-end-regexp       "\fheader}")
125 (defvar notmuch-show-body-begin-regexp       "\fbody{")
126 (defvar notmuch-show-body-end-regexp         "\fbody}")
127 (defvar notmuch-show-attachment-begin-regexp "\fattachment{")
128 (defvar notmuch-show-attachment-end-regexp   "\fattachment}")
129 (defvar notmuch-show-part-begin-regexp       "\fpart{")
130 (defvar notmuch-show-part-end-regexp         "\fpart}")
131 (defvar notmuch-show-marker-regexp "\f\\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$")
132
133 (defvar notmuch-show-id-regexp "\\(id:[^ ]*\\)")
134 (defvar notmuch-show-depth-match-regexp " depth:\\([0-9]*\\).*match:\\([01]\\) ")
135 (defvar notmuch-show-filename-regexp "filename:\\(.*\\)$")
136 (defvar notmuch-show-contentype-regexp "Content-type: \\(.*\\)")
137
138 (defvar notmuch-show-tags-regexp "(\\([^)]*\\))$")
139
140 (defvar notmuch-show-parent-buffer nil)
141 (defvar notmuch-show-body-read-visible nil)
142 (defvar notmuch-show-citations-visible nil)
143 (defvar notmuch-show-signatures-visible nil)
144 (defvar notmuch-show-headers-visible nil)
145
146 ; XXX: This should be a generic function in emacs somewhere, not here
147 (defun point-invisible-p ()
148   "Return whether the character at point is invisible.
149
150 Here visibility is determined by `buffer-invisibility-spec' and
151 the invisible property of any overlays for point. It doesn't have
152 anything to do with whether point is currently being displayed
153 within the current window."
154   (let ((prop (get-char-property (point) 'invisible)))
155     (if (eq buffer-invisibility-spec t)
156         prop
157       (or (memq prop buffer-invisibility-spec)
158           (assq prop buffer-invisibility-spec)))))
159
160 (defun notmuch-select-tag-with-completion (prompt &rest search-terms)
161   (let ((tag-list
162          (with-output-to-string
163            (with-current-buffer standard-output
164              (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
165     (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
166
167 (defun notmuch-show-next-line ()
168   "Like builtin `next-line' but ensuring we end on a visible character.
169
170 By advancing forward until reaching a visible character.
171
172 Unlike builtin `next-line' this version accepts no arguments."
173   (interactive)
174   (set 'this-command 'next-line)
175   (call-interactively 'next-line)
176   (while (point-invisible-p)
177     (forward-char)))
178
179 (defun notmuch-show-previous-line ()
180   "Like builtin `previous-line' but ensuring we end on a visible character.
181
182 By advancing forward until reaching a visible character.
183
184 Unlike builtin `previous-line' this version accepts no arguments."
185   (interactive)
186   (set 'this-command 'previous-line)
187   (call-interactively 'previous-line)
188   (while (point-invisible-p)
189     (forward-char)))
190
191 (defun notmuch-show-get-message-id ()
192   (save-excursion
193     (beginning-of-line)
194     (if (not (looking-at notmuch-show-message-begin-regexp))
195         (re-search-backward notmuch-show-message-begin-regexp))
196     (re-search-forward notmuch-show-id-regexp)
197     (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
198
199 (defun notmuch-show-get-filename ()
200   (save-excursion
201     (beginning-of-line)
202     (if (not (looking-at notmuch-show-message-begin-regexp))
203         (re-search-backward notmuch-show-message-begin-regexp))
204     (re-search-forward notmuch-show-filename-regexp)
205     (buffer-substring-no-properties (match-beginning 1) (match-end 1))))
206
207 (defun notmuch-show-set-tags (tags)
208   (save-excursion
209     (beginning-of-line)
210     (if (not (looking-at notmuch-show-message-begin-regexp))
211         (re-search-backward notmuch-show-message-begin-regexp))
212     (re-search-forward notmuch-show-tags-regexp)
213     (let ((inhibit-read-only t)
214           (beg (match-beginning 1))
215           (end (match-end 1)))
216       (delete-region beg end)
217       (goto-char beg)
218       (insert (mapconcat 'identity tags " ")))))
219
220 (defun notmuch-show-get-tags ()
221   (save-excursion
222     (beginning-of-line)
223     (if (not (looking-at notmuch-show-message-begin-regexp))
224         (re-search-backward notmuch-show-message-begin-regexp))
225     (re-search-forward notmuch-show-tags-regexp)
226     (split-string (buffer-substring (match-beginning 1) (match-end 1)))))
227
228 (defun notmuch-show-get-bcc ()
229   "Return BCC address(es) of current message"
230   (notmuch-show-get-header-field 'bcc))
231
232 (defun notmuch-show-get-cc ()
233   "Return CC address(es) of current message"
234   (notmuch-show-get-header-field 'cc))
235
236 (defun notmuch-show-get-date ()
237   "Return Date of current message"
238   (notmuch-show-get-header-field 'date))
239
240 (defun notmuch-show-get-from ()
241   "Return From address of current message"
242   (notmuch-show-get-header-field 'from))
243
244 (defun notmuch-show-get-subject ()
245   "Return Subject of current message"
246   (notmuch-show-get-header-field 'subject))
247
248 (defun notmuch-show-get-to ()
249   "Return To address(es) of current message"
250   (notmuch-show-get-header-field 'to))
251
252 (defun notmuch-show-get-header-field (name)
253   "Retrieve the header field NAME from the current message.
254 NAME should be a symbol, in lower case, as returned by
255 mail-header-extract-no-properties"
256   (let* ((result (assoc name (notmuch-show-get-header)))
257         (val (and result (cdr result))))
258     val))
259
260 (defun notmuch-show-get-header ()
261   "Retrieve and parse the header from the current message. Returns an alist with of (header . value)
262 where header is a symbol and value is a string.  The summary from notmuch-show is returned as the
263 pseudoheader summary"
264   (require 'mailheader)
265   (save-excursion
266     (beginning-of-line)
267     (if (not (looking-at notmuch-show-message-begin-regexp))
268         (re-search-backward notmuch-show-message-begin-regexp))
269     (re-search-forward (concat notmuch-show-header-begin-regexp "\n[[:space:]]*\\(.*\\)\n"))
270     (let* ((summary (buffer-substring-no-properties (match-beginning 1) (match-end 1)))
271           (beg (point)))
272       (re-search-forward notmuch-show-header-end-regexp)
273       (let ((text (buffer-substring beg (match-beginning 0))))
274         (with-temp-buffer
275           (insert text)
276           (goto-char (point-min))
277           (while (looking-at "\\([[:space:]]*\\)[A-Za-z][-A-Za-z0-9]*:")
278             (delete-region (match-beginning 1) (match-end 1))
279             (forward-line)
280             )
281           (goto-char (point-min))
282           (cons (cons 'summary summary) (mail-header-extract-no-properties)))))))
283
284 (defun notmuch-show-add-tag (&rest toadd)
285   "Add a tag to the current message."
286   (interactive
287    (list (notmuch-select-tag-with-completion "Tag to add: ")))
288   (apply 'notmuch-call-notmuch-process
289          (append (cons "tag"
290                        (mapcar (lambda (s) (concat "+" s)) toadd))
291                  (cons (notmuch-show-get-message-id) nil)))
292   (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
293
294 (defun notmuch-show-remove-tag (&rest toremove)
295   "Remove a tag from the current message."
296   (interactive
297    (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-show-get-message-id))))
298   (let ((tags (notmuch-show-get-tags)))
299     (if (intersection tags toremove :test 'string=)
300         (progn
301           (apply 'notmuch-call-notmuch-process
302                  (append (cons "tag"
303                                (mapcar (lambda (s) (concat "-" s)) toremove))
304                          (cons (notmuch-show-get-message-id) nil)))
305           (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
306
307 (defun notmuch-show-archive-thread-maybe-mark-read (markread)
308   (save-excursion
309     (goto-char (point-min))
310     (while (not (eobp))
311       (if markread
312           (notmuch-show-remove-tag "unread" "inbox")
313         (notmuch-show-remove-tag "inbox"))
314       (if (not (eobp))
315           (forward-char))
316       (if (not (re-search-forward notmuch-show-message-begin-regexp nil t))
317           (goto-char (point-max)))))
318   (let ((parent-buffer notmuch-show-parent-buffer))
319     (kill-this-buffer)
320     (if parent-buffer
321         (progn
322           (switch-to-buffer parent-buffer)
323           (forward-line)
324           (notmuch-search-show-thread)))))
325
326 (defun notmuch-show-mark-read-then-archive-thread ()
327   "Remove unread tags from thread, then archive and show next thread.
328
329 Archive each message currently shown by removing the \"unread\"
330 and \"inbox\" tag from each. Then kill this buffer and show the
331 next thread from the search from which this thread was originally
332 shown.
333
334 Note: This command is safe from any race condition of new messages
335 being delivered to the same thread. It does not archive the
336 entire thread, but only the messages shown in the current
337 buffer."
338   (interactive)
339   (notmuch-show-archive-thread-maybe-mark-read t))
340
341 (defun notmuch-show-archive-thread ()
342   "Archive each message in thread, then show next thread from search.
343
344 Archive each message currently shown by removing the \"inbox\"
345 tag from each. Then kill this buffer and show the next thread
346 from the search from which this thread was originally shown.
347
348 Note: This command is safe from any race condition of new messages
349 being delivered to the same thread. It does not archive the
350 entire thread, but only the messages shown in the current
351 buffer."
352   (interactive)
353   (notmuch-show-archive-thread-maybe-mark-read nil))
354
355 (defun notmuch-show-archive-thread-then-exit ()
356   "Archive each message in thread, then exit back to search results."
357   (interactive)
358   (notmuch-show-archive-thread)
359   (kill-this-buffer))
360
361 (defun notmuch-show-mark-read-then-archive-then-exit ()
362   "Remove unread tags from thread, then archive and exit to search results."
363   (interactive)
364   (notmuch-show-mark-read-then-archive-thread)
365   (kill-this-buffer))
366
367 (defun notmuch-show-view-raw-message ()
368   "View the raw email of the current message."
369   (interactive)
370   (view-file (notmuch-show-get-filename)))
371
372 (defmacro with-current-notmuch-show-message (&rest body)
373   "Evaluate body with current buffer set to the text of current message"
374   `(save-excursion
375      (let ((filename (notmuch-show-get-filename)))
376        (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
377          (with-current-buffer buf
378            (insert-file-contents filename nil nil nil t)
379            ,@body)
380          (kill-buffer buf)))))
381
382 (defun notmuch-show-view-all-mime-parts ()
383   "Use external viewers to view all attachments from the current message."
384   (interactive)
385   (with-current-notmuch-show-message
386    ; We ovverride the mm-inline-media-tests to indicate which message
387    ; parts are already sufficiently handled by the original
388    ; presentation of the message in notmuch-show mode. These parts
389    ; will be inserted directly into the temporary buffer of
390    ; with-current-notmuch-show-message and silently discarded.
391    ;
392    ; Any MIME part not explicitly mentioned here will be handled by an
393    ; external viewer as configured in the various mailcap files.
394    (let ((mm-inline-media-tests '(
395                                   ("text/.*" ignore identity)
396                                   ("application/pgp-signature" ignore identity)
397                                   ("multipart/alternative" ignore identity)
398                                   ("multipart/mixed" ignore identity)
399                                   ("multipart/related" ignore identity)
400                                  )))
401      (mm-display-parts (mm-dissect-buffer)))))
402
403 (defun notmuch-foreach-mime-part (function mm-handle)
404   (cond ((stringp (car mm-handle))
405          (dolist (part (cdr mm-handle))
406            (notmuch-foreach-mime-part function part)))
407         ((bufferp (car mm-handle))
408          (funcall function mm-handle))
409         (t (dolist (part mm-handle)
410              (notmuch-foreach-mime-part function part)))))
411
412 (defun notmuch-count-attachments (mm-handle)
413   (let ((count 0))
414     (notmuch-foreach-mime-part
415      (lambda (p)
416        (let ((disposition (mm-handle-disposition p)))
417          (and (listp disposition)
418               (or (equal (car disposition) "attachment")
419                   (and (equal (car disposition) "inline")
420                        (assq 'filename disposition)))
421               (incf count))))
422      mm-handle)
423     count))
424
425 (defun notmuch-save-attachments (mm-handle &optional queryp)
426   (notmuch-foreach-mime-part
427    (lambda (p)
428      (let ((disposition (mm-handle-disposition p)))
429        (and (listp disposition)
430             (or (equal (car disposition) "attachment")
431                 (and (equal (car disposition) "inline")
432                      (assq 'filename disposition)))
433             (or (not queryp)
434                 (y-or-n-p
435                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
436             (mm-save-part p))))
437    mm-handle))
438
439 (defun notmuch-show-save-attachments ()
440   "Save all attachments from the current message."
441   (interactive)
442   (with-current-notmuch-show-message
443    (let ((mm-handle (mm-dissect-buffer)))
444      (notmuch-save-attachments
445       mm-handle (> (notmuch-count-attachments mm-handle) 1))))
446   (message "Done"))
447
448 (defun notmuch-reply (query-string)
449   (switch-to-buffer (generate-new-buffer "notmuch-draft"))
450   (call-process notmuch-command nil t nil "reply" query-string)
451   (message-insert-signature)
452   (goto-char (point-min))
453   (if (re-search-forward "^$" nil t)
454       (progn
455         (insert "--text follows this line--")
456         (forward-line)))
457   (message-mode))
458
459 (defun notmuch-show-reply ()
460   "Begin composing a reply to the current message in a new buffer."
461   (interactive)
462   (let ((message-id (notmuch-show-get-message-id)))
463     (notmuch-reply message-id)))
464
465 (defun notmuch-show-forward-current ()
466   "Forward the current message."
467   (interactive)
468   (with-current-notmuch-show-message
469    (message-forward)))
470
471 (defun notmuch-show-pipe-message (command)
472   "Pipe the contents of the current message to the given command.
473
474 The given command will be executed with the raw contents of the
475 current email message as stdin. Anything printed by the command
476 to stdout or stderr will appear in the *Messages* buffer."
477   (interactive "sPipe message to command: ")
478   (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
479          (list command " < " (shell-quote-argument (notmuch-show-get-filename)))))
480
481 (defun notmuch-show-move-to-current-message-summary-line ()
482   "Move to the beginning of the one-line summary of the current message.
483
484 This gives us a stable place to move to and work from since the
485 summary line is always visible. This is important since moving to
486 an invisible location is unreliable, (the main command loop moves
487 point either forward or backward to the next visible character
488 when a command ends with point on an invisible character).
489
490 Emits an error if point is not within a valid message, (that is
491 no pattern of `notmuch-show-message-begin-regexp' could be found
492 by searching backward)."
493   (beginning-of-line)
494   (if (not (looking-at notmuch-show-message-begin-regexp))
495       (if (re-search-backward notmuch-show-message-begin-regexp nil t)
496           (forward-line 2)
497         (error "Not within a valid message."))
498     (forward-line 2)))
499
500 (defun notmuch-show-last-message-p ()
501   "Predicate testing whether point is within the last message."
502   (save-window-excursion
503     (save-excursion
504       (notmuch-show-move-to-current-message-summary-line)
505       (not (re-search-forward notmuch-show-message-begin-regexp nil t)))))
506
507 (defun notmuch-show-message-unread-p ()
508   "Predicate testing whether current message is unread."
509   (member "unread" (notmuch-show-get-tags)))
510
511 (defun notmuch-show-message-open-p ()
512   "Predicate testing whether current message is open (body is visible)."
513   (let ((btn (previous-button (point) t)))
514     (while (not (button-has-type-p btn 'notmuch-button-body-toggle-type))
515       (setq btn (previous-button (button-start btn))))
516     (not (invisible-p (button-get btn 'invisibility-spec)))))
517
518 (defun notmuch-show-next-message ()
519   "Advance to the beginning of the next message in the buffer.
520
521 Moves to the last visible character of the current message if
522 already on the last message in the buffer.
523
524 Returns nil if already on the last message in the buffer."
525   (interactive)
526   (notmuch-show-move-to-current-message-summary-line)
527   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
528       (progn
529         (notmuch-show-move-to-current-message-summary-line)
530         (recenter 0)
531         t)
532     (goto-char (- (point-max) 1))
533     (while (point-invisible-p)
534       (backward-char))
535     (recenter 0)
536     nil))
537
538 (defun notmuch-show-find-next-message ()
539   "Returns the position of the next message in the buffer.
540
541 Or the position of the last visible character of the current
542 message if already within the last message in the buffer."
543   ; save-excursion doesn't save our window position
544   ; save-window-excursion doesn't save point
545   ; Looks like we have to use both.
546   (save-excursion
547     (save-window-excursion
548       (notmuch-show-next-message)
549       (point))))
550
551 (defun notmuch-show-next-unread-message ()
552   "Advance to the beginning of the next unread message in the buffer.
553
554 Moves to the last visible character of the current message if
555 there are no more unread messages past the current point."
556   (notmuch-show-next-message)
557   (while (and (not (notmuch-show-last-message-p))
558               (not (notmuch-show-message-unread-p)))
559     (notmuch-show-next-message))
560   (if (not (notmuch-show-message-unread-p))
561       (notmuch-show-next-message)))
562
563 (defun notmuch-show-next-open-message ()
564   "Advance to the next open message (that is, body is not invisible)."
565   (while (and (notmuch-show-next-message)
566               (not (notmuch-show-message-open-p)))))
567
568 (defun notmuch-show-previous-message ()
569   "Backup to the beginning of the previous message in the buffer.
570
571 If within a message rather than at the beginning of it, then
572 simply move to the beginning of the current message."
573   (interactive)
574   (let ((start (point)))
575     (notmuch-show-move-to-current-message-summary-line)
576     (if (not (< (point) start))
577         ; Go backward twice to skip the current message's marker
578         (progn
579           (re-search-backward notmuch-show-message-begin-regexp nil t)
580           (re-search-backward notmuch-show-message-begin-regexp nil t)
581           (notmuch-show-move-to-current-message-summary-line)
582           ))
583     (recenter 0)))
584
585 (defun notmuch-show-find-previous-message ()
586   "Returns the position of the previous message in the buffer.
587
588 Or the position of the beginning of the current message if point
589 is originally within the message rather than at the beginning of
590 it."
591   ; save-excursion doesn't save our window position
592   ; save-window-excursion doesn't save point
593   ; Looks like we have to use both.
594   (save-excursion
595     (save-window-excursion
596       (notmuch-show-previous-message)
597       (point))))
598
599 (defun notmuch-show-mark-read-then-next-open-message ()
600   "Remove unread tag from this message, then advance to next open message."
601   (interactive)
602   (notmuch-show-remove-tag "unread")
603   (notmuch-show-next-open-message))
604
605 (defun notmuch-show-rewind ()
606   "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-marking-read-and-archiving]).
607
608 Specifically, if the beginning of the previous email is fewer
609 than `window-height' lines from the current point, move to it
610 just like `notmuch-show-previous-message'.
611
612 Otherwise, just scroll down a screenful of the current message.
613
614 This command does not modify any message tags, (it does not undo
615 any effects from previous calls to
616 `notmuch-show-advance-marking-read-and-archiving'."
617   (interactive)
618   (let ((previous (notmuch-show-find-previous-message)))
619     (if (> (count-lines previous (point)) (- (window-height) next-screen-context-lines))
620         (progn
621           (condition-case nil
622               (scroll-down nil)
623             ((beginning-of-buffer) nil))
624           (goto-char (window-start)))
625       (notmuch-show-previous-message))))
626
627 (defun notmuch-show-advance-marking-read-and-archiving ()
628   "Advance through thread, marking read and archiving.
629
630 This command is intended to be one of the simplest ways to
631 process a thread of email. It does the following:
632
633 If the current message in the thread is not yet fully visible,
634 scroll by a near screenful to read more of the message.
635
636 Otherwise, (the end of the current message is already within the
637 current window), remove the \"unread\" tag (if present) from the
638 current message and advance to the next open message.
639
640 Finally, if there is no further message to advance to, and this
641 last message is already read, then archive the entire current
642 thread, (remove the \"inbox\" tag from each message). Also kill
643 this buffer, and display the next thread from the search from
644 which this thread was originally shown."
645   (interactive)
646   (let ((next (notmuch-show-find-next-message))
647         (unread (notmuch-show-message-unread-p)))
648     (if (> next (window-end))
649         (scroll-up nil)
650       (let ((last (notmuch-show-last-message-p)))
651         (notmuch-show-mark-read-then-next-open-message)
652         (if last
653             (notmuch-show-archive-thread))))))
654
655 (defun notmuch-show-next-button ()
656   "Advance point to the next button in the buffer."
657   (interactive)
658   (forward-button 1))
659
660 (defun notmuch-show-previous-button ()
661   "Move point back to the previous button in the buffer."
662   (interactive)
663   (backward-button 1))
664
665 (defun notmuch-toggle-invisible-action (cite-button)
666   (let ((invis-spec (button-get cite-button 'invisibility-spec)))
667         (if (invisible-p invis-spec)
668             (remove-from-invisibility-spec invis-spec)
669           (add-to-invisibility-spec invis-spec)
670           ))
671   (force-window-update)
672   (redisplay t))
673
674 (defun notmuch-show-toggle-current-body ()
675   "Toggle the display of the current message body."
676   (interactive)
677   (save-excursion
678     (notmuch-show-move-to-current-message-summary-line)
679     (unless (button-at (point))
680       (notmuch-show-next-button))
681     (push-button))
682   )
683
684 (defun notmuch-show-toggle-current-header ()
685   "Toggle the display of the current message header."
686   (interactive)
687   (save-excursion
688     (notmuch-show-move-to-current-message-summary-line)
689     (forward-line)
690     (unless (button-at (point))
691       (notmuch-show-next-button))
692     (push-button))
693   )
694
695 (define-button-type 'notmuch-button-invisibility-toggle-type
696   'action 'notmuch-toggle-invisible-action
697   'follow-link t
698   'face 'font-lock-comment-face)
699 (define-button-type 'notmuch-button-citation-toggle-type 'help-echo "mouse-1, RET: Show citation"
700   :supertype 'notmuch-button-invisibility-toggle-type)
701 (define-button-type 'notmuch-button-signature-toggle-type 'help-echo "mouse-1, RET: Show signature"
702   :supertype 'notmuch-button-invisibility-toggle-type)
703 (define-button-type 'notmuch-button-headers-toggle-type 'help-echo "mouse-1, RET: Show headers"
704   :supertype 'notmuch-button-invisibility-toggle-type)
705 (define-button-type 'notmuch-button-body-toggle-type
706   'help-echo "mouse-1, RET: Show message"
707   'face 'notmuch-message-summary-face
708   :supertype 'notmuch-button-invisibility-toggle-type)
709
710 (defun notmuch-show-citation-regexp (depth)
711   "Build a regexp for matching citations at a given DEPTH (indent)"
712   (let ((line-regexp (format "[[:space:]]\\{%d\\}>.*\n" depth)))
713     (concat "\\(?:^" line-regexp
714             "\\(?:[[:space:]]*\n" line-regexp
715             "\\)?\\)+")))
716
717 (defun notmuch-show-region-to-button (beg end type prefix button-text)
718   "Auxilary function to do the actual making of overlays and buttons
719
720 BEG and END are buffer locations. TYPE should a string, either
721 \"citation\" or \"signature\". PREFIX is some arbitrary text to
722 insert before the button, probably for indentation.  BUTTON-TEXT
723 is what to put on the button."
724
725 ;; This uses some slightly tricky conversions between strings and
726 ;; symbols because of the way the button code works. Note that
727 ;; replacing intern-soft with make-symbol will cause this to fail,
728 ;; since the newly created symbol has no plist.
729
730   (let ((overlay (make-overlay beg end))
731         (invis-spec (make-symbol (concat "notmuch-" type "-region")))
732         (button-type (intern-soft (concat "notmuch-button-"
733                                           type "-toggle-type"))))
734     (add-to-invisibility-spec invis-spec)
735     (overlay-put overlay 'invisible invis-spec)
736     (goto-char (1+ end))
737     (save-excursion
738       (goto-char (1- beg))
739       (insert prefix)
740       (insert-button button-text
741                      'invisibility-spec invis-spec
742                      :type button-type)
743       )))
744
745
746 (defun notmuch-show-markup-citations-region (beg end depth)
747   "Markup citations, and up to one signature in the given region"
748   ;; it would be nice if the untabify was not required, but
749   ;; that would require notmuch to indent with spaces.
750   (untabify beg end)
751   (let ((citation-regexp (notmuch-show-citation-regexp depth))
752         (signature-regexp (concat (format "^[[:space:]]\\{%d\\}" depth)
753                                   notmuch-show-signature-regexp))
754         (indent (concat "\n" (make-string depth ? ))))
755     (goto-char beg)
756     (beginning-of-line)
757     (while (and (< (point) end)
758                 (re-search-forward citation-regexp end t))
759       (let* ((cite-start (match-beginning 0))
760              (cite-end  (match-end 0))
761              (cite-lines (count-lines cite-start cite-end)))
762         (when (> cite-lines (1+ notmuch-show-citation-lines-prefix))
763           (goto-char cite-start)
764           (forward-line notmuch-show-citation-lines-prefix)
765           (notmuch-show-region-to-button
766            (point) cite-end
767            "citation"
768            indent
769            (format notmuch-show-citation-button-format
770                    (- cite-lines notmuch-show-citation-lines-prefix))
771            ))))
772     (if (and (< (point) end)
773              (re-search-forward signature-regexp end t))
774         (let* ((sig-start (match-beginning 0))
775                (sig-end (match-end 0))
776                (sig-lines (1- (count-lines sig-start end))))
777           (if (<= sig-lines notmuch-show-signature-lines-max)
778               (notmuch-show-region-to-button
779                sig-start
780                end
781                "signature"
782                indent
783                (format notmuch-show-signature-button-format sig-lines)
784                ))))))
785
786 (defun notmuch-show-markup-part (beg end depth)
787   (if (re-search-forward notmuch-show-part-begin-regexp nil t)
788       (progn
789         (let (mime-message mime-type)
790           (save-excursion
791             (re-search-forward notmuch-show-contentype-regexp end t)
792             (setq mime-type (car (split-string (buffer-substring
793                                                 (match-beginning 1) (match-end 1))))))
794
795           (if (equal mime-type "text/html")
796               (let ((filename (notmuch-show-get-filename)))
797                 (with-temp-buffer
798                   (insert-file-contents filename nil nil nil t)
799                   (setq mime-message (mm-dissect-buffer)))))
800           (forward-line)
801           (let ((beg (point-marker)))
802             (re-search-forward notmuch-show-part-end-regexp)
803             (let ((end (copy-marker (match-beginning 0))))
804               (goto-char end)
805               (if (not (bolp))
806                   (insert "\n"))
807               (indent-rigidly beg end depth)
808               (if (not (eq mime-message nil))
809                   (save-excursion
810                     (goto-char beg)
811                     (forward-line -1)
812                     (let ((handle-type (mm-handle-type mime-message))
813                           mime-type)
814                       (if (sequencep (car handle-type))
815                           (setq mime-type (car handle-type))
816                         (setq mime-type (car (car (cdr handle-type))))
817                         )
818                       (if (equal mime-type "text/html")
819                           (mm-display-part mime-message))))
820                 )
821               (notmuch-show-markup-citations-region beg end depth)
822               ; Advance to the next part (if any) (so the outer loop can
823               ; determine whether we've left the current message.
824               (if (re-search-forward notmuch-show-part-begin-regexp nil t)
825                   (beginning-of-line)))))
826         (goto-char end))
827     (goto-char end)))
828
829 (defun notmuch-show-markup-parts-region (beg end depth)
830   (save-excursion
831     (goto-char beg)
832     (while (< (point) end)
833       (notmuch-show-markup-part beg end depth))))
834
835 (defun notmuch-show-markup-body (depth match btn)
836   "Markup a message body, (indenting, buttonizing citations,
837 etc.), and conditionally hiding the body itself if the message
838 has been read and does not match the current search.
839
840 DEPTH specifies the depth at which this message appears in the
841 tree of the current thread, (the top-level messages have depth 0
842 and each reply increases depth by 1). MATCH indicates whether
843 this message is regarded as matching the current search. BTN is
844 the button which is used to toggle the visibility of this
845 message.
846
847 When this function is called, point must be within the message, but
848 before the delimiter marking the beginning of the body."
849   (re-search-forward notmuch-show-body-begin-regexp)
850   (forward-line)
851   (let ((beg (point-marker)))
852     (re-search-forward notmuch-show-body-end-regexp)
853     (let ((end (copy-marker (match-beginning 0))))
854       (notmuch-show-markup-parts-region beg end depth)
855       (let ((invis-spec (make-symbol "notmuch-show-body-read")))
856         (overlay-put (make-overlay beg end)
857                      'invisible invis-spec)
858         (button-put btn 'invisibility-spec invis-spec)
859         (if (not (or (notmuch-show-message-unread-p) match))
860             (add-to-invisibility-spec invis-spec)))
861       (set-marker beg nil)
862       (set-marker end nil)
863       )))
864
865 (defun notmuch-fontify-headers ()
866   (while (looking-at "[[:space:]]")
867     (forward-char))
868   (if (looking-at "[Tt]o:")
869       (progn
870         (overlay-put (make-overlay (point) (re-search-forward ":"))
871                      'face 'message-header-name)
872         (overlay-put (make-overlay (point) (re-search-forward ".*$"))
873                      'face 'message-header-to))
874     (if (looking-at "[B]?[Cc][Cc]:")
875         (progn
876           (overlay-put (make-overlay (point) (re-search-forward ":"))
877                        'face 'message-header-name)
878           (overlay-put (make-overlay (point) (re-search-forward ".*$"))
879                        'face 'message-header-cc))
880       (if (looking-at "[Ss]ubject:")
881           (progn
882             (overlay-put (make-overlay (point) (re-search-forward ":"))
883                          'face 'message-header-name)
884             (overlay-put (make-overlay (point) (re-search-forward ".*$"))
885                          'face 'message-header-subject))
886         (if (looking-at "[Ff]rom:")
887             (progn
888               (overlay-put (make-overlay (point) (re-search-forward ":"))
889                            'face 'message-header-name)
890               (overlay-put (make-overlay (point) (re-search-forward ".*$"))
891                            'face 'message-header-other)))))))
892
893 (defun notmuch-show-markup-header (message-begin depth)
894   "Buttonize and decorate faces in a message header.
895
896 MESSAGE-BEGIN is the position of the absolute first character in
897 the message (including all delimiters that will end up being
898 invisible etc.). This is to allow a button to reliably extend to
899 the beginning of the message even if point is positioned at an
900 invisible character (such as the beginning of the buffer).
901
902 DEPTH specifies the depth at which this message appears in the
903 tree of the current thread, (the top-level messages have depth 0
904 and each reply increases depth by 1)."
905   (re-search-forward notmuch-show-header-begin-regexp)
906   (forward-line)
907   (let ((beg (point-marker))
908         (summary-end (copy-marker (line-beginning-position 2)))
909         (subject-end (copy-marker (line-end-position 2)))
910         (invis-spec (make-symbol "notmuch-show-header"))
911         (btn nil))
912     (re-search-forward notmuch-show-header-end-regexp)
913     (beginning-of-line)
914     (let ((end (point-marker)))
915       (indent-rigidly beg end depth)
916       (goto-char beg)
917       (setq btn (make-button message-begin summary-end :type 'notmuch-button-body-toggle-type))
918       (forward-line)
919       (add-to-invisibility-spec invis-spec)
920       (overlay-put (make-overlay subject-end end)
921                    'invisible invis-spec)
922       (make-button (line-beginning-position) subject-end
923                    'invisibility-spec invis-spec
924                    :type 'notmuch-button-headers-toggle-type)
925       (while (looking-at "[[:space:]]*[A-Za-z][-A-Za-z0-9]*:")
926         (beginning-of-line)
927         (notmuch-fontify-headers)
928         (forward-line)
929         )
930       (goto-char end)
931       (insert "\n")
932       (set-marker beg nil)
933       (set-marker summary-end nil)
934       (set-marker subject-end nil)
935       (set-marker end nil)
936       )
937   btn))
938
939 (defun notmuch-show-markup-message ()
940   (if (re-search-forward notmuch-show-message-begin-regexp nil t)
941       (let ((message-begin (match-beginning 0)))
942         (re-search-forward notmuch-show-depth-match-regexp)
943         (let ((depth (string-to-number (buffer-substring (match-beginning 1) (match-end 1))))
944               (match (string= "1" (buffer-substring (match-beginning 2) (match-end 2))))
945               (btn nil))
946           (setq btn (notmuch-show-markup-header message-begin depth))
947           (notmuch-show-markup-body depth match btn)))
948     (goto-char (point-max))))
949
950 (defun notmuch-show-hide-markers ()
951   (save-excursion
952     (goto-char (point-min))
953     (while (not (eobp))
954       (if (re-search-forward notmuch-show-marker-regexp nil t)
955           (progn
956             (overlay-put (make-overlay (match-beginning 0) (+ (match-end 0) 1))
957                          'invisible 'notmuch-show-marker))
958         (goto-char (point-max))))))
959
960 (defun notmuch-show-markup-messages ()
961   (save-excursion
962     (goto-char (point-min))
963     (while (not (eobp))
964       (notmuch-show-markup-message)))
965   (notmuch-show-hide-markers))
966
967 (defun notmuch-documentation-first-line (symbol)
968   "Return the first line of the documentation string for SYMBOL."
969   (let ((doc (documentation symbol)))
970     (if doc
971         (with-temp-buffer
972           (insert (documentation symbol t))
973           (goto-char (point-min))
974           (let ((beg (point)))
975             (end-of-line)
976             (buffer-substring beg (point))))
977       "")))
978
979 (defun notmuch-prefix-key-description (key)
980   "Given a prefix key code, return a human-readable string representation.
981
982 This is basically just `format-kbd-macro' but we also convert ESC to M-."
983   (let ((desc (format-kbd-macro (vector key))))
984     (if (string= desc "ESC")
985         "M-"
986       (concat desc " "))))
987
988 ; I would think that emacs would have code handy for walking a keymap
989 ; and generating strings for each key, and I would prefer to just call
990 ; that. But I couldn't find any (could be all implemented in C I
991 ; suppose), so I wrote my own here.
992 (defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
993   "For a key binding, return a string showing a human-readable
994 representation of the prefixed key as well as the first line of
995 documentation from the bound function.
996
997 For a mouse binding, return nil."
998   (let ((key (car binding))
999         (action (cdr binding)))
1000     (if (mouse-event-p key)
1001         nil
1002       (if (keymapp action)
1003           (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key))))
1004             (mapconcat substitute (cdr action) "\n"))
1005         (concat prefix (format-kbd-macro (vector key))
1006                 "\t"
1007                 (notmuch-documentation-first-line action))))))
1008
1009 (defalias 'notmuch-substitute-one-command-key
1010   (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
1011
1012 (defun notmuch-substitute-command-keys (doc)
1013   "Like `substitute-command-keys' but with documentation, not function names."
1014   (let ((beg 0))
1015     (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
1016       (let ((map (substring doc (match-beginning 1) (match-end 1))))
1017         (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
1018                                             (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
1019       (setq beg (match-end 0)))
1020     doc))
1021
1022 (defun notmuch-help ()
1023   "Display help for the current notmuch mode."
1024   (interactive)
1025   (let* ((mode major-mode)
1026          (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
1027     (with-current-buffer (generate-new-buffer "*notmuch-help*")
1028       (insert doc)
1029       (goto-char (point-min))
1030       (set-buffer-modified-p nil)
1031       (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
1032
1033 ;;;###autoload
1034 (defun notmuch-show-mode ()
1035   "Major mode for viewing a thread with notmuch.
1036
1037 This buffer contains the results of the \"notmuch show\" command
1038 for displaying a single thread of email from your email archives.
1039
1040 By default, various components of email messages, (citations,
1041 signatures, already-read messages), are hidden. You can make
1042 these parts visible by clicking with the mouse button or by
1043 pressing RET after positioning the cursor on a hidden part, (for
1044 which \\[notmuch-show-next-button] and \\[notmuch-show-previous-button] are helpful).
1045
1046 Reading the thread sequentially is well-supported by pressing
1047 \\[notmuch-show-advance-marking-read-and-archiving]. This will scroll the current message (if necessary),
1048 advance to the next message, or advance to the next thread (if
1049 already on the last message of a thread). As each message is
1050 scrolled away its \"unread\" tag will be removed, and as each
1051 thread is scrolled away the \"inbox\" tag will be removed from
1052 each message in the thread.
1053
1054 Other commands are available to read or manipulate the thread more
1055 selectively, (such as '\\[notmuch-show-next-message]' and '\\[notmuch-show-previous-message]' to advance to messages without
1056 removing any tags, and '\\[notmuch-show-archive-thread]' to archive an entire thread without
1057 scrolling through with \\[notmuch-show-advance-marking-read-and-archiving]).
1058
1059 You can add or remove arbitary tags from the current message with
1060 '\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
1061
1062 All currently available key bindings:
1063
1064 \\{notmuch-show-mode-map}"
1065   (interactive)
1066   (kill-all-local-variables)
1067   (add-to-invisibility-spec 'notmuch-show-marker)
1068   (use-local-map notmuch-show-mode-map)
1069   (setq major-mode 'notmuch-show-mode
1070         mode-name "notmuch-show")
1071   (setq buffer-read-only t))
1072
1073 (defgroup notmuch nil
1074   "Notmuch mail reader for Emacs."
1075   :group 'mail)
1076
1077 (defcustom notmuch-show-hook nil
1078   "List of functions to call when notmuch displays a message."
1079   :type 'hook
1080   :options '(goto-address)
1081   :group 'notmuch)
1082
1083 (defcustom notmuch-search-hook nil
1084   "List of functions to call when notmuch displays the search results."
1085   :type 'hook
1086   :options '(hl-line-mode)
1087   :group 'notmuch)
1088
1089 ; Make show mode a bit prettier, highlighting URLs and using word wrap
1090
1091 (defun notmuch-show-pretty-hook ()
1092   (goto-address-mode 1)
1093   (visual-line-mode))
1094
1095 (add-hook 'notmuch-show-hook 'notmuch-show-pretty-hook)
1096 (add-hook 'notmuch-search-hook
1097           (lambda()
1098             (hl-line-mode 1) ))
1099
1100 (defun notmuch-show (thread-id &optional parent-buffer query-context)
1101   "Run \"notmuch show\" with the given thread ID and display results.
1102
1103 The optional PARENT-BUFFER is the notmuch-search buffer from
1104 which this notmuch-show command was executed, (so that the next
1105 thread from that buffer can be show when done with this one).
1106
1107 The optional QUERY-CONTEXT is a notmuch search term. Only messages from the thread
1108 matching this search term are shown if non-nil. "
1109   (interactive "sNotmuch show: ")
1110   (let ((buffer (get-buffer-create (concat "*notmuch-show-" thread-id "*"))))
1111     (switch-to-buffer buffer)
1112     (notmuch-show-mode)
1113     (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
1114     (let ((proc (get-buffer-process (current-buffer)))
1115           (inhibit-read-only t))
1116       (if proc
1117           (error "notmuch search process already running for query `%s'" thread-id)
1118         )
1119       (erase-buffer)
1120       (goto-char (point-min))
1121       (save-excursion
1122         (let* ((basic-args (list notmuch-command nil t nil "show" "--entire-thread" thread-id))
1123                 (args (if query-context (append basic-args (list "and (" query-context ")")) basic-args)))
1124           (apply 'call-process args)
1125           (when (and (eq (buffer-size) 0) query-context)
1126             (apply 'call-process basic-args)))
1127         (notmuch-show-markup-messages)
1128         )
1129       (run-hooks 'notmuch-show-hook)
1130       ; Move straight to the first open message
1131       (if (not (notmuch-show-message-open-p))
1132           (notmuch-show-next-open-message))
1133       )))
1134
1135 (defvar notmuch-search-authors-width 40
1136   "Number of columns to use to display authors in a notmuch-search buffer.")
1137
1138 (defvar notmuch-search-mode-map
1139   (let ((map (make-sparse-keymap)))
1140     (define-key map "?" 'notmuch-help)
1141     (define-key map "q" 'kill-this-buffer)
1142     (define-key map "x" 'kill-this-buffer)
1143     (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
1144     (define-key map "b" 'notmuch-search-scroll-down)
1145     (define-key map " " 'notmuch-search-scroll-up)
1146     (define-key map "<" 'notmuch-search-first-thread)
1147     (define-key map ">" 'notmuch-search-last-thread)
1148     (define-key map "p" 'notmuch-search-previous-thread)
1149     (define-key map "n" 'notmuch-search-next-thread)
1150     (define-key map "r" 'notmuch-search-reply-to-thread)
1151     (define-key map "m" 'message-mail)
1152     (define-key map "s" 'notmuch-search)
1153     (define-key map "o" 'notmuch-search-toggle-order)
1154     (define-key map "=" 'notmuch-search-refresh-view)
1155     (define-key map "t" 'notmuch-search-filter-by-tag)
1156     (define-key map "f" 'notmuch-search-filter)
1157     (define-key map [mouse-1] 'notmuch-search-show-thread)
1158     (define-key map "*" 'notmuch-search-operate-all)
1159     (define-key map "a" 'notmuch-search-archive-thread)
1160     (define-key map "-" 'notmuch-search-remove-tag)
1161     (define-key map "+" 'notmuch-search-add-tag)
1162     (define-key map (kbd "RET") 'notmuch-search-show-thread)
1163     map)
1164   "Keymap for \"notmuch search\" buffers.")
1165 (fset 'notmuch-search-mode-map notmuch-search-mode-map)
1166
1167 (defvar notmuch-search-query-string)
1168 (defvar notmuch-search-oldest-first t
1169   "Show the oldest mail first in the search-mode")
1170
1171 (defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
1172
1173 (defun notmuch-search-scroll-up ()
1174   "Move forward through search results by one window's worth."
1175   (interactive)
1176   (condition-case nil
1177       (scroll-up nil)
1178     ((end-of-buffer) (notmuch-search-last-thread))))
1179
1180 (defun notmuch-search-scroll-down ()
1181   "Move backward through the search results by one window's worth."
1182   (interactive)
1183   ; I don't know why scroll-down doesn't signal beginning-of-buffer
1184   ; the way that scroll-up signals end-of-buffer, but c'est la vie.
1185   ;
1186   ; So instead of trapping a signal we instead check whether the
1187   ; window begins on the first line of the buffer and if so, move
1188   ; directly to that position. (We have to count lines since the
1189   ; window-start position is not the same as point-min due to the
1190   ; invisible thread-ID characters on the first line.
1191   (if (equal (count-lines (point-min) (window-start)) 0)
1192       (goto-char (point-min))
1193     (scroll-down nil)))
1194
1195 (defun notmuch-search-next-thread ()
1196   "Select the next thread in the search results."
1197   (interactive)
1198   (forward-line 1))
1199
1200 (defun notmuch-search-previous-thread ()
1201   "Select the previous thread in the search results."
1202   (interactive)
1203   (forward-line -1))
1204
1205 (defun notmuch-search-last-thread ()
1206   "Select the last thread in the search results."
1207   (interactive)
1208   (goto-char (point-max))
1209   (forward-line -2))
1210
1211 (defun notmuch-search-first-thread ()
1212   "Select the first thread in the search results."
1213   (interactive)
1214   (goto-char (point-min)))
1215
1216 (defface notmuch-message-summary-face
1217  '((((class color) (background light)) (:background "#f0f0f0"))
1218    (((class color) (background dark)) (:background "#303030")))
1219  "Face for the single-line message summary in notmuch-show-mode."
1220  :group 'notmuch)
1221
1222 (defface notmuch-tag-face
1223   '((((class color)
1224       (background dark))
1225      (:foreground "OliveDrab1"))
1226     (((class color)
1227       (background light))
1228      (:foreground "navy blue" :bold t))
1229     (t
1230      (:bold t)))
1231   "Notmuch search mode face used to highligh tags."
1232   :group 'notmuch)
1233
1234 (defvar notmuch-tag-face-alist nil
1235   "List containing the tag list that need to be highlighed")
1236
1237 (defvar notmuch-search-font-lock-keywords  nil)
1238
1239 ;;;###autoload
1240 (defun notmuch-search-mode ()
1241   "Major mode displaying results of a notmuch search.
1242
1243 This buffer contains the results of a \"notmuch search\" of your
1244 email archives. Each line in the buffer represents a single
1245 thread giving a summary of the thread (a relative date, the
1246 number of matched messages and total messages in the thread,
1247 participants in the thread, a representative subject line, and
1248 any tags).
1249
1250 Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
1251 keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
1252 is a convenience for archiving a thread (removing the \"inbox\"
1253 tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all
1254 threads in the current buffer.
1255
1256 Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
1257 based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
1258 only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
1259 search.
1260
1261 Complete list of currently available key bindings:
1262
1263 \\{notmuch-search-mode-map}"
1264   (interactive)
1265   (kill-all-local-variables)
1266   (make-local-variable 'notmuch-search-query-string)
1267   (make-local-variable 'notmuch-search-oldest-first)
1268   (set (make-local-variable 'scroll-preserve-screen-position) t)
1269   (add-to-invisibility-spec 'notmuch-search)
1270   (use-local-map notmuch-search-mode-map)
1271   (setq truncate-lines t)
1272   (setq major-mode 'notmuch-search-mode
1273         mode-name "notmuch-search")
1274   (setq buffer-read-only t)
1275   (if (not notmuch-tag-face-alist)
1276       (add-to-list 'notmuch-search-font-lock-keywords (list
1277                 "(\\([^)]*\\))$" '(1  'notmuch-tag-face)))
1278     (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)))
1279       (loop for notmuch-search-tag  in notmuch-search-tags
1280             do (add-to-list 'notmuch-search-font-lock-keywords (list
1281                         (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$")
1282                         `(1  ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist))))))))
1283   (set (make-local-variable 'font-lock-defaults)
1284          '(notmuch-search-font-lock-keywords t)))
1285
1286 (defun notmuch-search-find-thread-id ()
1287   "Return the thread for the current thread"
1288   (get-text-property (point) 'notmuch-search-thread-id))
1289
1290 (defun notmuch-search-find-authors ()
1291   "Return the authors for the current thread"
1292   (get-text-property (point) 'notmuch-search-authors))
1293
1294 (defun notmuch-search-find-subject ()
1295   "Return the subject for the current thread"
1296   (get-text-property (point) 'notmuch-search-subject))
1297
1298 (defun notmuch-search-show-thread ()
1299   "Display the currently selected thread."
1300   (interactive)
1301   (let ((thread-id (notmuch-search-find-thread-id)))
1302     (if (> (length thread-id) 0)
1303         (notmuch-show thread-id (current-buffer) notmuch-search-query-string)
1304       (error "End of search results"))))
1305
1306 (defun notmuch-search-reply-to-thread ()
1307   "Begin composing a reply to the entire current thread in a new buffer."
1308   (interactive)
1309   (let ((message-id (notmuch-search-find-thread-id)))
1310     (notmuch-reply message-id)))
1311
1312 (defun notmuch-call-notmuch-process (&rest args)
1313   "Synchronously invoke \"notmuch\" with the given list of arguments.
1314
1315 Output from the process will be presented to the user as an error
1316 and will also appear in a buffer named \"*Notmuch errors*\"."
1317   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
1318     (with-current-buffer error-buffer
1319         (erase-buffer))
1320     (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
1321         (point)
1322       (progn
1323         (with-current-buffer error-buffer
1324           (let ((beg (point-min))
1325                 (end (- (point-max) 1)))
1326             (error (buffer-substring beg end))
1327             ))))))
1328
1329 (defun notmuch-search-set-tags (tags)
1330   (save-excursion
1331     (end-of-line)
1332     (re-search-backward "(")
1333     (forward-char)
1334     (let ((beg (point))
1335           (inhibit-read-only t))
1336       (re-search-forward ")")
1337       (backward-char)
1338       (let ((end (point)))
1339         (delete-region beg end)
1340         (insert (mapconcat  'identity tags " "))))))
1341
1342 (defun notmuch-search-get-tags ()
1343   (save-excursion
1344     (end-of-line)
1345     (re-search-backward "(")
1346     (let ((beg (+ (point) 1)))
1347       (re-search-forward ")")
1348       (let ((end (- (point) 1)))
1349         (split-string (buffer-substring beg end))))))
1350
1351 (defun notmuch-search-add-tag (tag)
1352   "Add a tag to the currently selected thread.
1353
1354 The tag is added to messages in the currently selected thread
1355 which match the current search terms."
1356   (interactive
1357    (list (notmuch-select-tag-with-completion "Tag to add: ")))
1358   (notmuch-call-notmuch-process "tag" (concat "+" tag) (notmuch-search-find-thread-id))
1359   (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<))))
1360
1361 (defun notmuch-search-remove-tag (tag)
1362   "Remove a tag from the currently selected thread.
1363
1364 The tag is removed from messages in the currently selected thread
1365 which match the current search terms."
1366   (interactive
1367    (list (notmuch-select-tag-with-completion "Tag to remove: " (notmuch-search-find-thread-id))))
1368   (notmuch-call-notmuch-process "tag" (concat "-" tag) (notmuch-search-find-thread-id))
1369   (notmuch-search-set-tags (delete tag (notmuch-search-get-tags))))
1370
1371 (defun notmuch-search-archive-thread ()
1372   "Archive the currently selected thread (remove its \"inbox\" tag).
1373
1374 This function advances the next thread when finished."
1375   (interactive)
1376   (notmuch-search-remove-tag "inbox")
1377   (forward-line))
1378
1379 (defun notmuch-search-process-sentinel (proc msg)
1380   "Add a message to let user know when \"notmuch search\" exits"
1381   (let ((buffer (process-buffer proc))
1382         (status (process-status proc))
1383         (exit-status (process-exit-status proc)))
1384     (if (memq status '(exit signal))
1385         (if (buffer-live-p buffer)
1386             (with-current-buffer buffer
1387               (save-excursion
1388                 (let ((inhibit-read-only t))
1389                   (goto-char (point-max))
1390                   (if (eq status 'signal)
1391                       (insert "Incomplete search results (search process was killed).\n"))
1392                   (if (eq status 'exit)
1393                       (progn
1394                         (insert "End of search results.")
1395                         (if (not (= exit-status 0))
1396                             (insert (format " (process returned %d)" exit-status)))
1397                         (insert "\n"))))))))))
1398
1399 (defun notmuch-search-process-filter (proc string)
1400   "Process and filter the output of \"notmuch search\""
1401   (let ((buffer (process-buffer proc)))
1402     (if (buffer-live-p buffer)
1403         (with-current-buffer buffer
1404           (save-excursion
1405             (let ((line 0)
1406                   (more t)
1407                   (inhibit-read-only t))
1408               (while more
1409                 (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\(.*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
1410                     (let* ((thread-id (match-string 1 string))
1411                            (date (match-string 2 string))
1412                            (count (match-string 3 string))
1413                            (authors (match-string 4 string))
1414                            (authors-length (length authors))
1415                            (subject (match-string 5 string))
1416                            (tags (match-string 6 string)))
1417                       (if (> authors-length 40)
1418                           (set 'authors (concat (substring authors 0 (- 40 3)) "...")))
1419                       (goto-char (point-max))
1420                       (let ((beg (point-marker)))
1421                         (insert (format "%s %-7s %-40s %s (%s)\n" date count authors subject tags))
1422                         (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
1423                         (put-text-property beg (point-marker) 'notmuch-search-authors authors)
1424                         (put-text-property beg (point-marker) 'notmuch-search-subject subject))
1425                       (set 'line (match-end 0)))
1426                   (set 'more nil))))))
1427       (delete-process proc))))
1428
1429 (defun notmuch-search-operate-all (action)
1430   "Add/remove tags from all matching messages.
1431
1432 Tis command adds or removes tags from all messages matching the
1433 current search terms. When called interactively, this command
1434 will prompt for tags to be added or removed. Tags prefixed with
1435 '+' will be added and tags prefixed with '-' will be removed.
1436
1437 Each character of the tag name may consist of alphanumeric
1438 characters as well as `_.+-'.
1439 "
1440   (interactive "sOperation (+add -drop): notmuch tag ")
1441   (let ((action-split (split-string action " +")))
1442     ;; Perform some validation
1443     (let ((words action-split))
1444       (when (null words) (error "No operation given"))
1445       (while words
1446         (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
1447           (error "Action must be of the form `+thistag -that_tag'"))
1448         (setq words (cdr words))))
1449     (apply 'notmuch-call-notmuch-process "tag"
1450            (append action-split (list notmuch-search-query-string) nil))))
1451
1452 ;;;###autoload
1453 (defun notmuch-search (query &optional oldest-first)
1454   "Run \"notmuch search\" with the given query string and display results."
1455   (interactive "sNotmuch search: ")
1456   (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
1457     (switch-to-buffer buffer)
1458     (notmuch-search-mode)
1459     (set 'notmuch-search-query-string query)
1460     (set 'notmuch-search-oldest-first oldest-first)
1461     (let ((proc (get-buffer-process (current-buffer)))
1462           (inhibit-read-only t))
1463       (if proc
1464           (error "notmuch search process already running for query `%s'" query)
1465         )
1466       (erase-buffer)
1467       (goto-char (point-min))
1468       (save-excursion
1469         (let ((proc (start-process-shell-command
1470                      "notmuch-search" buffer notmuch-command "search"
1471                      (if oldest-first "--sort=oldest-first" "--sort=newest-first")
1472                      (shell-quote-argument query))))
1473           (set-process-sentinel proc 'notmuch-search-process-sentinel)
1474           (set-process-filter proc 'notmuch-search-process-filter))))
1475     (run-hooks 'notmuch-search-hook)))
1476
1477 (defun notmuch-search-refresh-view ()
1478   "Refresh the current view.
1479
1480 Kills the current buffer and runs a new search with the same
1481 query string as the current search. If the current thread is in
1482 the new search results, then point will be placed on the same
1483 thread. Otherwise, point will be moved to attempt to be in the
1484 same relative position within the new buffer."
1485   (interactive)
1486   (let ((here (point))
1487         (oldest-first notmuch-search-oldest-first)
1488         (thread (notmuch-search-find-thread-id))
1489         (query notmuch-search-query-string))
1490     (kill-this-buffer)
1491     (notmuch-search query oldest-first)
1492     (goto-char (point-min))
1493     (if (re-search-forward (concat "^" thread) nil t)
1494         (beginning-of-line)
1495       (goto-char here))))
1496
1497 (defun notmuch-search-toggle-order ()
1498   "Toggle the current search order.
1499
1500 By default, the \"inbox\" view created by `notmuch' is displayed
1501 in chronological order (oldest thread at the beginning of the
1502 buffer), while any global searches created by `notmuch-search'
1503 are displayed in reverse-chronological order (newest thread at
1504 the beginning of the buffer).
1505
1506 This command toggles the sort order for the current search.
1507
1508 Note that any filtered searches created by
1509 `notmuch-search-filter' retain the search order of the parent
1510 search."
1511   (interactive)
1512   (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
1513   (notmuch-search-refresh-view))
1514
1515 (defun notmuch-search-filter (query)
1516   "Filter the current search results based on an additional query string.
1517
1518 Runs a new search matching only messages that match both the
1519 current search results AND the additional query string provided."
1520   (interactive "sFilter search: ")
1521   (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query) (concat "( " query " )") query)))
1522     (notmuch-search (concat notmuch-search-query-string " and " grouped-query) notmuch-search-oldest-first)))
1523
1524 (defun notmuch-search-filter-by-tag (tag)
1525   "Filter the current search results based on a single tag.
1526
1527 Runs a new search matching only messages that match both the
1528 current search results AND that are tagged with the given tag."
1529   (interactive
1530    (list (notmuch-select-tag-with-completion "Filter by tag: ")))
1531   (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
1532
1533
1534 ;;;###autoload
1535 (defun notmuch ()
1536   "Run notmuch to display all mail with tag of 'inbox'"
1537   (interactive)
1538   (notmuch-search "tag:inbox" notmuch-search-oldest-first))
1539
1540 (setq mail-user-agent 'message-user-agent)
1541
1542 (defvar notmuch-folder-mode-map
1543   (let ((map (make-sparse-keymap)))
1544     (define-key map "?" 'notmuch-help)
1545     (define-key map "x" 'kill-this-buffer)
1546     (define-key map "q" 'kill-this-buffer)
1547     (define-key map "m" 'message-mail)
1548     (define-key map "e" 'notmuch-folder-show-empty-toggle)
1549     (define-key map ">" 'notmuch-folder-last)
1550     (define-key map "<" 'notmuch-folder-first)
1551     (define-key map "=" 'notmuch-folder)
1552     (define-key map "s" 'notmuch-search)
1553     (define-key map [mouse-1] 'notmuch-folder-show-search)
1554     (define-key map (kbd "RET") 'notmuch-folder-show-search)
1555     (define-key map " " 'notmuch-folder-show-search)
1556     (define-key map "p" 'notmuch-folder-previous)
1557     (define-key map "n" 'notmuch-folder-next)
1558     map)
1559   "Keymap for \"notmuch folder\" buffers.")
1560
1561 (fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
1562
1563 (defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
1564   "List of searches for the notmuch folder view"
1565   :type '(alist :key-type (string) :value-type (string))
1566   :group 'notmuch)
1567
1568 (defun notmuch-folder-mode ()
1569   "Major mode for showing notmuch 'folders'.
1570
1571 This buffer contains a list of message counts returned by a
1572 customizable set of searches of your email archives. Each line in
1573 the buffer shows the name of a saved search and the resulting
1574 message count.
1575
1576 Pressing RET on any line opens a search window containing the
1577 results for the saved search on that line.
1578
1579 Here is an example of how the search list could be
1580 customized, (the following text would be placed in your ~/.emacs
1581 file):
1582
1583 (setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
1584                         (\"unread\" . \"tag:inbox AND tag:unread\")
1585                         (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
1586
1587 Of course, you can have any number of folders, each configured
1588 with any supported search terms (see \"notmuch help search-terms\").
1589
1590 Currently available key bindings:
1591
1592 \\{notmuch-folder-mode-map}"
1593   (interactive)
1594   (kill-all-local-variables)
1595   (use-local-map 'notmuch-folder-mode-map)
1596   (setq truncate-lines t)
1597   (hl-line-mode 1)
1598   (setq major-mode 'notmuch-folder-mode
1599         mode-name "notmuch-folder")
1600   (setq buffer-read-only t))
1601
1602 (defun notmuch-folder-next ()
1603   "Select the next folder in the list."
1604   (interactive)
1605   (forward-line 1)
1606   (if (eobp)
1607       (forward-line -1)))
1608
1609 (defun notmuch-folder-previous ()
1610   "Select the previous folder in the list."
1611   (interactive)
1612   (forward-line -1))
1613
1614 (defun notmuch-folder-first ()
1615   "Select the first folder in the list."
1616   (interactive)
1617   (goto-char (point-min)))
1618
1619 (defun notmuch-folder-last ()
1620   "Select the last folder in the list."
1621   (interactive)
1622   (goto-char (point-max))
1623   (forward-line -1))
1624
1625 (defun notmuch-folder-count (search)
1626   (car (process-lines notmuch-command "count" search)))
1627
1628 (setq notmuch-folder-show-empty t)
1629
1630 (defun notmuch-folder-show-empty-toggle ()
1631   "Toggle the listing of empty folders"
1632   (interactive)
1633   (setq notmuch-folder-show-empty (not notmuch-folder-show-empty))
1634   (notmuch-folder))
1635
1636 (defun notmuch-folder-add (folders)
1637   (if folders
1638       (let* ((name (car (car folders)))
1639             (inhibit-read-only t)
1640             (search (cdr (car folders)))
1641             (count (notmuch-folder-count search)))
1642         (if (or notmuch-folder-show-empty
1643                 (not (equal count "0")))
1644             (progn
1645               (insert name)
1646               (indent-to 16 1)
1647               (insert count)
1648               (insert "\n")
1649               )
1650           )
1651         (notmuch-folder-add (cdr folders)))))
1652
1653 (defun notmuch-folder-find-name ()
1654   (save-excursion
1655     (beginning-of-line)
1656     (let ((beg (point)))
1657       (re-search-forward "\\([ \t]*[^ \t]+\\)")
1658       (filter-buffer-substring (match-beginning 1) (match-end 1)))))
1659
1660 (defun notmuch-folder-show-search (&optional folder)
1661   "Show a search window for the search related to the specified folder."
1662   (interactive)
1663   (if (null folder)
1664       (setq folder (notmuch-folder-find-name)))
1665   (let ((search (assoc folder notmuch-folders)))
1666     (if search
1667         (notmuch-search (cdr search) notmuch-search-oldest-first))))
1668
1669 ;;;###autoload
1670 (defun notmuch-folder ()
1671   "Show the notmuch folder view and update the displayed counts."
1672   (interactive)
1673   (let ((buffer (get-buffer-create "*notmuch-folders*")))
1674     (switch-to-buffer buffer)
1675     (let ((inhibit-read-only t)
1676           (n (line-number-at-pos)))
1677       (erase-buffer)
1678       (notmuch-folder-mode)
1679       (notmuch-folder-add notmuch-folders)
1680       (goto-char (point-min))
1681       (goto-line n))))
1682
1683 (provide 'notmuch)