]> git.notmuchmail.org Git - notmuch/blob - emacs/notmuch.el
1e617752ad3174ec081bb4935999b376bf30a126
[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 (eval-when-compile (require 'cl))
51 (require 'mm-view)
52 (require 'message)
53
54 (require 'notmuch-lib)
55 (require 'notmuch-show)
56 (require 'notmuch-mua)
57 (require 'notmuch-hello)
58 (require 'notmuch-maildir-fcc)
59 (require 'notmuch-message)
60
61 (defcustom notmuch-search-result-format
62   `(("date" . "%s ")
63     ("count" . "%-7s ")
64     ("authors" . "%-20s ")
65     ("subject" . "%s ")
66     ("tags" . "(%s)"))
67   "Search result formatting. Supported fields are:
68         date, count, authors, subject, tags
69 For example:
70         (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
71                                              \(\"subject\" . \"%s\"\)\)\)"
72   :type '(alist :key-type (string) :value-type (string))
73   :group 'notmuch)
74
75 (defvar notmuch-query-history nil
76   "Variable to store minibuffer history for notmuch queries")
77
78 (defun notmuch-select-tag-with-completion (prompt &rest search-terms)
79   (let ((tag-list
80          (with-output-to-string
81            (with-current-buffer standard-output
82              (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
83     (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
84
85 (defun notmuch-foreach-mime-part (function mm-handle)
86   (cond ((stringp (car mm-handle))
87          (dolist (part (cdr mm-handle))
88            (notmuch-foreach-mime-part function part)))
89         ((bufferp (car mm-handle))
90          (funcall function mm-handle))
91         (t (dolist (part mm-handle)
92              (notmuch-foreach-mime-part function part)))))
93
94 (defun notmuch-count-attachments (mm-handle)
95   (let ((count 0))
96     (notmuch-foreach-mime-part
97      (lambda (p)
98        (let ((disposition (mm-handle-disposition p)))
99          (and (listp disposition)
100               (or (equal (car disposition) "attachment")
101                   (and (equal (car disposition) "inline")
102                        (assq 'filename disposition)))
103               (incf count))))
104      mm-handle)
105     count))
106
107 (defun notmuch-save-attachments (mm-handle &optional queryp)
108   (notmuch-foreach-mime-part
109    (lambda (p)
110      (let ((disposition (mm-handle-disposition p)))
111        (and (listp disposition)
112             (or (equal (car disposition) "attachment")
113                 (and (equal (car disposition) "inline")
114                      (assq 'filename disposition)))
115             (or (not queryp)
116                 (y-or-n-p
117                  (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
118             (mm-save-part p))))
119    mm-handle))
120
121 (defun notmuch-documentation-first-line (symbol)
122   "Return the first line of the documentation string for SYMBOL."
123   (let ((doc (documentation symbol)))
124     (if doc
125         (with-temp-buffer
126           (insert (documentation symbol t))
127           (goto-char (point-min))
128           (let ((beg (point)))
129             (end-of-line)
130             (buffer-substring beg (point))))
131       "")))
132
133 (defun notmuch-prefix-key-description (key)
134   "Given a prefix key code, return a human-readable string representation.
135
136 This is basically just `format-kbd-macro' but we also convert ESC to M-."
137   (let ((desc (format-kbd-macro (vector key))))
138     (if (string= desc "ESC")
139         "M-"
140       (concat desc " "))))
141
142 ; I would think that emacs would have code handy for walking a keymap
143 ; and generating strings for each key, and I would prefer to just call
144 ; that. But I couldn't find any (could be all implemented in C I
145 ; suppose), so I wrote my own here.
146 (defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
147   "For a key binding, return a string showing a human-readable
148 representation of the prefixed key as well as the first line of
149 documentation from the bound function.
150
151 For a mouse binding, return nil."
152   (let ((key (car binding))
153         (action (cdr binding)))
154     (if (mouse-event-p key)
155         nil
156       (if (keymapp action)
157           (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
158                 (as-list))
159             (map-keymap (lambda (a b)
160                           (push (cons a b) as-list))
161                         action)
162             (mapconcat substitute as-list "\n"))
163         (concat prefix (format-kbd-macro (vector key))
164                 "\t"
165                 (notmuch-documentation-first-line action))))))
166
167 (defun notmuch-substitute-command-keys-one (key)
168   ;; A `keymap' key indicates inheritance from a parent keymap - the
169   ;; inherited mappings follow, so there is nothing to print for
170   ;; `keymap' itself.
171   (when (not (eq key 'keymap))
172     (notmuch-substitute-one-command-key-with-prefix nil key)))
173
174 (defun notmuch-substitute-command-keys (doc)
175   "Like `substitute-command-keys' but with documentation, not function names."
176   (let ((beg 0))
177     (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
178       (let* ((keymap-name (substring doc (match-beginning 1) (match-end 1)))
179              (keymap (symbol-value (intern keymap-name))))
180         (setq doc (replace-match
181                    (mapconcat #'notmuch-substitute-command-keys-one
182                               (cdr keymap) "\n")
183                    1 1 doc)))
184       (setq beg (match-end 0)))
185     doc))
186
187 (defun notmuch-help ()
188   "Display help for the current notmuch mode."
189   (interactive)
190   (let* ((mode major-mode)
191          (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
192     (with-current-buffer (generate-new-buffer "*notmuch-help*")
193       (insert doc)
194       (goto-char (point-min))
195       (set-buffer-modified-p nil)
196       (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
197
198 (defcustom notmuch-search-hook '(hl-line-mode)
199   "List of functions to call when notmuch displays the search results."
200   :type 'hook
201   :options '(hl-line-mode)
202   :group 'notmuch)
203
204 (defvar notmuch-search-mode-map
205   (let ((map (make-sparse-keymap)))
206     (define-key map "?" 'notmuch-help)
207     (define-key map "q" 'notmuch-search-quit)
208     (define-key map "x" 'notmuch-search-quit)
209     (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
210     (define-key map "b" 'notmuch-search-scroll-down)
211     (define-key map " " 'notmuch-search-scroll-up)
212     (define-key map "<" 'notmuch-search-first-thread)
213     (define-key map ">" 'notmuch-search-last-thread)
214     (define-key map "p" 'notmuch-search-previous-thread)
215     (define-key map "n" 'notmuch-search-next-thread)
216     (define-key map "r" 'notmuch-search-reply-to-thread)
217     (define-key map "m" 'notmuch-mua-new-mail)
218     (define-key map "s" 'notmuch-search)
219     (define-key map "o" 'notmuch-search-toggle-order)
220     (define-key map "c" 'notmuch-search-stash-map)
221     (define-key map "=" 'notmuch-search-refresh-view)
222     (define-key map "G" 'notmuch-search-poll-and-refresh-view)
223     (define-key map "t" 'notmuch-search-filter-by-tag)
224     (define-key map "f" 'notmuch-search-filter)
225     (define-key map [mouse-1] 'notmuch-search-show-thread)
226     (define-key map "*" 'notmuch-search-operate-all)
227     (define-key map "a" 'notmuch-search-archive-thread)
228     (define-key map "-" 'notmuch-search-remove-tag)
229     (define-key map "+" 'notmuch-search-add-tag)
230     (define-key map (kbd "RET") 'notmuch-search-show-thread)
231     map)
232   "Keymap for \"notmuch search\" buffers.")
233 (fset 'notmuch-search-mode-map notmuch-search-mode-map)
234
235 (defvar notmuch-search-stash-map
236   (let ((map (make-sparse-keymap)))
237     (define-key map "i" 'notmuch-search-stash-thread-id)
238     map)
239   "Submap for stash commands")
240 (fset 'notmuch-search-stash-map notmuch-search-stash-map)
241
242 (defun notmuch-search-stash-thread-id ()
243   "Copy thread ID of current thread to kill-ring."
244   (interactive)
245   (notmuch-common-do-stash (notmuch-search-find-thread-id)))
246
247 (defvar notmuch-search-query-string)
248 (defvar notmuch-search-target-thread)
249 (defvar notmuch-search-target-line)
250 (defvar notmuch-search-continuation)
251
252 (defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
253
254 (defun notmuch-search-quit ()
255   "Exit the search buffer, calling any defined continuation function."
256   (interactive)
257   (let ((continuation notmuch-search-continuation))
258     (notmuch-kill-this-buffer)
259     (when continuation
260       (funcall continuation))))
261
262 (defun notmuch-search-scroll-up ()
263   "Move forward through search results by one window's worth."
264   (interactive)
265   (condition-case nil
266       (scroll-up nil)
267     ((end-of-buffer) (notmuch-search-last-thread))))
268
269 (defun notmuch-search-scroll-down ()
270   "Move backward through the search results by one window's worth."
271   (interactive)
272   ; I don't know why scroll-down doesn't signal beginning-of-buffer
273   ; the way that scroll-up signals end-of-buffer, but c'est la vie.
274   ;
275   ; So instead of trapping a signal we instead check whether the
276   ; window begins on the first line of the buffer and if so, move
277   ; directly to that position. (We have to count lines since the
278   ; window-start position is not the same as point-min due to the
279   ; invisible thread-ID characters on the first line.
280   (if (equal (count-lines (point-min) (window-start)) 0)
281       (goto-char (point-min))
282     (scroll-down nil)))
283
284 (defun notmuch-search-next-thread ()
285   "Select the next thread in the search results."
286   (interactive)
287   (forward-line 1))
288
289 (defun notmuch-search-previous-thread ()
290   "Select the previous thread in the search results."
291   (interactive)
292   (forward-line -1))
293
294 (defun notmuch-search-last-thread ()
295   "Select the last thread in the search results."
296   (interactive)
297   (goto-char (point-max))
298   (forward-line -2))
299
300 (defun notmuch-search-first-thread ()
301   "Select the first thread in the search results."
302   (interactive)
303   (goto-char (point-min)))
304
305 (defface notmuch-message-summary-face
306  '((((class color) (background light)) (:background "#f0f0f0"))
307    (((class color) (background dark)) (:background "#303030")))
308  "Face for the single-line message summary in notmuch-show-mode."
309  :group 'notmuch)
310
311 (defface notmuch-search-date
312   '((t :inherit default))
313   "Face used in search mode for dates."
314   :group 'notmuch)
315
316 (defface notmuch-search-count
317   '((t :inherit default))
318   "Face used in search mode for the count matching the query."
319   :group 'notmuch)
320
321 (defface notmuch-search-subject
322   '((t :inherit default))
323   "Face used in search mode for subjects."
324   :group 'notmuch)
325
326 (defface notmuch-search-matching-authors
327   '((t :inherit default))
328   "Face used in search mode for authors matching the query."
329   :group 'notmuch)
330
331 (defface notmuch-search-non-matching-authors
332   '((((class color)
333       (background dark))
334      (:foreground "grey30"))
335     (((class color)
336       (background light))
337      (:foreground "grey60"))
338     (t
339      (:italic t)))
340   "Face used in search mode for authors not matching the query."
341   :group 'notmuch)
342
343 (defface notmuch-tag-face
344   '((((class color)
345       (background dark))
346      (:foreground "OliveDrab1"))
347     (((class color)
348       (background light))
349      (:foreground "navy blue" :bold t))
350     (t
351      (:bold t)))
352   "Face used in search mode face for tags."
353   :group 'notmuch)
354
355 (defun notmuch-search-mode ()
356   "Major mode displaying results of a notmuch search.
357
358 This buffer contains the results of a \"notmuch search\" of your
359 email archives. Each line in the buffer represents a single
360 thread giving a summary of the thread (a relative date, the
361 number of matched messages and total messages in the thread,
362 participants in the thread, a representative subject line, and
363 any tags).
364
365 Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
366 keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
367 is a convenience for archiving a thread (removing the \"inbox\"
368 tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all
369 threads in the current buffer.
370
371 Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
372 based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
373 only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
374 search.
375
376 Complete list of currently available key bindings:
377
378 \\{notmuch-search-mode-map}"
379   (interactive)
380   (kill-all-local-variables)
381   (make-local-variable 'notmuch-search-query-string)
382   (make-local-variable 'notmuch-search-oldest-first)
383   (make-local-variable 'notmuch-search-target-thread)
384   (make-local-variable 'notmuch-search-target-line)
385   (set (make-local-variable 'notmuch-search-continuation) nil)
386   (set (make-local-variable 'scroll-preserve-screen-position) t)
387   (add-to-invisibility-spec (cons 'ellipsis t))
388   (use-local-map notmuch-search-mode-map)
389   (setq truncate-lines t)
390   (setq major-mode 'notmuch-search-mode
391         mode-name "notmuch-search")
392   (setq buffer-read-only t))
393
394 (defun notmuch-search-properties-in-region (property beg end)
395   (save-excursion
396     (let ((output nil)
397           (last-line (line-number-at-pos end))
398           (max-line (- (line-number-at-pos (point-max)) 2)))
399       (goto-char beg)
400       (beginning-of-line)
401       (while (<= (line-number-at-pos) (min last-line max-line))
402         (setq output (cons (get-text-property (point) property) output))
403         (forward-line 1))
404       output)))
405
406 (defun notmuch-search-find-thread-id ()
407   "Return the thread for the current thread"
408   (get-text-property (point) 'notmuch-search-thread-id))
409
410 (defun notmuch-search-find-thread-id-region (beg end)
411   "Return a list of threads for the current region"
412   (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end))
413
414 (defun notmuch-search-find-authors ()
415   "Return the authors for the current thread"
416   (get-text-property (point) 'notmuch-search-authors))
417
418 (defun notmuch-search-find-authors-region (beg end)
419   "Return a list of authors for the current region"
420   (notmuch-search-properties-in-region 'notmuch-search-authors beg end))
421
422 (defun notmuch-search-find-subject ()
423   "Return the subject for the current thread"
424   (get-text-property (point) 'notmuch-search-subject))
425
426 (defun notmuch-search-find-subject-region (beg end)
427   "Return a list of authors for the current region"
428   (notmuch-search-properties-in-region 'notmuch-search-subject beg end))
429
430 (defun notmuch-search-show-thread (&optional crypto-switch)
431   "Display the currently selected thread."
432   (interactive "P")
433   (let ((thread-id (notmuch-search-find-thread-id))
434         (subject (notmuch-search-find-subject)))
435     (if (> (length thread-id) 0)
436         (notmuch-show thread-id
437                       (current-buffer)
438                       notmuch-search-query-string
439                       ;; name the buffer based on notmuch-search-find-subject
440                       (if (string-match "^[ \t]*$" subject)
441                           "[No Subject]"
442                         (truncate-string-to-width
443                          (concat "*"
444                                  (truncate-string-to-width subject 32 nil nil t)
445                                  "*")
446                          32 nil nil t))
447                       crypto-switch)
448       (message "End of search results."))))
449
450 (defun notmuch-search-reply-to-thread (&optional prompt-for-sender)
451   "Begin composing a reply to the entire current thread in a new buffer."
452   (interactive "P")
453   (let ((message-id (notmuch-search-find-thread-id)))
454     (notmuch-mua-new-reply message-id prompt-for-sender)))
455
456 (defun notmuch-call-notmuch-process (&rest args)
457   "Synchronously invoke \"notmuch\" with the given list of arguments.
458
459 Output from the process will be presented to the user as an error
460 and will also appear in a buffer named \"*Notmuch errors*\"."
461   (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
462     (with-current-buffer error-buffer
463         (erase-buffer))
464     (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
465         (point)
466       (progn
467         (with-current-buffer error-buffer
468           (let ((beg (point-min))
469                 (end (- (point-max) 1)))
470             (error (buffer-substring beg end))
471             ))))))
472
473 (defun notmuch-tag (query &rest tags)
474   "Add/remove tags in TAGS to messages matching QUERY.
475
476 TAGS should be a list of strings of the form \"+TAG\" or \"-TAG\" and
477 QUERY should be a string containing the search-query.
478
479 Note: Other code should always use this function alter tags of
480 messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
481 directly, so that hooks specified in notmuch-before-tag-hook and
482 notmuch-after-tag-hook will be run."
483   (run-hooks 'notmuch-before-tag-hook)
484   (apply 'notmuch-call-notmuch-process
485          (append (list "tag") tags (list "--" query)))
486   (run-hooks 'notmuch-after-tag-hook))
487
488 (defcustom notmuch-before-tag-hook nil
489   "Hooks that are run before tags of a message are modified.
490
491 'tags' will contain the tags that are about to be added or removed as
492 a list of strings of the form \"+TAG\" or \"-TAG\".
493 'query' will be a string containing the search query that determines
494 the messages that are about to be tagged"
495
496   :type 'hook
497   :options '(hl-line-mode)
498   :group 'notmuch)
499
500 (defcustom notmuch-after-tag-hook nil
501   "Hooks that are run after tags of a message are modified.
502
503 'tags' will contain the tags that were added or removed as
504 a list of strings of the form \"+TAG\" or \"-TAG\".
505 'query' will be a string containing the search query that determines
506 the messages that were tagged"
507   :type 'hook
508   :options '(hl-line-mode)
509   :group 'notmuch)
510
511 (defun notmuch-search-set-tags (tags)
512   (save-excursion
513     (end-of-line)
514     (re-search-backward "(")
515     (forward-char)
516     (let ((beg (point))
517           (inhibit-read-only t))
518       (re-search-forward ")")
519       (backward-char)
520       (let ((end (point)))
521         (delete-region beg end)
522         (insert (propertize (mapconcat  'identity tags " ")
523                             'face 'notmuch-tag-face))))))
524
525 (defun notmuch-search-get-tags ()
526   (save-excursion
527     (end-of-line)
528     (re-search-backward "(")
529     (let ((beg (+ (point) 1)))
530       (re-search-forward ")")
531       (let ((end (- (point) 1)))
532         (split-string (buffer-substring beg end))))))
533
534 (defun notmuch-search-get-tags-region (beg end)
535   (save-excursion
536     (let ((output nil)
537           (last-line (line-number-at-pos end))
538           (max-line (- (line-number-at-pos (point-max)) 2)))
539       (goto-char beg)
540       (while (<= (line-number-at-pos) (min last-line max-line))
541         (setq output (append output (notmuch-search-get-tags)))
542         (forward-line 1))
543       output)))
544
545 (defun notmuch-search-add-tag-thread (tag)
546   (notmuch-search-add-tag-region tag (point) (point)))
547
548 (defun notmuch-search-add-tag-region (tag beg end)
549   (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
550     (notmuch-tag search-id-string (concat "+" tag))
551     (save-excursion
552       (let ((last-line (line-number-at-pos end))
553             (max-line (- (line-number-at-pos (point-max)) 2)))
554         (goto-char beg)
555         (while (<= (line-number-at-pos) (min last-line max-line))
556           (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<)))
557           (forward-line))))))
558
559 (defun notmuch-search-remove-tag-thread (tag)
560   (notmuch-search-remove-tag-region tag (point) (point)))
561
562 (defun notmuch-search-remove-tag-region (tag beg end)
563   (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
564     (notmuch-tag search-id-string (concat "-" tag))
565     (save-excursion
566       (let ((last-line (line-number-at-pos end))
567             (max-line (- (line-number-at-pos (point-max)) 2)))
568         (goto-char beg)
569         (while (<= (line-number-at-pos) (min last-line max-line))
570           (notmuch-search-set-tags (delete tag (notmuch-search-get-tags)))
571           (forward-line))))))
572
573 (defun notmuch-search-add-tag (tag)
574   "Add a tag to the currently selected thread or region.
575
576 The tag is added to all messages in the currently selected thread
577 or threads in the current region."
578   (interactive
579    (list (notmuch-select-tag-with-completion "Tag to add: ")))
580   (save-excursion
581     (if (region-active-p)
582         (let* ((beg (region-beginning))
583                (end (region-end)))
584           (notmuch-search-add-tag-region tag beg end))
585       (notmuch-search-add-tag-thread tag))))
586
587 (defun notmuch-search-remove-tag (tag)
588   "Remove a tag from the currently selected thread or region.
589
590 The tag is removed from all messages in the currently selected
591 thread or threads in the current region."
592   (interactive
593    (list (notmuch-select-tag-with-completion
594           "Tag to remove: "
595           (if (region-active-p)
596               (mapconcat 'identity
597                          (notmuch-search-find-thread-id-region (region-beginning) (region-end))
598                          " ")
599             (notmuch-search-find-thread-id)))))
600   (save-excursion
601     (if (region-active-p)
602         (let* ((beg (region-beginning))
603                (end (region-end)))
604           (notmuch-search-remove-tag-region tag beg end))
605       (notmuch-search-remove-tag-thread tag))))
606
607 (defun notmuch-search-archive-thread ()
608   "Archive the currently selected thread (remove its \"inbox\" tag).
609
610 This function advances the next thread when finished."
611   (interactive)
612   (notmuch-search-remove-tag-thread "inbox")
613   (forward-line))
614
615 (defvar notmuch-search-process-filter-data nil
616   "Data that has not yet been processed.")
617 (make-variable-buffer-local 'notmuch-search-process-filter-data)
618
619 (defun notmuch-search-process-sentinel (proc msg)
620   "Add a message to let user know when \"notmuch search\" exits"
621   (let ((buffer (process-buffer proc))
622         (status (process-status proc))
623         (exit-status (process-exit-status proc))
624         (never-found-target-thread nil))
625     (if (memq status '(exit signal))
626         (if (buffer-live-p buffer)
627             (with-current-buffer buffer
628               (save-excursion
629                 (let ((inhibit-read-only t)
630                       (atbob (bobp)))
631                   (goto-char (point-max))
632                   (if (eq status 'signal)
633                       (insert "Incomplete search results (search process was killed).\n"))
634                   (if (eq status 'exit)
635                       (progn
636                         (if notmuch-search-process-filter-data
637                             (insert (concat "Error: Unexpected output from notmuch search:\n" notmuch-search-process-filter-data)))
638                         (insert "End of search results.")
639                         (if (not (= exit-status 0))
640                             (insert (format " (process returned %d)" exit-status)))
641                         (insert "\n")
642                         (if (and atbob
643                                  (not (string= notmuch-search-target-thread "found")))
644                             (set 'never-found-target-thread t))))))
645               (when (and never-found-target-thread
646                        notmuch-search-target-line)
647                   (goto-char (point-min))
648                   (forward-line (1- notmuch-search-target-line))))))))
649
650 (defcustom notmuch-search-line-faces nil
651   "Tag/face mapping for line highlighting in notmuch-search.
652
653 Here is an example of how to color search results based on tags.
654  (the following text would be placed in your ~/.emacs file):
655
656  (setq notmuch-search-line-faces '((\"delete\" . (:foreground \"red\"
657                                                   :background \"blue\"))
658                                    (\"unread\" . (:foreground \"green\"))))
659
660 The attributes defined for matching tags are merged, with later
661 attributes overriding earlier. A message having both \"delete\"
662 and \"unread\" tags with the above settings would have a green
663 foreground and blue background."
664   :type '(alist :key-type (string) :value-type (custom-face-edit))
665   :group 'notmuch)
666
667 (defun notmuch-search-color-line (start end line-tag-list)
668   "Colorize lines in `notmuch-show' based on tags."
669   ;; Create the overlay only if the message has tags which match one
670   ;; of those specified in `notmuch-search-line-faces'.
671   (let (overlay)
672     (mapc (lambda (elem)
673             (let ((tag (car elem))
674                   (attributes (cdr elem)))
675               (when (member tag line-tag-list)
676                 (when (not overlay)
677                   (setq overlay (make-overlay start end)))
678                 ;; Merge the specified properties with any already
679                 ;; applied from an earlier match.
680                 (overlay-put overlay 'face
681                              (append (overlay-get overlay 'face) attributes)))))
682           notmuch-search-line-faces)))
683
684 (defun notmuch-search-author-propertize (authors)
685   "Split `authors' into matching and non-matching authors and
686 propertize appropriately. If no boundary between authors and
687 non-authors is found, assume that all of the authors match."
688   (if (string-match "\\(.*\\)|\\(.*\\)" authors)
689       (concat (propertize (concat (match-string 1 authors) ",")
690                           'face 'notmuch-search-matching-authors)
691               (propertize (match-string 2 authors)
692                           'face 'notmuch-search-non-matching-authors))
693     (propertize authors 'face 'notmuch-search-matching-authors)))
694
695 (defun notmuch-search-insert-authors (format-string authors)
696   ;; Save the match data to avoid interfering with
697   ;; `notmuch-search-process-filter'.
698   (save-match-data
699     (let* ((formatted-authors (format format-string authors))
700            (formatted-sample (format format-string ""))
701            (visible-string formatted-authors)
702            (invisible-string "")
703            (padding ""))
704
705       ;; Truncate the author string to fit the specification.
706       (if (> (length formatted-authors)
707              (length formatted-sample))
708           (let ((visible-length (- (length formatted-sample)
709                                    (length "... "))))
710             ;; Truncate the visible string according to the width of
711             ;; the display string.
712             (setq visible-string (substring formatted-authors 0 visible-length)
713                   invisible-string (substring formatted-authors visible-length))
714             ;; If possible, truncate the visible string at a natural
715             ;; break (comma or pipe), as incremental search doesn't
716             ;; match across the visible/invisible border.
717             (when (string-match "\\(.*\\)\\([,|] \\)\\([^,|]*\\)" visible-string)
718               ;; Second clause is destructive on `visible-string', so
719               ;; order is important.
720               (setq invisible-string (concat (match-string 3 visible-string)
721                                              invisible-string)
722                     visible-string (concat (match-string 1 visible-string)
723                                            (match-string 2 visible-string))))
724             ;; `visible-string' may be shorter than the space allowed
725             ;; by `format-string'. If so we must insert some padding
726             ;; after `invisible-string'.
727             (setq padding (make-string (- (length formatted-sample)
728                                           (length visible-string)
729                                           (length "..."))
730                                        ? ))))
731
732       ;; Use different faces to show matching and non-matching authors.
733       (if (string-match "\\(.*\\)|\\(.*\\)" visible-string)
734           ;; The visible string contains both matching and
735           ;; non-matching authors.
736           (setq visible-string (notmuch-search-author-propertize visible-string)
737                 ;; The invisible string must contain only non-matching
738                 ;; authors, as the visible-string contains both.
739                 invisible-string (propertize invisible-string
740                                              'face 'notmuch-search-non-matching-authors))
741         ;; The visible string contains only matching authors.
742         (setq visible-string (propertize visible-string
743                                          'face 'notmuch-search-matching-authors)
744               ;; The invisible string may contain both matching and
745               ;; non-matching authors.
746               invisible-string (notmuch-search-author-propertize invisible-string)))
747
748       ;; If there is any invisible text, add it as a tooltip to the
749       ;; visible text.
750       (when (not (string= invisible-string ""))
751         (setq visible-string (propertize visible-string 'help-echo (concat "..." invisible-string))))
752
753       ;; Insert the visible and, if present, invisible author strings.
754       (insert visible-string)
755       (when (not (string= invisible-string ""))
756         (let ((start (point))
757               overlay)
758           (insert invisible-string)
759           (setq overlay (make-overlay start (point)))
760           (overlay-put overlay 'invisible 'ellipsis)
761           (overlay-put overlay 'isearch-open-invisible #'delete-overlay)))
762       (insert padding))))
763
764 (defun notmuch-search-insert-field (field date count authors subject tags)
765   (cond
766    ((string-equal field "date")
767     (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) date)
768                         'face 'notmuch-search-date)))
769    ((string-equal field "count")
770     (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) count)
771                         'face 'notmuch-search-count)))
772    ((string-equal field "subject")
773     (insert (propertize (format (cdr (assoc field notmuch-search-result-format)) subject)
774                         'face 'notmuch-search-subject)))
775
776    ((string-equal field "authors")
777     (notmuch-search-insert-authors (cdr (assoc field notmuch-search-result-format)) authors))
778
779    ((string-equal field "tags")
780     (insert (concat "(" (propertize tags 'font-lock-face 'notmuch-tag-face) ")")))))
781
782 (defun notmuch-search-show-result (date count authors subject tags)
783   (let ((fields) (field))
784     (setq fields (mapcar 'car notmuch-search-result-format))
785     (loop for field in fields
786           do (notmuch-search-insert-field field date count authors subject tags)))
787   (insert "\n"))
788
789 (defun notmuch-search-process-filter (proc string)
790   "Process and filter the output of \"notmuch search\""
791   (let ((buffer (process-buffer proc))
792         (found-target nil))
793     (if (buffer-live-p buffer)
794         (with-current-buffer buffer
795           (save-excursion
796             (let ((line 0)
797                   (more t)
798                   (inhibit-read-only t)
799                   (string (concat notmuch-search-process-filter-data string)))
800               (setq notmuch-search-process-filter-data nil)
801               (while more
802                 (while (and (< line (length string)) (= (elt string line) ?\n))
803                   (setq line (1+ line)))
804                 (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\([^][]*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
805                     (let* ((thread-id (match-string 1 string))
806                            (date (match-string 2 string))
807                            (count (match-string 3 string))
808                            (authors (match-string 4 string))
809                            (subject (match-string 5 string))
810                            (tags (match-string 6 string))
811                            (tag-list (if tags (save-match-data (split-string tags)))))
812                       (goto-char (point-max))
813                       (if (/= (match-beginning 1) line)
814                           (insert (concat "Error: Unexpected output from notmuch search:\n" (substring string line (match-beginning 1)) "\n")))
815                       (let ((beg (point)))
816                         (notmuch-search-show-result date count authors subject tags)
817                         (notmuch-search-color-line beg (point) tag-list)
818                         (put-text-property beg (point) 'notmuch-search-thread-id thread-id)
819                         (put-text-property beg (point) 'notmuch-search-authors authors)
820                         (put-text-property beg (point) 'notmuch-search-subject subject)
821                         (if (string= thread-id notmuch-search-target-thread)
822                             (progn
823                               (set 'found-target beg)
824                               (set 'notmuch-search-target-thread "found"))))
825                       (set 'line (match-end 0)))
826                   (set 'more nil)
827                   (while (and (< line (length string)) (= (elt string line) ?\n))
828                     (setq line (1+ line)))
829                   (if (< line (length string))
830                       (setq notmuch-search-process-filter-data (substring string line)))
831                   ))))
832           (if found-target
833               (goto-char found-target)))
834       (delete-process proc))))
835
836 (defun notmuch-search-operate-all (action)
837   "Add/remove tags from all matching messages.
838
839 This command adds or removes tags from all messages matching the
840 current search terms. When called interactively, this command
841 will prompt for tags to be added or removed. Tags prefixed with
842 '+' will be added and tags prefixed with '-' will be removed.
843
844 Each character of the tag name may consist of alphanumeric
845 characters as well as `_.+-'.
846 "
847   (interactive "sOperation (+add -drop): notmuch tag ")
848   (let ((action-split (split-string action " +")))
849     ;; Perform some validation
850     (let ((words action-split))
851       (when (null words) (error "No operation given"))
852       (while words
853         (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
854           (error "Action must be of the form `+thistag -that_tag'"))
855         (setq words (cdr words))))
856     (apply 'notmuch-tag notmuch-search-query-string action-split)))
857
858 (defun notmuch-search-buffer-title (query)
859   "Returns the title for a buffer with notmuch search results."
860   (let* ((saved-search
861           (let (longest
862                 (longest-length 0))
863             (loop for tuple in notmuch-saved-searches
864                   if (let ((quoted-query (regexp-quote (cdr tuple))))
865                        (and (string-match (concat "^" quoted-query) query)
866                             (> (length (match-string 0 query))
867                                longest-length)))
868                   do (setq longest tuple))
869             longest))
870          (saved-search-name (car saved-search))
871          (saved-search-query (cdr saved-search)))
872     (cond ((and saved-search (equal saved-search-query query))
873            ;; Query is the same as saved search (ignoring case)
874            (concat "*notmuch-saved-search-" saved-search-name "*"))
875           (saved-search
876            (concat "*notmuch-search-"
877                    (replace-regexp-in-string (concat "^" (regexp-quote saved-search-query))
878                                              (concat "[ " saved-search-name " ]")
879                                              query)
880                    "*"))
881           (t
882            (concat "*notmuch-search-" query "*"))
883           )))
884
885 (defun notmuch-read-query (prompt)
886   "Read a notmuch-query from the minibuffer with completion.
887
888 PROMPT is the string to prompt with."
889   (lexical-let
890       ((completions
891         (append (list "folder:" "thread:" "id:" "date:" "from:" "to:"
892                       "subject:" "attachment:")
893                 (mapcar (lambda (tag)
894                           (concat "tag:" tag))
895                         (process-lines notmuch-command "search" "--output=tags" "*")))))
896     (let ((keymap (copy-keymap minibuffer-local-map))
897           (minibuffer-completion-table
898            (completion-table-dynamic
899             (lambda (string)
900               ;; generate a list of possible completions for the current input
901               (cond
902                ;; this ugly regexp is used to get the last word of the input
903                ;; possibly preceded by a '('
904                ((string-match "\\(^\\|.* (?\\)\\([^ ]*\\)$" string)
905                 (mapcar (lambda (compl)
906                           (concat (match-string-no-properties 1 string) compl))
907                         (all-completions (match-string-no-properties 2 string)
908                                          completions)))
909                (t (list string)))))))
910       ;; this was simpler than convincing completing-read to accept spaces:
911       (define-key keymap (kbd "<tab>") 'minibuffer-complete)
912       (read-from-minibuffer prompt nil keymap nil
913                             'notmuch-query-history nil nil))))
914
915 ;;;###autoload
916 (defun notmuch-search (query &optional oldest-first target-thread target-line continuation)
917   "Run \"notmuch search\" with the given query string and display results.
918
919 The optional parameters are used as follows:
920
921   oldest-first: A Boolean controlling the sort order of returned threads
922   target-thread: A thread ID (with the thread: prefix) that will be made
923                  current if it appears in the search results.
924   target-line: The line number to move to if the target thread does not
925                appear in the search results."
926   (interactive (list (notmuch-read-query "Notmuch search: ")))
927   (let ((buffer (get-buffer-create (notmuch-search-buffer-title query))))
928     (switch-to-buffer buffer)
929     (notmuch-search-mode)
930     ;; Don't track undo information for this buffer
931     (set 'buffer-undo-list t)
932     (set 'notmuch-search-query-string query)
933     (set 'notmuch-search-oldest-first oldest-first)
934     (set 'notmuch-search-target-thread target-thread)
935     (set 'notmuch-search-target-line target-line)
936     (set 'notmuch-search-continuation continuation)
937     (let ((proc (get-buffer-process (current-buffer)))
938           (inhibit-read-only t))
939       (if proc
940           (error "notmuch search process already running for query `%s'" query)
941         )
942       (erase-buffer)
943       (goto-char (point-min))
944       (save-excursion
945         (let ((proc (start-process
946                      "notmuch-search" buffer
947                      notmuch-command "search"
948                      (if oldest-first
949                          "--sort=oldest-first"
950                        "--sort=newest-first")
951                      query)))
952           (set-process-sentinel proc 'notmuch-search-process-sentinel)
953           (set-process-filter proc 'notmuch-search-process-filter)
954           (set-process-query-on-exit-flag proc nil))))
955     (run-hooks 'notmuch-search-hook)))
956
957 (defun notmuch-search-refresh-view ()
958   "Refresh the current view.
959
960 Kills the current buffer and runs a new search with the same
961 query string as the current search. If the current thread is in
962 the new search results, then point will be placed on the same
963 thread. Otherwise, point will be moved to attempt to be in the
964 same relative position within the new buffer."
965   (interactive)
966   (let ((target-line (line-number-at-pos))
967         (oldest-first notmuch-search-oldest-first)
968         (target-thread (notmuch-search-find-thread-id))
969         (query notmuch-search-query-string)
970         (continuation notmuch-search-continuation))
971     (notmuch-kill-this-buffer)
972     (notmuch-search query oldest-first target-thread target-line continuation)
973     (goto-char (point-min))))
974
975 (defcustom notmuch-poll-script nil
976   "An external script to incorporate new mail into the notmuch database.
977
978 This variable controls the action invoked by
979 `notmuch-search-poll-and-refresh-view' and
980 `notmuch-hello-poll-and-update' (each have a default keybinding
981 of 'G') to incorporate new mail into the notmuch database.
982
983 If set to nil (the default), new mail is processed by invoking
984 \"notmuch new\". Otherwise, this should be set to a string that
985 gives the name of an external script that processes new mail. If
986 set to the empty string, no command will be run.
987
988 The external script could do any of the following depending on
989 the user's needs:
990
991 1. Invoke a program to transfer mail to the local mail store
992 2. Invoke \"notmuch new\" to incorporate the new mail
993 3. Invoke one or more \"notmuch tag\" commands to classify the mail
994
995 Note that the recommended way of achieving the same is using
996 \"notmuch new\" hooks."
997   :type '(choice (const :tag "notmuch new" nil)
998                  (const :tag "Disabled" "")
999                  (string :tag "Custom script"))
1000   :group 'notmuch)
1001
1002 (defun notmuch-poll ()
1003   "Run \"notmuch new\" or an external script to import mail.
1004
1005 Invokes `notmuch-poll-script', \"notmuch new\", or does nothing
1006 depending on the value of `notmuch-poll-script'."
1007   (interactive)
1008   (if (stringp notmuch-poll-script)
1009       (if (not (string= notmuch-poll-script ""))
1010           (call-process notmuch-poll-script nil nil))
1011     (call-process notmuch-command nil nil nil "new")))
1012
1013 (defun notmuch-search-poll-and-refresh-view ()
1014   "Invoke `notmuch-poll' to import mail, then refresh the current view."
1015   (interactive)
1016   (notmuch-poll)
1017   (notmuch-search-refresh-view))
1018
1019 (defun notmuch-search-toggle-order ()
1020   "Toggle the current search order.
1021
1022 By default, the \"inbox\" view created by `notmuch' is displayed
1023 in chronological order (oldest thread at the beginning of the
1024 buffer), while any global searches created by `notmuch-search'
1025 are displayed in reverse-chronological order (newest thread at
1026 the beginning of the buffer).
1027
1028 This command toggles the sort order for the current search.
1029
1030 Note that any filtered searches created by
1031 `notmuch-search-filter' retain the search order of the parent
1032 search."
1033   (interactive)
1034   (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
1035   (notmuch-search-refresh-view))
1036
1037 (defun notmuch-search-filter (query)
1038   "Filter the current search results based on an additional query string.
1039
1040 Runs a new search matching only messages that match both the
1041 current search results AND the additional query string provided."
1042   (interactive (list (notmuch-read-query "Filter search: ")))
1043   (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query)
1044                            (concat "( " query " )")
1045                          query)))
1046     (notmuch-search (if (string= notmuch-search-query-string "*")
1047                         grouped-query
1048                       (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first)))
1049
1050 (defun notmuch-search-filter-by-tag (tag)
1051   "Filter the current search results based on a single tag.
1052
1053 Runs a new search matching only messages that match both the
1054 current search results AND that are tagged with the given tag."
1055   (interactive
1056    (list (notmuch-select-tag-with-completion "Filter by tag: ")))
1057   (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
1058
1059 ;;;###autoload
1060 (defun notmuch ()
1061   "Run notmuch and display saved searches, known tags, etc."
1062   (interactive)
1063   (notmuch-hello))
1064
1065 ;;;###autoload
1066 (defun notmuch-jump-to-recent-buffer ()
1067   "Jump to the most recent notmuch buffer (search, show or hello).
1068
1069 If no recent buffer is found, run `notmuch'."
1070   (interactive)
1071   (let ((last
1072          (loop for buffer in (buffer-list)
1073                if (with-current-buffer buffer
1074                     (memq major-mode '(notmuch-show-mode
1075                                        notmuch-search-mode
1076                                        notmuch-hello-mode)))
1077                return buffer)))
1078     (if last
1079         (switch-to-buffer last)
1080       (notmuch))))
1081
1082 (setq mail-user-agent 'notmuch-user-agent)
1083
1084 (provide 'notmuch)