[PATCH v4 16/16] add "notmuch reindex" subcommand
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>
Fri, 8 Jul 2016 09:27:27 +0000 (11:27 +0200)
committerW. Trevor King <wking@tremily.us>
Sat, 20 Aug 2016 23:22:12 +0000 (16:22 -0700)
8c/96cc9ab9e5fd9d4eb74cdd018fb0be550580b3 [new file with mode: 0644]

diff --git a/8c/96cc9ab9e5fd9d4eb74cdd018fb0be550580b3 b/8c/96cc9ab9e5fd9d4eb74cdd018fb0be550580b3
new file mode 100644 (file)
index 0000000..1f1abde
--- /dev/null
@@ -0,0 +1,466 @@
+Return-Path: <dkg@fifthhorseman.net>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+ by arlo.cworth.org (Postfix) with ESMTP id 21D6D6DE01BA\r
+ for <notmuch@notmuchmail.org>; Fri,  8 Jul 2016 03:13:17 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at cworth.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: 0\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=0 tagged_above=-999 required=5 tests=[none]\r
+ autolearn=disabled\r
+Received: from arlo.cworth.org ([127.0.0.1])\r
+ by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024)\r
+ with ESMTP id 4gR5kbHFHJCr for <notmuch@notmuchmail.org>;\r
+ Fri,  8 Jul 2016 03:13:08 -0700 (PDT)\r
+Received: from che.mayfirst.org (che.mayfirst.org [162.247.75.118])\r
+ by arlo.cworth.org (Postfix) with ESMTP id 0ED3E6DE00BD\r
+ for <notmuch@notmuchmail.org>; Fri,  8 Jul 2016 03:13:07 -0700 (PDT)\r
+Received: from fifthhorseman.net (unknown [88.128.80.54])\r
+ by che.mayfirst.org (Postfix) with ESMTPSA id 502AEF98B\r
+ for <notmuch@notmuchmail.org>; Fri,  8 Jul 2016 06:13:03 -0400 (EDT)\r
+Received: by fifthhorseman.net (Postfix, from userid 1000)\r
+ id CE17F21753; Fri,  8 Jul 2016 11:27:34 +0200 (CEST)\r
+From: Daniel Kahn Gillmor <dkg@fifthhorseman.net>\r
+To: Notmuch Mail <notmuch@notmuchmail.org>\r
+Subject: [PATCH v4 16/16] add "notmuch reindex" subcommand\r
+Date: Fri,  8 Jul 2016 11:27:27 +0200\r
+Message-Id: <1467970047-8013-17-git-send-email-dkg@fifthhorseman.net>\r
+X-Mailer: git-send-email 2.8.1\r
+In-Reply-To: <1467970047-8013-1-git-send-email-dkg@fifthhorseman.net>\r
+References: <1467970047-8013-1-git-send-email-dkg@fifthhorseman.net>\r
+MIME-Version: 1.0\r
+Content-Type: text/plain; charset=UTF-8\r
+Content-Transfer-Encoding: 8bit\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.20\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+ <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <https://notmuchmail.org/mailman/options/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch/>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <https://notmuchmail.org/mailman/listinfo/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Fri, 08 Jul 2016 10:13:17 -0000\r
+\r
+This new subcommand takes a set of search terms, and re-indexes the\r
+list of matching messages using the supplied options.\r
+\r
+This can be used to index the cleartext of encrypted messages with\r
+something like:\r
+\r
+ notmuch reindex --try-decrypt \\r
+    tag:encrypted and not has:index-decryption=success\r
+---\r
+ Makefile.local                    |   1 +\r
+ doc/conf.py                       |   7 ++\r
+ doc/index.rst                     |   1 +\r
+ doc/man1/notmuch-reindex.rst      |  41 ++++++++++\r
+ doc/man1/notmuch.rst              |   1 +\r
+ doc/man7/notmuch-search-terms.rst |   7 +-\r
+ notmuch-client.h                  |   3 +\r
+ notmuch-reindex.c                 | 152 ++++++++++++++++++++++++++++++++++++++\r
+ notmuch.c                         |   2 +\r
+ test/T357-index-decryption.sh     |  67 +++++++++++++++++\r
+ 10 files changed, 280 insertions(+), 2 deletions(-)\r
+ create mode 100644 doc/man1/notmuch-reindex.rst\r
+ create mode 100644 notmuch-reindex.c\r
+\r
+diff --git a/Makefile.local b/Makefile.local\r
+index c13ba76..a916bd0 100644\r
+--- a/Makefile.local\r
++++ b/Makefile.local\r
+@@ -281,6 +281,7 @@ notmuch_client_srcs =              \\r
+       notmuch-dump.c          \\r
+       notmuch-insert.c        \\r
+       notmuch-new.c           \\r
++      notmuch-reindex.c       \\r
+       notmuch-reply.c         \\r
+       notmuch-restore.c       \\r
+       notmuch-search.c        \\r
+diff --git a/doc/conf.py b/doc/conf.py\r
+index 8b93296..8455a51 100644\r
+--- a/doc/conf.py\r
++++ b/doc/conf.py\r
+@@ -94,6 +94,10 @@ man_pages = [\r
+         u'incorporate new mail into the notmuch database',\r
+         [u'Carl Worth and many others'], 1),\r
\r
++('man1/notmuch-reindex','notmuch-reindex',\r
++        u're-index matching messages',\r
++        [u'Carl Worth and many others'], 1),\r
++\r
+ ('man1/notmuch-reply','notmuch-reply',\r
+         u'constructs a reply template for a set of messages',\r
+         [u'Carl Worth and many others'], 1),\r
+@@ -162,6 +166,9 @@ texinfo_documents = [\r
+ ('man1/notmuch-new','notmuch-new',u'notmuch Documentation',\r
+       u'Carl Worth and many others', 'notmuch-new',\r
+       'incorporate new mail into the notmuch database','Miscellaneous'),\r
++('man1/notmuch-reindex','notmuch-reindex',u'notmuch Documentation',\r
++      u'Carl Worth and many others', 'notmuch-reindex',\r
++      're-index matching messages','Miscellaneous'),\r
+ ('man1/notmuch-reply','notmuch-reply',u'notmuch Documentation',\r
+       u'Carl Worth and many others', 'notmuch-reply',\r
+       'constructs a reply template for a set of messages','Miscellaneous'),\r
+diff --git a/doc/index.rst b/doc/index.rst\r
+index 344606d..aa6c9f4 100644\r
+--- a/doc/index.rst\r
++++ b/doc/index.rst\r
+@@ -18,6 +18,7 @@ Contents:\r
+    man5/notmuch-hooks\r
+    man1/notmuch-insert\r
+    man1/notmuch-new\r
++   man1/notmuch-reindex\r
+    man1/notmuch-reply\r
+    man1/notmuch-restore\r
+    man1/notmuch-search\r
+diff --git a/doc/man1/notmuch-reindex.rst b/doc/man1/notmuch-reindex.rst\r
+new file mode 100644\r
+index 0000000..7ccc947\r
+--- /dev/null\r
++++ b/doc/man1/notmuch-reindex.rst\r
+@@ -0,0 +1,41 @@\r
++===========\r
++notmuch-reindex\r
++===========\r
++\r
++SYNOPSIS\r
++========\r
++\r
++**notmuch** **reindex** [*option* ...] <*search-term*> ...\r
++\r
++DESCRIPTION\r
++===========\r
++\r
++Re-index all messages matching the search terms.\r
++\r
++See **notmuch-search-terms(7)** for details of the supported syntax for\r
++<*search-term*\ >.\r
++\r
++The **reindex** command searches for all messages matching the\r
++supplied search terms, and re-creates the full-text index on these\r
++messages using the supplied options.\r
++\r
++Supported options for **reindex** include\r
++\r
++    ``--try-decrypt``\r
++\r
++        For each message, if it is encrypted, try to decrypt it while\r
++        indexing.  If decryption is successful, index the cleartext\r
++        itself.  Be aware that the index is likely sufficient to\r
++        reconstruct the cleartext of the message itself, so please\r
++        ensure that the notmuch message index is adequately\r
++        protected. DO NOT USE THIS FLAG without considering the\r
++        security of your index.\r
++\r
++SEE ALSO\r
++========\r
++\r
++**notmuch(1)**, **notmuch-config(1)**, **notmuch-count(1)**,\r
++**notmuch-dump(1)**, **notmuch-hooks(5)**, **notmuch-insert(1)**,\r
++**notmuch-new(1)**,\r
++**notmuch-reply(1)**, **notmuch-restore(1)**, **notmuch-search(1)**,\r
++**notmuch-search-terms(7)**, **notmuch-show(1)**, **notmuch-tag(1)**\r
+diff --git a/doc/man1/notmuch.rst b/doc/man1/notmuch.rst\r
+index edd04ef..e6a14f6 100644\r
+--- a/doc/man1/notmuch.rst\r
++++ b/doc/man1/notmuch.rst\r
+@@ -140,6 +140,7 @@ SEE ALSO\r
\r
+ **notmuch-config(1)**, **notmuch-count(1)**, **notmuch-dump(1)**,\r
+ **notmuch-hooks(5)**, **notmuch-insert(1)**, **notmuch-new(1)**,\r
++**notmuch-reindex(1)**,\r
+ **notmuch-reply(1)**, **notmuch-restore(1)**, **notmuch-search(1)**,\r
+ **notmuch-search-terms(7)**, **notmuch-show(1)**, **notmuch-tag(1)**,\r
+ **notmuch-address(1)**\r
+diff --git a/doc/man7/notmuch-search-terms.rst b/doc/man7/notmuch-search-terms.rst\r
+index 075f88c..e5ea899 100644\r
+--- a/doc/man7/notmuch-search-terms.rst\r
++++ b/doc/man7/notmuch-search-terms.rst\r
+@@ -9,6 +9,8 @@ SYNOPSIS\r
\r
+ **notmuch** **dump** [--format=(batch-tag|sup)] [--] [--output=<*file*>] [--] [<*search-term*> ...]\r
\r
++**notmuch** **reindex** [option ...] <*search-term*> ...\r
++\r
+ **notmuch** **search** [option ...] <*search-term*> ...\r
\r
+ **notmuch** **show** [option ...] <*search-term*> ...\r
+@@ -395,5 +397,6 @@ SEE ALSO\r
\r
+ **notmuch(1)**, **notmuch-config(1)**, **notmuch-count(1)**,\r
+ **notmuch-dump(1)**, **notmuch-hooks(5)**, **notmuch-insert(1)**,\r
+-**notmuch-new(1)**, **notmuch-reply(1)**, **notmuch-restore(1)**,\r
+-**notmuch-search(1)**, **notmuch-show(1)**, **notmuch-tag(1)**\r
++**notmuch-new(1)**, **notmuch-reindex(1)**, **notmuch-reply(1)**,\r
++**notmuch-restore(1)**, **notmuch-search(1)**, **notmuch-show(1)**,\r
++**notmuch-tag(1)**\r
+diff --git a/notmuch-client.h b/notmuch-client.h\r
+index a65bb6d..a32ef7a 100644\r
+--- a/notmuch-client.h\r
++++ b/notmuch-client.h\r
+@@ -169,6 +169,9 @@ int\r
+ notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[]);\r
\r
+ int\r
++notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[]);\r
++\r
++int\r
+ notmuch_reply_command (notmuch_config_t *config, int argc, char *argv[]);\r
\r
+ int\r
+diff --git a/notmuch-reindex.c b/notmuch-reindex.c\r
+new file mode 100644\r
+index 0000000..6fc88c5\r
+--- /dev/null\r
++++ b/notmuch-reindex.c\r
+@@ -0,0 +1,152 @@\r
++/* notmuch - Not much of an email program, (just index and search)\r
++ *\r
++ * Copyright © 2016 Daniel Kahn Gillmor\r
++ *\r
++ * This program is free software: you can redistribute it and/or modify\r
++ * it under the terms of the GNU General Public License as published by\r
++ * the Free Software Foundation, either version 3 of the License, or\r
++ * (at your option) any later version.\r
++ *\r
++ * This program is distributed in the hope that it will be useful,\r
++ * but WITHOUT ANY WARRANTY; without even the implied warranty of\r
++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
++ * GNU General Public License for more details.\r
++ *\r
++ * You should have received a copy of the GNU General Public License\r
++ * along with this program.  If not, see http://www.gnu.org/licenses/ .\r
++ *\r
++ * Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>\r
++ */\r
++\r
++#include "notmuch-client.h"\r
++#include "string-util.h"\r
++\r
++static volatile sig_atomic_t interrupted;\r
++\r
++static void\r
++handle_sigint (unused (int sig))\r
++{\r
++    static char msg[] = "Stopping...         \n";\r
++\r
++    /* This write is "opportunistic", so it's okay to ignore the\r
++     * result.  It is not required for correctness, and if it does\r
++     * fail or produce a short write, we want to get out of the signal\r
++     * handler as quickly as possible, not retry it. */\r
++    IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));\r
++    interrupted = 1;\r
++}\r
++\r
++/* reindex all messages matching 'query_string' using the passed-in indexopts\r
++ */\r
++static int\r
++reindex_query (notmuch_database_t *notmuch, const char *query_string,\r
++             notmuch_indexopts_t *indexopts)\r
++{\r
++    notmuch_query_t *query;\r
++    notmuch_messages_t *messages;\r
++    notmuch_message_t *message;\r
++    notmuch_status_t status;\r
++\r
++    int ret = NOTMUCH_STATUS_SUCCESS;\r
++\r
++    query = notmuch_query_create (notmuch, query_string);\r
++    if (query == NULL) {\r
++      fprintf (stderr, "Out of memory.\n");\r
++      return 1;\r
++    }\r
++\r
++    /* reindexing is not interested in any special sort order */\r
++    notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);\r
++\r
++    status = notmuch_query_search_messages_st (query, &messages);\r
++    if (print_status_query ("notmuch reindex", query, status))\r
++      return status;\r
++\r
++    for (;\r
++       notmuch_messages_valid (messages) && ! interrupted;\r
++       notmuch_messages_move_to_next (messages)) {\r
++      message = notmuch_messages_get (messages);\r
++\r
++      notmuch_message_reindex(message, indexopts);\r
++      notmuch_message_destroy (message);\r
++      if (ret != NOTMUCH_STATUS_SUCCESS)\r
++          break;\r
++    }\r
++\r
++    notmuch_query_destroy (query);\r
++\r
++    return ret || interrupted;\r
++}\r
++\r
++int\r
++notmuch_reindex_command (notmuch_config_t *config, int argc, char *argv[])\r
++{\r
++    char *query_string = NULL;\r
++    notmuch_database_t *notmuch;\r
++    struct sigaction action;\r
++    notmuch_bool_t try_decrypt = FALSE;\r
++    int opt_index;\r
++    int ret;\r
++    notmuch_status_t status;\r
++    notmuch_indexopts_t *indexopts = NULL;\r
++\r
++    /* Set up our handler for SIGINT */\r
++    memset (&action, 0, sizeof (struct sigaction));\r
++    action.sa_handler = handle_sigint;\r
++    sigemptyset (&action.sa_mask);\r
++    action.sa_flags = SA_RESTART;\r
++    sigaction (SIGINT, &action, NULL);\r
++\r
++    notmuch_opt_desc_t options[] = {\r
++      { NOTMUCH_OPT_BOOLEAN, &try_decrypt, "try-decrypt", 0, 0 },\r
++      { NOTMUCH_OPT_INHERIT, (void *) &notmuch_shared_options, NULL, 0, 0 },\r
++      { 0, 0, 0, 0, 0 }\r
++    };\r
++\r
++    opt_index = parse_arguments (argc, argv, options, 1);\r
++    if (opt_index < 0)\r
++      return EXIT_FAILURE;\r
++\r
++    notmuch_process_shared_options (argv[0]);\r
++\r
++    if (notmuch_database_open (notmuch_config_get_database_path (config),\r
++                             NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))\r
++      return EXIT_FAILURE;\r
++\r
++    notmuch_exit_if_unmatched_db_uuid (notmuch);\r
++\r
++    indexopts = notmuch_indexopts_create();\r
++    if (!indexopts)\r
++      return EXIT_FAILURE;\r
++\r
++    status = notmuch_indexopts_set_try_decrypt (indexopts, try_decrypt);\r
++    if (status)\r
++      fprintf (stderr, "Warning: failed to set --try-decrypt to %d (%s)\n",\r
++               try_decrypt, notmuch_status_to_string (status));\r
++\r
++    if (try_decrypt) {\r
++      const char* gpg_path = notmuch_config_get_crypto_gpg_path (config);\r
++      status = notmuch_indexopts_set_gpg_path (indexopts, gpg_path);\r
++      if (status)\r
++          fprintf (stderr, "Warning: failed to set gpg_path for reindexing to '%s' (%s)\n",\r
++                   gpg_path ? gpg_path : "(NULL)",\r
++                   notmuch_status_to_string (status));\r
++    }\r
++\r
++    query_string = query_string_from_args (config, argc-opt_index, argv+opt_index);\r
++    if (query_string == NULL) {\r
++      fprintf (stderr, "Out of memory\n");\r
++      return EXIT_FAILURE;\r
++    }\r
++\r
++    if (*query_string == '\0') {\r
++      fprintf (stderr, "Error: notmuch reindex requires at least one search term.\n");\r
++      return EXIT_FAILURE;\r
++    }\r
++    \r
++    ret = reindex_query (notmuch, query_string, indexopts);\r
++\r
++    notmuch_database_destroy (notmuch);\r
++\r
++    return ret || interrupted ? EXIT_FAILURE : EXIT_SUCCESS;\r
++}\r
+diff --git a/notmuch.c b/notmuch.c\r
+index 38b73c1..5aadea5 100644\r
+--- a/notmuch.c\r
++++ b/notmuch.c\r
+@@ -123,6 +123,8 @@ static command_t commands[] = {\r
+       "Restore the tags from the given dump file (see 'dump')." },\r
+     { "compact", notmuch_compact_command, FALSE,\r
+       "Compact the notmuch database." },\r
++    { "reindex", notmuch_reindex_command, FALSE,\r
++      "Re-index all messages matching the search terms." },\r
+     { "config", notmuch_config_command, FALSE,\r
+       "Get or set settings in the notmuch configuration file." },\r
+     { "help", notmuch_help_command, TRUE, /* create but don't save config */\r
+diff --git a/test/T357-index-decryption.sh b/test/T357-index-decryption.sh\r
+index 27d6885..9510c57 100755\r
+--- a/test/T357-index-decryption.sh\r
++++ b/test/T357-index-decryption.sh\r
+@@ -46,4 +46,71 @@ test_expect_equal \\r
+     "$output" \\r
+     "$expected"\r
\r
++# add a tag to all messages to ensure that it stays after reindexing\r
++test_expect_success 'tagging all messages' \\r
++                    'notmuch tag +blarney "encrypted message"'\r
++test_begin_subtest "verify that tags have not changed"\r
++output=$(notmuch search tag:blarney)\r
++expected='thread:0000000000000001   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)\r
++thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox)'\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++# see if first message shows up after reindexing with --try-decrypt\r
++test_expect_success 'reindex old messages' \\r
++                    'notmuch reindex --try-decrypt tag:encrypted and not has:index-decryption=success'\r
++test_begin_subtest "reindexed encrypted message, including cleartext"\r
++output=$(notmuch search wumpus)\r
++expected='thread:0000000000000002   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox)\r
++thread:0000000000000003   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)'\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++# and the same search, but by property ($expected is untouched):\r
++test_begin_subtest "emacs search by property for both messages"\r
++output=$(notmuch search has:index-decryption=success)\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++\r
++# try to remove cleartext indexing\r
++test_expect_success 'reindex without cleartext' \\r
++                    'notmuch reindex tag:encrypted and has:index-decryption=success'\r
++test_begin_subtest "reindexed encrypted messages, without cleartext"\r
++output=$(notmuch search wumpus)\r
++expected=''\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++# and the same search, but by property ($expected is untouched):\r
++test_begin_subtest "emacs search by property with both messages unindexed"\r
++output=$(notmuch search has:index-decryption=success)\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++# ensure that the tags remain even when we are dropping the cleartext.\r
++test_begin_subtest "verify that tags remain without cleartext"\r
++output=$(notmuch search tag:blarney)\r
++expected='thread:0000000000000004   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 002 (blarney encrypted inbox)\r
++thread:0000000000000005   2000-01-01 [1/1] Notmuch Test Suite; test encrypted message for cleartext index 001 (blarney encrypted inbox)'\r
++test_expect_equal \\r
++    "$output" \\r
++    "$expected"\r
++\r
++\r
++# TODO: test removal of a message from the message store between\r
++# indexing and reindexing.\r
++\r
++# TODO: insert the same message into the message store twice, index,\r
++# remove one of them from the message store, and then reindex.\r
++# reindexing should return a failure but the message should still be\r
++# present? -- or what should the semantics be if you ask to reindex a\r
++# message whose underlying files have been renamed or moved or\r
++# removed?\r
++\r
+ test_done\r
+-- \r
+2.8.1\r
+\r