6e73a2b904ab3d53a1bccf01f629829b83c546c5
[notmuch] / emacs / 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 (require 'notmuch-lib)
55 (require 'notmuch-show)
56
57 (defun notmuch-select-tag-with-completion (prompt &rest search-terms)
58   (let ((tag-list
59          (with-output-to-string
60            (with-current-buffer standard-output
61              (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
62     (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
63
64 (defun notmuch-foreach-mime-part (function mm-handle)
65   (cond ((stringp (car mm-handle))
66          (dolist (part (cdr mm-handle))
67            (notmuch-foreach-mime-part function part)))
68         ((bufferp (car mm-handle))
69          (funcall function mm-handle))
70         (t (dolist (part mm-handle)
71              (notmuch-foreach-mime-part function part)))))
72
73 (defun notmuch-count-attachments (mm-handle)
74   (let ((count 0))
75     (notmuch-foreach-mime-part
76      (lambda (p)
77        (let ((disposition (mm-handle-disposition p)))
78          (and (listp disposition)
79               (or (equal (car disposition) "attachment")
80                   (and (equal (car disposition) "inline")
81                        (assq 'filename disposition)))
82               (incf count))))
83      mm-handle)
84     count))
85
86 (defun notmuch-save-attachments (mm-handle &optional queryp)
87   (notmuch-foreach-mime-part
88    (lambda (p)
89      (let ((disposition (mm-handle-disposition p)))
90        (and (listp disposition)
91             (or (equal (car disposition) "attachment")
92                 (and (equal (car disposition) "inline")
93                      (assq 'filename disposition)))
94             (or (not queryp)
95                 (y-or-n-p
96                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
97             (mm-save-part p))))
98    mm-handle))
99
100 (defun notmuch-reply (query-string)
101   (switch-to-buffer (generate-new-buffer "notmuch-draft"))
102   (call-process notmuch-command nil t nil "reply" query-string)
103   (message-insert-signature)
104   (goto-char (point-min))
105   (if (re-search-forward "^$" nil t)
106       (progn
107         (insert "--text follows this line--")
108         (forward-line)))
109   (message-mode))
110
111 (defun notmuch-documentation-first-line (symbol)
112   "Return the first line of the documentation string for SYMBOL."
113   (let ((doc (documentation symbol)))
114     (if doc
115         (with-temp-buffer
116           (insert (documentation symbol t))
117           (goto-char (point-min))
118           (let ((beg (point)))
119             (end-of-line)
120             (buffer-substring beg (point))))
121       "")))
122
123 (defun notmuch-prefix-key-description (key)
124   "Given a prefix key code, return a human-readable string representation.
125
126 This is basically just `format-kbd-macro' but we also convert ESC to M-."
127   (let ((desc (format-kbd-macro (vector key))))
128     (if (string= desc "ESC")
129         "M-"
130       (concat desc " "))))
131
132 ; I would think that emacs would have code handy for walking a keymap
133 ; and generating strings for each key, and I would prefer to just call
134 ; that. But I couldn't find any (could be all implemented in C I
135 ; suppose), so I wrote my own here.
136 (defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
137   "For a key binding, return a string showing a human-readable
138 representation of the prefixed key as well as the first line of
139 documentation from the bound function.
140
141 For a mouse binding, return nil."
142   (let ((key (car binding))
143         (action (cdr binding)))
144     (if (mouse-event-p key)
145         nil
146       (if (keymapp action)
147           (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
148                 (as-list))
149             (map-keymap (lambda (a b)
150                           (push (cons a b) as-list))
151                         action)
152             (mapconcat substitute as-list "\n"))
153         (concat prefix (format-kbd-macro (vector key))
154                 "\t"
155                 (notmuch-documentation-first-line action))))))
156
157 (defalias 'notmuch-substitute-one-command-key
158   (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
159
160 (defun notmuch-substitute-command-keys (doc)
161   "Like `substitute-command-keys' but with documentation, not function names."
162   (let ((beg 0))
163     (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
164       (let ((map (substring doc (match-beginning 1) (match-end 1))))
165         (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
166                                             (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
167       (setq beg (match-end 0)))
168     doc))
169
170 (defun notmuch-help ()
171   "Display help for the current notmuch mode."
172   (interactive)
173   (let* ((mode major-mode)
174          (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
175     (with-current-buffer (generate-new-buffer "*notmuch-help*")
176       (insert doc)
177       (goto-char (point-min))
178       (set-buffer-modified-p nil)
179       (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
180
181 (defgroup notmuch nil
182   "Notmuch mail reader for Emacs."
183   :group 'mail)
184
185 (defcustom notmuch-search-hook '(hl-line-mode)
186   "List of functions to call when notmuch displays the search results."
187   :type 'hook
188   :options '(hl-line-mode)
189   :group 'notmuch)
190
191 (defvar notmuch-search-authors-width 20
192   "Number of columns to use to display authors in a notmuch-search buffer.")
193
194 (defvar notmuch-search-mode-map
195   (let ((map (make-sparse-keymap)))
196     (define-key map "?" 'notmuch-help)
197     (define-key map "q" 'kill-this-buffer)
198     (define-key map "x" 'kill-this-buffer)
199     (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
200     (define-key map "b" 'notmuch-search-scroll-down)
201     (define-key map " " 'notmuch-search-scroll-up)
202     (define-key map "<" 'notmuch-search-first-thread)
203     (define-key map ">" 'notmuch-search-last-thread)
204     (define-key map "p" 'notmuch-search-previous-thread)
205     (define-key map "n" 'notmuch-search-next-thread)
206     (define-key map "r" 'notmuch-search-reply-to-thread)
207     (define-key map "m" 'message-mail)
208     (define-key map "s" 'notmuch-search)
209     (define-key map "o" 'notmuch-search-toggle-order)
210     (define-key map "=" 'notmuch-search-refresh-view)
211     (define-key map "t" 'notmuch-search-filter-by-tag)
212     (define-key map "f" 'notmuch-search-filter)
213     (define-key map [mouse-1] 'notmuch-search-show-thread)
214     (define-key map "*" 'notmuch-search-operate-all)
215     (define-key map "a" 'notmuch-search-archive-thread)
216     (define-key map "-" 'notmuch-search-remove-tag)
217     (define-key map "+" 'notmuch-search-add-tag)
218     (define-key map (kbd "RET") 'notmuch-search-show-thread)
219     map)
220   "Keymap for \"notmuch search\" buffers.")
221 (fset 'notmuch-search-mode-map notmuch-search-mode-map)
222
223 (defvar notmuch-search-query-string)
224 (defvar notmuch-search-target-thread)
225 (defvar notmuch-search-target-line)
226 (defvar notmuch-search-oldest-first t
227   "Show the oldest mail first in the search-mode")
228
229 (defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
230
231 (defun notmuch-search-scroll-up ()
232   "Move forward through search results by one window's worth."
233   (interactive)
234   (condition-case nil
235       (scroll-up nil)
236     ((end-of-buffer) (notmuch-search-last-thread))))
237
238 (defun notmuch-search-scroll-down ()
239   "Move backward through the search results by one window's worth."
240   (interactive)
241   ; I don't know why scroll-down doesn't signal beginning-of-buffer
242   ; the way that scroll-up signals end-of-buffer, but c'est la vie.
243   ;
244   ; So instead of trapping a signal we instead check whether the
245   ; window begins on the first line of the buffer and if so, move
246   ; directly to that position. (We have to count lines since the
247   ; window-start position is not the same as point-min due to the
248   ; invisible thread-ID characters on the first line.
249   (if (equal (count-lines (point-min) (window-start)) 0)
250       (goto-char (point-min))
251     (scroll-down nil)))
252
253 (defun notmuch-search-next-thread ()
254   "Select the next thread in the search results."
255   (interactive)
256   (forward-line 1))
257
258 (defun notmuch-search-previous-thread ()
259   "Select the previous thread in the search results."
260   (interactive)
261   (forward-line -1))
262
263 (defun notmuch-search-last-thread ()
264   "Select the last thread in the search results."
265   (interactive)
266   (goto-char (point-max))
267   (forward-line -2))
268
269 (defun notmuch-search-first-thread ()
270   "Select the first thread in the search results."
271   (interactive)
272   (goto-char (point-min)))
273
274 (defface notmuch-message-summary-face
275  '((((class color) (background light)) (:background "#f0f0f0"))
276    (((class color) (background dark)) (:background "#303030")))
277  "Face for the single-line message summary in notmuch-show-mode."
278  :group 'notmuch)
279
280 (defface notmuch-tag-face
281   '((((class color)
282       (background dark))
283      (:foreground "OliveDrab1"))
284     (((class color)
285       (background light))
286      (:foreground "navy blue" :bold t))
287     (t
288      (:bold t)))
289   "Notmuch search mode face used to highligh tags."
290   :group 'notmuch)
291
292 (defvar notmuch-tag-face-alist nil
293   "List containing the tag list that need to be highlighed")
294
295 (defvar notmuch-search-font-lock-keywords  nil)
296
297 ;;;###autoload
298 (defun notmuch-search-mode ()
299   "Major mode displaying results of a notmuch search.
300
301 This buffer contains the results of a \"notmuch search\" of your
302 email archives. Each line in the buffer represents a single
303 thread giving a summary of the thread (a relative date, the
304 number of matched messages and total messages in the thread,
305 participants in the thread, a representative subject line, and
306 any tags).
307
308 Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
309 keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
310 is a convenience for archiving a thread (removing the \"inbox\"
311 tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all
312 threads in the current buffer.
313
314 Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
315 based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
316 only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
317 search.
318
319 Complete list of currently available key bindings:
320
321 \\{notmuch-search-mode-map}"
322   (interactive)
323   (kill-all-local-variables)
324   (make-local-variable 'notmuch-search-query-string)
325   (make-local-variable 'notmuch-search-oldest-first)
326   (make-local-variable 'notmuch-search-target-thread)
327   (make-local-variable 'notmuch-search-target-line)
328   (set (make-local-variable 'scroll-preserve-screen-position) t)
329   (add-to-invisibility-spec 'notmuch-search)
330   (use-local-map notmuch-search-mode-map)
331   (setq truncate-lines t)
332   (setq major-mode 'notmuch-search-mode
333         mode-name "notmuch-search")
334   (setq buffer-read-only t)
335   (if (not notmuch-tag-face-alist)
336       (add-to-list 'notmuch-search-font-lock-keywords (list
337                 "(\\([^()]*\\))$" '(1  'notmuch-tag-face)))
338     (let ((notmuch-search-tags (mapcar 'car notmuch-tag-face-alist)))
339       (loop for notmuch-search-tag  in notmuch-search-tags
340             do (add-to-list 'notmuch-search-font-lock-keywords (list
341                         (concat "([^)]*\\(" notmuch-search-tag "\\)[^)]*)$")
342                         `(1  ,(cdr (assoc notmuch-search-tag notmuch-tag-face-alist))))))))
343   (set (make-local-variable 'font-lock-defaults)
344          '(notmuch-search-font-lock-keywords t)))
345
346 (defun notmuch-search-properties-in-region (property beg end)
347   (save-excursion
348     (let ((output nil)
349           (last-line (line-number-at-pos end))
350           (max-line (- (line-number-at-pos (point-max)) 2)))
351       (goto-char beg)
352       (beginning-of-line)
353       (while (<= (line-number-at-pos) (min last-line max-line))
354         (setq output (cons (get-text-property (point) property) output))
355         (forward-line 1))
356       output)))
357
358 (defun notmuch-search-find-thread-id ()
359   "Return the thread for the current thread"
360   (get-text-property (point) 'notmuch-search-thread-id))
361
362 (defun notmuch-search-find-thread-id-region (beg end)
363   "Return a list of threads for the current region"
364   (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end))
365
366 (defun notmuch-search-find-authors ()
367   "Return the authors for the current thread"
368   (get-text-property (point) 'notmuch-search-authors))
369
370 (defun notmuch-search-find-authors-region (beg end)
371   "Return a list of authors for the current region"
372   (notmuch-search-properties-in-region 'notmuch-search-authors beg end))
373
374 (defun notmuch-search-find-subject ()
375   "Return the subject for the current thread"
376   (get-text-property (point) 'notmuch-search-subject))
377
378 (defun notmuch-search-find-subject-region (beg end)
379   "Return a list of authors for the current region"
380   (notmuch-search-properties-in-region 'notmuch-search-subject beg end))
381
382 (defun notmuch-search-show-thread ()
383   "Display the currently selected thread."
384   (interactive)
385   (let ((thread-id (notmuch-search-find-thread-id))
386         (subject (notmuch-search-find-subject)))
387     (if (> (length thread-id) 0)
388         (notmuch-show thread-id
389                       (current-buffer)
390                       notmuch-search-query-string
391                       ;; name the buffer based on notmuch-search-find-subject
392                       (if (string-match "^[ \t]*$" subject)
393                           "[No Subject]"
394                         (truncate-string-to-width
395                          (concat "*"
396                                  (truncate-string-to-width subject 32 nil nil t)
397                                  "*")
398                          32 nil nil t)))
399       (error "End of search results"))))
400
401 (defun notmuch-search-reply-to-thread ()
402   "Begin composing a reply to the entire current thread in a new buffer."
403   (interactive)
404   (let ((message-id (notmuch-search-find-thread-id)))
405     (notmuch-reply message-id)))
406
407 (defun notmuch-call-notmuch-process (&rest args)
408   "Synchronously invoke \"notmuch\" with the given list of arguments.
409
410 Output from the process will be presented to the user as an error
411 and will also appear in a buffer named \"*Notmuch errors*\"."
412   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
413     (with-current-buffer error-buffer
414         (erase-buffer))
415     (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
416         (point)
417       (progn
418         (with-current-buffer error-buffer
419           (let ((beg (point-min))
420                 (end (- (point-max) 1)))
421             (error (buffer-substring beg end))
422             ))))))
423
424 (defun notmuch-search-set-tags (tags)
425   (save-excursion
426     (end-of-line)
427     (re-search-backward "(")
428     (forward-char)
429     (let ((beg (point))
430           (inhibit-read-only t))
431       (re-search-forward ")")
432       (backward-char)
433       (let ((end (point)))
434         (delete-region beg end)
435         (insert (mapconcat  'identity tags " "))))))
436
437 (defun notmuch-search-get-tags ()
438   (save-excursion
439     (end-of-line)
440     (re-search-backward "(")
441     (let ((beg (+ (point) 1)))
442       (re-search-forward ")")
443       (let ((end (- (point) 1)))
444         (split-string (buffer-substring beg end))))))
445
446 (defun notmuch-search-get-tags-region (beg end)
447   (save-excursion
448     (let ((output nil)
449           (last-line (line-number-at-pos end))
450           (max-line (- (line-number-at-pos (point-max)) 2)))
451       (goto-char beg)
452       (while (<= (line-number-at-pos) (min last-line max-line))
453         (setq output (append output (notmuch-search-get-tags)))
454         (forward-line 1))
455       output)))
456
457 (defun notmuch-search-add-tag-thread (tag)
458   (notmuch-search-add-tag-region tag (point) (point)))
459
460 (defun notmuch-search-add-tag-region (tag beg end)
461   (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
462     (notmuch-call-notmuch-process "tag" (concat "+" tag) search-id-string)
463     (save-excursion
464       (let ((last-line (line-number-at-pos end))
465             (max-line (- (line-number-at-pos (point-max)) 2)))
466         (goto-char beg)
467         (while (<= (line-number-at-pos) (min last-line max-line))
468           (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<)))
469           (forward-line))))))
470
471 (defun notmuch-search-remove-tag-thread (tag)
472   (notmuch-search-remove-tag-region tag (point) (point)))
473
474 (defun notmuch-search-remove-tag-region (tag beg end)
475   (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
476     (notmuch-call-notmuch-process "tag" (concat "-" tag) search-id-string)
477     (save-excursion
478       (let ((last-line (line-number-at-pos end))
479             (max-line (- (line-number-at-pos (point-max)) 2)))
480         (goto-char beg)
481         (while (<= (line-number-at-pos) (min last-line max-line))
482           (notmuch-search-set-tags (delete tag (notmuch-search-get-tags)))
483           (forward-line))))))
484
485 (defun notmuch-search-add-tag (tag)
486   "Add a tag to the currently selected thread or region.
487
488 The tag is added to all messages in the currently selected thread
489 or threads in the current region."
490   (interactive
491    (list (notmuch-select-tag-with-completion "Tag to add: ")))
492   (save-excursion
493     (if (region-active-p)
494         (let* ((beg (region-beginning))
495                (end (region-end)))
496           (notmuch-search-add-tag-region tag beg end))
497       (notmuch-search-add-tag-thread tag))))
498
499 (defun notmuch-search-remove-tag (tag)
500   "Remove a tag from the currently selected thread or region.
501
502 The tag is removed from all messages in the currently selected
503 thread or threads in the current region."
504   (interactive
505    (list (notmuch-select-tag-with-completion
506           "Tag to remove: "
507           (if (region-active-p)
508               (mapconcat 'identity
509                          (notmuch-search-find-thread-id-region (region-beginning) (region-end))
510                          " ")
511             (notmuch-search-find-thread-id)))))
512   (save-excursion
513     (if (region-active-p)
514         (let* ((beg (region-beginning))
515                (end (region-end)))
516           (notmuch-search-remove-tag-region tag beg end))
517       (notmuch-search-remove-tag-thread tag))))
518
519 (defun notmuch-search-archive-thread ()
520   "Archive the currently selected thread (remove its \"inbox\" tag).
521
522 This function advances the next thread when finished."
523   (interactive)
524   (notmuch-search-remove-tag-thread "inbox")
525   (forward-line))
526
527 (defun notmuch-search-process-sentinel (proc msg)
528   "Add a message to let user know when \"notmuch search\" exits"
529   (let ((buffer (process-buffer proc))
530         (status (process-status proc))
531         (exit-status (process-exit-status proc))
532         (never-found-target-thread nil))
533     (if (memq status '(exit signal))
534         (if (buffer-live-p buffer)
535             (with-current-buffer buffer
536               (save-excursion
537                 (let ((inhibit-read-only t)
538                       (atbob (bobp)))
539                   (goto-char (point-max))
540                   (if (eq status 'signal)
541                       (insert "Incomplete search results (search process was killed).\n"))
542                   (if (eq status 'exit)
543                       (progn
544                         (insert "End of search results.")
545                         (if (not (= exit-status 0))
546                             (insert (format " (process returned %d)" exit-status)))
547                         (insert "\n")
548                         (if (and atbob
549                                  (not (string= notmuch-search-target-thread "found")))
550                             (set 'never-found-target-thread t))))))
551               (if (and never-found-target-thread
552                        notmuch-search-target-line)
553                   (goto-line notmuch-search-target-line)))))))
554
555 (defcustom notmuch-search-line-faces nil
556   "Tag/face mapping for line highlighting in notmuch-search.
557
558 Here is an example of how to color search results based on tags.
559 (the following text would be placed in your ~/.emacs file):
560
561 (setq notmuch-search-line-faces '((\"delete\" . '(:foreground \"red\"))
562                                  (\"unread\" . '(:foreground \"green\"))))
563
564 Order matters: for lines with multiple tags, the the first
565 matching will be applied."
566   :type '(alist :key-type (string) :value-type (list))
567   :group 'notmuch)
568
569 (defun notmuch-search-color-line (start end line-tag-list)
570   "Colorize lines in notmuch-show based on tags"
571   (if notmuch-search-line-faces
572       (let ((overlay (make-overlay start end))
573             (tags-faces (copy-alist notmuch-search-line-faces)))
574         (while tags-faces
575           (let* ((tag-face (car tags-faces))
576                  (tag (car tag-face))
577                  (face (cdr tag-face)))
578             (cond ((member tag line-tag-list)
579                    (overlay-put overlay 'face face)
580                    (setq tags-faces nil))
581                   (t
582                    (setq tags-faces (cdr tags-faces)))))))))
583
584 (defun notmuch-search-process-filter (proc string)
585   "Process and filter the output of \"notmuch search\""
586   (let ((buffer (process-buffer proc))
587         (found-target nil))
588     (if (buffer-live-p buffer)
589         (with-current-buffer buffer
590           (save-excursion
591             (let ((line 0)
592                   (more t)
593                   (inhibit-read-only t))
594               (while more
595                 (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\(.*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
596                     (let* ((thread-id (match-string 1 string))
597                            (date (match-string 2 string))
598                            (count (match-string 3 string))
599                            (authors (match-string 4 string))
600                            (authors-length (length authors))
601                            (subject (match-string 5 string))
602                            (tags (match-string 6 string))
603                            (tag-list (if tags (save-match-data (split-string tags)))))
604                       (if (> authors-length notmuch-search-authors-width)
605                           (set 'authors (concat (substring authors 0 (- notmuch-search-authors-width 3)) "...")))
606                       (goto-char (point-max))
607                       (let ((beg (point-marker))
608                             (format-string (format "%%s %%-7s %%-%ds %%s (%%s)\n" notmuch-search-authors-width)))
609                         (insert (format format-string date count authors subject tags))
610                         (notmuch-search-color-line beg (point-marker) tag-list)
611                         (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
612                         (put-text-property beg (point-marker) 'notmuch-search-authors authors)
613                         (put-text-property beg (point-marker) 'notmuch-search-subject subject)
614                         (if (string= thread-id notmuch-search-target-thread)
615                             (progn
616                               (set 'found-target beg)
617                               (set 'notmuch-search-target-thread "found"))))
618                       (set 'line (match-end 0)))
619                   (set 'more nil)))))
620           (if found-target
621               (goto-char found-target)))
622       (delete-process proc))))
623
624 (defun notmuch-search-operate-all (action)
625   "Add/remove tags from all matching messages.
626
627 Tis command adds or removes tags from all messages matching the
628 current search terms. When called interactively, this command
629 will prompt for tags to be added or removed. Tags prefixed with
630 '+' will be added and tags prefixed with '-' will be removed.
631
632 Each character of the tag name may consist of alphanumeric
633 characters as well as `_.+-'.
634 "
635   (interactive "sOperation (+add -drop): notmuch tag ")
636   (let ((action-split (split-string action " +")))
637     ;; Perform some validation
638     (let ((words action-split))
639       (when (null words) (error "No operation given"))
640       (while words
641         (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
642           (error "Action must be of the form `+thistag -that_tag'"))
643         (setq words (cdr words))))
644     (apply 'notmuch-call-notmuch-process "tag"
645            (append action-split (list notmuch-search-query-string) nil))))
646
647 ;;;###autoload
648 (defun notmuch-search (query &optional oldest-first target-thread target-line)
649   "Run \"notmuch search\" with the given query string and display results.
650
651 The optional parameters are used as follows:
652
653   oldest-first: A Boolean controlling the sort order of returned threads
654   target-thread: A thread ID (with the thread: prefix) that will be made
655                  current if it appears in the search results.
656   target-line: The line number to move to if the target thread does not
657                appear in the search results."
658   (interactive "sNotmuch search: ")
659   (let ((buffer (get-buffer-create (concat "*notmuch-search-" query "*"))))
660     (switch-to-buffer buffer)
661     (notmuch-search-mode)
662     (set 'notmuch-search-query-string query)
663     (set 'notmuch-search-oldest-first oldest-first)
664     (set 'notmuch-search-target-thread target-thread)
665     (set 'notmuch-search-target-line target-line)
666     (let ((proc (get-buffer-process (current-buffer)))
667           (inhibit-read-only t))
668       (if proc
669           (error "notmuch search process already running for query `%s'" query)
670         )
671       (erase-buffer)
672       (goto-char (point-min))
673       (save-excursion
674         (let ((proc (start-process-shell-command
675                      "notmuch-search" buffer notmuch-command "search"
676                      (if oldest-first "--sort=oldest-first" "--sort=newest-first")
677                      (shell-quote-argument query))))
678           (set-process-sentinel proc 'notmuch-search-process-sentinel)
679           (set-process-filter proc 'notmuch-search-process-filter))))
680     (run-hooks 'notmuch-search-hook)))
681
682 (defun notmuch-search-refresh-view ()
683   "Refresh the current view.
684
685 Kills the current buffer and runs a new search with the same
686 query string as the current search. If the current thread is in
687 the new search results, then point will be placed on the same
688 thread. Otherwise, point will be moved to attempt to be in the
689 same relative position within the new buffer."
690   (interactive)
691   (let ((target-line (line-number-at-pos))
692         (oldest-first notmuch-search-oldest-first)
693         (target-thread (notmuch-search-find-thread-id))
694         (query notmuch-search-query-string))
695     (kill-this-buffer)
696     (notmuch-search query oldest-first target-thread target-line)
697     (goto-char (point-min))
698     ))
699
700 (defun notmuch-search-toggle-order ()
701   "Toggle the current search order.
702
703 By default, the \"inbox\" view created by `notmuch' is displayed
704 in chronological order (oldest thread at the beginning of the
705 buffer), while any global searches created by `notmuch-search'
706 are displayed in reverse-chronological order (newest thread at
707 the beginning of the buffer).
708
709 This command toggles the sort order for the current search.
710
711 Note that any filtered searches created by
712 `notmuch-search-filter' retain the search order of the parent
713 search."
714   (interactive)
715   (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
716   (notmuch-search-refresh-view))
717
718 (defun notmuch-search-filter (query)
719   "Filter the current search results based on an additional query string.
720
721 Runs a new search matching only messages that match both the
722 current search results AND the additional query string provided."
723   (interactive "sFilter search: ")
724   (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query)
725                            (concat "( " query " )")
726                          query)))
727     (notmuch-search (if (string= notmuch-search-query-string "*")
728                         grouped-query
729                       (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first)))
730
731 (defun notmuch-search-filter-by-tag (tag)
732   "Filter the current search results based on a single tag.
733
734 Runs a new search matching only messages that match both the
735 current search results AND that are tagged with the given tag."
736   (interactive
737    (list (notmuch-select-tag-with-completion "Filter by tag: ")))
738   (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
739
740 ;;;###autoload
741 (defun notmuch ()
742   "Run notmuch to display all mail with tag of 'inbox'"
743   (interactive)
744   (notmuch-search "tag:inbox" notmuch-search-oldest-first))
745
746 (setq mail-user-agent 'message-user-agent)
747
748 (defvar notmuch-folder-mode-map
749   (let ((map (make-sparse-keymap)))
750     (define-key map "?" 'notmuch-help)
751     (define-key map "x" 'kill-this-buffer)
752     (define-key map "q" 'kill-this-buffer)
753     (define-key map "m" 'message-mail)
754     (define-key map "e" 'notmuch-folder-show-empty-toggle)
755     (define-key map ">" 'notmuch-folder-last)
756     (define-key map "<" 'notmuch-folder-first)
757     (define-key map "=" 'notmuch-folder)
758     (define-key map "s" 'notmuch-search)
759     (define-key map [mouse-1] 'notmuch-folder-show-search)
760     (define-key map (kbd "RET") 'notmuch-folder-show-search)
761     (define-key map " " 'notmuch-folder-show-search)
762     (define-key map "p" 'notmuch-folder-previous)
763     (define-key map "n" 'notmuch-folder-next)
764     map)
765   "Keymap for \"notmuch folder\" buffers.")
766
767 (fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
768
769 (defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
770   "List of searches for the notmuch folder view"
771   :type '(alist :key-type (string) :value-type (string))
772   :group 'notmuch)
773
774 (defun notmuch-folder-mode ()
775   "Major mode for showing notmuch 'folders'.
776
777 This buffer contains a list of message counts returned by a
778 customizable set of searches of your email archives. Each line in
779 the buffer shows the name of a saved search and the resulting
780 message count.
781
782 Pressing RET on any line opens a search window containing the
783 results for the saved search on that line.
784
785 Here is an example of how the search list could be
786 customized, (the following text would be placed in your ~/.emacs
787 file):
788
789 (setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
790                         (\"unread\" . \"tag:inbox AND tag:unread\")
791                         (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
792
793 Of course, you can have any number of folders, each configured
794 with any supported search terms (see \"notmuch help search-terms\").
795
796 Currently available key bindings:
797
798 \\{notmuch-folder-mode-map}"
799   (interactive)
800   (kill-all-local-variables)
801   (use-local-map 'notmuch-folder-mode-map)
802   (setq truncate-lines t)
803   (hl-line-mode 1)
804   (setq major-mode 'notmuch-folder-mode
805         mode-name "notmuch-folder")
806   (setq buffer-read-only t))
807
808 (defun notmuch-folder-next ()
809   "Select the next folder in the list."
810   (interactive)
811   (forward-line 1)
812   (if (eobp)
813       (forward-line -1)))
814
815 (defun notmuch-folder-previous ()
816   "Select the previous folder in the list."
817   (interactive)
818   (forward-line -1))
819
820 (defun notmuch-folder-first ()
821   "Select the first folder in the list."
822   (interactive)
823   (goto-char (point-min)))
824
825 (defun notmuch-folder-last ()
826   "Select the last folder in the list."
827   (interactive)
828   (goto-char (point-max))
829   (forward-line -1))
830
831 (defun notmuch-folder-count (search)
832   (car (process-lines notmuch-command "count" search)))
833
834 (defvar notmuch-folder-show-empty t
835   "Whether `notmuch-folder-mode' should display empty folders.")
836
837 (defun notmuch-folder-show-empty-toggle ()
838   "Toggle the listing of empty folders"
839   (interactive)
840   (setq notmuch-folder-show-empty (not notmuch-folder-show-empty))
841   (notmuch-folder))
842
843 (defun notmuch-folder-add (folders)
844   (if folders
845       (let* ((name (car (car folders)))
846             (inhibit-read-only t)
847             (search (cdr (car folders)))
848             (count (notmuch-folder-count search)))
849         (if (or notmuch-folder-show-empty
850                 (not (equal count "0")))
851             (progn
852               (insert name)
853               (indent-to 16 1)
854               (insert count)
855               (insert "\n")
856               )
857           )
858         (notmuch-folder-add (cdr folders)))))
859
860 (defun notmuch-folder-find-name ()
861   (save-excursion
862     (beginning-of-line)
863     (let ((beg (point)))
864       (re-search-forward "\\([ \t]*[^ \t]+\\)")
865       (filter-buffer-substring (match-beginning 1) (match-end 1)))))
866
867 (defun notmuch-folder-show-search (&optional folder)
868   "Show a search window for the search related to the specified folder."
869   (interactive)
870   (if (null folder)
871       (setq folder (notmuch-folder-find-name)))
872   (let ((search (assoc folder notmuch-folders)))
873     (if search
874         (notmuch-search (cdr search) notmuch-search-oldest-first))))
875
876 ;;;###autoload
877 (defun notmuch-folder ()
878   "Show the notmuch folder view and update the displayed counts."
879   (interactive)
880   (let ((buffer (get-buffer-create "*notmuch-folders*")))
881     (switch-to-buffer buffer)
882     (let ((inhibit-read-only t)
883           (n (line-number-at-pos)))
884       (erase-buffer)
885       (notmuch-folder-mode)
886       (notmuch-folder-add notmuch-folders)
887       (goto-char (point-min))
888       (goto-line n))))
889
890 (provide 'notmuch)