]> git.notmuchmail.org Git - notmuch/commitdiff
Merge tag '0.25.1'
authorDavid Bremner <david@tethera.net>
Tue, 12 Sep 2017 01:32:00 +0000 (22:32 -0300)
committerDavid Bremner <david@tethera.net>
Tue, 12 Sep 2017 01:32:00 +0000 (22:32 -0300)
notmuch 0.25.1 release (bugfix for emacs bug 28350)

86 files changed:
.gitignore
.travis.yml
Makefile.local
bindings/python/.gitignore
bindings/python/README
bindings/python/docs/source/database.rst
bindings/python/notmuch/database.py
bindings/python/notmuch/directory.py
bindings/python/notmuch/message.py
bindings/ruby/.gitignore
bindings/ruby/database.c
compat/.gitignore
completion/notmuch-completion.bash
configure
contrib/go/.gitignore
contrib/go/src/notmuch/notmuch.go
contrib/notmuch-mutt/.gitignore
debian/.gitignore
devel/nmbug/doc/.gitignore
doc/.gitignore
doc/conf.py
doc/index.rst
doc/man1/notmuch-reindex.rst [new file with mode: 0644]
doc/man1/notmuch-search.rst
doc/man1/notmuch.rst
doc/man7/notmuch-search-terms.rst
emacs/.gitignore
emacs/notmuch-lib.el
emacs/notmuch-show.el
emacs/notmuch-tag.el
emacs/notmuch-tree.el
emacs/notmuch.el
gmime-filter-reply.c
lib/Makefile.local
lib/add-message.cc [new file with mode: 0644]
lib/database-private.h
lib/database.cc
lib/index.cc
lib/indexopts.c [new file with mode: 0644]
lib/message-file.c
lib/message-id.c [new file with mode: 0644]
lib/message.cc
lib/notmuch-private.h
lib/notmuch.h
lib/string-list.c
lib/thread.cc
mime-node.c
notmuch-client.h
notmuch-count.c
notmuch-insert.c
notmuch-new.c
notmuch-reindex.c [new file with mode: 0644]
notmuch-reply.c
notmuch-search.c
notmuch-show.c
notmuch-tag.c
notmuch.c
performance-test/.gitignore
performance-test/M05-reindex.sh [new file with mode: 0755]
performance-test/M06-insert.sh [new file with mode: 0755]
performance-test/T03-reindex.sh [new file with mode: 0755]
performance-test/download/.gitignore
test/.gitignore
test/T000-basic.sh
test/T050-new.sh
test/T070-insert.sh
test/T080-search.sh
test/T100-search-by-folder.sh
test/T160-json.sh
test/T170-sexp.sh
test/T340-maildir-sync.sh
test/T350-crypto.sh
test/T370-search-folder-coherence.sh
test/T500-search-date.sh
test/T560-lib-error.sh
test/T650-regexp-query.sh
test/T670-duplicate-mid.sh [new file with mode: 0755]
test/T700-reindex.sh [new file with mode: 0755]
test/notmuch-test
test/smtp-dummy.c
test/test-databases/.gitignore
test/test-lib-common.sh
test/test-lib.sh
util/error_util.c
util/string-util.c
util/string-util.h

index 7b283fb3bff2482738b29e5a502596ed2176ec4b..e06101ce51c67c65d9118b198b4c165501b0d6e5 100644 (file)
@@ -1,16 +1,17 @@
-.first-build-message
-Makefile.config
-sh.config
-version.stamp
+/.first-build-message
+/Makefile.config
+/sh.config
+/version.stamp
 TAGS
 tags
 *cscope*
-.deps
+/.deps
 /notmuch
-notmuch-shared
-libnotmuch.so*
-libnotmuch*.dylib
+/notmuch-shared
+/lib/libnotmuch.so*
+/lib/libnotmuch*.dylib
 *.[ao]
 *~
 .*.swp
-releases
+/releases
+/.stamps
index dbd6434e6ab3dcc66b3a52626f983b5e6960a9db..802efd98c30d2eeef2963ad210368a62091b5e5a 100644 (file)
@@ -1,19 +1,22 @@
 language: c
-before_install:
-  - sudo apt-get update -qq
-  - sudo apt-get install dtach libxapian-dev libgmime-2.6-dev libtalloc-dev python-sphinx
 
-  # Notmuch requires zlib 1.2.5.2, unfortunately travis runs on Ubuntu 12.04LTS which
-  # ships with zlib 1.2.3.3. We need to update to zlib 1.2.5.2 to be able to build.
-  # TODO: Watch https://github.com/travis-ci/travis-ci/issues/2046 and remove
-  #       this hack once travis-ci switches to Ubuntu 14.04
-  - wget 'https://github.com/notmuch/travis-files/raw/master/zlib1g-dev_1.2.8.dfsg-1ubuntu1_amd64.deb'
-  - wget 'https://github.com/notmuch/travis-files/raw/master/zlib1g_1.2.8.dfsg-1ubuntu1_amd64.deb'
-  - sudo dpkg -i zlib1g-dev_1.2.8.dfsg-1ubuntu1_amd64.deb zlib1g_1.2.8.dfsg-1ubuntu1_amd64.deb
-  - sudo apt-get install -f
+dist: trusty
+sudo: false
+
+addons:
+  apt:
+    packages:
+    - dtach
+    - libxapian-dev
+    - libgmime-2.6-dev
+    - libtalloc-dev
+    - python-sphinx
+    - gdb
+    - gpgsm
 
 script:
   - ./configure
+  - make download-test-databases
   - make test
 
 notifications:
index 6bc78ef8e969b2d8649d68de6b590e8ded886cec..9d9c52c26d9e85d7eac1420b0002e9b6dd9e1872 100644 (file)
@@ -182,14 +182,14 @@ verify-newer:
 # user how to enable verbose compiles.
 ifeq ($(V),)
 quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
-quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
 endif
 # The user has explicitly enabled quiet compilation.
 ifeq ($(V),0)
-quiet = @printf "$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+quiet = @printf "$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
 endif
 # Otherwise, print the full command line.
-quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
+quiet ?= $($(word 1, $(1)))
 
 %.o: %.cc $(global_deps)
        @mkdir -p $(patsubst %/.,%,.deps/$(@D))
@@ -199,9 +199,17 @@ quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
        @mkdir -p $(patsubst %/.,%,.deps/$(@D))
        $(call quiet,CC $(CPPFLAGS) $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
 
+CPPCHECK=cppcheck
+.stamps/cppcheck/%: %
+       @mkdir -p $(@D)
+       $(call quiet,CPPCHECK,$<) --template=gcc --error-exitcode=1 --quiet $<
+       @touch $@
+
+CLEAN := $(CLEAN) .stamps
+
 .PHONY : clean
 clean:
-       rm -rf $(CLEAN); rm -rf .deps
+       rm -rf $(CLEAN)
 
 .PHONY: distclean
 distclean: clean
@@ -225,6 +233,7 @@ notmuch_client_srcs =               \
        notmuch-dump.c          \
        notmuch-insert.c        \
        notmuch-new.c           \
+       notmuch-reindex.c       \
        notmuch-reply.c         \
        notmuch-restore.c       \
        notmuch-search.c        \
@@ -279,9 +288,20 @@ endif
 SRCS  := $(SRCS) $(notmuch_client_srcs)
 CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
 CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
+CLEAN := $(CLEAN) .deps
 
 DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config
 
+CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%)
+.PHONY: cppcheck
+ifeq ($(HAVE_CPPCHECK),1)
+cppcheck: ${CPPCHECK_STAMPS}
+else
+cppcheck:
+       @echo "No cppcheck found during configure; skipping static checking"
+endif
+
+
 DEPS := $(SRCS:%.c=.deps/%.d)
 DEPS := $(DEPS:%.cc=.deps/%.d)
 -include $(DEPS)
index da0732e90b31349143a60dd4f273776481efe80e..601acdd79095677e43b0c2683debc1ab6d588fa6 100644 (file)
@@ -1,4 +1,4 @@
 *.py[co]
 /docs/build
 /docs/html
-build/
+/build/
index fe7a21819d67f3ad0be98573675e998971c7f834..5bf076d262f93efadbf971f8fcddd97636ef2a33 100644 (file)
@@ -3,7 +3,7 @@ notmuch -- The python interface to notmuch
 
 This module makes the functionality of the notmuch library
 (`https://notmuchmail.org`_) available to python. Successful import of
-this modul depends on a libnotmuch.so|dll being available on the
+this module depends on a libnotmuch.so|dll being available on the
 user's system.
 
 If you have downloaded the full source tarball, you can create the
index 5f1cdc14b3f6dcbf107d4b708bf7a9228fdbdd7c..079dc75492f589f0a5357bbc90bddb20e1415039 100644 (file)
@@ -25,7 +25,7 @@
 
    .. automethod:: get_directory
 
-   .. automethod:: add_message
+   .. automethod:: index_file
 
    .. automethod:: remove_message
 
index 8f918069f34d24ad5059a3a89e0a5e55cd8a8b95..a2c025ebe6c068247530c11512e34b89768e93ef 100644 (file)
@@ -285,7 +285,7 @@ class Database(object):
         """Does this database need to be upgraded before writing to it?
 
         If this function returns `True` then no functions that modify the
-        database (:meth:`add_message`,
+        database (:meth:`index_file`,
         :meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
         etc.) will work unless :meth:`upgrade` is called successfully first.
 
@@ -399,12 +399,13 @@ class Database(object):
         # return the Directory, init it with the absolute path
         return Directory(abs_dirpath, dir_p, self)
 
-    _add_message = nmlib.notmuch_database_add_message
-    _add_message.argtypes = [NotmuchDatabaseP, c_char_p,
+    _index_file = nmlib.notmuch_database_index_file
+    _index_file.argtypes = [NotmuchDatabaseP, c_char_p,
+                             c_void_p,
                              POINTER(NotmuchMessageP)]
-    _add_message.restype = c_uint
+    _index_file.restype = c_uint
 
-    def add_message(self, filename, sync_maildir_flags=False):
+    def index_file(self, filename, sync_maildir_flags=False):
         """Adds a new message to the database
 
         :param filename: should be a path relative to the path of the
@@ -455,7 +456,7 @@ class Database(object):
         """
         self._assert_db_is_initialized()
         msg_p = NotmuchMessageP()
-        status = self._add_message(self._db, _str(filename), byref(msg_p))
+        status = self._index_file(self._db, _str(filename), c_void_p(None), byref(msg_p))
 
         if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
             raise NotmuchError(status)
@@ -467,6 +468,11 @@ class Database(object):
             msg.maildir_flags_to_tags()
         return (msg, status)
 
+    def add_message(self, filename, sync_maildir_flags=False):
+        """Deprecated alias for :meth:`index_file`
+        """
+        self.index_file(self, filename, sync_maildir_flags=sync_maildir_flags)
+
     _remove_message = nmlib.notmuch_database_remove_message
     _remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
     _remove_message.restype = c_uint
index 7f86b1ac15588aac8836d3bd31dcb5a3a945dc0c..b30c9e35e6427322630e0c7afd118e97e09484f0 100644 (file)
@@ -93,7 +93,7 @@ class Directory(object):
 
         * Read the mtime of a directory from the filesystem
 
-        * Call :meth:`Database.add_message` for all mail files in
+        * Call :meth:`Database.index_file` for all mail files in
           the directory
 
         * Call notmuch_directory_set_mtime with the mtime read from the
index fc177eb2b1557cb68c7c5c17bcedaf974b563869..cce377d02a8024608bcf840252faae191d937acb 100644 (file)
@@ -566,7 +566,7 @@ class Message(Python3StringMixIn):
         logical OR operator.)
 
         As a convenience, you can set the sync_maildir_flags parameter in
-        :meth:`Database.add_message` to implicitly call this.
+        :meth:`Database.index_file` to implicitly call this.
 
         :returns: a :class:`STATUS`. In short, you want to see
             notmuch.STATUS.SUCCESS here. See there for details."""
index d682798abfe6421881c0f113fbecaae3c455abb6..c57ae63f43921f026ac32cf3c222c8d1b3530431 100644 (file)
@@ -1,7 +1,7 @@
 # .gitignore for bindings/ruby
 
 # Generated files
-Makefile
-mkmf.log
-notmuch.so
+/Makefile
+/mkmf.log
+/notmuch.so
 *.o
index 12e6bab7b095388e356e25507146283b3da61e03..416eb709f19d231bfdf6460c37a8e2262fad24af 100644 (file)
@@ -291,7 +291,7 @@ notmuch_rb_database_add_message (VALUE self, VALUE pathv)
     SafeStringValue (pathv);
     path = RSTRING_PTR (pathv);
 
-    ret = notmuch_database_add_message (db, path, &message);
+    ret = notmuch_database_index_file (db, path, NULL, &message);
     notmuch_rb_status_raise (ret);
     return rb_assoc_new (Data_Wrap_Struct (notmuch_rb_cMessage, NULL, NULL, message),
         (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse);
index 107ba17aaeba555153a95cbdc8e3d94fe0619e61..7ede45e93d50148284a77d35bc7d07f22ccff976 100644 (file)
@@ -1 +1 @@
-zlib.pc
+/zlib.pc
index e4e4b36bccf723ead969dd8c48bb01cccfc96682..5201be6378ddcce745d7734da5b0a2d57928a1ce 100644 (file)
@@ -417,6 +417,24 @@ _notmuch_search()
     esac
 }
 
+_notmuch_reindex()
+{
+    local cur prev words cword split
+    _init_completion -s || return
+
+    ! $split &&
+    case "${cur}" in
+       -*)
+           local options="${_notmuch_shared_options}"
+           compopt -o nospace
+           COMPREPLY=( $(compgen -W "$options" -- ${cur}) )
+           ;;
+       *)
+           _notmuch_search_terms
+           ;;
+    esac
+}
+
 _notmuch_address()
 {
     local cur prev words cword split
@@ -532,7 +550,7 @@ _notmuch_tag()
 
 _notmuch()
 {
-    local _notmuch_commands="compact config count dump help insert new reply restore search address setup show tag emacs-mua"
+    local _notmuch_commands="compact config count dump help insert new reply restore reindex search address setup show tag emacs-mua"
     local arg cur prev words cword split
 
     # require bash-completion with _init_completion
index c5e2ffed9e6705a9b16f5cf52f3c57235bc40044..c0a550c7f9fd74402ab42974075ddd40ac88cc47 100755 (executable)
--- a/configure
+++ b/configure
@@ -646,6 +646,15 @@ if [ $WITH_DESKTOP = "1" ]; then
     fi
 fi
 
+printf "Checking for cppcheck... "
+if command -v cppcheck > /dev/null; then
+    have_cppcheck=1
+    printf "Yes.\n"
+else
+    have_cppcheck=0
+    printf "No.\n"
+fi
+
 libdir_in_ldconfig=0
 
 printf "Checking which platform we are on... "
@@ -1065,6 +1074,9 @@ zsh_completion_dir = ${ZSHCOMLETIONDIR:=\$(prefix)/share/zsh/functions/Completio
 # build its own version)
 HAVE_CANONICALIZE_FILE_NAME = ${have_canonicalize_file_name}
 
+# Whether the cppcheck static checker is available
+HAVE_CPPCHECK = ${have_cppcheck}
+
 # Whether the getline function is available (if not, then notmuch will
 # build its own version)
 HAVE_GETLINE = ${have_getline}
index c394479fe7cba2be7384954122f88c5f1f839ac3..223504b17147446a4ff16f128d1f1fd6b6e4b629 100644 (file)
@@ -1,3 +1,3 @@
-src/github.com/
-pkg/
-bin/
+/src/github.com/
+/pkg/
+/bin/
index 2d6843112ae0ba5cec49ae068cdb4070442f35a6..89093b2da34dab037e4ccba6f4f7f767fe8564d3 100644 (file)
@@ -170,7 +170,7 @@ func (self *Database) GetVersion() uint {
 /* Does this database need to be upgraded before writing to it?
  *
  * If this function returns TRUE then no functions that modify the
- * database (notmuch_database_add_message, notmuch_message_add_tag,
+ * database (notmuch_database_index_file, notmuch_message_add_tag,
  * notmuch_directory_set_mtime, etc.) will work unless the function
  * notmuch_database_upgrade is called successfully first. */
 func (self *Database) NeedsUpgrade() bool {
index 682a57793ce75520cd975f32333229dfdbb37666..116bb715b681d16c25ec0f715db6fd1f5be26e3d 100644 (file)
@@ -1,2 +1,2 @@
-notmuch-mutt.1
-README.html
+/notmuch-mutt.1
+/README.html
index e8c2e82a0247b187671752e7053c4b73ebd48526..ab384182257a1fa2aa76efb8553186e98c9201fc 100644 (file)
@@ -1,14 +1,14 @@
-tmp/
-libnotmuch-dev/
-libnotmuch*/
-notmuch-emacs/
-notmuch/
-notmuch-dbg/
-notmuch-mutt/
-notmuch-vim/
-ruby-notmuch/
-python*-notmuch/
-*.debhelper
-*.debhelper.log
-*.substvars
-files
+/tmp/
+/libnotmuch-dev/
+/libnotmuch*/
+/notmuch-emacs/
+/notmuch/
+/notmuch-dbg/
+/notmuch-mutt/
+/notmuch-vim/
+/ruby-notmuch/
+/python*-notmuch/
+/*.debhelper
+/*.debhelper.log
+/*.substvars
+/files
index 4930881a69ad694c5401d9f71f38ca3b98ce0d20..f25d695cccbf924085af44ed3f67e57c7e993103 100644 (file)
@@ -1,2 +1,2 @@
 *.pyc
-_build
+/_build
index 9fa35d08a95e3c850ceafccdea523f310f775e43..bbb749fabda3b6820db81a46330440df6052006a 100644 (file)
@@ -1,3 +1,3 @@
 *.pyc
-_build
-config.dox
+/_build
+/config.dox
index a3d8269696a366dca12b1caa4a8d67e6005d993c..0e65413d917aec68b71c8314b280e7f729de661a 100644 (file)
@@ -47,6 +47,10 @@ html_static_path = []
 # Output file base name for HTML help builder.
 htmlhelp_basename = 'notmuchdoc'
 
+# Disable SmartyPants, as it mangles command lines.
+# Despite the name, this actually affects manual pages as well.
+html_use_smartypants = False
+
 # -- Options for manual page output ---------------------------------------
 
 # One entry per manual page. List of tuples
@@ -95,6 +99,10 @@ man_pages = [
      u'incorporate new mail into the notmuch database',
      [notmuch_authors], 1),
 
+    ('man1/notmuch-reindex', 'notmuch-reindex',
+     u're-index matching messages',
+     [notmuch_authors], 1),
+
     ('man1/notmuch-reply', 'notmuch-reply',
      u'constructs a reply template for a set of messages',
      [notmuch_authors], 1),
index 344606d9e1814434dac78a722a138bb204adf19f..aa6c9f40462c5c89afae747041ddbf9075a4e157 100644 (file)
@@ -18,6 +18,7 @@ Contents:
    man5/notmuch-hooks
    man1/notmuch-insert
    man1/notmuch-new
+   man1/notmuch-reindex
    man1/notmuch-reply
    man1/notmuch-restore
    man1/notmuch-search
diff --git a/doc/man1/notmuch-reindex.rst b/doc/man1/notmuch-reindex.rst
new file mode 100644 (file)
index 0000000..e39cc4e
--- /dev/null
@@ -0,0 +1,29 @@
+===============
+notmuch-reindex
+===============
+
+SYNOPSIS
+========
+
+**notmuch** **reindex** [*option* ...] <*search-term*> ...
+
+DESCRIPTION
+===========
+
+Re-index all messages matching the search terms.
+
+See **notmuch-search-terms(7)** for details of the supported syntax for
+<*search-term*\ >.
+
+The **reindex** command searches for all messages matching the
+supplied search terms, and re-creates the full-text index on these
+messages using the supplied options.
+
+SEE ALSO
+========
+
+**notmuch(1)**, **notmuch-config(1)**, **notmuch-count(1)**,
+**notmuch-dump(1)**, **notmuch-hooks(5)**, **notmuch-insert(1)**,
+**notmuch-new(1)**,
+**notmuch-reply(1)**, **notmuch-restore(1)**, **notmuch-search(1)**,
+**notmuch-search-terms(7)**, **notmuch-show(1)**, **notmuch-tag(1)**
index 4e660a6facbbb25295688795a28c8b7991627809..fd6dcadd6bded884ff55514b7ea98f30eee5c1dd 100644 (file)
@@ -42,7 +42,9 @@ Supported options for **search** include
             the search terms. The summary includes the thread ID, date,
             the number of messages in the thread (both the number
             matched and the total number), the authors of the thread and
-            the subject.
+           the subject. In the case where a thread contains multiple files for
+           some messages, the total number of files is printed in parentheses
+           (see below for an example).
 
         **threads**
             Output the thread IDs of all threads with any message
@@ -135,6 +137,19 @@ Supported options for **search** include
         prefix. The prefix matches messages based on filenames. This
         option filters filenames of the matching messages.
 
+EXAMPLE
+=======
+
+The following shows an example of the summary output format, with one
+message having multiple filenames.
+
+::
+
+  % notmuch search date:today.. and tag:bad-news
+  thread:0000000000063c10 Today [1/1] Some Persun; To the bone (inbox unread)
+  thread:0000000000063c25 Today [1/1(2)] Ann Other; Bears (inbox unread)
+  thread:0000000000063c00 Today [1/1] A Thurd; Bites, stings, sad feelings (inbox unread)
+
 EXIT STATUS
 ===========
 
index cb350d1a19767e4ac354925b525f8fc2c0aa25f9..40fd335bd8240c1eeb3b7c96a267fa19e6f624b8 100644 (file)
@@ -163,8 +163,8 @@ SEE ALSO
 
 **notmuch-address(1)**, **notmuch-compact(1)**, **notmuch-config(1)**,
 **notmuch-count(1)**, **notmuch-dump(1)**, **notmuch-hooks(5)**,
-**notmuch-insert(1)**, **notmuch-new(1)**, **notmuch-reply(1)**,
-**notmuch-restore(1)**, **notmuch-search(1)**,
+**notmuch-insert(1)**, **notmuch-new(1)**, **notmuch-reindex(1)**,
+**notmuch-reply(1)**, **notmuch-restore(1)**, **notmuch-search(1)**,
 **notmuch-search-terms(7)**, **notmuch-show(1)**, **notmuch-tag(1)**
 
 The notmuch website: **https://notmuchmail.org**
index 47cab48d3ee97df0d9db80028a586c40fd1a3b2f..dd76972ee3893fe369d508fd4f7e92ec5ad2622a 100644 (file)
@@ -9,6 +9,8 @@ SYNOPSIS
 
 **notmuch** **dump** [--format=(batch-tag|sup)] [--] [--output=<*file*>] [--] [<*search-term*> ...]
 
+**notmuch** **reindex** [option ...] <*search-term*> ...
+
 **notmuch** **search** [option ...] <*search-term*> ...
 
 **notmuch** **show** [option ...] <*search-term*> ...
@@ -421,5 +423,6 @@ SEE ALSO
 
 **notmuch(1)**, **notmuch-config(1)**, **notmuch-count(1)**,
 **notmuch-dump(1)**, **notmuch-hooks(5)**, **notmuch-insert(1)**,
-**notmuch-new(1)**, **notmuch-reply(1)**, **notmuch-restore(1)**,
-**notmuch-search(1)**, **notmuch-show(1)**, **notmuch-tag(1)**
+**notmuch-new(1)**, **notmuch-reindex(1)**, **notmuch-reply(1)**,
+**notmuch-restore(1)**, **notmuch-search(1)**, **notmuch-show(1)**,
+**notmuch-tag(1)**
index 8e15eed7111806cad24eaa0185d73cea6b23e83a..fbf8dde6f1760c409036d6b9b473a59d3c943ae6 100644 (file)
@@ -1,4 +1,4 @@
-.eldeps*
-*.elc
-notmuch-version.el
-notmuch-pkg.el
+/.eldeps*
+/*.elc
+/notmuch-version.el
+/notmuch-pkg.el
index 337b20ace74f36b91b5ce76614f7fc3552a54ac5..010be454e77420c2f945fdfc3a7895283a8d7209 100644 (file)
@@ -768,23 +768,23 @@ signaled error.  This function does not return."
   (error "%s" (concat msg (when extra
                            " (see *Notmuch errors* for more details)"))))
 
-(defun notmuch-check-async-exit-status (proc msg &optional command err-file)
+(defun notmuch-check-async-exit-status (proc msg &optional command err)
   "If PROC exited abnormally, pop up an error buffer and signal an error.
 
 This is a wrapper around `notmuch-check-exit-status' for
 asynchronous process sentinels.  PROC and MSG must be the
-arguments passed to the sentinel.  COMMAND and ERR-FILE, if
-provided, are passed to `notmuch-check-exit-status'.  If COMMAND
-is not provided, it is taken from `process-command'."
+arguments passed to the sentinel.  COMMAND and ERR, if provided,
+are passed to `notmuch-check-exit-status'.  If COMMAND is not
+provided, it is taken from `process-command'."
   (let ((exit-status
         (case (process-status proc)
           ((exit) (process-exit-status proc))
           ((signal) msg))))
     (when exit-status
       (notmuch-check-exit-status exit-status (or command (process-command proc))
-                                nil err-file))))
+                                nil err))))
 
-(defun notmuch-check-exit-status (exit-status command &optional output err-file)
+(defun notmuch-check-exit-status (exit-status command &optional output err)
   "If EXIT-STATUS is non-zero, pop up an error buffer and signal an error.
 
 If EXIT-STATUS is non-zero, pop up a notmuch error buffer
@@ -793,9 +793,9 @@ be a number indicating the exit status code of a process or a
 string describing the signal that terminated the process (such as
 returned by `call-process').  COMMAND must be a list giving the
 command and its arguments.  OUTPUT, if provided, is a string
-giving the output of command.  ERR-FILE, if provided, is the name
-of a file containing the error output of command.  OUTPUT and the
-contents of ERR-FILE will be included in the error message."
+giving the output of command.  ERR, if provided, is the error
+output of command.  OUTPUT and ERR will be included in the error
+message."
 
   (cond
    ((eq exit-status 0) t)
@@ -808,12 +808,7 @@ You may need to restart Emacs or upgrade your notmuch Emacs package."))
 Emacs requested a newer output format than supported by the notmuch CLI.
 You may need to restart Emacs or upgrade your notmuch package."))
    (t
-    (let* ((err (when err-file
-                 (with-temp-buffer
-                   (insert-file-contents err-file)
-                   (unless (eobp)
-                     (buffer-string)))))
-          (command-string
+    (let* ((command-string
            (mapconcat (lambda (arg)
                         (shell-quote-argument
                          (cond ((stringp arg) arg)
@@ -889,9 +884,13 @@ error."
   (with-temp-buffer
     (let ((err-file (make-temp-file "nmerr")))
       (unwind-protect
-         (let ((status (notmuch-call-notmuch--helper (list t err-file) args)))
+         (let ((status (notmuch-call-notmuch--helper (list t err-file) args))
+               (err (with-temp-buffer
+                      (insert-file-contents err-file)
+                      (unless (eobp)
+                        (buffer-string)))))
            (notmuch-check-exit-status status (cons notmuch-command args)
-                                      (buffer-string) err-file)
+                                      (buffer-string) err)
            (goto-char (point-min))
            (read (current-buffer)))
        (delete-file err-file)))))
@@ -910,30 +909,56 @@ invoke `set-process-sentinel' directly on the returned process,
 as that will interfere with the handling of stderr and the exit
 status."
 
-  ;; There is no way (as of Emacs 24.3) to capture stdout and stderr
-  ;; separately for asynchronous processes, or even to redirect stderr
-  ;; to a file, so we use a trivial shell wrapper to send stderr to a
-  ;; temporary file and clean things up in the sentinel.
-  (let* ((err-file (make-temp-file "nmerr"))
-        ;; Use a pipe
-        (process-connection-type nil)
-        ;; Find notmuch using Emacs' `exec-path'
-        (command (or (executable-find notmuch-command)
-                     (error "command not found: %s" notmuch-command)))
-        (proc (apply #'start-process name buffer
-                     "/bin/sh" "-c"
-                     "exec 2>\"$1\"; shift; exec \"$0\" \"$@\""
-                     command err-file args)))
-    (process-put proc 'err-file err-file)
+  (let (err-file err-buffer proc
+       ;; Find notmuch using Emacs' `exec-path'
+       (command (or (executable-find notmuch-command)
+                    (error "Command not found: %s" notmuch-command))))
+    (if (fboundp 'make-process)
+       (progn
+         (setq err-buffer (generate-new-buffer " *notmuch-stderr*"))
+         ;; Emacs 25 and newer has `make-process', which allows
+         ;; redirecting stderr independently from stdout to a
+         ;; separate buffer. As this allows us to avoid using a
+         ;; temporary file and shell invocation, use it when
+         ;; available.
+         (setq proc (make-process
+                     :name name
+                     :buffer buffer
+                     :command (cons command args)
+                     :connection-type 'pipe
+                     :stderr err-buffer))
+         (process-put proc 'err-buffer err-buffer)
+         ;; Silence "Process NAME stderr finished" in stderr by adding a
+         ;; no-op sentinel to the fake stderr process object
+         (set-process-sentinel (get-buffer-process err-buffer) #'ignore))
+
+      ;; On Emacs versions before 25, there is no way to capture
+      ;; stdout and stderr separately for asynchronous processes, or
+      ;; even to redirect stderr to a file, so we use a trivial shell
+      ;; wrapper to send stderr to a temporary file and clean things
+      ;; up in the sentinel.
+      (setq err-file (make-temp-file "nmerr"))
+      (let ((process-connection-type nil)) ;; Use a pipe
+       (setq proc (apply #'start-process name buffer
+                         "/bin/sh" "-c"
+                         "exec 2>\"$1\"; shift; exec \"$0\" \"$@\""
+                         command err-file args)))
+      (process-put proc 'err-file err-file))
+
     (process-put proc 'sub-sentinel sentinel)
     (process-put proc 'real-command (cons notmuch-command args))
     (set-process-sentinel proc #'notmuch-start-notmuch-sentinel)
     proc))
 
 (defun notmuch-start-notmuch-sentinel (proc event)
-  (let ((err-file (process-get proc 'err-file))
-       (sub-sentinel (process-get proc 'sub-sentinel))
-       (real-command (process-get proc 'real-command)))
+  "Process sentinel function used by `notmuch-start-notmuch'."
+  (let* ((err-file (process-get proc 'err-file))
+        (err-buffer (or (process-get proc 'err-buffer)
+                        (find-file-noselect err-file)))
+        (err (when (not (zerop (buffer-size err-buffer)))
+               (with-current-buffer err-buffer (buffer-string))))
+        (sub-sentinel (process-get proc 'sub-sentinel))
+        (real-command (process-get proc 'real-command)))
     (condition-case err
        (progn
          ;; Invoke the sub-sentinel, if any
@@ -945,12 +970,13 @@ status."
          ;; and there's no point in telling the user that (but we
          ;; still check for and report stderr output below).
          (when (buffer-live-p (process-buffer proc))
-           (notmuch-check-async-exit-status proc event real-command err-file))
+           (notmuch-check-async-exit-status proc event real-command err))
          ;; If that didn't signal an error, then any error output was
          ;; really warning output.  Show warnings, if any.
          (let ((warnings
-                (with-temp-buffer
-                  (unless (= (second (insert-file-contents err-file)) 0)
+                (when err
+                  (with-current-buffer err-buffer
+                    (goto-char (point-min))
                     (end-of-line)
                     ;; Show first line; stuff remaining lines in the
                     ;; errors buffer.
@@ -964,7 +990,8 @@ status."
        ;; Emacs behaves strangely if an error escapes from a sentinel,
        ;; so turn errors into messages.
        (message "%s" (error-message-string err))))
-    (ignore-errors (delete-file err-file))))
+    (when err-buffer (kill-buffer err-buffer))
+    (when err-file (ignore-errors (delete-file err-file)))))
 
 ;; This variable is used only buffer local, but it needs to be
 ;; declared globally first to avoid compiler warnings.
index dd423765d7139cc2004c5a3b03c6989cdca57ff1..9939027700952c0f5255700cc9be5a274ae4f99b 100644 (file)
@@ -1269,7 +1269,9 @@ matched."
     ;; aren't wiped out.
     (setq notmuch-show-thread-id thread-id
          notmuch-show-parent-buffer parent-buffer
-         notmuch-show-query-context query-context
+         notmuch-show-query-context (if (or (string= query-context "")
+                                            (string= query-context "*"))
+                                        nil query-context)
 
          notmuch-show-process-crypto notmuch-crypto-process-mime
          ;; If `elide-toggle', invert the default value.
index 09d182dfb818d6844efdab4ca3d626e9f2dd7d89..0500927d37ce2b1e0173fb0f6ff7bc159a85018e 100644 (file)
@@ -457,7 +457,7 @@ QUERY should be a string containing the search-terms.
 TAG-CHANGES is a list of strings of the form \"+tag\" or
 \"-tag\" to add or remove tags, respectively.
 
-Note: Other code should always use this function alter tags of
+Note: Other code should always use this function to alter tags of
 messages instead of running (notmuch-call-notmuch-process \"tag\" ..)
 directly, so that hooks specified in notmuch-before-tag-hook and
 notmuch-after-tag-hook will be run."
index 022525aec0f5d45c97dbb1079016fab58d1ab17d..c00315e8dd40edaadfc369a95b2545fce49affbf 100644 (file)
@@ -598,6 +598,8 @@ message will be \"unarchived\", i.e. the tag changes in
 (defun notmuch-tree-refresh-view ()
   "Refresh view."
   (interactive)
+  (when (get-buffer-process (current-buffer))
+    (error "notmuch tree process already running for current buffer"))
   (let ((inhibit-read-only t)
        (basic-query notmuch-tree-basic-query)
        (query-context notmuch-tree-query-context)
@@ -897,7 +899,9 @@ the same as for the function notmuch-tree."
   (notmuch-tree-mode)
   (add-hook 'post-command-hook #'notmuch-tree-command-hook t t)
   (setq notmuch-tree-basic-query basic-query)
-  (setq notmuch-tree-query-context query-context)
+  (setq notmuch-tree-query-context (if (or (string= query-context "")
+                                          (string= query-context "*"))
+                                      nil query-context))
   (setq notmuch-tree-target-msg target)
   (setq notmuch-tree-open-target open-target)
   ;; Set the default value for `notmuch-show-process-crypto' in this
index 0aeff560fdfc5de08630be749d4d7e4a1689f89a..44402f8aa8256b55816b9a256b6b7f7468e54e9d 100644 (file)
 ;; Have fun, and let us know if you have any comment, questions, or
 ;; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
 ;; required, but is available from https://notmuchmail.org).
-
+;;
+;; Note for MELPA users (and others tracking the development version
+;; of notmuch-emacs):
+;;
+;; This emacs package needs a fairly closely matched version of the
+;; notmuch program. If you use the MELPA version of notmuch.el (as
+;; opposed to MELPA stable), you should be prepared to track the
+;; master development branch (i.e. build from git) for the notmuch
+;; program as well. Upgrading notmuch-emacs too far beyond the notmuch
+;; program can CAUSE YOUR EMAIL TO STOP WORKING.
+;;
+;; TL;DR: notmuch-emacs from MELPA and notmuch from distro packages is
+;; NOT SUPPORTED.
+;;
 ;;; Code:
 
 (eval-when-compile (require 'cl))
index b269db4e821265a065989a9b00c5f714c6b96c43..847426bfe663c29695182eb5c365ac7cae831a88 100644 (file)
@@ -201,7 +201,7 @@ g_mime_filter_reply_new (gboolean encode)
 {
        GMimeFilterReply *new_reply;
 
-       new_reply = (GMimeFilterReply *) g_object_newv (GMIME_TYPE_FILTER_REPLY, 0, NULL);
+       new_reply = (GMimeFilterReply *) g_object_new (GMIME_TYPE_FILTER_REPLY, NULL);
        new_reply->encode = encode;
 
        return (GMimeFilter *) new_reply;
index bf6e06494748d965a67dc33643b38a81f817e1a1..93b08d1500675f62ac0ba74099339d1ab777a2f7 100644 (file)
@@ -38,10 +38,12 @@ libnotmuch_c_srcs =         \
        $(dir)/filenames.c      \
        $(dir)/string-list.c    \
        $(dir)/message-file.c   \
+       $(dir)/message-id.c     \
        $(dir)/messages.c       \
        $(dir)/sha1.c           \
        $(dir)/built-with.c     \
        $(dir)/string-map.c     \
+       $(dir)/indexopts.c      \
        $(dir)/tags.c
 
 libnotmuch_cxx_srcs =          \
@@ -50,6 +52,7 @@ libnotmuch_cxx_srcs =         \
        $(dir)/directory.cc     \
        $(dir)/index.cc         \
        $(dir)/message.cc       \
+       $(dir)/add-message.cc   \
        $(dir)/message-property.cc \
        $(dir)/query.cc         \
        $(dir)/query-fp.cc      \
diff --git a/lib/add-message.cc b/lib/add-message.cc
new file mode 100644 (file)
index 0000000..73bde7f
--- /dev/null
@@ -0,0 +1,589 @@
+#include "database-private.h"
+
+/* Parse a References header value, putting a (talloc'ed under 'ctx')
+ * copy of each referenced message-id into 'hash'.
+ *
+ * We explicitly avoid including any reference identical to
+ * 'message_id' in the result (to avoid mass confusion when a single
+ * message references itself cyclically---and yes, mail messages are
+ * not infrequent in the wild that do this---don't ask me why).
+ *
+ * Return the last reference parsed, if it is not equal to message_id.
+ */
+static char *
+parse_references (void *ctx,
+                 const char *message_id,
+                 GHashTable *hash,
+                 const char *refs)
+{
+    char *ref, *last_ref = NULL;
+
+    if (refs == NULL || *refs == '\0')
+       return NULL;
+
+    while (*refs) {
+       ref = _notmuch_message_id_parse (ctx, refs, &refs);
+
+       if (ref && strcmp (ref, message_id)) {
+           g_hash_table_add (hash, ref);
+           last_ref = ref;
+       }
+    }
+
+    /* The return value of this function is used to add a parent
+     * reference to the database.  We should avoid making a message
+     * its own parent, thus the above check.
+     */
+    return talloc_strdup(ctx, last_ref);
+}
+
+static const char *
+_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
+{
+    /* 16 bytes (+ terminator) for hexadecimal representation of
+     * a 64-bit integer. */
+    static char thread_id[17];
+    Xapian::WritableDatabase *db;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    notmuch->last_thread_id++;
+
+    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
+
+    db->set_metadata ("last_thread_id", thread_id);
+
+    return thread_id;
+}
+
+static char *
+_get_metadata_thread_id_key (void *ctx, const char *message_id)
+{
+    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
+       message_id = _notmuch_message_id_compressed (ctx, message_id);
+
+    return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
+                           message_id);
+}
+
+
+static notmuch_status_t
+_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
+                                     void *ctx,
+                                     const char *message_id,
+                                     const char **thread_id_ret);
+
+
+/* Find the thread ID to which the message with 'message_id' belongs.
+ *
+ * Note: 'thread_id_ret' must not be NULL!
+ * On success '*thread_id_ret' is set to a newly talloced string belonging to
+ * 'ctx'.
+ *
+ * Note: If there is no message in the database with the given
+ * 'message_id' then a new thread_id will be allocated for this
+ * message ID and stored in the database metadata so that the
+ * thread ID can be looked up if the message is added to the database
+ * later.
+ */
+static notmuch_status_t
+_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
+                                 void *ctx,
+                                 const char *message_id,
+                                 const char **thread_id_ret)
+{
+    notmuch_private_status_t status;
+    notmuch_message_t *message;
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS))
+       return _resolve_message_id_to_thread_id_old (notmuch, ctx, message_id,
+                                                    thread_id_ret);
+
+    /* Look for this message (regular or ghost) */
+    message = _notmuch_message_create_for_message_id (
+       notmuch, message_id, &status);
+    if (status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
+       /* Message exists */
+       *thread_id_ret = talloc_steal (
+           ctx, notmuch_message_get_thread_id (message));
+    } else if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+       /* Message did not exist.  Give it a fresh thread ID and
+        * populate this message as a ghost message. */
+       *thread_id_ret = talloc_strdup (
+           ctx, _notmuch_database_generate_thread_id (notmuch));
+       if (! *thread_id_ret) {
+           status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+       } else {
+           status = _notmuch_message_initialize_ghost (message, *thread_id_ret);
+           if (status == 0)
+               /* Commit the new ghost message */
+               _notmuch_message_sync (message);
+       }
+    } else {
+       /* Create failed. Fall through. */
+    }
+
+    notmuch_message_destroy (message);
+
+    return COERCE_STATUS (status, "Error creating ghost message");
+}
+
+/* Pre-ghost messages _resolve_message_id_to_thread_id */
+static notmuch_status_t
+_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
+                                     void *ctx,
+                                     const char *message_id,
+                                     const char **thread_id_ret)
+{
+    notmuch_status_t status;
+    notmuch_message_t *message;
+    std::string thread_id_string;
+    char *metadata_key;
+    Xapian::WritableDatabase *db;
+
+    status = notmuch_database_find_message (notmuch, message_id, &message);
+
+    if (status)
+       return status;
+
+    if (message) {
+       *thread_id_ret = talloc_steal (ctx,
+                                      notmuch_message_get_thread_id (message));
+
+       notmuch_message_destroy (message);
+
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    /* Message has not been seen yet.
+     *
+     * We may have seen a reference to it already, in which case, we
+     * can return the thread ID stored in the metadata. Otherwise, we
+     * generate a new thread ID and store it there.
+     */
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
+    thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
+
+    if (thread_id_string.empty()) {
+       *thread_id_ret = talloc_strdup (ctx,
+                                       _notmuch_database_generate_thread_id (notmuch));
+       db->set_metadata (metadata_key, *thread_id_ret);
+    } else {
+       *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
+    }
+
+    talloc_free (metadata_key);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+static notmuch_status_t
+_merge_threads (notmuch_database_t *notmuch,
+               const char *winner_thread_id,
+               const char *loser_thread_id)
+{
+    Xapian::PostingIterator loser, loser_end;
+    notmuch_message_t *message = NULL;
+    notmuch_private_status_t private_status;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    _notmuch_database_find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
+
+    for ( ; loser != loser_end; loser++) {
+       message = _notmuch_message_create (notmuch, notmuch,
+                                          *loser, &private_status);
+       if (message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       _notmuch_message_remove_term (message, "thread", loser_thread_id);
+       _notmuch_message_add_term (message, "thread", winner_thread_id);
+       _notmuch_message_sync (message);
+
+       notmuch_message_destroy (message);
+       message = NULL;
+    }
+
+  DONE:
+    if (message)
+       notmuch_message_destroy (message);
+
+    return ret;
+}
+
+static void
+_my_talloc_free_for_g_hash (void *ptr)
+{
+    talloc_free (ptr);
+}
+
+notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+                                          notmuch_message_t *message,
+                                          notmuch_message_file_t *message_file,
+                                          const char **thread_id)
+{
+    GHashTable *parents = NULL;
+    const char *refs, *in_reply_to, *in_reply_to_message_id;
+    const char *last_ref_message_id, *this_message_id;
+    GList *l, *keys = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    parents = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                    _my_talloc_free_for_g_hash, NULL);
+    this_message_id = notmuch_message_get_message_id (message);
+
+    refs = _notmuch_message_file_get_header (message_file, "references");
+    last_ref_message_id = parse_references (message,
+                                           this_message_id,
+                                           parents, refs);
+
+    in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
+    in_reply_to_message_id = parse_references (message,
+                                              this_message_id,
+                                              parents, in_reply_to);
+
+    /* For the parent of this message, use the last message ID of the
+     * References header, if available.  If not, fall back to the
+     * first message ID in the In-Reply-To header. */
+    if (last_ref_message_id) {
+       _notmuch_message_add_term (message, "replyto",
+                                  last_ref_message_id);
+    } else if (in_reply_to_message_id) {
+       _notmuch_message_add_term (message, "replyto",
+                            in_reply_to_message_id);
+    }
+
+    keys = g_hash_table_get_keys (parents);
+    for (l = keys; l; l = l->next) {
+       char *parent_message_id;
+       const char *parent_thread_id = NULL;
+
+       parent_message_id = (char *) l->data;
+
+       _notmuch_message_add_term (message, "reference",
+                                  parent_message_id);
+
+       ret = _resolve_message_id_to_thread_id (notmuch,
+                                               message,
+                                               parent_message_id,
+                                               &parent_thread_id);
+       if (ret)
+           goto DONE;
+
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, parent_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, parent_thread_id)) {
+           ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
+           if (ret)
+               goto DONE;
+       }
+    }
+
+  DONE:
+    if (keys)
+       g_list_free (keys);
+    if (parents)
+       g_hash_table_unref (parents);
+
+    return ret;
+}
+
+static notmuch_status_t
+_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
+                                           notmuch_message_t *message,
+                                           const char **thread_id)
+{
+    const char *message_id = notmuch_message_get_message_id (message);
+    Xapian::PostingIterator child, children_end;
+    notmuch_message_t *child_message = NULL;
+    const char *child_thread_id;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+
+    _notmuch_database_find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
+
+    for ( ; child != children_end; child++) {
+
+       child_message = _notmuch_message_create (message, notmuch,
+                                                *child, &private_status);
+       if (child_message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       child_thread_id = notmuch_message_get_thread_id (child_message);
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, child_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, child_thread_id)) {
+           _notmuch_message_remove_term (child_message, "reference",
+                                         message_id);
+           _notmuch_message_sync (child_message);
+           ret = _merge_threads (notmuch, *thread_id, child_thread_id);
+           if (ret)
+               goto DONE;
+       }
+
+       notmuch_message_destroy (child_message);
+       child_message = NULL;
+    }
+
+  DONE:
+    if (child_message)
+       notmuch_message_destroy (child_message);
+
+    return ret;
+}
+
+/* Fetch and clear the stored thread_id for message, or NULL if none. */
+static char *
+_consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
+                            notmuch_message_t *message)
+{
+    const char *message_id;
+    std::string stored_id;
+    char *metadata_key;
+
+    message_id = notmuch_message_get_message_id (message);
+    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
+
+    /* Check if we have already seen related messages to this one.
+     * If we have then use the thread_id that we stored at that time.
+     */
+    stored_id = notmuch->xapian_db->get_metadata (metadata_key);
+    if (stored_id.empty ()) {
+       return NULL;
+    } else {
+       Xapian::WritableDatabase *db;
+
+       db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+       /* Clear the metadata for this message ID. We don't need it
+        * anymore. */
+       db->set_metadata (metadata_key, "");
+
+       return talloc_strdup (ctx, stored_id.c_str ());
+    }
+}
+
+/* Given a blank or ghost 'message' and its corresponding
+ * 'message_file' link it to existing threads in the database.
+ *
+ * First, if is_ghost, this retrieves the thread ID already stored in
+ * the message (which will be the case if a message was previously
+ * added that referenced this one).  If the message is blank
+ * (!is_ghost), it doesn't have a thread ID yet (we'll generate one
+ * later in this function).  If the database does not support ghost
+ * messages, this checks for a thread ID stored in database metadata
+ * for this message ID.
+ *
+ * Second, we look at 'message_file' and its link-relevant headers
+ * (References and In-Reply-To) for message IDs.
+ *
+ * Finally, we look in the database for existing message that
+ * reference 'message'.
+ *
+ * In all cases, we assign to the current message the first thread ID
+ * found. We will also merge any existing, distinct threads where this
+ * message belongs to both, (which is not uncommon when messages are
+ * processed out of order).
+ *
+ * Finally, if no thread ID has been found through referenced messages, we
+ * call _notmuch_message_generate_thread_id to generate a new thread
+ * ID. This should only happen for new, top-level messages, (no
+ * References or In-Reply-To header in this message, and no previously
+ * added message refers to this message).
+ */
+static notmuch_status_t
+_notmuch_database_link_message (notmuch_database_t *notmuch,
+                               notmuch_message_t *message,
+                               notmuch_message_file_t *message_file,
+                               notmuch_bool_t is_ghost)
+{
+    void *local = talloc_new (NULL);
+    notmuch_status_t status;
+    const char *thread_id = NULL;
+
+    /* Check if the message already had a thread ID */
+    if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
+       if (is_ghost)
+           thread_id = notmuch_message_get_thread_id (message);
+    } else {
+       thread_id = _consume_metadata_thread_id (local, notmuch, message);
+       if (thread_id)
+           _notmuch_message_add_term (message, "thread", thread_id);
+    }
+
+    status = _notmuch_database_link_message_to_parents (notmuch, message,
+                                                       message_file,
+                                                       &thread_id);
+    if (status)
+       goto DONE;
+
+    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS)) {
+       /* In general, it shouldn't be necessary to link children,
+        * since the earlier indexing of those children will have
+        * stored a thread ID for the missing parent.  However, prior
+        * to ghost messages, these stored thread IDs were NOT
+        * rewritten during thread merging (and there was no
+        * performant way to do so), so if indexed children were
+        * pulled into a different thread ID by a merge, it was
+        * necessary to pull them *back* into the stored thread ID of
+        * the parent.  With ghost messages, we just rewrite the
+        * stored thread IDs during merging, so this workaround isn't
+        * necessary. */
+       status = _notmuch_database_link_message_to_children (notmuch, message,
+                                                            &thread_id);
+       if (status)
+           goto DONE;
+    }
+
+    /* If not part of any existing thread, generate a new thread ID. */
+    if (thread_id == NULL) {
+       thread_id = _notmuch_database_generate_thread_id (notmuch);
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+    }
+
+ DONE:
+    talloc_free (local);
+
+    return status;
+}
+
+notmuch_status_t
+notmuch_database_index_file (notmuch_database_t *notmuch,
+                            const char *filename,
+                            notmuch_indexopts_t unused (*indexopts),
+                            notmuch_message_t **message_ret)
+{
+    notmuch_message_file_t *message_file;
+    notmuch_message_t *message = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
+    notmuch_private_status_t private_status;
+    notmuch_bool_t is_ghost = FALSE, is_new = FALSE;
+
+    const char *date;
+    const char *from, *to, *subject;
+    char *message_id = NULL;
+
+    if (message_ret)
+       *message_ret = NULL;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    message_file = _notmuch_message_file_open (notmuch, filename);
+    if (message_file == NULL)
+       return NOTMUCH_STATUS_FILE_ERROR;
+
+    /* Adding a message may change many documents.  Do this all
+     * atomically. */
+    ret = notmuch_database_begin_atomic (notmuch);
+    if (ret)
+       goto DONE;
+
+    ret = _notmuch_message_file_get_headers (message_file,
+                                            &from, &subject, &to, &date,
+                                            &message_id);
+    if (ret)
+       goto DONE;
+
+    try {
+       /* Now that we have a message ID, we get a message object,
+        * (which may or may not reference an existing document in the
+        * database). */
+
+       message = _notmuch_message_create_for_message_id (notmuch,
+                                                         message_id,
+                                                         &private_status);
+
+       talloc_free (message_id);
+
+       /* We cannot call notmuch_message_get_flag for a new message */
+       switch (private_status) {
+       case NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
+           is_ghost = FALSE;
+           is_new = TRUE;
+           break;
+       case NOTMUCH_PRIVATE_STATUS_SUCCESS:
+           is_ghost = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_GHOST);
+           is_new = FALSE;
+           break;
+       default:
+           ret = COERCE_STATUS (private_status,
+                                "Unexpected status value from _notmuch_message_create_for_message_id");
+           goto DONE;
+       }
+
+       _notmuch_message_add_filename (message, filename);
+
+       if (is_new || is_ghost) {
+           _notmuch_message_add_term (message, "type", "mail");
+           if (is_ghost)
+               /* Convert ghost message to a regular message */
+               _notmuch_message_remove_term (message, "type", "ghost");
+       }
+
+       ret = _notmuch_database_link_message (notmuch, message,
+                                                 message_file, is_ghost);
+       if (ret)
+           goto DONE;
+
+       if (is_new || is_ghost)
+           _notmuch_message_set_header_values (message, date, from, subject);
+
+       ret = _notmuch_message_index_file (message, message_file);
+       if (ret)
+           goto DONE;
+
+       if (! is_new && !is_ghost)
+           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+
+       _notmuch_message_sync (message);
+    } catch (const Xapian::Error &error) {
+       _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
+                error.get_msg().c_str());
+       notmuch->exception_reported = TRUE;
+       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       goto DONE;
+    }
+
+  DONE:
+    if (message) {
+       if ((ret == NOTMUCH_STATUS_SUCCESS ||
+            ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
+           *message_ret = message;
+       else
+           notmuch_message_destroy (message);
+    }
+
+    if (message_file)
+       _notmuch_message_file_close (message_file);
+
+    ret2 = notmuch_database_end_atomic (notmuch);
+    if ((ret == NOTMUCH_STATUS_SUCCESS ||
+        ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
+       ret2 != NOTMUCH_STATUS_SUCCESS)
+       ret = ret2;
+
+    return ret;
+}
+
+notmuch_status_t
+notmuch_database_add_message (notmuch_database_t *notmuch,
+                             const char *filename,
+                             notmuch_message_t **message_ret)
+{
+    return notmuch_database_index_file (notmuch, filename,
+                                       NULL,
+                                       message_ret);
+
+}
index 727b1d616f248f6aa3c5662985e0f69048a3c700..5555554bf27c8b896655cbe98e4fd35be19fec87 100644 (file)
@@ -246,4 +246,10 @@ _notmuch_database_get_terms_with_prefix (void *ctx, Xapian::TermIterator &i,
                                         Xapian::TermIterator &end,
                                         const char *prefix);
 
+void
+_notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
+                               const char *prefix_name,
+                               const char *value,
+                               Xapian::PostingIterator *begin,
+                               Xapian::PostingIterator *end);
 #endif
index 5b13f5417118ce5282b5b148941e65cd22637cde..79eb3d6977343e50e15e12746ac33ac01f505c28 100644 (file)
@@ -463,12 +463,12 @@ find_doc_ids_for_term (notmuch_database_t *notmuch,
     *end = notmuch->xapian_db->postlist_end (term);
 }
 
-static void
-find_doc_ids (notmuch_database_t *notmuch,
-             const char *prefix_name,
-             const char *value,
-             Xapian::PostingIterator *begin,
-             Xapian::PostingIterator *end)
+void
+_notmuch_database_find_doc_ids (notmuch_database_t *notmuch,
+                               const char *prefix_name,
+                               const char *value,
+                               Xapian::PostingIterator *begin,
+                               Xapian::PostingIterator *end)
 {
     char *term;
 
@@ -488,7 +488,7 @@ _notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
 {
     Xapian::PostingIterator i, end;
 
-    find_doc_ids (notmuch, prefix_name, value, &i, &end);
+    _notmuch_database_find_doc_ids (notmuch, prefix_name, value, &i, &end);
 
     if (i == end) {
        *doc_id = 0;
@@ -568,147 +568,6 @@ notmuch_database_find_message (notmuch_database_t *notmuch,
     }
 }
 
-/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
- * a (potentially nested) parenthesized sequence with '\' used to
- * escape any character (including parentheses).
- *
- * If the sequence to be skipped continues to the end of the string,
- * then 'str' will be left pointing at the final terminating '\0'
- * character.
- */
-static void
-skip_space_and_comments (const char **str)
-{
-    const char *s;
-
-    s = *str;
-    while (*s && (isspace (*s) || *s == '(')) {
-       while (*s && isspace (*s))
-           s++;
-       if (*s == '(') {
-           int nesting = 1;
-           s++;
-           while (*s && nesting) {
-               if (*s == '(') {
-                   nesting++;
-               } else if (*s == ')') {
-                   nesting--;
-               } else if (*s == '\\') {
-                   if (*(s+1))
-                       s++;
-               }
-               s++;
-           }
-       }
-    }
-
-    *str = s;
-}
-
-/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
- * comments, and the '<' and '>' delimiters.
- *
- * If not NULL, then *next will be made to point to the first character
- * not parsed, (possibly pointing to the final '\0' terminator.
- *
- * Returns a newly talloc'ed string belonging to 'ctx'.
- *
- * Returns NULL if there is any error parsing the message-id. */
-static char *
-_parse_message_id (void *ctx, const char *message_id, const char **next)
-{
-    const char *s, *end;
-    char *result;
-
-    if (message_id == NULL || *message_id == '\0')
-       return NULL;
-
-    s = message_id;
-
-    skip_space_and_comments (&s);
-
-    /* Skip any unstructured text as well. */
-    while (*s && *s != '<')
-       s++;
-
-    if (*s == '<') {
-       s++;
-    } else {
-       if (next)
-           *next = s;
-       return NULL;
-    }
-
-    skip_space_and_comments (&s);
-
-    end = s;
-    while (*end && *end != '>')
-       end++;
-    if (next) {
-       if (*end)
-           *next = end + 1;
-       else
-           *next = end;
-    }
-
-    if (end > s && *end == '>')
-       end--;
-    if (end <= s)
-       return NULL;
-
-    result = talloc_strndup (ctx, s, end - s + 1);
-
-    /* Finally, collapse any whitespace that is within the message-id
-     * itself. */
-    {
-       char *r;
-       int len;
-
-       for (r = result, len = strlen (r); *r; r++, len--)
-           if (*r == ' ' || *r == '\t')
-               memmove (r, r+1, len);
-    }
-
-    return result;
-}
-
-/* Parse a References header value, putting a (talloc'ed under 'ctx')
- * copy of each referenced message-id into 'hash'.
- *
- * We explicitly avoid including any reference identical to
- * 'message_id' in the result (to avoid mass confusion when a single
- * message references itself cyclically---and yes, mail messages are
- * not infrequent in the wild that do this---don't ask me why).
- *
- * Return the last reference parsed, if it is not equal to message_id.
- */
-static char *
-parse_references (void *ctx,
-                 const char *message_id,
-                 GHashTable *hash,
-                 const char *refs)
-{
-    char *ref, *last_ref = NULL;
-
-    if (refs == NULL || *refs == '\0')
-       return NULL;
-
-    while (*refs) {
-       ref = _parse_message_id (ctx, refs, &refs);
-
-       if (ref && strcmp (ref, message_id)) {
-           g_hash_table_add (hash, ref);
-           last_ref = ref;
-       }
-    }
-
-    /* The return value of this function is used to add a parent
-     * reference to the database.  We should avoid making a message
-     * its own parent, thus the above check.
-     */
-    return talloc_strdup(ctx, last_ref);
-}
-
 notmuch_status_t
 notmuch_database_create (const char *path, notmuch_database_t **database)
 {
@@ -999,8 +858,7 @@ notmuch_database_open_verbose (const char *path,
     notmuch->status_string = NULL;
     notmuch->path = talloc_strdup (notmuch, path);
 
-    if (notmuch->path[strlen (notmuch->path) - 1] == '/')
-       notmuch->path[strlen (notmuch->path) - 1] = '\0';
+    strip_trailing(notmuch->path, '/');
 
     notmuch->mode = mode;
     notmuch->atomic_nesting = 0;
@@ -2044,583 +1902,6 @@ _notmuch_database_generate_doc_id (notmuch_database_t *notmuch)
     return notmuch->last_doc_id;
 }
 
-static const char *
-_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
-{
-    /* 16 bytes (+ terminator) for hexadecimal representation of
-     * a 64-bit integer. */
-    static char thread_id[17];
-    Xapian::WritableDatabase *db;
-
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-
-    notmuch->last_thread_id++;
-
-    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
-
-    db->set_metadata ("last_thread_id", thread_id);
-
-    return thread_id;
-}
-
-static char *
-_get_metadata_thread_id_key (void *ctx, const char *message_id)
-{
-    if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
-       message_id = _notmuch_message_id_compressed (ctx, message_id);
-
-    return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
-                           message_id);
-}
-
-static notmuch_status_t
-_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
-                                     void *ctx,
-                                     const char *message_id,
-                                     const char **thread_id_ret);
-
-/* Find the thread ID to which the message with 'message_id' belongs.
- *
- * Note: 'thread_id_ret' must not be NULL!
- * On success '*thread_id_ret' is set to a newly talloced string belonging to
- * 'ctx'.
- *
- * Note: If there is no message in the database with the given
- * 'message_id' then a new thread_id will be allocated for this
- * message ID and stored in the database metadata so that the
- * thread ID can be looked up if the message is added to the database
- * later.
- */
-static notmuch_status_t
-_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
-                                 void *ctx,
-                                 const char *message_id,
-                                 const char **thread_id_ret)
-{
-    notmuch_private_status_t status;
-    notmuch_message_t *message;
-
-    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS))
-       return _resolve_message_id_to_thread_id_old (notmuch, ctx, message_id,
-                                                    thread_id_ret);
-
-    /* Look for this message (regular or ghost) */
-    message = _notmuch_message_create_for_message_id (
-       notmuch, message_id, &status);
-    if (status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
-       /* Message exists */
-       *thread_id_ret = talloc_steal (
-           ctx, notmuch_message_get_thread_id (message));
-    } else if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
-       /* Message did not exist.  Give it a fresh thread ID and
-        * populate this message as a ghost message. */
-       *thread_id_ret = talloc_strdup (
-           ctx, _notmuch_database_generate_thread_id (notmuch));
-       if (! *thread_id_ret) {
-           status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
-       } else {
-           status = _notmuch_message_initialize_ghost (message, *thread_id_ret);
-           if (status == 0)
-               /* Commit the new ghost message */
-               _notmuch_message_sync (message);
-       }
-    } else {
-       /* Create failed. Fall through. */
-    }
-
-    notmuch_message_destroy (message);
-
-    return COERCE_STATUS (status, "Error creating ghost message");
-}
-
-/* Pre-ghost messages _resolve_message_id_to_thread_id */
-static notmuch_status_t
-_resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
-                                     void *ctx,
-                                     const char *message_id,
-                                     const char **thread_id_ret)
-{
-    notmuch_status_t status;
-    notmuch_message_t *message;
-    string thread_id_string;
-    char *metadata_key;
-    Xapian::WritableDatabase *db;
-
-    status = notmuch_database_find_message (notmuch, message_id, &message);
-
-    if (status)
-       return status;
-
-    if (message) {
-       *thread_id_ret = talloc_steal (ctx,
-                                      notmuch_message_get_thread_id (message));
-
-       notmuch_message_destroy (message);
-
-       return NOTMUCH_STATUS_SUCCESS;
-    }
-
-    /* Message has not been seen yet.
-     *
-     * We may have seen a reference to it already, in which case, we
-     * can return the thread ID stored in the metadata. Otherwise, we
-     * generate a new thread ID and store it there.
-     */
-    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
-    thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
-
-    if (thread_id_string.empty()) {
-       *thread_id_ret = talloc_strdup (ctx,
-                                       _notmuch_database_generate_thread_id (notmuch));
-       db->set_metadata (metadata_key, *thread_id_ret);
-    } else {
-       *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
-    }
-
-    talloc_free (metadata_key);
-
-    return NOTMUCH_STATUS_SUCCESS;
-}
-
-static notmuch_status_t
-_merge_threads (notmuch_database_t *notmuch,
-               const char *winner_thread_id,
-               const char *loser_thread_id)
-{
-    Xapian::PostingIterator loser, loser_end;
-    notmuch_message_t *message = NULL;
-    notmuch_private_status_t private_status;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-
-    find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
-
-    for ( ; loser != loser_end; loser++) {
-       message = _notmuch_message_create (notmuch, notmuch,
-                                          *loser, &private_status);
-       if (message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Cannot find document for doc_id from query");
-           goto DONE;
-       }
-
-       _notmuch_message_remove_term (message, "thread", loser_thread_id);
-       _notmuch_message_add_term (message, "thread", winner_thread_id);
-       _notmuch_message_sync (message);
-
-       notmuch_message_destroy (message);
-       message = NULL;
-    }
-
-  DONE:
-    if (message)
-       notmuch_message_destroy (message);
-
-    return ret;
-}
-
-static void
-_my_talloc_free_for_g_hash (void *ptr)
-{
-    talloc_free (ptr);
-}
-
-static notmuch_status_t
-_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
-                                          notmuch_message_t *message,
-                                          notmuch_message_file_t *message_file,
-                                          const char **thread_id)
-{
-    GHashTable *parents = NULL;
-    const char *refs, *in_reply_to, *in_reply_to_message_id;
-    const char *last_ref_message_id, *this_message_id;
-    GList *l, *keys = NULL;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-
-    parents = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                    _my_talloc_free_for_g_hash, NULL);
-    this_message_id = notmuch_message_get_message_id (message);
-
-    refs = _notmuch_message_file_get_header (message_file, "references");
-    last_ref_message_id = parse_references (message,
-                                           this_message_id,
-                                           parents, refs);
-
-    in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
-    in_reply_to_message_id = parse_references (message,
-                                              this_message_id,
-                                              parents, in_reply_to);
-
-    /* For the parent of this message, use the last message ID of the
-     * References header, if available.  If not, fall back to the
-     * first message ID in the In-Reply-To header. */
-    if (last_ref_message_id) {
-       _notmuch_message_add_term (message, "replyto",
-                                  last_ref_message_id);
-    } else if (in_reply_to_message_id) {
-       _notmuch_message_add_term (message, "replyto",
-                            in_reply_to_message_id);
-    }
-
-    keys = g_hash_table_get_keys (parents);
-    for (l = keys; l; l = l->next) {
-       char *parent_message_id;
-       const char *parent_thread_id = NULL;
-
-       parent_message_id = (char *) l->data;
-
-       _notmuch_message_add_term (message, "reference",
-                                  parent_message_id);
-
-       ret = _resolve_message_id_to_thread_id (notmuch,
-                                               message,
-                                               parent_message_id,
-                                               &parent_thread_id);
-       if (ret)
-           goto DONE;
-
-       if (*thread_id == NULL) {
-           *thread_id = talloc_strdup (message, parent_thread_id);
-           _notmuch_message_add_term (message, "thread", *thread_id);
-       } else if (strcmp (*thread_id, parent_thread_id)) {
-           ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
-           if (ret)
-               goto DONE;
-       }
-    }
-
-  DONE:
-    if (keys)
-       g_list_free (keys);
-    if (parents)
-       g_hash_table_unref (parents);
-
-    return ret;
-}
-
-static notmuch_status_t
-_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
-                                           notmuch_message_t *message,
-                                           const char **thread_id)
-{
-    const char *message_id = notmuch_message_get_message_id (message);
-    Xapian::PostingIterator child, children_end;
-    notmuch_message_t *child_message = NULL;
-    const char *child_thread_id;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
-    notmuch_private_status_t private_status;
-
-    find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
-
-    for ( ; child != children_end; child++) {
-
-       child_message = _notmuch_message_create (message, notmuch,
-                                                *child, &private_status);
-       if (child_message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Cannot find document for doc_id from query");
-           goto DONE;
-       }
-
-       child_thread_id = notmuch_message_get_thread_id (child_message);
-       if (*thread_id == NULL) {
-           *thread_id = talloc_strdup (message, child_thread_id);
-           _notmuch_message_add_term (message, "thread", *thread_id);
-       } else if (strcmp (*thread_id, child_thread_id)) {
-           _notmuch_message_remove_term (child_message, "reference",
-                                         message_id);
-           _notmuch_message_sync (child_message);
-           ret = _merge_threads (notmuch, *thread_id, child_thread_id);
-           if (ret)
-               goto DONE;
-       }
-
-       notmuch_message_destroy (child_message);
-       child_message = NULL;
-    }
-
-  DONE:
-    if (child_message)
-       notmuch_message_destroy (child_message);
-
-    return ret;
-}
-
-/* Fetch and clear the stored thread_id for message, or NULL if none. */
-static char *
-_consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
-                            notmuch_message_t *message)
-{
-    const char *message_id;
-    string stored_id;
-    char *metadata_key;
-
-    message_id = notmuch_message_get_message_id (message);
-    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
-
-    /* Check if we have already seen related messages to this one.
-     * If we have then use the thread_id that we stored at that time.
-     */
-    stored_id = notmuch->xapian_db->get_metadata (metadata_key);
-    if (stored_id.empty ()) {
-       return NULL;
-    } else {
-       Xapian::WritableDatabase *db;
-
-       db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
-
-       /* Clear the metadata for this message ID. We don't need it
-        * anymore. */
-       db->set_metadata (metadata_key, "");
-
-       return talloc_strdup (ctx, stored_id.c_str ());
-    }
-}
-
-/* Given a blank or ghost 'message' and its corresponding
- * 'message_file' link it to existing threads in the database.
- *
- * First, if is_ghost, this retrieves the thread ID already stored in
- * the message (which will be the case if a message was previously
- * added that referenced this one).  If the message is blank
- * (!is_ghost), it doesn't have a thread ID yet (we'll generate one
- * later in this function).  If the database does not support ghost
- * messages, this checks for a thread ID stored in database metadata
- * for this message ID.
- *
- * Second, we look at 'message_file' and its link-relevant headers
- * (References and In-Reply-To) for message IDs.
- *
- * Finally, we look in the database for existing message that
- * reference 'message'.
- *
- * In all cases, we assign to the current message the first thread ID
- * found. We will also merge any existing, distinct threads where this
- * message belongs to both, (which is not uncommon when messages are
- * processed out of order).
- *
- * Finally, if no thread ID has been found through referenced messages, we
- * call _notmuch_message_generate_thread_id to generate a new thread
- * ID. This should only happen for new, top-level messages, (no
- * References or In-Reply-To header in this message, and no previously
- * added message refers to this message).
- */
-static notmuch_status_t
-_notmuch_database_link_message (notmuch_database_t *notmuch,
-                               notmuch_message_t *message,
-                               notmuch_message_file_t *message_file,
-                               notmuch_bool_t is_ghost)
-{
-    void *local = talloc_new (NULL);
-    notmuch_status_t status;
-    const char *thread_id = NULL;
-
-    /* Check if the message already had a thread ID */
-    if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
-       if (is_ghost)
-           thread_id = notmuch_message_get_thread_id (message);
-    } else {
-       thread_id = _consume_metadata_thread_id (local, notmuch, message);
-       if (thread_id)
-           _notmuch_message_add_term (message, "thread", thread_id);
-    }
-
-    status = _notmuch_database_link_message_to_parents (notmuch, message,
-                                                       message_file,
-                                                       &thread_id);
-    if (status)
-       goto DONE;
-
-    if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS)) {
-       /* In general, it shouldn't be necessary to link children,
-        * since the earlier indexing of those children will have
-        * stored a thread ID for the missing parent.  However, prior
-        * to ghost messages, these stored thread IDs were NOT
-        * rewritten during thread merging (and there was no
-        * performant way to do so), so if indexed children were
-        * pulled into a different thread ID by a merge, it was
-        * necessary to pull them *back* into the stored thread ID of
-        * the parent.  With ghost messages, we just rewrite the
-        * stored thread IDs during merging, so this workaround isn't
-        * necessary. */
-       status = _notmuch_database_link_message_to_children (notmuch, message,
-                                                            &thread_id);
-       if (status)
-           goto DONE;
-    }
-
-    /* If not part of any existing thread, generate a new thread ID. */
-    if (thread_id == NULL) {
-       thread_id = _notmuch_database_generate_thread_id (notmuch);
-
-       _notmuch_message_add_term (message, "thread", thread_id);
-    }
-
- DONE:
-    talloc_free (local);
-
-    return status;
-}
-
-notmuch_status_t
-notmuch_database_add_message (notmuch_database_t *notmuch,
-                             const char *filename,
-                             notmuch_message_t **message_ret)
-{
-    notmuch_message_file_t *message_file;
-    notmuch_message_t *message = NULL;
-    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
-    notmuch_private_status_t private_status;
-    notmuch_bool_t is_ghost = false;
-
-    const char *date, *header;
-    const char *from, *to, *subject;
-    char *message_id = NULL;
-
-    if (message_ret)
-       *message_ret = NULL;
-
-    ret = _notmuch_database_ensure_writable (notmuch);
-    if (ret)
-       return ret;
-
-    message_file = _notmuch_message_file_open (notmuch, filename);
-    if (message_file == NULL)
-       return NOTMUCH_STATUS_FILE_ERROR;
-
-    /* Adding a message may change many documents.  Do this all
-     * atomically. */
-    ret = notmuch_database_begin_atomic (notmuch);
-    if (ret)
-       goto DONE;
-
-    /* Parse message up front to get better error status. */
-    ret = _notmuch_message_file_parse (message_file);
-    if (ret)
-       goto DONE;
-
-    /* Before we do any real work, (especially before doing a
-     * potential SHA-1 computation on the entire file's contents),
-     * let's make sure that what we're looking at looks like an
-     * actual email message.
-     */
-    from = _notmuch_message_file_get_header (message_file, "from");
-    subject = _notmuch_message_file_get_header (message_file, "subject");
-    to = _notmuch_message_file_get_header (message_file, "to");
-
-    if ((from == NULL || *from == '\0') &&
-       (subject == NULL || *subject == '\0') &&
-       (to == NULL || *to == '\0')) {
-       ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
-       goto DONE;
-    }
-
-    /* Now that we're sure it's mail, the first order of business
-     * is to find a message ID (or else create one ourselves).
-     */
-    header = _notmuch_message_file_get_header (message_file, "message-id");
-    if (header && *header != '\0') {
-       message_id = _parse_message_id (message_file, header, NULL);
-
-       /* So the header value isn't RFC-compliant, but it's
-        * better than no message-id at all.
-        */
-       if (message_id == NULL)
-           message_id = talloc_strdup (message_file, header);
-    }
-
-    if (message_id == NULL ) {
-       /* No message-id at all, let's generate one by taking a
-        * hash over the file's contents.
-        */
-       char *sha1 = _notmuch_sha1_of_file (filename);
-
-       /* If that failed too, something is really wrong. Give up. */
-       if (sha1 == NULL) {
-           ret = NOTMUCH_STATUS_FILE_ERROR;
-           goto DONE;
-       }
-
-       message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
-       free (sha1);
-    }
-
-    try {
-       /* Now that we have a message ID, we get a message object,
-        * (which may or may not reference an existing document in the
-        * database). */
-
-       message = _notmuch_message_create_for_message_id (notmuch,
-                                                         message_id,
-                                                         &private_status);
-
-       talloc_free (message_id);
-
-       if (message == NULL) {
-           ret = COERCE_STATUS (private_status,
-                                "Unexpected status value from _notmuch_message_create_for_message_id");
-           goto DONE;
-       }
-
-       _notmuch_message_add_filename (message, filename);
-
-       /* Is this a newly created message object or a ghost
-        * message?  We have to be slightly careful: if this is a
-        * blank message, it's not safe to call
-        * notmuch_message_get_flag yet. */
-       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND ||
-           (is_ghost = notmuch_message_get_flag (
-               message, NOTMUCH_MESSAGE_FLAG_GHOST))) {
-           _notmuch_message_add_term (message, "type", "mail");
-           if (is_ghost)
-               /* Convert ghost message to a regular message */
-               _notmuch_message_remove_term (message, "type", "ghost");
-
-           ret = _notmuch_database_link_message (notmuch, message,
-                                                 message_file, is_ghost);
-           if (ret)
-               goto DONE;
-
-           date = _notmuch_message_file_get_header (message_file, "date");
-           _notmuch_message_set_header_values (message, date, from, subject);
-
-           ret = _notmuch_message_index_file (message, message_file);
-           if (ret)
-               goto DONE;
-       } else {
-           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
-       }
-
-       _notmuch_message_sync (message);
-    } catch (const Xapian::Error &error) {
-       _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
-                error.get_msg().c_str());
-       notmuch->exception_reported = TRUE;
-       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
-       goto DONE;
-    }
-
-  DONE:
-    if (message) {
-       if ((ret == NOTMUCH_STATUS_SUCCESS ||
-            ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
-           *message_ret = message;
-       else
-           notmuch_message_destroy (message);
-    }
-
-    if (message_file)
-       _notmuch_message_file_close (message_file);
-
-    ret2 = notmuch_database_end_atomic (notmuch);
-    if ((ret == NOTMUCH_STATUS_SUCCESS ||
-        ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
-       ret2 != NOTMUCH_STATUS_SUCCESS)
-       ret = ret2;
-
-    return ret;
-}
-
 notmuch_status_t
 notmuch_database_remove_message (notmuch_database_t *notmuch,
                                 const char *filename)
index 10420d84426ca165659cba5118def218d6020cc3..2b98b588d38d4532173a503cc652aa5e8d3c3de1 100644 (file)
@@ -261,7 +261,7 @@ notmuch_filter_discard_non_term_new (GMimeContentType *content_type)
        type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardNonTerm", &info, (GTypeFlags) 0);
     }
 
-    filter = (NotmuchFilterDiscardNonTerm *) g_object_newv (type, 0, NULL);
+    filter = (NotmuchFilterDiscardNonTerm *) g_object_new (type, NULL);
     filter->content_type = content_type;
     filter->state = 0;
     if (g_mime_content_type_is_type (content_type, "text", "html")) {
diff --git a/lib/indexopts.c b/lib/indexopts.c
new file mode 100644 (file)
index 0000000..2f9b841
--- /dev/null
@@ -0,0 +1,33 @@
+/* indexopts.c - options for indexing messages (currently a stub)
+ *
+ * Copyright Â© 2017 Daniel Kahn Gillmor
+ *
+ * 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 https://www.gnu.org/licenses/ .
+ *
+ * Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ */
+
+#include "notmuch-private.h"
+
+notmuch_indexopts_t *
+notmuch_database_get_default_indexopts (notmuch_database_t unused (*db))
+{
+    return NULL;
+}
+
+void
+notmuch_indexopts_destroy (notmuch_indexopts_t *indexopts)
+{
+    talloc_free (indexopts);
+}
index d7acf0d5b4e2739eac56756c1ca35f30eeacf424..b90df305b6306097b6d9708e33e2a64e1e74a69c 100644 (file)
@@ -92,6 +92,12 @@ _notmuch_message_file_open (notmuch_database_t *notmuch,
     return _notmuch_message_file_open_ctx (notmuch, NULL, filename);
 }
 
+const char *
+_notmuch_message_file_get_filename (notmuch_message_file_t *message_file)
+{
+    return message_file->filename;
+}
+
 void
 _notmuch_message_file_close (notmuch_message_file_t *message)
 {
@@ -345,3 +351,83 @@ _notmuch_message_file_get_header (notmuch_message_file_t *message,
 
     return decoded;
 }
+
+notmuch_status_t
+_notmuch_message_file_get_headers (notmuch_message_file_t *message_file,
+                                  const char **from_out,
+                                  const char **subject_out,
+                                  const char **to_out,
+                                  const char **date_out,
+                                  char **message_id_out)
+{
+    notmuch_status_t ret;
+    const char *header;
+    const char *from, *to, *subject, *date;
+    char *message_id = NULL;
+
+    /* Parse message up front to get better error status. */
+    ret = _notmuch_message_file_parse (message_file);
+    if (ret)
+       goto DONE;
+
+    /* Before we do any real work, (especially before doing a
+     * potential SHA-1 computation on the entire file's contents),
+     * let's make sure that what we're looking at looks like an
+     * actual email message.
+     */
+    from = _notmuch_message_file_get_header (message_file, "from");
+    subject = _notmuch_message_file_get_header (message_file, "subject");
+    to = _notmuch_message_file_get_header (message_file, "to");
+    date = _notmuch_message_file_get_header (message_file, "date");
+
+    if ((from == NULL || *from == '\0') &&
+       (subject == NULL || *subject == '\0') &&
+       (to == NULL || *to == '\0')) {
+       ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+       goto DONE;
+    }
+
+    /* Now that we're sure it's mail, the first order of business
+     * is to find a message ID (or else create one ourselves).
+     */
+    header = _notmuch_message_file_get_header (message_file, "message-id");
+    if (header && *header != '\0') {
+       message_id = _notmuch_message_id_parse (message_file, header, NULL);
+
+       /* So the header value isn't RFC-compliant, but it's
+        * better than no message-id at all.
+        */
+       if (message_id == NULL)
+           message_id = talloc_strdup (message_file, header);
+    }
+
+    if (message_id == NULL ) {
+       /* No message-id at all, let's generate one by taking a
+        * hash over the file's contents.
+        */
+       char *sha1 = _notmuch_sha1_of_file (_notmuch_message_file_get_filename (message_file));
+
+       /* If that failed too, something is really wrong. Give up. */
+       if (sha1 == NULL) {
+           ret = NOTMUCH_STATUS_FILE_ERROR;
+           goto DONE;
+       }
+
+       message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
+       free (sha1);
+    }
+ DONE:
+    if (ret == NOTMUCH_STATUS_SUCCESS) {
+       if (from_out)
+           *from_out = from;
+       if (subject_out)
+           *subject_out = subject;
+       if (to_out)
+           *to_out = to;
+       if (date_out)
+           *date_out = date;
+       if (message_id_out)
+           *message_id_out = message_id;
+    }
+    return ret;
+}
diff --git a/lib/message-id.c b/lib/message-id.c
new file mode 100644 (file)
index 0000000..d7541d5
--- /dev/null
@@ -0,0 +1,96 @@
+#include "notmuch-private.h"
+
+/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
+ * a (potentially nested) parenthesized sequence with '\' used to
+ * escape any character (including parentheses).
+ *
+ * If the sequence to be skipped continues to the end of the string,
+ * then 'str' will be left pointing at the final terminating '\0'
+ * character.
+ */
+static void
+skip_space_and_comments (const char **str)
+{
+    const char *s;
+
+    s = *str;
+    while (*s && (isspace (*s) || *s == '(')) {
+       while (*s && isspace (*s))
+           s++;
+       if (*s == '(') {
+           int nesting = 1;
+           s++;
+           while (*s && nesting) {
+               if (*s == '(') {
+                   nesting++;
+               } else if (*s == ')') {
+                   nesting--;
+               } else if (*s == '\\') {
+                   if (*(s+1))
+                       s++;
+               }
+               s++;
+           }
+       }
+    }
+
+    *str = s;
+}
+
+char *
+_notmuch_message_id_parse (void *ctx, const char *message_id, const char **next)
+{
+    const char *s, *end;
+    char *result;
+
+    if (message_id == NULL || *message_id == '\0')
+       return NULL;
+
+    s = message_id;
+
+    skip_space_and_comments (&s);
+
+    /* Skip any unstructured text as well. */
+    while (*s && *s != '<')
+       s++;
+
+    if (*s == '<') {
+       s++;
+    } else {
+       if (next)
+           *next = s;
+       return NULL;
+    }
+
+    skip_space_and_comments (&s);
+
+    end = s;
+    while (*end && *end != '>')
+       end++;
+    if (next) {
+       if (*end)
+           *next = end + 1;
+       else
+           *next = end;
+    }
+
+    if (end > s && *end == '>')
+       end--;
+    if (end <= s)
+       return NULL;
+
+    result = talloc_strndup (ctx, s, end - s + 1);
+
+    /* Finally, collapse any whitespace that is within the message-id
+     * itself. */
+    {
+       char *r;
+       int len;
+
+       for (r = result, len = strlen (r); *r; r++, len--)
+           if (*r == ' ' || *r == '\t')
+               memmove (r, r+1, len);
+    }
+
+    return result;
+}
index f78e5a9d302fd62c09e41dfc59966b57e3056c3b..0e3b5a4f1510722729d60e73322baf2d529219bc 100644 (file)
@@ -36,6 +36,7 @@ struct _notmuch_message {
     notmuch_string_list_t *tag_list;
     notmuch_string_list_t *filename_term_list;
     notmuch_string_list_t *filename_list;
+    char *maildir_flags;
     char *author;
     notmuch_message_file_t *message_file;
     notmuch_string_list_t *property_term_list;
@@ -123,6 +124,7 @@ _notmuch_message_create_for_document (const void *talloc_owner,
     message->tag_list = NULL;
     message->filename_term_list = NULL;
     message->filename_list = NULL;
+    message->maildir_flags = NULL;
     message->message_file = NULL;
     message->author = NULL;
     message->property_term_list = NULL;
@@ -579,7 +581,9 @@ void
 _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
 {
     Xapian::TermIterator i;
-    size_t prefix_len = strlen (prefix);
+    size_t prefix_len = 0;
+
+    prefix_len = strlen (prefix);
 
     while (1) {
        i = message->doc.termlist_begin ();
@@ -599,6 +603,59 @@ _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
     }
 }
 
+
+/* Remove all terms generated by indexing, i.e. not tags or
+ * properties, along with any automatic tags*/
+notmuch_private_status_t
+_notmuch_message_remove_indexed_terms (notmuch_message_t *message)
+{
+    Xapian::TermIterator i;
+
+    const std::string
+       id_prefix = _find_prefix ("id"),
+       property_prefix = _find_prefix ("property"),
+       tag_prefix = _find_prefix ("tag"),
+       type_prefix = _find_prefix ("type");
+
+    for (i = message->doc.termlist_begin ();
+        i != message->doc.termlist_end (); i++) {
+
+       const std::string term = *i;
+
+       if (term.compare (0, type_prefix.size (), type_prefix) == 0)
+           continue;
+
+       if (term.compare (0, id_prefix.size (), id_prefix) == 0)
+           continue;
+
+       if (term.compare (0, property_prefix.size (), property_prefix) == 0)
+           continue;
+
+       if (term.compare (0, tag_prefix.size (), tag_prefix) == 0 &&
+           term.compare (1, strlen("encrypted"), "encrypted") != 0 &&
+           term.compare (1, strlen("signed"), "signed") != 0 &&
+           term.compare (1, strlen("attachment"), "attachment") != 0)
+           continue;
+
+       try {
+           message->doc.remove_term ((*i));
+           message->modified = TRUE;
+       } catch (const Xapian::InvalidArgumentError) {
+           /* Ignore failure to remove non-existent term. */
+       } catch (const Xapian::Error &error) {
+           notmuch_database_t *notmuch = message->notmuch;
+
+           if (!notmuch->exception_reported) {
+               _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
+                                     error.get_msg().c_str());
+               notmuch->exception_reported = TRUE;
+           }
+           return NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+       }
+    }
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
 /* Return true if p points at "new" or "cur". */
 static bool is_maildir (const char *p)
 {
@@ -646,6 +703,7 @@ _notmuch_message_add_folder_terms (notmuch_message_t *message,
 
     talloc_free (folder);
 
+    message->modified = TRUE;
     return NOTMUCH_STATUS_SUCCESS;
 }
 
@@ -946,6 +1004,14 @@ notmuch_message_get_filenames (notmuch_message_t *message)
     return _notmuch_filenames_create (message, message->filename_list);
 }
 
+int
+notmuch_message_count_files (notmuch_message_t *message)
+{
+    _notmuch_message_ensure_filename_list (message);
+
+    return _notmuch_string_list_length (message->filename_list);
+}
+
 notmuch_bool_t
 notmuch_message_get_flag (notmuch_message_t *message,
                          notmuch_message_flag_t flag)
@@ -1449,17 +1515,22 @@ _filename_is_in_maildir (const char *filename)
     return NULL;
 }
 
-notmuch_status_t
-notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
+static void
+_ensure_maildir_flags (notmuch_message_t *message, notmuch_bool_t force)
 {
     const char *flags;
-    notmuch_status_t status;
     notmuch_filenames_t *filenames;
     const char *filename, *dir;
     char *combined_flags = talloc_strdup (message, "");
-    unsigned i;
     int seen_maildir_info = 0;
 
+    if (message->maildir_flags) {
+       if (force) {
+           talloc_free (message->maildir_flags);
+           message->maildir_flags = NULL;
+       }
+    }
+
     for (filenames = notmuch_message_get_filenames (message);
         notmuch_filenames_valid (filenames);
         notmuch_filenames_move_to_next (filenames))
@@ -1485,11 +1556,28 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
            seen_maildir_info = 1;
        }
     }
+    if (seen_maildir_info)
+       message->maildir_flags = combined_flags;
+}
+
+notmuch_bool_t
+notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag)
+{
+    _ensure_maildir_flags (message, FALSE);
+    return message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL);
+}
+
+notmuch_status_t
+notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
+{
+    notmuch_status_t status;
+    unsigned i;
 
+    _ensure_maildir_flags (message, TRUE);
     /* If none of the filenames have any maildir info field (not even
      * an empty info with no flags set) then there's no information to
      * go on, so do nothing. */
-    if (! seen_maildir_info)
+    if (! message->maildir_flags)
        return NOTMUCH_STATUS_SUCCESS;
 
     status = notmuch_message_freeze (message);
@@ -1497,7 +1585,7 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
        return status;
 
     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
-       if ((strchr (combined_flags, flag2tag[i].flag) != NULL)
+       if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
            ^
            flag2tag[i].inverse)
        {
@@ -1510,8 +1598,6 @@ notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
     }
     status = notmuch_message_thaw (message);
 
-    talloc_free (combined_flags);
-
     return status;
 }
 
@@ -1872,3 +1958,109 @@ _notmuch_message_frozen (notmuch_message_t *message)
 {
     return message->frozen;
 }
+
+notmuch_status_t
+notmuch_message_reindex (notmuch_message_t *message,
+                        notmuch_indexopts_t unused (*indexopts))
+{
+    notmuch_database_t *notmuch = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+    notmuch_filenames_t *orig_filenames = NULL;
+    const char *orig_thread_id = NULL;
+    notmuch_message_file_t *message_file = NULL;
+
+    int found = 0;
+
+    if (message == NULL)
+       return NOTMUCH_STATUS_NULL_POINTER;
+
+    /* Save in case we need to delete message */
+    orig_thread_id = notmuch_message_get_thread_id (message);
+    if (!orig_thread_id) {
+       /* XXX TODO: make up new error return? */
+       INTERNAL_ERROR ("message without thread-id");
+    }
+
+    /* strdup it because the metadata may be invalidated */
+    orig_thread_id = talloc_strdup (message, orig_thread_id);
+
+    notmuch = _notmuch_message_database (message);
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    orig_filenames = notmuch_message_get_filenames (message);
+
+    private_status = _notmuch_message_remove_indexed_terms (message);
+    if (private_status) {
+       ret = COERCE_STATUS(private_status, "error removing terms");
+       goto DONE;
+    }
+
+    /* re-add the filenames with the associated indexopts */
+    for (; notmuch_filenames_valid (orig_filenames);
+        notmuch_filenames_move_to_next (orig_filenames)) {
+
+       const char *date;
+       const char *from, *to, *subject;
+       char *message_id = NULL;
+       const char *thread_id = NULL;
+
+       const char *filename = notmuch_filenames_get (orig_filenames);
+
+       message_file = _notmuch_message_file_open (notmuch, filename);
+       if (message_file == NULL)
+           continue;
+
+       ret = _notmuch_message_file_get_headers (message_file,
+                                                &from, &subject, &to, &date,
+                                                &message_id);
+       if (ret)
+           goto DONE;
+
+       /* XXX TODO: deal with changing message id? */
+
+       _notmuch_message_add_filename (message, filename);
+
+       ret = _notmuch_database_link_message_to_parents (notmuch, message,
+                                                        message_file,
+                                                        &thread_id);
+       if (ret)
+           goto DONE;
+
+       if (thread_id == NULL)
+           thread_id = orig_thread_id;
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+       /* Take header values only from first filename */
+       if (found == 0)
+           _notmuch_message_set_header_values (message, date, from, subject);
+
+       ret = _notmuch_message_index_file (message, message_file);
+
+       if (ret == NOTMUCH_STATUS_FILE_ERROR)
+           continue;
+       if (ret)
+           goto DONE;
+
+       found++;
+       _notmuch_message_file_close (message_file);
+       message_file = NULL;
+    }
+    if (found == 0) {
+       /* put back thread id to help cleanup */
+       _notmuch_message_add_term (message, "thread", orig_thread_id);
+       ret = _notmuch_message_delete (message);
+    } else {
+       _notmuch_message_sync (message);
+    }
+
+ DONE:
+    if (message_file)
+       _notmuch_message_file_close (message_file);
+
+    /* XXX TODO destroy orig_filenames? */
+    return ret;
+}
index 5dfebf5dfd0d3a34f2953bbec90aca1dc74e3a29..b187a80ff1979015ae59292b18a0e997267abfa8 100644 (file)
@@ -425,6 +425,23 @@ const char *
 _notmuch_message_file_get_header (notmuch_message_file_t *message,
                                 const char *header);
 
+notmuch_status_t
+_notmuch_message_file_get_headers (notmuch_message_file_t *message_file,
+                                  const char **from_out,
+                                  const char **subject_out,
+                                  const char **to_out,
+                                  const char **date_out,
+                                  char **message_id_out);
+
+const char *
+_notmuch_message_file_get_filename (notmuch_message_file_t *message);
+
+/* add-message.cc */
+notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+                                          notmuch_message_t *message,
+                                          notmuch_message_file_t *message_file,
+                                          const char **thread_id);
 /* index.cc */
 
 notmuch_status_t
@@ -492,6 +509,20 @@ notmuch_status_t
 _notmuch_query_count_documents (notmuch_query_t *query,
                                const char *type,
                                unsigned *count_out);
+/* message-id.c */
+
+/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
+ * comments, and the '<' and '>' delimiters.
+ *
+ * If not NULL, then *next will be made to point to the first character
+ * not parsed, (possibly pointing to the final '\0' terminator.
+ *
+ * Returns a newly talloc'ed string belonging to 'ctx'.
+ *
+ * Returns NULL if there is any error parsing the message-id. */
+char *
+_notmuch_message_id_parse (void *ctx, const char *message_id, const char **next);
+
 
 /* message.cc */
 
@@ -501,6 +532,8 @@ _notmuch_message_add_reply (notmuch_message_t *message,
 notmuch_database_t *
 _notmuch_message_database (notmuch_message_t *message);
 
+void
+_notmuch_message_remove_unprefixed_terms (notmuch_message_t *message);
 /* sha1.c */
 
 char *
@@ -525,6 +558,12 @@ typedef struct _notmuch_string_list {
 notmuch_string_list_t *
 _notmuch_string_list_create (const void *ctx);
 
+/*
+ * return the number of strings in 'list'
+ */
+int
+_notmuch_string_list_length (notmuch_string_list_t *list);
+
 /* Add 'string' to 'list'.
  *
  * The list will create its own talloced copy of 'string'.
index 17f0872e9edf80189602712a5d5be652de8a8e2d..f26565f356d49e3c5906d1f833562ff9f866c510 100644 (file)
@@ -219,6 +219,7 @@ typedef struct _notmuch_tags notmuch_tags_t;
 typedef struct _notmuch_directory notmuch_directory_t;
 typedef struct _notmuch_filenames notmuch_filenames_t;
 typedef struct _notmuch_config_list notmuch_config_list_t;
+typedef struct _notmuch_indexopts notmuch_indexopts_t;
 #endif /* __DOXYGEN__ */
 
 /**
@@ -236,7 +237,7 @@ typedef struct _notmuch_config_list notmuch_config_list_t;
  * The database will not yet have any data in it
  * (notmuch_database_create itself is a very cheap function). Messages
  * contained within 'path' can be added to the database by calling
- * notmuch_database_add_message.
+ * notmuch_database_index_file.
  *
  * In case of any failure, this function returns an error status and
  * sets *database to NULL (after printing an error message on stderr).
@@ -541,8 +542,10 @@ notmuch_database_get_directory (notmuch_database_t *database,
                                notmuch_directory_t **directory);
 
 /**
- * Add a new message to the given notmuch database or associate an
- * additional filename with an existing message.
+ * Add a message file to a database, indexing it for retrieval by
+ * future searches.  If a message already exists with the same message
+ * ID as the specified file, their indexes will be merged, and this
+ * new filename will also be associated with the existing message.
  *
  * Here, 'filename' should be a path relative to the path of
  * 'database' (see notmuch_database_get_path), or else should be an
@@ -555,8 +558,14 @@ notmuch_database_get_directory (notmuch_database_t *database,
  * entire contents of the file.
  *
  * If another message with the same message ID already exists in the
- * database, rather than creating a new message, this adds 'filename'
- * to the list of the filenames for the existing message.
+ * database, rather than creating a new message, this adds the search
+ * terms from the identified file to the existing message's index, and
+ * adds 'filename' to the list of filenames known for the message.
+ *
+ * The 'indexopts' parameter can be NULL (meaning, use the indexing
+ * defaults from the database), or can be an explicit choice of
+ * indexing options that should govern the indexing of this specific
+ * 'filename'.
  *
  * If 'message' is not NULL, then, on successful return
  * (NOTMUCH_STATUS_SUCCESS or NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) '*message'
@@ -589,8 +598,25 @@ notmuch_database_get_directory (notmuch_database_t *database,
  *
  * NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
  *     database to use this function.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
  */
 notmuch_status_t
+notmuch_database_index_file (notmuch_database_t *database,
+                            const char *filename,
+                            notmuch_indexopts_t *indexopts,
+                            notmuch_message_t **message);
+
+/**
+ * Deprecated alias for notmuch_database_index_file called with
+ * NULL indexopts.
+ *
+ * @deprecated Deprecated as of libnotmuch 5.1 (notmuch 0.26). Please
+ * use notmuch_database_index_file instead.
+ *
+ */
+NOTMUCH_DEPRECATED(5,1)
+notmuch_status_t
 notmuch_database_add_message (notmuch_database_t *database,
                              const char *filename,
                              notmuch_message_t **message);
@@ -1096,6 +1122,18 @@ notmuch_thread_get_thread_id (notmuch_thread_t *thread);
 int
 notmuch_thread_get_total_messages (notmuch_thread_t *thread);
 
+/**
+ * Get the total number of files in 'thread'.
+ *
+ * This sums notmuch_message_count_files over all messages in the
+ * thread
+ * @returns Non-negative integer
+ * @since libnotmuch 5.0 (notmuch 0.25)
+ */
+
+int
+notmuch_thread_get_total_files (notmuch_thread_t *thread);
+
 /**
  * Get a notmuch_messages_t iterator for the top-level messages in
  * 'thread' in oldest-first order.
@@ -1341,6 +1379,14 @@ notmuch_message_get_thread_id (notmuch_message_t *message);
 notmuch_messages_t *
 notmuch_message_get_replies (notmuch_message_t *message);
 
+/**
+ * Get the total number of files associated with a message.
+ * @returns Non-negative integer
+ * @since libnotmuch 5.0 (notmuch 0.25)
+ */
+int
+notmuch_message_count_files (notmuch_message_t *message);
+
 /**
  * Get a filename for the email corresponding to 'message'.
  *
@@ -1373,6 +1419,20 @@ notmuch_message_get_filename (notmuch_message_t *message);
 notmuch_filenames_t *
 notmuch_message_get_filenames (notmuch_message_t *message);
 
+/**
+ * Re-index the e-mail corresponding to 'message' using the supplied index options
+ *
+ * Returns the status of the re-index operation.  (see the return
+ * codes documented in notmuch_database_index_file)
+ *
+ * After reindexing, the user should discard the message object passed
+ * in here by calling notmuch_message_destroy, since it refers to the
+ * original message, not to the reindexed message.
+ */
+notmuch_status_t
+notmuch_message_reindex (notmuch_message_t *message,
+                        notmuch_indexopts_t *indexopts);
+
 /**
  * Message flags.
  */
@@ -1546,13 +1606,21 @@ notmuch_message_remove_all_tags (notmuch_message_t *message);
  *
  * A client can ensure that notmuch database tags remain synchronized
  * with maildir flags by calling this function after each call to
- * notmuch_database_add_message. See also
+ * notmuch_database_index_file. See also
  * notmuch_message_tags_to_maildir_flags for synchronizing tag changes
  * back to maildir flags.
  */
 notmuch_status_t
 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
 
+/**
+ * return TRUE if any filename of 'message' has maildir flag 'flag',
+ * FALSE otherwise.
+ *
+ */
+notmuch_bool_t
+notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag);
+
 /**
  * Rename message filename(s) to encode tags as maildir flags.
  *
@@ -1908,7 +1976,7 @@ notmuch_tags_destroy (notmuch_tags_t *tags);
  *
  *   o Read the mtime of a directory from the filesystem
  *
- *   o Call add_message for all mail files in the directory
+ *   o Call index_file for all mail files in the directory
  *
  *   o Call notmuch_directory_set_mtime with the mtime read from the
  *     filesystem.
@@ -2113,6 +2181,31 @@ notmuch_config_list_move_to_next (notmuch_config_list_t *config_list);
 void
 notmuch_config_list_destroy (notmuch_config_list_t *config_list);
 
+
+/**
+ * get the current default indexing options for a given database.
+ *
+ * This object will survive until the database itself is destroyed,
+ * but the caller may also release it earlier with
+ * notmuch_indexopts_destroy.
+ *
+ * This object represents a set of options on how a message can be
+ * added to the index.  At the moment it is a featureless stub.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+notmuch_indexopts_t *
+notmuch_database_get_default_indexopts (notmuch_database_t *db);
+
+/**
+ * Destroy a notmuch_indexopts_t object.
+ *
+ * @since libnotmuch 5.1 (notmuch 0.26)
+ */
+void
+notmuch_indexopts_destroy (notmuch_indexopts_t *options);
+
+
 /**
  * interrogate the library for compile time features
  *
index 43ebe4993e8f26db6441f46e7af5dc6b42886d0e..9c3ae7ef2a30eb6127cb3b180f2385ee5da0d5ea 100644 (file)
@@ -42,6 +42,12 @@ _notmuch_string_list_create (const void *ctx)
     return list;
 }
 
+int
+_notmuch_string_list_length (notmuch_string_list_t *list)
+{
+    return list->length;
+}
+
 void
 _notmuch_string_list_append (notmuch_string_list_t *list,
                             const char *string)
index 1a1ecfa5507ee8cf35e243eb4b44ddfdcd0526eb..e17ef63ef48e4657719b0c147835bb86e309f9c0 100644 (file)
@@ -44,6 +44,7 @@ struct _notmuch_thread {
 
     GHashTable *message_hash;
     int total_messages;
+    int total_files;
     int matched_messages;
     time_t oldest;
     time_t newest;
@@ -266,6 +267,7 @@ _thread_add_message (notmuch_thread_t *thread,
     _notmuch_message_list_add_message (thread->message_list,
                                       talloc_steal (thread, message));
     thread->total_messages++;
+    thread->total_files += notmuch_message_count_files (message);
 
     g_hash_table_insert (thread->message_hash,
                         xstrdup (notmuch_message_get_message_id (message)),
@@ -495,6 +497,7 @@ _notmuch_thread_create (void *ctx,
                                                  free, NULL);
 
     thread->total_messages = 0;
+    thread->total_files = 0;
     thread->matched_messages = 0;
     thread->oldest = 0;
     thread->newest = 0;
@@ -566,6 +569,12 @@ notmuch_thread_get_total_messages (notmuch_thread_t *thread)
     return thread->total_messages;
 }
 
+int
+notmuch_thread_get_total_files (notmuch_thread_t *thread)
+{
+    return thread->total_files;
+}
+
 int
 notmuch_thread_get_matched_messages (notmuch_thread_t *thread)
 {
index bb0870d024ad1f82127e37eeb564578bc9a2695a..24d73afa84589f9ee49c50b9a82225fd9b76865b 100644 (file)
@@ -79,12 +79,32 @@ mime_node_open (const void *ctx, notmuch_message_t *message,
     }
     talloc_set_destructor (mctx, _mime_node_context_free);
 
+    /* Fast path */
     mctx->file = fopen (filename, "r");
     if (! mctx->file) {
-       fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
-       status = NOTMUCH_STATUS_FILE_ERROR;
-       goto DONE;
-    }
+       /* Slow path - for some reason the first file in the list is
+        * not available anymore. This is clearly a problem in the
+        * database, but we are not going to let this problem be a
+        * show stopper */
+       notmuch_filenames_t *filenames;
+       for (filenames = notmuch_message_get_filenames (message);
+            notmuch_filenames_valid (filenames);
+            notmuch_filenames_move_to_next (filenames))
+       {
+           filename = notmuch_filenames_get (filenames);
+           mctx->file = fopen (filename, "r");
+           if (mctx->file)
+               break;
+       }
+
+       talloc_free (filenames);
+       if (! mctx->file) {
+           /* Give up */
+           fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+               status = NOTMUCH_STATUS_FILE_ERROR;
+               goto DONE;
+           }
+       }
 
     mctx->stream = g_mime_stream_file_new (mctx->file);
     if (!mctx->stream) {
index ae37360b83600bf6a2bac76d47caf397cc53901c..9d0f367d5e4ed849afb6d6b8b9f6b1693b43bf62 100644 (file)
@@ -200,6 +200,9 @@ notmuch_new_command (notmuch_config_t *config, int argc, char *argv[]);
 int
 notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]);
 
+int
+notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]);
+
 int
 notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]);
 
@@ -244,7 +247,7 @@ show_one_part (const char *filename, int part);
 
 void
 format_part_sprinter (const void *ctx, struct sprinter *sp, mime_node_t *node,
-                     notmuch_bool_t first, notmuch_bool_t output_body,
+                     notmuch_bool_t output_body,
                      notmuch_bool_t include_html);
 
 void
index 50b0c193f04625c794b9ab9287ab6bdd9c4f29da..97281374b22252059fcbf65ff28d426615b2a632 100644 (file)
@@ -204,6 +204,8 @@ notmuch_count_command (notmuch_config_t *config, int argc, char *argv[])
 
     if (batch && opt_index != argc) {
        fprintf (stderr, "--batch and query string are not compatible\n");
+       if (input)
+           fclose (input);
        return EXIT_FAILURE;
     }
 
index bc96af0e881c4ec2b2dac8130bf9aea22588fb81..648bd944a7b122c20d05358a510efa69b25bc4e1 100644 (file)
@@ -27,6 +27,7 @@
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <fcntl.h>
+#include "string-util.h"
 
 static volatile sig_atomic_t interrupted;
 
@@ -383,7 +384,7 @@ add_file (notmuch_database_t *notmuch, const char *path, tag_op_list_t *tag_ops,
     notmuch_message_t *message;
     notmuch_status_t status;
 
-    status = notmuch_database_add_message (notmuch, path, &message);
+    status = notmuch_database_index_file (notmuch, path, NULL, &message);
     if (status == NOTMUCH_STATUS_SUCCESS) {
        status = tag_op_list_apply (message, tag_ops, 0);
        if (status) {
@@ -451,7 +452,7 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[])
     size_t new_tags_length;
     tag_op_list_t *tag_ops;
     char *query_string = NULL;
-    const char *folder = NULL;
+    char *folder = NULL;
     notmuch_bool_t create_folder = FALSE;
     notmuch_bool_t keep = FALSE;
     notmuch_bool_t no_hooks = FALSE;
@@ -511,6 +512,7 @@ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[])
     if (folder == NULL) {
        maildir = db_path;
     } else {
+       strip_trailing (folder, '/');
        if (! is_valid_folder_name (folder)) {
            fprintf (stderr, "Error: invalid folder name: '%s'\n", folder);
            return EXIT_FAILURE;
index 3a60f7cae47fee568d8002f5db178b6ea7587d7a..e011788da590a74433a526c5fe727e604be7db3e 100644 (file)
@@ -261,16 +261,22 @@ add_file (notmuch_database_t *notmuch, const char *filename,
     if (status)
        goto DONE;
 
-    status = notmuch_database_add_message (notmuch, filename, &message);
+    status = notmuch_database_index_file (notmuch, filename, NULL, &message);
     switch (status) {
     /* Success. */
     case NOTMUCH_STATUS_SUCCESS:
        state->added_messages++;
        notmuch_message_freeze (message);
-       for (tag = state->new_tags; *tag != NULL; tag++)
-           notmuch_message_add_tag (message, *tag);
        if (state->synchronize_flags)
            notmuch_message_maildir_flags_to_tags (message);
+
+       for (tag = state->new_tags; *tag != NULL; tag++) {
+           if (strcmp ("unread", *tag) !=0 ||
+               !notmuch_message_has_maildir_flag (message, 'S')) {
+               notmuch_message_add_tag (message, *tag);
+           }
+       }
+
        notmuch_message_thaw (message);
        break;
     /* Non-fatal issues (go on to next file). */
@@ -291,8 +297,7 @@ add_file (notmuch_database_t *notmuch, const char *filename,
     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
     case NOTMUCH_STATUS_OUT_OF_MEMORY:
-       fprintf (stderr, "Error: %s. Halting processing.\n",
-                notmuch_status_to_string (status));
+       (void) print_status_database("add_file", notmuch, status);
        goto DONE;
     default:
        INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
diff --git a/notmuch-reindex.c b/notmuch-reindex.c
new file mode 100644 (file)
index 0000000..bceac72
--- /dev/null
@@ -0,0 +1,134 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright Â© 2016 Daniel Kahn Gillmor
+ *
+ * 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/ .
+ *
+ * Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ */
+
+#include "notmuch-client.h"
+#include "string-util.h"
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+    static char msg[] = "Stopping...         \n";
+
+    /* This write is "opportunistic", so it's okay to ignore the
+     * result.  It is not required for correctness, and if it does
+     * fail or produce a short write, we want to get out of the signal
+     * handler as quickly as possible, not retry it. */
+    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
+    interrupted = 1;
+}
+
+/* reindex all messages matching 'query_string' using the passed-in indexopts
+ */
+static int
+reindex_query (notmuch_database_t *notmuch, const char *query_string,
+              notmuch_indexopts_t *indexopts)
+{
+    notmuch_query_t *query;
+    notmuch_messages_t *messages;
+    notmuch_message_t *message;
+    notmuch_status_t status;
+
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    query = notmuch_query_create (notmuch, query_string);
+    if (query == NULL) {
+       fprintf (stderr, "Out of memory.\n");
+       return 1;
+    }
+
+    /* reindexing is not interested in any special sort order */
+    notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
+
+    status = notmuch_query_search_messages (query, &messages);
+    if (print_status_query ("notmuch reindex", query, status))
+       return status;
+
+    ret = notmuch_database_begin_atomic (notmuch);
+    for (;
+        notmuch_messages_valid (messages) && ! interrupted;
+        notmuch_messages_move_to_next (messages)) {
+       message = notmuch_messages_get (messages);
+
+       ret = notmuch_message_reindex(message, indexopts);
+       if (ret != NOTMUCH_STATUS_SUCCESS)
+           break;
+    }
+
+    if (!ret)
+       ret = notmuch_database_end_atomic (notmuch);
+
+    notmuch_query_destroy (query);
+
+    return ret || interrupted;
+}
+
+int
+notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[])
+{
+    char *query_string = NULL;
+    notmuch_database_t *notmuch;
+    struct sigaction action;
+    int opt_index;
+    int ret;
+    notmuch_indexopts_t *indexopts = NULL;
+
+    /* Set up our handler for SIGINT */
+    memset (&action, 0, sizeof (struct sigaction));
+    action.sa_handler = handle_sigint;
+    sigemptyset (&action.sa_mask);
+    action.sa_flags = SA_RESTART;
+    sigaction (SIGINT, &action, NULL);
+
+    notmuch_opt_desc_t options[] = {
+       { NOTMUCH_OPT_INHERIT, (void *) &notmuch_shared_options, NULL, 0, 0 },
+       { 0, 0, 0, 0, 0 }
+    };
+
+    opt_index = parse_arguments (argc, argv, options, 1);
+    if (opt_index < 0)
+       return EXIT_FAILURE;
+
+    notmuch_process_shared_options (argv[0]);
+
+    if (notmuch_database_open (notmuch_config_get_database_path (config),
+                              NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
+       return EXIT_FAILURE;
+
+    notmuch_exit_if_unmatched_db_uuid (notmuch);
+
+    query_string = query_string_from_args (config, argc-opt_index, argv+opt_index);
+    if (query_string == NULL) {
+       fprintf (stderr, "Out of memory\n");
+       return EXIT_FAILURE;
+    }
+
+    if (*query_string == '\0') {
+       fprintf (stderr, "Error: notmuch reindex requires at least one search term.\n");
+       return EXIT_FAILURE;
+    }
+    
+    ret = reindex_query (notmuch, query_string, indexopts);
+
+    notmuch_database_destroy (notmuch);
+
+    return ret || interrupted ? EXIT_FAILURE : EXIT_SUCCESS;
+}
index 40201b805369307727507bb2a6a4b206af25d7a8..929f3077862f93e6f8c2b213f9d064369cc42fc9 100644 (file)
@@ -667,7 +667,7 @@ static int do_reply(notmuch_config_t *config,
 
            /* Start the original */
            sp->map_key (sp, "original");
-           format_part_sprinter (config, sp, node, TRUE, TRUE, FALSE);
+           format_part_sprinter (config, sp, node, TRUE, FALSE);
 
            /* End */
            sp->end (sp);
index 019e14eea846d6a68fe36a0210318bcd35d84063..380e9d8fbd8f6dffc582cdd0e8788f7dc11b3913 100644 (file)
@@ -160,6 +160,7 @@ do_search_threads (search_context_t *ctx)
            const char *subject = notmuch_thread_get_subject (thread);
            const char *thread_id = notmuch_thread_get_thread_id (thread);
            int matched = notmuch_thread_get_matched_messages (thread);
+           int files = notmuch_thread_get_total_files (thread);
            int total = notmuch_thread_get_total_messages (thread);
            const char *relative_date = NULL;
            notmuch_bool_t first_tag = TRUE;
@@ -175,13 +176,23 @@ do_search_threads (search_context_t *ctx)
 
            if (format->is_text_printer) {
                 /* Special case for the text formatter */
-               printf ("thread:%s %12s [%d/%d] %s; %s (",
+               printf ("thread:%s %12s ",
                        thread_id,
-                       relative_date,
+                       relative_date);
+               if (total == files)
+                   printf ("[%d/%d] %s; %s (",
                        matched,
                        total,
                        sanitize_string (ctx_quote, authors),
                        sanitize_string (ctx_quote, subject));
+               else
+                   printf ("[%d/%d(%d)] %s; %s (",
+                       matched,
+                       total,
+                       files,
+                       sanitize_string (ctx_quote, authors),
+                       sanitize_string (ctx_quote, subject));
+
            } else { /* Structured Output */
                format->map_key (format, "thread");
                format->string (format, thread_id);
index 74e77249d123816257388d3a993811f37fba682b..cdcc2a982bd9daac222f9961402e5d8612b56978 100644 (file)
@@ -603,7 +603,7 @@ format_omitted_part_meta_sprinter (sprinter_t *sp, GMimeObject *meta, GMimePart
 
 void
 format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
-                     notmuch_bool_t first, notmuch_bool_t output_body,
+                     notmuch_bool_t output_body,
                      notmuch_bool_t include_html)
 {
     /* Any changes to the JSON or S-Expression format should be
@@ -619,7 +619,7 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
        if (output_body) {
            sp->map_key (sp, "body");
            sp->begin_list (sp);
-           format_part_sprinter (ctx, sp, mime_node_child (node, 0), first, TRUE, include_html);
+           format_part_sprinter (ctx, sp, mime_node_child (node, 0), TRUE, include_html);
            sp->end (sp);
        }
        sp->end (sp);
@@ -721,7 +721,7 @@ format_part_sprinter (const void *ctx, sprinter_t *sp, mime_node_t *node,
     }
 
     for (i = 0; i < node->nchildren; i++)
-       format_part_sprinter (ctx, sp, mime_node_child (node, i), i == 0, TRUE, include_html);
+       format_part_sprinter (ctx, sp, mime_node_child (node, i), TRUE, include_html);
 
     /* Close content structures */
     for (i = 0; i < nclose; i++)
@@ -735,7 +735,7 @@ format_part_sprinter_entry (const void *ctx, sprinter_t *sp,
                            mime_node_t *node, unused (int indent),
                            const notmuch_show_params_t *params)
 {
-    format_part_sprinter (ctx, sp, node, TRUE, params->output_body, params->include_html);
+    format_part_sprinter (ctx, sp, node, params->output_body, params->include_html);
 
     return NOTMUCH_STATUS_SUCCESS;
 }
index 9c03754d58568a04f9a7971d2296ab014c19f054..130de6343f8ec42bac91c7b6114e83ab2a4f854c 100644 (file)
@@ -235,6 +235,8 @@ notmuch_tag_command (notmuch_config_t *config, int argc, char *argv[])
     if (batch) {
        if (opt_index != argc) {
            fprintf (stderr, "Can't specify both cmdline and stdin!\n");
+           if (input)
+               fclose (input);
            return EXIT_FAILURE;
        }
     } else {
index 8e332ce644101addf99304952334b71838817f11..201c7454ee7377a33127a4f5a9e9b8921539dbd9 100644 (file)
--- a/notmuch.c
+++ b/notmuch.c
@@ -123,6 +123,8 @@ static command_t commands[] = {
       "Restore the tags from the given dump file (see 'dump')." },
     { "compact", notmuch_compact_command, NOTMUCH_CONFIG_OPEN,
       "Compact the notmuch database." },
+    { "reindex", notmuch_reindex_command, NOTMUCH_CONFIG_OPEN,
+      "Re-index all messages matching the search terms." },
     { "config", notmuch_config_command, NOTMUCH_CONFIG_OPEN,
       "Get or set settings in the notmuch configuration file." },
     { "help", notmuch_help_command, NOTMUCH_CONFIG_CREATE, /* create but don't save config */
index f3f9be41856b89863d20059ec3877110907b76d2..8a5dabf5a0346bffc7512eddd915581f4ed8c49d 100644 (file)
@@ -1,4 +1,4 @@
-tmp.*/
-log.*/
-corpus/
-notmuch.cache.*/
+/tmp.*/
+/log.*/
+/corpus/
+/notmuch.cache.*/
diff --git a/performance-test/M05-reindex.sh b/performance-test/M05-reindex.sh
new file mode 100755 (executable)
index 0000000..d36e061
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+test_description='reindex'
+
+. ./perf-test-lib.sh || exit 1
+
+memory_start
+
+memory_run 'reindex *' "notmuch reindex '*'"
+
+memory_done
diff --git a/performance-test/M06-insert.sh b/performance-test/M06-insert.sh
new file mode 100755 (executable)
index 0000000..9fcc2a0
--- /dev/null
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+test_description='search'
+
+. ./perf-test-lib.sh || exit 1
+
+memory_start
+
+mkdir -p "$MAIL_DIR"/{cur,new,tmp}
+
+for count in {1..20}; do
+    generate_message "[file]=\"insert-$count\"" "[dir]='tmp/'"
+    memory_run "insert $count" "notmuch insert < $gen_msg_filename"
+done
+
+memory_done
diff --git a/performance-test/T03-reindex.sh b/performance-test/T03-reindex.sh
new file mode 100755 (executable)
index 0000000..7af2d22
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+test_description='tagging'
+
+. ./perf-test-lib.sh || exit 1
+
+time_start
+
+time_run 'reindex *' "notmuch reindex '*'"
+time_run 'reindex *' "notmuch reindex '*'"
+time_run 'reindex *' "notmuch reindex '*'"
+
+time_done
index 7b092346e4239ed9ced54115b05e67e35d60883f..5c3562045af230d93163323b39dbff144f4ed665 100644 (file)
@@ -1,2 +1,2 @@
-*.tar.gz
-*.tar.xz
+/*.tar.gz
+/*.tar.xz
index 0579feef25c659f6da7752b54e93bb672d95b21c..73fe7e24620f923826803dbaa0449e1350cd1716 100644 (file)
@@ -1,11 +1,11 @@
-arg-test
-corpus.mail
-hex-xcode
-parse-time
-random-corpus
-smtp-dummy
-symbol-test
-make-db-version
-test-results
-ghost-report
-tmp.*
+/arg-test
+/corpora.mail
+/hex-xcode
+/parse-time
+/random-corpus
+/smtp-dummy
+/symbol-test
+/make-db-version
+/test-results
+/ghost-report
+/tmp.*
index 36a7ca4c5bdfb78ed28430ab6c471123780b85f9..a4efa3cab8b5095a0de997497c7d57276b34840e 100755 (executable)
@@ -23,8 +23,7 @@ test_expect_success ':'
 
 test_begin_subtest 'test runs if prerequisite is satisfied'
 test_set_prereq HAVEIT
-haveit=no
-test_expect_success 'test_have_prereq HAVEIT && haveit=yes'
+test_expect_success 'test_have_prereq HAVEIT'
 
 test_begin_subtest 'tests clean up after themselves'
 clean=no
index ffa303efaceca99cdc7c0d6cbf75a83f6184bb1d..272ed417aa2eb8f4a6f9724b8bb540449f45c7fd 100755 (executable)
@@ -309,7 +309,7 @@ cat <<EOF > notmuch-new-vanish.gdb
 set breakpoint pending on
 set logging file notmuch-new-vanish-gdb.log
 set logging on
-break add_file
+break notmuch_database_index_file
 commands
 shell rm -f ${MAIL_DIR}/vanish
 continue
index 48f212ee09283f45468a405c55544223c31daf0f..e56a9d21880ee9d093b47e00d9ac014cf1807940 100755 (executable)
@@ -132,6 +132,13 @@ output=$(notmuch search --output=files path:Drafts/new)
 dirname=$(dirname "$output")
 test_expect_equal "$dirname" "$MAIL_DIR/Drafts/new"
 
+test_begin_subtest "Insert message into folder with trailing /"
+gen_insert_msg
+notmuch insert --folder=Drafts/ < "$gen_msg_filename"
+output=$(notmuch search --output=files id:${gen_msg_id})
+dirname=$(dirname "$output")
+test_expect_equal "$dirname" "$MAIL_DIR/Drafts/new"
+
 test_begin_subtest "Insert message into folder, add/remove tags"
 gen_insert_msg
 notmuch insert --folder=Drafts +draft -unread < "$gen_msg_filename"
@@ -193,7 +200,7 @@ cat <<EOF > index-file-$code.gdb
 set breakpoint pending on
 set logging file index-file-$code.log
 set logging on
-break notmuch_database_add_message
+break notmuch_database_index_file
 commands
 return NOTMUCH_STATUS_$code
 continue
@@ -205,13 +212,13 @@ done
 gen_insert_msg
 
 for code in  FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do
-    test_begin_subtest "EXIT_FAILURE when add_message returns $code"
+    test_begin_subtest "EXIT_FAILURE when index_file returns $code"
     test_expect_code 1 \
          "${TEST_GDB} --batch-silent --return-child-result \
             -ex 'set args insert < $gen_msg_filename' \
             -x index-file-$code.gdb notmuch"
 
-    test_begin_subtest "success exit with --keep when add_message returns $code"
+    test_begin_subtest "success exit with --keep when index_file returns $code"
     test_expect_code 0 \
          "${TEST_GDB} --batch-silent --return-child-result \
             -ex 'set args insert --keep < $gen_msg_filename' \
@@ -219,13 +226,13 @@ for code in  FILE_NOT_EMAIL READ_ONLY_DATABASE UPGRADE_REQUIRED PATH_ERROR; do
 done
 
 for code in OUT_OF_MEMORY XAPIAN_EXCEPTION ; do
-    test_begin_subtest "EX_TEMPFAIL when add_message returns $code"
+    test_begin_subtest "EX_TEMPFAIL when index_file returns $code"
     test_expect_code 75 \
          "${TEST_GDB} --batch-silent --return-child-result \
             -ex 'set args insert < $gen_msg_filename' \
             -x index-file-$code.gdb notmuch"
 
-    test_begin_subtest "success exit with --keep when add_message returns $code"
+    test_begin_subtest "success exit with --keep when index_file returns $code"
     test_expect_code 0 \
          "${TEST_GDB} --batch-silent --return-child-result \
             -ex 'set args insert --keep < $gen_msg_filename' \
index d2d71ca9e7c2aa154d591192a4ec008a1834aa08..3bb3dced89159c2d4a5279359498032427bc3ad0 100755 (executable)
@@ -111,7 +111,7 @@ thread:XXX   2009-11-18 [3/3] Adrian Perez de Castro, Keith Packard, Carl Worth;
 thread:XXX   2009-11-18 [3/3] Israel Herraiz, Keith Packard, Carl Worth; [notmuch] New to the list (inbox unread)
 thread:XXX   2009-11-18 [3/3] Jan Janak, Carl Worth; [notmuch] What a great idea! (inbox unread)
 thread:XXX   2009-11-18 [2/2] Jan Janak, Carl Worth; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
-thread:XXX   2009-11-18 [3/3] Aron Griffis, Keith Packard, Carl Worth; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [3/3(4)] Aron Griffis, Keith Packard, Carl Worth; [notmuch] archive (inbox unread)
 thread:XXX   2009-11-18 [2/2] Keith Packard, Carl Worth; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
 thread:XXX   2009-11-18 [7/7] Lars Kellogg-Stedman, Mikhail Gusarov, Keith Packard, Carl Worth; [notmuch] Working with Maildir storage? (inbox signed unread)
 thread:XXX   2009-11-18 [5/5] Mikhail Gusarov, Carl Worth, Keith Packard; [notmuch] [PATCH 1/2] Close message file after parsing message headers (inbox unread)
index 2844ec61f47fc6f93cde459ac1e4e34c4b7af6f2..79c266e445f5fb8968979e65c0bd94dec6afca3c 100755 (executable)
@@ -15,7 +15,7 @@ add_message '[dir]=things/bad' '[subject]="Bites, stings, sad feelings"'
 test_begin_subtest "Single-world folder: specification (multiple results)"
 output=$(notmuch search folder:bad folder:bad/news folder:things/bad | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread)
-thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)
+thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread)
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)"
 
 test_begin_subtest "Top level folder"
@@ -24,7 +24,7 @@ test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; T
 
 test_begin_subtest "Two-word path to narrow results to one"
 output=$(notmuch search folder:bad/news | notmuch_search_sanitize)
-test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)"
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread)"
 
 test_begin_subtest "Folder search with --output=files"
 output=$(notmuch search --output=files folder:bad/news | notmuch_search_files_sanitize)
index ac51895e826d260601260d6aae1a55bb90138fa9..07955a2bb90ac8441cdcee5ab3b27d3b9ea910c7 100755 (executable)
@@ -71,8 +71,8 @@ test_begin_subtest "Format version: too high"
 test_expect_code 21 "notmuch search --format-version=999 \\*"
 
 test_begin_subtest "Show message: multiple filenames"
-add_message "[id]=message-id@example.com [filename]=copy1"
-add_message "[id]=message-id@example.com [filename]=copy2"
+add_message '[id]=message-id@example.com [filename]=copy1 [date]="Fri, 05 Jan 2001 15:43:52 +0000"'
+add_message '[id]=message-id@example.com [filename]=copy2 [date]="Fri, 05 Jan 2001 15:43:52 +0000"'
 cat <<EOF > EXPECTED
 [
     [
index 40e5e21d62cc2c87b2b70e0a67933b932505f40c..db142e49683d2279f845b8aa6a3351f1b1d79a60 100755 (executable)
@@ -26,6 +26,11 @@ add_message "[subject]=\"sexp-show-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan
 output=$(notmuch show --format=sexp "jsön-show-méssage")
 test_expect_equal "$output" "((((:id \"${gen_msg_id}\" :match t :excluded nil :filename (\"${gen_msg_filename}\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\" \"unread\") :headers (:Subject \"sexp-show-utf8-body-sübjéct\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\") :body ((:id 1 :content-type \"text/plain\" :content \"jsön-show-méssage\n\"))) ())))"
 
+test_begin_subtest "Search message: sexp, utf-8"
+add_message "[subject]=\"sexp-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
+output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_search_sanitize)
+test_expect_equal "$output" "((:thread \"0000000000000004\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
+
 test_begin_subtest "Show message: sexp, inline attachment filename"
 subject='sexp-show-inline-attachment-filename'
 id="sexp-show-inline-attachment-filename@notmuchmail.org"
@@ -41,10 +46,4 @@ filename=$(notmuch search --output=files "id:$id")
 attachment_length=$(( $(base64 $TEST_DIRECTORY/README | wc -c) - 1 ))
 test_expect_equal "$output" "((((:id \"$id\" :match t :excluded nil :filename (\"$filename\") :timestamp 946728000 :date_relative \"2000-01-01\" :tags (\"inbox\") :headers (:Subject \"sexp-show-inline-attachment-filename\" :From \"Notmuch Test Suite <test_suite@notmuchmail.org>\" :To \"test_suite@notmuchmail.org\" :Date \"Sat, 01 Jan 2000 12:00:00 +0000\") :body ((:id 1 :content-type \"multipart/mixed\" :content ((:id 2 :content-type \"text/plain\" :content \"This is a test message with inline attachment with a filename\") (:id 3 :content-type \"application/octet-stream\" :content-disposition \"inline\" :filename \"README\" :content-transfer-encoding \"base64\" :content-length $attachment_length))))) ())))"
 
-test_begin_subtest "Search message: sexp, utf-8"
-add_message "[subject]=\"sexp-search-utf8-body-sübjéct\"" "[date]=\"Sat, 01 Jan 2000 12:00:00 -0000\"" "[body]=\"jsön-search-méssage\""
-output=$(notmuch search --format=sexp "jsön-search-méssage" | notmuch_search_sanitize)
-test_expect_equal "$output" "((:thread \"0000000000000005\" :timestamp 946728000 :date_relative \"2000-01-01\" :matched 1 :total 1 :authors \"Notmuch Test Suite\" :subject \"sexp-search-utf8-body-sübjéct\" :query (\"id:$gen_msg_id\" nil) :tags (\"inbox\" \"unread\")))"
-
-
 test_done
index 6d9566354bb32b1b3705dcf6daeb993dc39fa29c..b473ae4e1c0f8b0d04c25bd64eeed699935b06d6 100755 (executable)
@@ -28,6 +28,14 @@ add_message [subject]='"Adding message with S"' [filename]='adding-with-s-flag:2
 output=$(notmuch search subject:"Adding message with S" | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding message with S (inbox)"
 
+test_begin_subtest "Adding message with 'S' w/o 'unread' in new.tags prevents 'unread' tag"
+OLDCONFIG=$(notmuch config get new.tags)
+notmuch config set new.tags "inbox"
+add_message [subject]='"Adding message with S 2"' [filename]='adding-with-s-flag2:2,S' [dir]=cur
+notmuch config set new.tags $OLDCONFIG
+output=$(notmuch search subject:Adding-message-with-S-2 | notmuch_search_sanitize)
+test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Adding message with S 2 (inbox)"
+
 test_begin_subtest "Adding 'replied' tag adds 'R' flag to filename"
 add_message [subject]='"Adding replied tag"' [filename]='adding-replied-tag:2,S' [dir]=cur
 notmuch tag +replied subject:"Adding replied tag"
@@ -49,7 +57,7 @@ test_expect_equal_json "$output" '[[[{"id": "XXXXX",
 "Date": "GENERATED_DATE"},
 "body": [{"id": 1,
 "content-type": "text/plain",
-"content": "This is just a test message (#3)\n"}]},
+"content": "This is just a test message (#4)\n"}]},
 []]]]'
 
 test_begin_subtest "notmuch reply works with renamed file (without notmuch new)"
@@ -153,14 +161,14 @@ cp "$MAIL_DIR/cur/duplicated-message:2," "$MAIL_DIR/cur/duplicated-message-copy:
 NOTMUCH_NEW > output
 notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output
 test_expect_equal "$(< output)" "No new mail.
-thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Duplicated message (inbox replied)"
+thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Duplicated message (inbox replied)"
 
 test_begin_subtest "Adding duplicate message without flags does not remove tags"
 cp "$MAIL_DIR/cur/duplicated-message-copy:2,RS" "$MAIL_DIR/cur/duplicated-message-another-copy:2,"
 NOTMUCH_NEW > output
 notmuch search subject:"Duplicated message" | notmuch_search_sanitize >> output
 test_expect_equal "$(< output)" "No new mail.
-thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Duplicated message (inbox replied)"
+thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; Duplicated message (inbox replied)"
 
 test_begin_subtest "Tag changes modify flags of multiple files"
 notmuch tag -replied subject:"Duplicated message"
@@ -181,11 +189,21 @@ test_expect_equal "$(cd $MAIL_DIR/cur/; ls non-compliant*)" "non-compliant-maild
 
 test_begin_subtest "Files in new/ get default synchronized tags"
 OLDCONFIG=$(notmuch config get new.tags)
-notmuch config set new.tags test
+notmuch config set new.tags "test;unread"
 add_message [subject]='"File in new/"' [dir]=new [filename]='file-in-new'
 notmuch config set new.tags $OLDCONFIG
 notmuch search 'subject:"File in new"' | notmuch_search_sanitize > output
 test_expect_equal "$(< output)" \
 "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; File in new/ (test unread)"
 
+for tag in draft flagged passed replied; do
+    test_begin_subtest "$tag is valid in new.tags"
+    OLDCONFIG=$(notmuch config get new.tags)
+    notmuch config set new.tags "$tag;unread"
+    add_message [subject]="\"$tag sync in new\"" [dir]=new
+    notmuch config set new.tags $OLDCONFIG
+    notmuch search "subject:\"$tag sync in new\"" | notmuch_search_sanitize > output
+    test_expect_equal "$(< output)" \
+                     "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; $tag sync in new ($tag unread)"
+done
 test_done
index 7dab39a2763160dd2dbbc946ce9094ba569b19ae..1d408af71dd7a67575a0993a0f7819d89275657b 100755 (executable)
@@ -394,7 +394,7 @@ test_expect_equal_json \
 
 test_begin_subtest "reply to encrypted message"
 output=$(notmuch reply --decrypt subject:"test encrypted message 002" \
-    | grep -v -e '^In-Reply-To:' -e '^References:')
+    | notmuch_drop_mail_headers In-Reply-To References)
 expected='From: Notmuch Test Suite <test_suite@notmuchmail.org>
 Subject: Re: test encrypted message 002
 
index d1cb45eccc143ed27273f9ef83ccc214306dba6a..8748b3d00eeb0cdae3df92a6c5299ca4abf59f78 100755 (executable)
@@ -32,7 +32,7 @@ test_expect_equal_file EXPECTED OUTPUT
 
 test_begin_subtest "Test matches folder:spam"
 output=$(notmuch search folder:spam)
-test_expect_equal "$output" "thread:0000000000000001   2001-01-05 [1/1] Notmuch Test Suite; Single new message (inbox unread)"
+test_expect_equal "$output" "thread:0000000000000001   2001-01-05 [1/1(2)] Notmuch Test Suite; Single new message (inbox unread)"
 
 test_begin_subtest "Remove folder:spam copy of email"
 rm $dir/spam/$(basename $file_x)
index f10207f8cf8e1840ac40628b23089ef657b4e51d..fc4ecdc30b1655d51346e7f68a2fbb38360f7a91 100755 (executable)
@@ -23,7 +23,7 @@ notmuch search date:18-Nov-2009_02:19:26-0800..2009-11-18_04:49:52-06:00 | notmu
 cat <<EOF >EXPECTED
 thread:XXX   2009-11-18 [1/3] Carl Worth| Jan Janak; [notmuch] What a great idea! (inbox unread)
 thread:XXX   2009-11-18 [1/2] Carl Worth| Jan Janak; [notmuch] [PATCH] Older versions of install do not support -C. (inbox unread)
-thread:XXX   2009-11-18 [1/3] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
+thread:XXX   2009-11-18 [1/3(4)] Carl Worth| Aron Griffis, Keith Packard; [notmuch] archive (inbox unread)
 thread:XXX   2009-11-18 [1/2] Carl Worth| Keith Packard; [notmuch] [PATCH] Make notmuch-show 'X' (and 'x') commands remove inbox (and unread) tags (inbox unread)
 EOF
 test_expect_equal_file EXPECTED OUTPUT
index 34d15698d9b69c80139c1bc54c3eef95b54d3c73..a50eca807700f7a3d87d58c5374c40b404fd3857 100755 (executable)
@@ -127,7 +127,7 @@ int main (int argc, char** argv)
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d\n", stat);
    }
-   stat = notmuch_database_add_message (db, "/dev/null", NULL);
+   stat = notmuch_database_index_file (db, "/dev/null", NULL, NULL);
    if (stat)
        fputs (notmuch_database_status_string (db), stderr);
 
@@ -152,7 +152,7 @@ int main (int argc, char** argv)
    if (stat != NOTMUCH_STATUS_SUCCESS) {
      fprintf (stderr, "error opening database: %d\n", stat);
    }
-   stat = notmuch_database_add_message (db, "./nonexistent", NULL);
+   stat = notmuch_database_index_file (db, "./nonexistent", NULL, NULL);
    if (stat) {
        char *status_string = notmuch_database_status_string (db);
        if (status_string) fputs (status_string, stderr);
index b7bdda118e97850f4c8c25bb9607c313fd4b1123..d5def7649d0f71fda66c9910e611ec04a5741b10 100755 (executable)
@@ -29,7 +29,7 @@ test_expect_equal_file EXPECTED OUTPUT
 test_begin_subtest "unanchored folder:// specification"
 output=$(notmuch search folder:/bad/ | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread)
-thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)
+thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread)
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)"
 
 test_begin_subtest "anchored folder:// search"
@@ -39,7 +39,7 @@ test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; T
 test_begin_subtest "unanchored path:// specification"
 output=$(notmuch search path:/bad/ | notmuch_search_sanitize)
 test_expect_equal "$output" "thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; To the bone (inbox unread)
-thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bears (inbox unread)
+thread:XXX   2001-01-05 [1/1(2)] Notmuch Test Suite; Bears (inbox unread)
 thread:XXX   2001-01-05 [1/1] Notmuch Test Suite; Bites, stings, sad feelings (inbox unread)"
 
 test_begin_subtest "anchored path:// search"
diff --git a/test/T670-duplicate-mid.sh b/test/T670-duplicate-mid.sh
new file mode 100755 (executable)
index 0000000..21a9689
--- /dev/null
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+test_description="duplicate message ids"
+. ./test-lib.sh || exit 1
+
+add_message '[id]="duplicate"' '[subject]="message 1" [filename]=copy1'
+add_message '[id]="duplicate"' '[subject]="message 2" [filename]=copy2'
+
+add_message '[id]="duplicate"' '[subject]="message 0" [filename]=copy0'
+test_begin_subtest 'search: first indexed subject preserved'
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; message 1 (inbox unread)
+EOF
+notmuch search id:duplicate | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'First subject preserved in notmuch-show (json)'
+test_subtest_known_broken
+output=$(notmuch show --body=false --format=json id:duplicate | notmuch_json_show_sanitize)
+expected='[[[{
+    "id": "XXXXX",
+    "match": true,
+    "excluded": false,
+    "filename": [
+        "'"${MAIL_DIR}"/copy0'",
+        "'"${MAIL_DIR}"/copy1'",
+        "'"${MAIL_DIR}"/copy2'"
+    ],
+    "timestamp": 42,
+    "date_relative": "2001-01-05",
+    "tags": ["inbox","unread"],
+    "headers": {
+        "Subject": "message 1",
+        "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "Date": "GENERATED_DATE"
+    }
+ },
+[]]]]'
+test_expect_equal_json "$output" "$expected"
+
+test_begin_subtest 'Search for second subject'
+cat <<EOF >EXPECTED
+MAIL_DIR/copy0
+MAIL_DIR/copy1
+MAIL_DIR/copy2
+EOF
+notmuch search --output=files subject:'"message 2"' | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+add_message '[id]="duplicate"' '[body]="sekrit" [filename]=copy3'
+test_begin_subtest 'search for body in duplicate file'
+cat <<EOF >EXPECTED
+MAIL_DIR/copy0
+MAIL_DIR/copy1
+MAIL_DIR/copy2
+MAIL_DIR/copy3
+EOF
+notmuch search --output=files "sekrit" | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+rm ${MAIL_DIR}/copy3
+test_begin_subtest 'reindex drops terms in duplicate file'
+cp /dev/null EXPECTED
+notmuch reindex '*'
+notmuch search --output=files "sekrit" | notmuch_dir_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'reindex choses subject from first filename'
+cat <<EOF > EXPECTED
+thread:XXX   2001-01-05 [1/1(3)] Notmuch Test Suite; message 0 (inbox unread)
+EOF
+notmuch search id:duplicate | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+rm ${MAIL_DIR}/copy0
+test_begin_subtest 'Deleted first duplicate file does not stop notmuch show from working'
+output=$(notmuch show --body=false --format=json id:duplicate |
+            notmuch_json_show_sanitize | sed 's/message [0-9]/A_SUBJECT/')
+expected='[[[{
+    "id": "XXXXX",
+    "match": true,
+    "excluded": false,
+    "filename": [
+        "'"${MAIL_DIR}"/copy0'",
+        "'"${MAIL_DIR}"/copy1'",
+        "'"${MAIL_DIR}"/copy2'"
+    ],
+    "timestamp": 42,
+    "date_relative": "2001-01-05",
+    "tags": ["inbox","unread"],
+    "headers": {
+        "Subject": "A_SUBJECT",
+        "From": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "To": "Notmuch Test Suite <test_suite@notmuchmail.org>",
+        "Date": "GENERATED_DATE"
+    }
+ },
+[]]]]'
+
+test_expect_equal_json "$output" "$expected"
+
+test_done
diff --git a/test/T700-reindex.sh b/test/T700-reindex.sh
new file mode 100755 (executable)
index 0000000..051fbb3
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+test_description='reindexing messages'
+. ./test-lib.sh || exit 1
+
+add_email_corpus
+
+notmuch tag +usertag1 '*'
+
+notmuch search '*' | notmuch_search_sanitize > initial-threads
+notmuch search --output=messages '*' > initial-message-ids
+notmuch dump > initial-dump
+
+test_begin_subtest 'reindex preserves threads'
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file initial-threads OUTPUT
+
+test_begin_subtest 'reindex after removing duplicate file preserves threads'
+# remove one copy
+sed 's,3/3(4),3/3,' < initial-threads > EXPECTED
+mv $MAIL_DIR/bar/18:2, duplicate-msg-1.eml
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'reindex preserves message-ids'
+notmuch reindex '*'
+notmuch search --output=messages '*' > OUTPUT
+test_expect_equal_file initial-message-ids OUTPUT
+
+test_begin_subtest 'reindex preserves tags'
+notmuch reindex '*'
+notmuch dump > OUTPUT
+test_expect_equal_file initial-dump OUTPUT
+
+test_begin_subtest 'reindex moves a message between threads'
+notmuch search --output=threads id:87iqd9rn3l.fsf@vertex.dottedmag > EXPECTED
+# re-parent
+sed -i 's/1258471718-6781-1-git-send-email-dottedmag@dottedmag.net/87iqd9rn3l.fsf@vertex.dottedmag/' $MAIL_DIR/02:2,*
+notmuch reindex id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net
+notmuch search --output=threads id:1258471718-6781-2-git-send-email-dottedmag@dottedmag.net > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest 'reindex detects removal of all files'
+notmuch search --output=messages not id:20091117232137.GA7669@griffis1.net> EXPECTED
+# remove both copies
+mv $MAIL_DIR/cur/51:2,* duplicate-message-2.eml
+notmuch reindex id:20091117232137.GA7669@griffis1.net
+notmuch search --output=messages '*' > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_begin_subtest "reindex preserves properties"
+cat <<EOF > prop-dump
+#= 1258471718-6781-1-git-send-email-dottedmag@dottedmag.net userprop=userval
+#= 1258471718-6781-2-git-send-email-dottedmag@dottedmag.net userprop=userval
+#= 1258491078-29658-1-git-send-email-dottedmag@dottedmag.net userprop=userval1
+#= 20091117190054.GU3165@dottiness.seas.harvard.edu userprop=userval
+#= 20091117203301.GV3165@dottiness.seas.harvard.edu userprop=userval3
+#= 87fx8can9z.fsf@vertex.dottedmag userprop=userval2
+#= 87iqd9rn3l.fsf@vertex.dottedmag userprop=userval
+#= 87lji4lx9v.fsf@yoom.home.cworth.org userprop=userval3
+#= 87lji5cbwo.fsf@yoom.home.cworth.org userprop=userval
+#= cf0c4d610911171136h1713aa59w9cf9aa31f052ad0a@mail.gmail.com userprop=userval
+EOF
+notmuch restore < prop-dump
+notmuch reindex '*'
+notmuch dump | grep '^#=' | sort > OUTPUT
+test_expect_equal_file prop-dump OUTPUT
+test_done
+
+add_email_corpus lkml
+
+test_begin_subtest "reindex of lkml corpus preserves threads"
+notmuch search '*' | notmuch_search_sanitize > EXPECTED
+notmuch reindex '*'
+notmuch search '*' | notmuch_search_sanitize > OUTPUT
+test_expect_equal_file EXPECTED OUTPUT
+
+test_done
index e7d3151ce8cbb528c68d6526ba4c39a035d5a1eb..9d9df7dfedbe8f80caf3816e88225567edd3f284 100755 (executable)
@@ -50,6 +50,6 @@ echo
 ev=$?
 
 # Clean up
-rm -rf test-results corpus.mail
+rm -rf test-results corpora.mail
 
 exit $ev
index a629927c263c461582a872a961ae59f6329f3e29..71992edd1d968d5d0d14907ef505f0a0427f3051 100644 (file)
 static void
 receive_data_to_file (FILE *peer, FILE *output)
 {
-       char *line = NULL;
-       size_t line_size;
-       ssize_t line_len;
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
 
-       while ((line_len = getline (&line, &line_size, peer)) != -1) {
-               if (STRNCMP_LITERAL (line, ".\r\n") == 0)
-                       break;
-               if (line_len < 2)
-                       continue;
-               if (line[line_len-1] == '\n' && line[line_len-2] == '\r') {
-                       line[line_len-2] = '\n';
-                       line[line_len-1] = '\0';
-               }
-               fprintf (output, "%s",
-                        line[0] == '.' ? line + 1 : line);
+    while ((line_len = getline (&line, &line_size, peer)) != -1) {
+       if (STRNCMP_LITERAL (line, ".\r\n") == 0)
+           break;
+       if (line_len < 2)
+           continue;
+       if (line[line_len - 1] == '\n' && line[line_len - 2] == '\r') {
+           line[line_len - 2] = '\n';
+           line[line_len - 1] = '\0';
        }
+       fprintf (output, "%s",
+                line[0] == '.' ? line + 1 : line);
+    }
 
-       free (line);
+    free (line);
 }
 
 static int
 process_command (FILE *peer, FILE *output, const char *command)
 {
-       if (STRNCMP_LITERAL (command, "EHLO ") == 0) {
-               fprintf (peer, "502 not implemented\r\n");
-               fflush (peer);
-       } else if (STRNCMP_LITERAL (command, "HELO ") == 0) {
-               fprintf (peer, "250 localhost\r\n");
-               fflush (peer);
-       } else if (STRNCMP_LITERAL (command, "MAIL FROM:") == 0 ||
-                  STRNCMP_LITERAL (command, "RCPT TO:") == 0) {
-               fprintf (peer, "250 OK\r\n");
-               fflush (peer);
-       } else if (STRNCMP_LITERAL (command, "DATA") == 0) {
-               fprintf (peer, "354 End data with <CR><LF>.<CR><LF>\r\n");
-               fflush (peer);
-               receive_data_to_file (peer, output);
-               fprintf (peer, "250 OK\r\n");
-               fflush (peer);
-       } else if (STRNCMP_LITERAL (command, "QUIT") == 0) {
-               fprintf (peer, "221 BYE\r\n");
-               fflush (peer);
-               return 1;
-       } else {
-               fprintf (stderr, "Unknown command: %s\n", command);
-       }
-       return 0;
+    if (STRNCMP_LITERAL (command, "EHLO ") == 0) {
+       fprintf (peer, "502 not implemented\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "HELO ") == 0) {
+       fprintf (peer, "250 localhost\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "MAIL FROM:") == 0 ||
+              STRNCMP_LITERAL (command, "RCPT TO:") == 0) {
+       fprintf (peer, "250 OK\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "DATA") == 0) {
+       fprintf (peer, "354 End data with <CR><LF>.<CR><LF>\r\n");
+       fflush (peer);
+       receive_data_to_file (peer, output);
+       fprintf (peer, "250 OK\r\n");
+       fflush (peer);
+    } else if (STRNCMP_LITERAL (command, "QUIT") == 0) {
+       fprintf (peer, "221 BYE\r\n");
+       fflush (peer);
+       return 1;
+    } else {
+       fprintf (stderr, "Unknown command: %s\n", command);
+    }
+    return 0;
 }
 
 static void
 do_smtp_to_file (FILE *peer, FILE *output)
 {
-       char *line = NULL;
-       size_t line_size;
-       ssize_t line_len;
+    char *line = NULL;
+    size_t line_size;
+    ssize_t line_len;
 
-       fprintf (peer, "220 localhost smtp-dummy\r\n");
-       fflush (peer);
+    fprintf (peer, "220 localhost smtp-dummy\r\n");
+    fflush (peer);
 
-       while ((line_len = getline (&line, &line_size, peer)) != -1) {
-               if (process_command (peer, output, line))
-                       break;
-       }
+    while ((line_len = getline (&line, &line_size, peer)) != -1) {
+       if (process_command (peer, output, line))
+           break;
+    }
 
-       free (line);
+    free (line);
 }
 
 int
 main (int argc, char *argv[])
 {
-       const char * progname;
-       char *output_filename;
-       FILE *peer_file, *output;
-       int sock, peer, err;
-       struct sockaddr_in addr, peer_addr;
-       struct hostent *hostinfo;
-       socklen_t peer_addr_len;
-       int reuse;
-       int background;
+    const char *progname;
+    char *output_filename;
+    FILE *peer_file = NULL, *output = NULL;
+    int sock = -1, peer, err;
+    struct sockaddr_in addr, peer_addr;
+    struct hostent *hostinfo;
+    socklen_t peer_addr_len;
+    int reuse;
+    int background;
+    int ret = 0;
 
-       progname = argv[0];
+    progname = argv[0];
 
-       background = 0;
-       for (; argc >= 2; argc--, argv++) {
-               if (argv[1][0] != '-')
-                       break;
-               if (strcmp (argv[1], "--") == 0) {
-                       argc--;
-                       argv++;
-                       break;
-               }
-               if (strcmp (argv[1], "--background") == 0) {
-                       background = 1;
-                       continue;
-               }
-               fprintf(stderr, "%s: unregognized option '%s'\n",
-                       progname, argv[1]);
-               return 1;
+    background = 0;
+    for (; argc >= 2; argc--, argv++) {
+       if (argv[1][0] != '-')
+           break;
+       if (strcmp (argv[1], "--") == 0) {
+           argc--;
+           argv++;
+           break;
        }
-
-       if (argc != 2) {
-               fprintf (stderr,
-                        "Usage: %s [--background] <output-file>\n", progname);
-               return 1;
+       if (strcmp (argv[1], "--background") == 0) {
+           background = 1;
+           continue;
        }
+       fprintf (stderr, "%s: unregognized option '%s'\n",
+                progname, argv[1]);
+       return 1;
+    }
 
-       output_filename = argv[1];
-       output = fopen (output_filename, "w");
-       if (output == NULL) {
-               fprintf (stderr, "Failed to open %s for writing: %s\n",
-                        output_filename, strerror (errno));
-               return 1;
-       }
+    if (argc != 2) {
+       fprintf (stderr,
+                "Usage: %s [--background] <output-file>\n", progname);
+       return 1;
+    }
 
-       sock = socket (AF_INET, SOCK_STREAM, 0);
-       if (sock == -1) {
-               fprintf (stderr, "Error: socket() failed: %s\n",
-                        strerror (errno));
-               return 1;
-       }
+    output_filename = argv[1];
+    output = fopen (output_filename, "w");
+    if (output == NULL) {
+       fprintf (stderr, "Failed to open %s for writing: %s\n",
+                output_filename, strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
 
-       reuse = 1;
-       err = setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse));
-       if (err) {
-               fprintf (stderr, "Error: setsockopt() failed: %s\n",
-                        strerror (errno));
-               return 1;
-       }
+    sock = socket (AF_INET, SOCK_STREAM, 0);
+    if (sock == -1) {
+       fprintf (stderr, "Error: socket() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
 
-       hostinfo = gethostbyname ("localhost");
-       if (hostinfo == NULL) {
-               fprintf (stderr, "Unknown host: localhost\n");
-               return 1;
-       }
+    reuse = 1;
+    err = setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse));
+    if (err) {
+       fprintf (stderr, "Error: setsockopt() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
 
-       memset (&addr, 0, sizeof (addr));
-       addr.sin_family = AF_INET;
-       addr.sin_port = htons (25025);
-       addr.sin_addr = *(struct in_addr *) hostinfo->h_addr;
-       err = bind (sock, (struct sockaddr *) &addr, sizeof(addr));
-       if (err) {
-               fprintf (stderr, "Error: bind() failed: %s\n",
-                        strerror (errno));
-               close (sock);
-               return 1;
-       }
+    hostinfo = gethostbyname ("localhost");
+    if (hostinfo == NULL) {
+       fprintf (stderr, "Unknown host: localhost\n");
+       ret = 1;
+       goto DONE;
+    }
 
-       err = listen (sock, 1);
-       if (err) {
-               fprintf (stderr, "Error: listen() failed: %s\n",
-                        strerror (errno));
-               close (sock);
-               return 1;
-       }
+    memset (&addr, 0, sizeof (addr));
+    addr.sin_family = AF_INET;
+    addr.sin_port = htons (25025);
+    addr.sin_addr = *(struct in_addr *) hostinfo->h_addr;
+    err = bind (sock, (struct sockaddr *) &addr, sizeof (addr));
+    if (err) {
+       fprintf (stderr, "Error: bind() failed: %s\n",
+                strerror (errno));
+       close (sock);
+       ret = 1;
+       goto DONE;
+    }
 
-       if (background) {
-               int pid = fork ();
-               if (pid > 0) {
-                       printf ("smtp_dummy_pid='%d'\n", pid);
-                       fflush (stdout);
-                       close (sock);
-                       return 0;
-               }
-               if (pid < 0) {
-                       fprintf (stderr, "Error: fork() failed: %s\n",
-                                strerror (errno));
-                       close (sock);
-                       return 1;
-               }
-               /* Reached if pid == 0 (the child process). */
-               /* Close stdout so that the one interested in pid value will
-                  also get EOF. */
-               close (STDOUT_FILENO);
-               /* dup2() will re-reserve fd of stdout (1) (opportunistically),
-                  in case fd of stderr (2) is open. If that was not open we
-                  don't care fd of stdout (1) either. */
-               dup2 (STDERR_FILENO, STDOUT_FILENO);
+    err = listen (sock, 1);
+    if (err) {
+       fprintf (stderr, "Error: listen() failed: %s\n",
+                strerror (errno));
+       close (sock);
+       ret = 1;
+       goto DONE;
+    }
 
-               /* This process is now out of reach of shell's job control.
-                  To resolve the rare but possible condition where this
-                  "daemon" is started but never connected this process will
-                  (only) have 30 seconds to exist. */
-               alarm (30);
+    if (background) {
+       int pid = fork ();
+       if (pid > 0) {
+           printf ("smtp_dummy_pid='%d'\n", pid);
+           fflush (stdout);
+           close (sock);
+           ret = 0;
+           goto DONE;
        }
-
-       peer_addr_len = sizeof (peer_addr);
-       peer = accept (sock, (struct sockaddr *) &peer_addr, &peer_addr_len);
-       if (peer == -1) {
-               fprintf (stderr, "Error: accept() failed: %s\n",
-                        strerror (errno));
-               return 1;
+       if (pid < 0) {
+           fprintf (stderr, "Error: fork() failed: %s\n",
+                    strerror (errno));
+           close (sock);
+           ret = 1;
+           goto DONE;
        }
+       /* Reached if pid == 0 (the child process). */
+       /* Close stdout so that the one interested in pid value will
+        * also get EOF. */
+       close (STDOUT_FILENO);
+       /* dup2() will re-reserve fd of stdout (1) (opportunistically),
+        * in case fd of stderr (2) is open. If that was not open we
+        * don't care fd of stdout (1) either. */
+       dup2 (STDERR_FILENO, STDOUT_FILENO);
 
-       peer_file = fdopen (peer, "w+");
-       if (peer_file == NULL) {
-               fprintf (stderr, "Error: fdopen() failed: %s\n",
-                        strerror (errno));
-               return 1;
-       }
+       /* This process is now out of reach of shell's job control.
+        * To resolve the rare but possible condition where this
+        * "daemon" is started but never connected this process will
+        * (only) have 30 seconds to exist. */
+       alarm (30);
+    }
+
+    peer_addr_len = sizeof (peer_addr);
+    peer = accept (sock, (struct sockaddr *) &peer_addr, &peer_addr_len);
+    if (peer == -1) {
+       fprintf (stderr, "Error: accept() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
+
+    peer_file = fdopen (peer, "w+");
+    if (peer_file == NULL) {
+       fprintf (stderr, "Error: fdopen() failed: %s\n",
+                strerror (errno));
+       ret = 1;
+       goto DONE;
+    }
 
-       do_smtp_to_file (peer_file, output);
+    do_smtp_to_file (peer_file, output);
 
+ DONE:
+    if (output)
        fclose (output);
+    if (peer_file)
        fclose (peer_file);
+    if (sock >= 0)
        close (sock);
 
-       return 0;
+    return ret;
 }
index b5624b74f80412bfa5317d790615a2605fca7095..9452199f13a84781558a555477c4e6b62d8148dd 100644 (file)
@@ -1 +1 @@
-*.tar.xz
+/*.tar.xz
index ef409171fa86d547f43d5b940c7afd6e98d5b08d..984a7c1a579845faa29ac74013e2fb0f4fa11fa5 100644 (file)
@@ -71,6 +71,183 @@ if [ -e ./test-lib-$PLATFORM.sh ]; then
        . ./test-lib-$PLATFORM.sh || exit 1
 fi
 
+# Generate a new message in the mail directory, with a unique message
+# ID and subject. The message is not added to the index.
+#
+# After this function returns, the filename of the generated message
+# is available as $gen_msg_filename and the message ID is available as
+# $gen_msg_id .
+#
+# This function supports named parameters with the bash syntax for
+# assigning a value to an associative array ([name]=value). The
+# supported parameters are:
+#
+#  [dir]=directory/of/choice
+#
+#      Generate the message in directory 'directory/of/choice' within
+#      the mail store. The directory will be created if necessary.
+#
+#  [filename]=name
+#
+#      Store the message in file 'name'. The default is to store it
+#      in 'msg-<count>', where <count> is three-digit number of the
+#      message.
+#
+#  [body]=text
+#
+#      Text to use as the body of the email message
+#
+#  '[from]="Some User <user@example.com>"'
+#  '[to]="Some User <user@example.com>"'
+#  '[subject]="Subject of email message"'
+#  '[date]="RFC 822 Date"'
+#
+#      Values for email headers. If not provided, default values will
+#      be generated instead.
+#
+#  '[cc]="Some User <user@example.com>"'
+#  [reply-to]=some-address
+#  [in-reply-to]=<message-id>
+#  [references]=<message-id>
+#  [content-type]=content-type-specification
+#  '[header]=full header line, including keyword'
+#
+#      Additional values for email headers. If these are not provided
+#      then the relevant headers will simply not appear in the
+#      message.
+#
+#  '[id]=message-id'
+#
+#      Controls the message-id of the created message.
+gen_msg_cnt=0
+gen_msg_filename=""
+gen_msg_id=""
+generate_message ()
+{
+    # This is our (bash-specific) magic for doing named parameters
+    local -A template="($@)"
+    local additional_headers
+
+    gen_msg_cnt=$((gen_msg_cnt + 1))
+    if [ -z "${template[filename]}" ]; then
+       gen_msg_name="msg-$(printf "%03d" $gen_msg_cnt)"
+    else
+       gen_msg_name=${template[filename]}
+    fi
+
+    if [ -z "${template[id]}" ]; then
+       gen_msg_id="${gen_msg_name%:2,*}@notmuch-test-suite"
+    else
+       gen_msg_id="${template[id]}"
+    fi
+
+    if [ -z "${template[dir]}" ]; then
+       gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
+    else
+       gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
+       mkdir -p "$(dirname "$gen_msg_filename")"
+    fi
+
+    if [ -z "${template[body]}" ]; then
+       template[body]="This is just a test message (#${gen_msg_cnt})"
+    fi
+
+    if [ -z "${template[from]}" ]; then
+       template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[to]}" ]; then
+       template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+    fi
+
+    if [ -z "${template[subject]}" ]; then
+       if [ -n "$test_subtest_name" ]; then
+           template[subject]="$test_subtest_name"
+       else
+           template[subject]="Test message #${gen_msg_cnt}"
+       fi
+    elif [ "${template[subject]}" = "@FORCE_EMPTY" ]; then
+       template[subject]=""
+    fi
+
+    if [ -z "${template[date]}" ]; then
+       # we use decreasing timestamps here for historical reasons;
+       # the existing test suite when we converted to unique timestamps just
+       # happened to have signicantly fewer failures with that choice.
+       local date_secs=$((978709437 - gen_msg_cnt))
+       # printf %(..)T is bash 4.2+ feature. use perl fallback if needed...
+       TZ=UTC printf -v template[date] "%(%a, %d %b %Y %T %z)T" $date_secs 2>/dev/null ||
+           template[date]=`perl -le 'use POSIX "strftime";
+                               @time = gmtime '"$date_secs"';
+                               print strftime "%a, %d %b %Y %T +0000", @time'`
+    fi
+
+    additional_headers=""
+    if [ ! -z "${template[header]}" ]; then
+       additional_headers="${template[header]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[reply-to]}" ]; then
+       additional_headers="Reply-To: ${template[reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[in-reply-to]}" ]; then
+       additional_headers="In-Reply-To: ${template[in-reply-to]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[cc]}" ]; then
+       additional_headers="Cc: ${template[cc]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[bcc]}" ]; then
+       additional_headers="Bcc: ${template[bcc]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[references]}" ]; then
+       additional_headers="References: ${template[references]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[content-type]}" ]; then
+       additional_headers="Content-Type: ${template[content-type]}
+${additional_headers}"
+    fi
+
+    if [ ! -z "${template[content-transfer-encoding]}" ]; then
+       additional_headers="Content-Transfer-Encoding: ${template[content-transfer-encoding]}
+${additional_headers}"
+    fi
+
+    # Note that in the way we're setting it above and using it below,
+    # `additional_headers' will also serve as the header / body separator
+    # (empty line in between).
+
+    cat <<EOF >"$gen_msg_filename"
+From: ${template[from]}
+To: ${template[to]}
+Message-Id: <${gen_msg_id}>
+Subject: ${template[subject]}
+Date: ${template[date]}
+${additional_headers}
+${template[body]}
+EOF
+}
+
+# Generate a new message and add it to the database.
+#
+# All of the arguments and return values supported by generate_message
+# are also supported here, so see that function for details.
+add_message ()
+{
+    generate_message "$@" &&
+    notmuch new > /dev/null
+}
+
 if test -n "$valgrind"
 then
        make_symlink () {
index 7ae96c6999aaed84007b99e4e2a6c8a6beb754da..350246492d8c2dbd0e6faf16c9dc26d19a77223c 100644 (file)
@@ -276,183 +276,6 @@ export GNUPGHOME="${TEST_TMPDIR}/gnupg"
 trap 'trap_exit' EXIT
 trap 'trap_signal' HUP INT TERM
 
-# Generate a new message in the mail directory, with a unique message
-# ID and subject. The message is not added to the index.
-#
-# After this function returns, the filename of the generated message
-# is available as $gen_msg_filename and the message ID is available as
-# $gen_msg_id .
-#
-# This function supports named parameters with the bash syntax for
-# assigning a value to an associative array ([name]=value). The
-# supported parameters are:
-#
-#  [dir]=directory/of/choice
-#
-#      Generate the message in directory 'directory/of/choice' within
-#      the mail store. The directory will be created if necessary.
-#
-#  [filename]=name
-#
-#      Store the message in file 'name'. The default is to store it
-#      in 'msg-<count>', where <count> is three-digit number of the
-#      message.
-#
-#  [body]=text
-#
-#      Text to use as the body of the email message
-#
-#  '[from]="Some User <user@example.com>"'
-#  '[to]="Some User <user@example.com>"'
-#  '[subject]="Subject of email message"'
-#  '[date]="RFC 822 Date"'
-#
-#      Values for email headers. If not provided, default values will
-#      be generated instead.
-#
-#  '[cc]="Some User <user@example.com>"'
-#  [reply-to]=some-address
-#  [in-reply-to]=<message-id>
-#  [references]=<message-id>
-#  [content-type]=content-type-specification
-#  '[header]=full header line, including keyword'
-#
-#      Additional values for email headers. If these are not provided
-#      then the relevant headers will simply not appear in the
-#      message.
-#
-#  '[id]=message-id'
-#
-#      Controls the message-id of the created message.
-gen_msg_cnt=0
-gen_msg_filename=""
-gen_msg_id=""
-generate_message ()
-{
-    # This is our (bash-specific) magic for doing named parameters
-    local -A template="($@)"
-    local additional_headers
-
-    gen_msg_cnt=$((gen_msg_cnt + 1))
-    if [ -z "${template[filename]}" ]; then
-       gen_msg_name="msg-$(printf "%03d" $gen_msg_cnt)"
-    else
-       gen_msg_name=${template[filename]}
-    fi
-
-    if [ -z "${template[id]}" ]; then
-       gen_msg_id="${gen_msg_name%:2,*}@notmuch-test-suite"
-    else
-       gen_msg_id="${template[id]}"
-    fi
-
-    if [ -z "${template[dir]}" ]; then
-       gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
-    else
-       gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
-       mkdir -p "$(dirname "$gen_msg_filename")"
-    fi
-
-    if [ -z "${template[body]}" ]; then
-       template[body]="This is just a test message (#${gen_msg_cnt})"
-    fi
-
-    if [ -z "${template[from]}" ]; then
-       template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
-    fi
-
-    if [ -z "${template[to]}" ]; then
-       template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
-    fi
-
-    if [ -z "${template[subject]}" ]; then
-       if [ -n "$test_subtest_name" ]; then
-           template[subject]="$test_subtest_name"
-       else
-           template[subject]="Test message #${gen_msg_cnt}"
-       fi
-    elif [ "${template[subject]}" = "@FORCE_EMPTY" ]; then
-       template[subject]=""
-    fi
-
-    if [ -z "${template[date]}" ]; then
-       # we use decreasing timestamps here for historical reasons;
-       # the existing test suite when we converted to unique timestamps just
-       # happened to have signicantly fewer failures with that choice.
-       local date_secs=$((978709437 - gen_msg_cnt))
-       # printf %(..)T is bash 4.2+ feature. use perl fallback if needed...
-       TZ=UTC printf -v template[date] "%(%a, %d %b %Y %T %z)T" $date_secs 2>/dev/null ||
-           template[date]=`perl -le 'use POSIX "strftime";
-                               @time = gmtime '"$date_secs"';
-                               print strftime "%a, %d %b %Y %T +0000", @time'`
-    fi
-
-    additional_headers=""
-    if [ ! -z "${template[header]}" ]; then
-       additional_headers="${template[header]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[reply-to]}" ]; then
-       additional_headers="Reply-To: ${template[reply-to]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[in-reply-to]}" ]; then
-       additional_headers="In-Reply-To: ${template[in-reply-to]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[cc]}" ]; then
-       additional_headers="Cc: ${template[cc]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[bcc]}" ]; then
-       additional_headers="Bcc: ${template[bcc]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[references]}" ]; then
-       additional_headers="References: ${template[references]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[content-type]}" ]; then
-       additional_headers="Content-Type: ${template[content-type]}
-${additional_headers}"
-    fi
-
-    if [ ! -z "${template[content-transfer-encoding]}" ]; then
-       additional_headers="Content-Transfer-Encoding: ${template[content-transfer-encoding]}
-${additional_headers}"
-    fi
-
-    # Note that in the way we're setting it above and using it below,
-    # `additional_headers' will also serve as the header / body separator
-    # (empty line in between).
-
-    cat <<EOF >"$gen_msg_filename"
-From: ${template[from]}
-To: ${template[to]}
-Message-Id: <${gen_msg_id}>
-Subject: ${template[subject]}
-Date: ${template[date]}
-${additional_headers}
-${template[body]}
-EOF
-}
-
-# Generate a new message and add it to the database.
-#
-# All of the arguments and return values supported by generate_message
-# are also supported here, so see that function for details.
-add_message ()
-{
-    generate_message "$@" &&
-    notmuch new > /dev/null
-}
-
 # Deliver a message with emacs and add it to the database
 #
 # Uses emacs to generate and deliver a message to the mail store.
@@ -627,9 +450,10 @@ test_expect_equal_json () {
     # The test suite forces LC_ALL=C, but this causes Python 3 to
     # decode stdin as ASCII.  We need to read JSON in UTF-8, so
     # override Python's stdio encoding defaults.
-    output=$(echo "$1" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -mjson.tool \
+    local script='import json, sys; json.dump(json.load(sys.stdin), sys.stdout, sort_keys=True, indent=4)'
+    output=$(echo "$1" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c "$script" \
         || echo "$1")
-    expected=$(echo "$2" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -mjson.tool \
+    expected=$(echo "$2" | PYTHONIOENCODING=utf-8 $NOTMUCH_PYTHON -c "$script" \
         || echo "$2")
     shift 2
     test_expect_equal "$output" "$expected" "$@"
@@ -683,6 +507,16 @@ NOTMUCH_DUMP_TAGS ()
     notmuch dump --include=tags "${@}" | sed '/^#/d' | sort
 }
 
+notmuch_drop_mail_headers ()
+{
+    $NOTMUCH_PYTHON -c '
+import email, sys
+msg = email.message_from_file(sys.stdin)
+for hdr in sys.argv[1:]: del msg[hdr]
+print(msg.as_string(False))
+' "$@"
+}
+
 notmuch_search_sanitize ()
 {
     perl -pe 's/("?thread"?: ?)("?)................("?)/\1\2XXX\3/'
index 778bbd522227b73d8aaaa53df64a17e1061e2ba6..e64162c7a885e622fd591b44c9d9a2175342bbd6 100644 (file)
@@ -34,6 +34,7 @@ _internal_error (const char *format, ...)
     fprintf (stderr, "Internal error: ");
     vfprintf (stderr, format, va_args);
 
+    va_end (va_args);
     exit (1);
 }
 
index 18125309ebe9ed33c048f928e02d1e6b35edca28..b0108811903b27153dcaece1fa10b893234c3b9f 100644 (file)
@@ -255,3 +255,16 @@ strcase_hash (const void *ptr)
 
     return hash;
 }
+
+void
+strip_trailing (char *str, char ch)
+{
+    int i;
+
+    for (i = strlen (str) - 1; i >= 0; i--) {
+       if (str[i] == ch)
+           str[i] = '\0';
+       else
+           break;
+    }
+}
index 87917b8fd279de6e07f2fb919429fb50512a85b4..97770614adf1ab6930e529cd148dea47c71547f6 100644 (file)
@@ -75,6 +75,8 @@ int strcase_equal (const void *a, const void *b);
 /* GLib GHashFunc compatible case insensitive hash function */
 unsigned int strcase_hash (const void *ptr);
 
+void strip_trailing (char *str, char ch);
+
 #ifdef __cplusplus
 }
 #endif