]> git.cworth.org Git - notmuch/commitdiff
Merge branch 'release'
authorDavid Bremner <david@tethera.net>
Mon, 23 Nov 2015 12:40:40 +0000 (08:40 -0400)
committerDavid Bremner <david@tethera.net>
Mon, 23 Nov 2015 12:40:40 +0000 (08:40 -0400)
Merge bugfixes applied directly to release

17 files changed:
devel/release-checks.sh
emacs/Makefile.local
emacs/notmuch-address.el
emacs/notmuch-company.el [new file with mode: 0644]
emacs/notmuch-lib.el
emacs/notmuch-mua.el
emacs/notmuch-show.el
lib/database.cc
lib/index.cc
lib/notmuch.h
notmuch-emacs-mua
notmuch-reply.c
notmuch-show.c
notmuch-tag.c
test/T150-tagging.sh
test/T190-multipart.sh
test/T580-thread-search.sh [new file with mode: 0755]

index 8604a9f7d10bb3e62a620de25ae02d7031bf8c42..5a7578b863aad83adeccb0abd9ce619662f71056 100755 (executable)
@@ -175,6 +175,21 @@ case $news_date in
        append_emsg "Date '$news_date' in NEWS file is not in format (yyyy-mm-dd)"
 esac
 
+year=`exec date +%Y`
+echo -n "Checking that copyright in documentation contains 2009-$year... "
+# Read the value of variable `copyright' defined in 'doc/conf.py'.
+# As __file__ is not defined when python command is given from command line,
+# it is defined before contents of 'doc/conf.py' (which dereferences __file__)
+# is executed.
+copyrightline=`exec python -c "with open('doc/conf.py') as cf: __file__ = ''; exec(cf.read()); print(copyright)"`
+case $copyrightline in
+       *2009-$year*)
+               echo Yes. ;;
+       *)
+               echo No.
+               append_emsg "The copyright in doc/conf.py line '$copyrightline' does not contain '2009-$year'"
+esac
+
 if [ -n "$emsgs" ]
 then
        echo
index 1109cfa6b09033769454ef8f8f27089197d72e7e..2d6aedbdfd5d5e1c98485ad9b27ee69e635431cd 100644 (file)
@@ -20,6 +20,7 @@ emacs_sources := \
        $(dir)/notmuch-print.el \
        $(dir)/notmuch-version.el \
        $(dir)/notmuch-jump.el \
+       $(dir)/notmuch-company.el
 
 $(dir)/notmuch-version.el: $(dir)/Makefile.local version.stamp
 $(dir)/notmuch-version.el: $(srcdir)/$(dir)/notmuch-version.el.tmpl
@@ -52,6 +53,10 @@ $(dir)/.eldeps: $(dir)/Makefile.local $(dir)/make-deps.el $(emacs_sources)
 $(dir)/.eldeps.x: $(dir)/.eldeps
        @cmp -s $^ $@ || cp $^ $@
 -include $(dir)/.eldeps.x
+
+# Add the one dependency make-deps.el does not have visibility to.
+$(dir)/notmuch-lib.elc: $(dir)/notmuch-version.elc
+
 endif
 CLEAN+=$(dir)/.eldeps $(dir)/.eldeps.tmp $(dir)/.eldeps.x
 
index fde3c1b2b861fb15063abe5fb4d480084149c88c..49e240236c7a439d87640c4fdd2b8102446298e9 100644 (file)
 ;; Authors: David Edmondson <dme@dme.org>
 
 (require 'message)
-
+(require 'notmuch-parser)
+(require 'notmuch-lib)
+(require 'notmuch-company)
 ;;
+(declare-function company-manual-begin "company")
 
-(defcustom notmuch-address-command "notmuch-addresses"
+(defcustom notmuch-address-command 'internal
   "The command which generates possible addresses. It must take a
 single argument and output a list of possible matches, one per
-line."
-  :type 'string
+line. The default value of `internal' uses built-in address
+completion."
+  :type '(radio
+         (const :tag "Use internal address completion" internal)
+         (const :tag "Disable address completion" nil)
+         (string :tag "Use external completion command" "notmuch-addresses"))
   :group 'notmuch-send
   :group 'notmuch-external)
 
@@ -42,53 +49,105 @@ to know how address selection is made by default."
   :group 'notmuch-send
   :group 'notmuch-external)
 
+(defvar notmuch-address-last-harvest 0
+  "Time of last address harvest")
+
+(defvar notmuch-address-completions (make-hash-table :test 'equal)
+  "Hash of email addresses for completion during email composition.
+  This variable is set by calling `notmuch-address-harvest'.")
+
+(defvar notmuch-address-full-harvest-finished nil
+  "t indicates that full completion address harvesting has been
+finished")
+
 (defun notmuch-address-selection-function (prompt collection initial-input)
   "Call (`completing-read'
       PROMPT COLLECTION nil nil INITIAL-INPUT 'notmuch-address-history)"
   (completing-read
    prompt collection nil nil initial-input 'notmuch-address-history))
 
-(defvar notmuch-address-message-alist-member
-  '("^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):"
-             . notmuch-address-expand-name))
+(defvar notmuch-address-completion-headers-regexp
+  "^\\(Resent-\\)?\\(To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\):")
 
 (defvar notmuch-address-history nil)
 
 (defun notmuch-address-message-insinuate ()
-  (unless (memq notmuch-address-message-alist-member message-completion-alist)
-    (setq message-completion-alist
-         (push notmuch-address-message-alist-member message-completion-alist))))
+  (message "calling notmuch-address-message-insinuate is no longer needed"))
+
+(defcustom notmuch-address-use-company t
+  "If available, use company mode for address completion"
+  :type 'boolean
+  :group 'notmuch-send)
+
+(defun notmuch-address-setup ()
+  (let* ((use-company (and notmuch-address-use-company
+                          (eq notmuch-address-command 'internal)
+                          (require 'company nil t)))
+        (pair (cons notmuch-address-completion-headers-regexp
+                    (if use-company
+                        #'company-manual-begin
+                      #'notmuch-address-expand-name))))
+      (when use-company
+       (notmuch-company-setup))
+      (unless (memq pair message-completion-alist)
+       (setq message-completion-alist
+             (push pair message-completion-alist)))))
+
+(defun notmuch-address-matching (substring)
+  "Returns a list of completion candidates matching SUBSTRING.
+The candidates are taken from `notmuch-address-completions'."
+  (let ((candidates)
+       (re (regexp-quote substring)))
+    (maphash (lambda (key val)
+              (when (string-match re key)
+                (push key candidates)))
+            notmuch-address-completions)
+    candidates))
 
 (defun notmuch-address-options (original)
-  (process-lines notmuch-address-command original))
+  "Returns a list of completion candidates. Uses either
+elisp-based implementation or older implementation requiring
+external commands."
+  (cond
+   ((eq notmuch-address-command 'internal)
+    (when (not notmuch-address-full-harvest-finished)
+      ;; First, run quick synchronous harvest based on what the user
+      ;; entered so far
+      (notmuch-address-harvest (format "to:%s*" original) t))
+    (prog1 (notmuch-address-matching original)
+      ;; Then start the (potentially long-running) full asynchronous harvest if necessary
+      (notmuch-address-harvest-trigger)))
+   (t
+    (process-lines notmuch-address-command original))))
 
 (defun notmuch-address-expand-name ()
-  (let* ((end (point))
-        (beg (save-excursion
-               (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
-               (goto-char (match-end 0))
-               (point)))
-        (orig (buffer-substring-no-properties beg end))
-        (completion-ignore-case t)
-        (options (with-temp-message "Looking for completion candidates..."
-                   (notmuch-address-options orig)))
-        (num-options (length options))
-        (chosen (cond
-                 ((eq num-options 0)
-                  nil)
-                 ((eq num-options 1)
-                  (car options))
-                 (t
-                  (funcall notmuch-address-selection-function
-                           (format "Address (%s matches): " num-options)
-                           (cdr options) (car options))))))
-    (if chosen
-       (progn
-         (push chosen notmuch-address-history)
-         (delete-region beg end)
-         (insert chosen))
-      (message "No matches.")
-      (ding))))
+  (when notmuch-address-command
+    (let* ((end (point))
+          (beg (save-excursion
+                 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+                 (goto-char (match-end 0))
+                 (point)))
+          (orig (buffer-substring-no-properties beg end))
+          (completion-ignore-case t)
+          (options (with-temp-message "Looking for completion candidates..."
+                     (notmuch-address-options orig)))
+          (num-options (length options))
+          (chosen (cond
+                   ((eq num-options 0)
+                    nil)
+                   ((eq num-options 1)
+                    (car options))
+                   (t
+                    (funcall notmuch-address-selection-function
+                             (format "Address (%s matches): " num-options)
+                             (cdr options) (car options))))))
+      (if chosen
+         (progn
+           (push chosen notmuch-address-history)
+           (delete-region beg end)
+           (insert chosen))
+       (message "No matches.")
+       (ding)))))
 
 ;; Copied from `w3m-which-command'.
 (defun notmuch-address-locate-command (command)
@@ -109,10 +168,82 @@ to know how address selection is made by default."
                           (not (file-directory-p bin))))
              (throw 'found-command bin))))))))
 
-;; If we can find the program specified by `notmuch-address-command',
-;; insinuate ourselves into `message-mode'.
-(when (notmuch-address-locate-command notmuch-address-command)
-  (notmuch-address-message-insinuate))
+(defun notmuch-address-harvest-addr (result)
+  (let ((name-addr (plist-get result :name-addr)))
+    (puthash name-addr t notmuch-address-completions)))
+
+(defun notmuch-address-harvest-handle-result (obj)
+  (notmuch-address-harvest-addr obj))
+
+(defun notmuch-address-harvest-filter (proc string)
+  (when (buffer-live-p (process-buffer proc))
+    (with-current-buffer (process-buffer proc)
+      (save-excursion
+       (goto-char (point-max))
+       (insert string))
+      (notmuch-sexp-parse-partial-list
+       'notmuch-address-harvest-handle-result (process-buffer proc)))))
+
+(defvar notmuch-address-harvest-procs '(nil . nil)
+  "The currently running harvests.
+
+The car is a partial harvest, and the cdr is a full harvest")
+
+(defun notmuch-address-harvest (&optional filter-query synchronous callback)
+  "Collect addresses completion candidates. It queries the
+notmuch database for all messages sent by the user optionally
+matching FILTER-QUERY (if not nil). It collects the destination
+addresses from those messages and stores them in
+`notmuch-address-completions'. Address harvesting may take some
+time so the address collection runs asynchronously unless
+SYNCHRONOUS is t. In case of asynchronous execution, CALLBACK is
+called when harvesting finishes."
+  (let* ((from-me-query (mapconcat (lambda (x) (concat "from:" x)) (notmuch-user-emails) " or "))
+        (query (if filter-query
+                   (format "(%s) and (%s)" from-me-query filter-query)
+                 from-me-query))
+        (args `("address" "--format=sexp" "--format-version=2"
+                "--output=recipients"
+                "--deduplicate=address"
+                ,query)))
+    (if synchronous
+       (mapc #'notmuch-address-harvest-addr
+                                  (apply 'notmuch-call-notmuch-sexp args))
+      ;; Asynchronous
+      (let* ((current-proc (if filter-query
+                              (car notmuch-address-harvest-procs)
+                            (cdr notmuch-address-harvest-procs)))
+            (proc-name (format "notmuch-address-%s-harvest"
+                               (if filter-query "partial" "full")))
+            (proc-buf (concat " *" proc-name "*")))
+       ;; Kill any existing process
+       (when current-proc
+         (kill-buffer (process-buffer current-proc))) ; this also kills the process
+
+       (setq current-proc
+             (apply 'notmuch-start-notmuch proc-name proc-buf
+                    callback                           ; process sentinel
+                    args))
+       (set-process-filter current-proc 'notmuch-address-harvest-filter)
+       (set-process-query-on-exit-flag current-proc nil)
+       (if filter-query
+           (setcar notmuch-address-harvest-procs current-proc)
+         (setcdr notmuch-address-harvest-procs current-proc)))))
+  ;; return value
+  nil)
+
+(defun notmuch-address-harvest-trigger ()
+  (let ((now (float-time)))
+    (when (> (- now notmuch-address-last-harvest) 86400)
+      (setq notmuch-address-last-harvest now)
+      (notmuch-address-harvest nil nil
+                              (lambda (proc event)
+                                ;; If harvest fails, we want to try
+                                ;; again when the trigger is next
+                                ;; called
+                                (if (string= event "finished\n")
+                                    (setq notmuch-address-full-harvest-finished t)
+                                  (setq notmuch-address-last-harvest 0)))))))
 
 ;;
 
diff --git a/emacs/notmuch-company.el b/emacs/notmuch-company.el
new file mode 100644 (file)
index 0000000..add3161
--- /dev/null
@@ -0,0 +1,86 @@
+;; notmuch-company.el --- Mail address completion for notmuch via company-mode  -*- lexical-binding: t -*-
+
+;; Authors: Trevor Jim <tjim@mac.com>
+;;         Michal Sojka <sojkam1@fel.cvut.cz>
+;;
+;; Keywords: mail, completion
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; To enable this, install company mode (https://company-mode.github.io/)
+;;
+;; NB company-minimum-prefix-length defaults to 3 so you don't get
+;; completion unless you type 3 characters
+
+;;; Code:
+
+(eval-when-compile (require 'cl))
+
+(defvar notmuch-company-last-prefix nil)
+(make-variable-buffer-local 'notmuch-company-last-prefix)
+(declare-function company-begin-backend "company")
+(declare-function company-grab "company")
+(declare-function company-mode "company")
+(declare-function company-manual-begin "company")
+(defvar company-backends)
+
+(declare-function notmuch-address-harvest "notmuch-address")
+(declare-function notmuch-address-harvest-trigger "notmuch-address")
+(declare-function notmuch-address-matching "notmuch-address")
+(defvar notmuch-address-full-harvest-finished)
+(defvar notmuch-address-completion-headers-regexp)
+
+;;;###autoload
+(defun notmuch-company-setup ()
+  (company-mode)
+  (make-local-variable 'company-backends)
+  (setq company-backends '(notmuch-company)))
+
+;;;###autoload
+(defun notmuch-company (command &optional arg &rest _ignore)
+  "`company-mode' completion back-end for `notmuch'."
+  (interactive (list 'interactive))
+  (require 'company)
+  (let ((case-fold-search t)
+       (completion-ignore-case t))
+    (case command
+      (interactive (company-begin-backend 'notmuch-company))
+      (prefix (and (derived-mode-p 'message-mode)
+                  (looking-back (concat notmuch-address-completion-headers-regexp ".*")
+                                (line-beginning-position))
+                  (setq notmuch-company-last-prefix (company-grab "[:,][ \t]*\\(.*\\)" 1 (point-at-bol)))))
+      (candidates (cond
+                  (notmuch-address-full-harvest-finished
+                   ;; Update harvested addressed from time to time
+                   (notmuch-address-harvest-trigger)
+                   (notmuch-address-matching arg))
+                  (t
+                   (cons :async
+                         (lambda (callback)
+                           ;; First run quick asynchronous harvest based on what the user entered so far
+                           (notmuch-address-harvest
+                            (format "to:%s*" arg) nil
+                            (lambda (_proc _event)
+                              (funcall callback (notmuch-address-matching arg))
+                              ;; Then start the (potentially long-running) full asynchronous harvest if necessary
+                              (notmuch-address-harvest-trigger))))))))
+      (match (if (string-match notmuch-company-last-prefix arg)
+                (match-end 0)
+              0))
+      (no-cache t))))
+
+
+(provide 'notmuch-company)
index dedcbeb865562903a2595e0a5883ef0e01a2d8c4..89c01a578ccdfe331599f21fc3174d1e15807923 100644 (file)
@@ -232,6 +232,9 @@ on the command line, and then retry your notmuch command")))
   "Return the user.other_email value (as a list) from the notmuch configuration."
   (split-string (notmuch-config-get "user.other_email") "\n" t))
 
+(defun notmuch-user-emails ()
+  (cons (notmuch-user-primary-email) (notmuch-user-other-email)))
+
 (defun notmuch-poll ()
   "Run \"notmuch new\" or an external script to import mail.
 
index 57465b205a60a7875a66fdbd39874ca56f2e376c..466edd7eb23cfca98b19401bd51bd42970f1163e 100644 (file)
@@ -269,7 +269,11 @@ Note that these functions use `mail-citation-hook' if that is non-nil."
   (set-buffer-modified-p nil))
 
 (define-derived-mode notmuch-message-mode message-mode "Message[Notmuch]"
-  "Notmuch message composition mode. Mostly like `message-mode'")
+  "Notmuch message composition mode. Mostly like `message-mode'"
+  (when notmuch-address-command
+    (notmuch-address-setup)))
+
+(put 'notmuch-message-mode 'flyspell-mode-predicate 'mail-mode-flyspell-verify)
 
 (define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)
 (define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send)
index 49fd198a7a5cc365b09c26b378b8f1f5e5aceeb2..3345878f513caac3c2e58e89905eacd64976e784 100644 (file)
@@ -353,8 +353,6 @@ operation on the contents of the current buffer."
                'message-header-cc)
               ((looking-at "[Ss]ubject:")
                'message-header-subject)
-              ((looking-at "[Ff]rom:")
-               'message-header-from)
               (t
                'message-header-other))))
 
@@ -1880,12 +1878,15 @@ to show, nil otherwise."
   "View the original source of the current message."
   (interactive)
   (let* ((id (notmuch-show-get-message-id))
-        (buf (get-buffer-create (concat "*notmuch-raw-" id "*"))))
-    (let ((coding-system-for-read 'no-conversion))
-      (call-process notmuch-command nil buf nil "show" "--format=raw" id))
+        (buf (get-buffer-create (concat "*notmuch-raw-" id "*")))
+        (inhibit-read-only t))
     (switch-to-buffer buf)
+    (erase-buffer)
+    (let ((coding-system-for-read 'no-conversion))
+      (call-process notmuch-command nil t nil "show" "--format=raw" id))
     (goto-char (point-min))
     (set-buffer-modified-p nil)
+    (setq buffer-read-only t)
     (view-buffer buf 'kill-buffer-if-not-modified)))
 
 (put 'notmuch-show-pipe-message 'notmuch-doc
index 5e86955d8dfd91b3880a5d53217b9265eb69c65e..3b342f136a5397b32096987d0ad3592c43e4fc63 100644 (file)
@@ -1635,6 +1635,9 @@ notmuch_database_begin_atomic (notmuch_database_t *notmuch)
        notmuch->atomic_nesting > 0)
        goto DONE;
 
+       if (notmuch_database_needs_upgrade(notmuch))
+               return NOTMUCH_STATUS_UPGRADE_REQUIRED;
+
     try {
        (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->begin_transaction (false);
     } catch (const Xapian::Error &error) {
index e81aa81902883b56f30b01e0aafa2476d8622737..f166aefd2fc1f6f6c5ea1e306e4fc033ead09c59 100644 (file)
@@ -377,7 +377,8 @@ _index_mime_part (notmuch_message_t *message,
 
     disposition = g_mime_object_get_content_disposition (part);
     if (disposition &&
-       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+       strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                   GMIME_DISPOSITION_ATTACHMENT) == 0)
     {
        const char *filename = g_mime_part_get_filename (GMIME_PART (part));
 
index 85b56bf1e4c34e96fe37e4df7a06eb4b762f2e75..310a8b8a313226b89455133b85feea139a944e37 100644 (file)
@@ -1752,7 +1752,7 @@ notmuch_filenames_t *
 notmuch_directory_get_child_files (notmuch_directory_t *directory);
 
 /**
- * Get a notmuch_filenams_t iterator listing all the filenames of
+ * Get a notmuch_filenames_t iterator listing all the filenames of
  * sub-directories in the database within the given directory.
  *
  * The returned filenames will be the basename-entries only (not
index 016fa12613c49c217b11fa8959c55a1e50a8e40f..4404cd7c33b870c9fa6c1a26c24143852b6e9c25 100755 (executable)
@@ -30,8 +30,8 @@ escape ()
     printf -v $2 '%s' "${__escape_arg__//\"/\\\"}"
 }
 
-EMACS=${EMACS-emacs}
-EMACSCLIENT=${EMACSCLIENT-emacsclient}
+EMACS=${EMACS:-emacs}
+EMACSCLIENT=${EMACSCLIENT:-emacsclient}
 
 PRINT_ONLY=
 NO_WINDOW=
index 1357142903d5771566678a132f15c6537e4e7714..6df54fc992bb39b2f61e115b83f53555e05c00a0 100644 (file)
@@ -80,7 +80,8 @@ format_part_reply (mime_node_t *node)
            show_text_part_content (node->part, stream_stdout, NOTMUCH_SHOW_TEXT_PART_REPLY);
            g_object_unref(stream_stdout);
        } else if (disposition &&
-                  strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0) {
+                  strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                              GMIME_DISPOSITION_ATTACHMENT) == 0) {
            const char *filename = g_mime_part_get_filename (GMIME_PART (node->part));
            printf ("Attachment: %s (%s)\n", filename,
                    g_mime_content_type_to_string (content_type));
index 5a83c6056ca5de85ef52b5dbb64359904dbba1a4..87e52bbc0e81a7f8b0c4c7ca5c374cfdbb5da068 100644 (file)
@@ -456,7 +456,8 @@ format_part_text (const void *ctx, sprinter_t *sp, mime_node_t *node,
            g_mime_part_get_filename (GMIME_PART (node->part)) : NULL;
 
        if (disposition &&
-           strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+           strcasecmp (g_mime_content_disposition_get_disposition (disposition),
+                       GMIME_DISPOSITION_ATTACHMENT) == 0)
            part_type = "attachment";
        else
            part_type = "part";
index c020cb6f40b8ad3677aac9fa6104ef3d4aba78b6..0d1532826a2e81999ba833d4b18fa800fc060d81 100644 (file)
@@ -237,10 +237,6 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[])
            fprintf (stderr, "Can't specify both cmdline and stdin!\n");
            return EXIT_FAILURE;
        }
-       if (remove_all) {
-           fprintf (stderr, "Can't specify both --remove-all and --batch\n");
-           return EXIT_FAILURE;
-       }
     } else {
        tag_ops = tag_op_list_create (config);
        if (tag_ops == NULL) {
index 821d39334e5c9684551c7dc4f03afa7502618b81..8adcabce0ea662ee90e548be2fa05af96c2fa811 100755 (executable)
@@ -38,6 +38,17 @@ test_expect_equal "$output" "\
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag5 tag6 unread)"
 
+test_begin_subtest "Remove all with batch"
+notmuch tag +tag1 One
+notmuch tag --remove-all --batch <<EOF
+-- One
++tag3 +tag4 +inbox -- Two
+EOF
+output=$(notmuch search \* | notmuch_search_sanitize)
+test_expect_equal "$output" "\
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One ()
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (inbox tag3 tag4)"
+
 test_begin_subtest "Remove all with a no-op"
 notmuch tag +inbox +tag1 +unread One
 notmuch tag --remove-all +foo +inbox +tag1 -foo +unread Two
@@ -102,6 +113,20 @@ notmuch search \* | notmuch_search_sanitize > OUTPUT
 notmuch restore --format=batch-tag < backup.tags
 test_expect_equal_file batch.expected OUTPUT
 
+test_begin_subtest "--batch --input --remove-all"
+notmuch dump --format=batch-tag > backup.tags
+notmuch tag +foo +bar -- One
+notmuch tag +tag7 -- Two
+notmuch tag --batch --input=batch.in --remove-all
+notmuch search \* | notmuch_search_sanitize > OUTPUT
+notmuch restore --format=batch-tag < backup.tags
+cat > batch_removeall.expected <<EOF
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; One (@ tag6)
+thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Two (tag6)
+EOF
+test_expect_equal_file batch_removeall.expected OUTPUT
+rm batch_removeall.expected
+
 test_begin_subtest "--batch, blank lines and comments"
 notmuch dump | sort > EXPECTED
 notmuch tag --batch <<EOF
index 7c4c9f71913fc695a29a668294d5ece21cf6f0d5..35678909b0ce110ca650c108a9478aae4139e5dd 100755 (executable)
@@ -763,4 +763,56 @@ test_begin_subtest "indexes mime-type #3"
 output=$(notmuch search from:todd and mimetype:multipart/alternative | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2014-01-12 [1/1] Todd; odd content types (inbox unread)"
 
+test_begin_subtest "case of Content-Disposition doesn't matter for indexing"
+cat <<EOF > ${MAIL_DIR}/content-disposition
+Return-path: <david@tethera.net>
+Envelope-to: david@tethera.net
+Delivery-date: Sun, 04 Oct 2015 09:16:03 -0300
+Received: from gitolite.debian.net ([87.98.215.224])
+       by yantan.tethera.net with esmtps (TLS1.2:DHE_RSA_AES_128_CBC_SHA1:128)
+       (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiCx-0007iz-RK
+       for david@tethera.net; Sun, 04 Oct 2015 09:16:03 -0300
+Received: from remotemail by gitolite.debian.net with local (Exim 4.80)
+       (envelope-from <david@tethera.net>)
+       id 1ZiiC8-0002Rz-Uf; Sun, 04 Oct 2015 12:15:12 +0000
+Received: (nullmailer pid 28621 invoked by uid 1000); Sun, 04 Oct 2015
+ 12:14:53 -0000
+From: David Bremner <david@tethera.net>
+To: David Bremner <david@tethera.net>
+Subject: test attachment
+User-Agent: Notmuch/0.20.2+93~g33c8777 (http://notmuchmail.org) Emacs/24.5.1
+ (x86_64-pc-linux-gnu)
+Date: Sun, 04 Oct 2015 09:14:53 -0300
+Message-ID: <87io6m96f6.fsf@zancas.localnet>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="=-=-="
+
+--=-=-=
+Content-Type: text/plain
+Content-Disposition: ATTACHMENT; filename=hello.txt
+Content-Description: this is a very exciting file
+
+hello
+
+--=-=-=
+Content-Type: text/plain
+
+
+world
+
+--=-=-=--
+
+EOF
+NOTMUCH_NEW
+
+cat <<EOF > EXPECTED
+attachment
+inbox
+unread
+EOF
+
+notmuch search --output=tags id:87io6m96f6.fsf@zancas.localnet > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
 test_done
diff --git a/test/T580-thread-search.sh b/test/T580-thread-search.sh
new file mode 100755 (executable)
index 0000000..6f7106d
--- /dev/null
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+#
+# Copyright (c) 2015 David Bremner
+#
+
+test_description='test of searching by thread-id'
+
+. ./test-lib.sh || exit 1
+
+add_email_corpus
+
+test_begin_subtest "Every message is found in exactly one thread"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    matches=$(notmuch search --output=threads "$id" | wc -l)
+    if [ "$matches" = 1 ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+test_begin_subtest "roundtripping message-ids via thread-ids"
+
+count=0
+success=0
+for id in $(notmuch search --output=messages '*'); do
+    count=$((count +1))
+    thread=$(notmuch search --output=threads "$id")
+    matched=$(notmuch search --output=messages "$thread" | grep "$id")
+    if [ "$matched" = "$id" ]; then
+       success=$((success + 1))
+    fi
+done
+
+test_expect_equal "$count" "$success"
+
+
+test_done