]> git.cworth.org Git - notmuch-old/blob - notmuch.c
test: add regression tests for oldest and newest dates
[notmuch-old] / notmuch.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  * Copyright © 2009 Keith Packard
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see https://www.gnu.org/licenses/ .
18  *
19  * Authors: Carl Worth <cworth@cworth.org>
20  *          Keith Packard <keithp@keithp.com>
21  */
22
23 #include "notmuch-client.h"
24
25 /*
26  * Notmuch subcommand hook.
27  *
28  * The return value will be used as notmuch exit status code,
29  * preferably EXIT_SUCCESS or EXIT_FAILURE.
30  */
31 typedef int (*command_function_t) (notmuch_config_t *config, int argc, char *argv[]);
32
33 typedef struct command {
34     const char *name;
35     command_function_t function;
36     notmuch_config_mode_t config_mode;
37     const char *summary;
38 } command_t;
39
40 static int
41 notmuch_help_command (notmuch_config_t *config, int argc, char *argv[]);
42
43 static int
44 notmuch_command (notmuch_config_t *config, int argc, char *argv[]);
45
46 static int
47 _help_for (const char *topic);
48
49 static bool print_version = false, print_help = false;
50 const char *notmuch_requested_db_uuid = NULL;
51
52 const notmuch_opt_desc_t notmuch_shared_options [] = {
53     { .opt_bool = &print_version, .name = "version" },
54     { .opt_bool = &print_help, .name = "help" },
55     { .opt_string = &notmuch_requested_db_uuid, .name = "uuid" },
56     { }
57 };
58
59 /* any subcommand wanting to support these options should call
60  * inherit notmuch_shared_options and call
61  * notmuch_process_shared_options (subcommand_name);
62  */
63 void
64 notmuch_process_shared_options (const char *subcommand_name)
65 {
66     if (print_version) {
67         printf ("notmuch " STRINGIFY (NOTMUCH_VERSION) "\n");
68         exit (EXIT_SUCCESS);
69     }
70
71     if (print_help) {
72         int ret = _help_for (subcommand_name);
73         exit (ret);
74     }
75 }
76
77 /* This is suitable for subcommands that do not actually open the
78  * database.
79  */
80 int
81 notmuch_minimal_options (const char *subcommand_name,
82                          int argc, char **argv)
83 {
84     int opt_index;
85
86     notmuch_opt_desc_t options[] = {
87         { .opt_inherit = notmuch_shared_options },
88         { }
89     };
90
91     opt_index = parse_arguments (argc, argv, options, 1);
92
93     if (opt_index < 0)
94         return -1;
95
96     /* We can't use argv here as it is sometimes NULL */
97     notmuch_process_shared_options (subcommand_name);
98     return opt_index;
99 }
100
101
102 struct _notmuch_client_indexing_cli_choices indexing_cli_choices = { };
103 const notmuch_opt_desc_t notmuch_shared_indexing_options [] = {
104     { .opt_keyword = &indexing_cli_choices.decrypt_policy,
105       .present = &indexing_cli_choices.decrypt_policy_set, .keywords =
106           (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
107                                   { "true", NOTMUCH_DECRYPT_TRUE },
108                                   { "auto", NOTMUCH_DECRYPT_AUTO },
109                                   { "nostash", NOTMUCH_DECRYPT_NOSTASH },
110                                   { 0, 0 } },
111       .name = "decrypt" },
112     { }
113 };
114
115
116 notmuch_status_t
117 notmuch_process_shared_indexing_options (notmuch_database_t *notmuch)
118 {
119     if (indexing_cli_choices.opts == NULL)
120         indexing_cli_choices.opts = notmuch_database_get_default_indexopts (notmuch);
121     if (indexing_cli_choices.decrypt_policy_set) {
122         notmuch_status_t status;
123         if (indexing_cli_choices.opts == NULL)
124             return NOTMUCH_STATUS_OUT_OF_MEMORY;
125         status = notmuch_indexopts_set_decrypt_policy (indexing_cli_choices.opts, indexing_cli_choices.decrypt_policy);
126         if (status != NOTMUCH_STATUS_SUCCESS) {
127             fprintf (stderr, "Error: Failed to set index decryption policy to %d. (%s)\n",
128                      indexing_cli_choices.decrypt_policy, notmuch_status_to_string (status));
129             notmuch_indexopts_destroy (indexing_cli_choices.opts);
130             indexing_cli_choices.opts = NULL;
131             return status;
132         }
133     }
134     return NOTMUCH_STATUS_SUCCESS;
135 }
136
137
138 static command_t commands[] = {
139     { NULL, notmuch_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE,
140       "Notmuch main command." },
141     { "setup", notmuch_setup_command, NOTMUCH_CONFIG_OPEN | NOTMUCH_CONFIG_CREATE,
142       "Interactively set up notmuch for first use." },
143     { "new", notmuch_new_command, NOTMUCH_CONFIG_OPEN,
144       "Find and import new messages to the notmuch database." },
145     { "insert", notmuch_insert_command, NOTMUCH_CONFIG_OPEN,
146       "Add a new message into the maildir and notmuch database." },
147     { "search", notmuch_search_command, NOTMUCH_CONFIG_OPEN,
148       "Search for messages matching the given search terms." },
149     { "address", notmuch_address_command, NOTMUCH_CONFIG_OPEN,
150       "Get addresses from messages matching the given search terms." },
151     { "show", notmuch_show_command, NOTMUCH_CONFIG_OPEN,
152       "Show all messages matching the search terms." },
153     { "count", notmuch_count_command, NOTMUCH_CONFIG_OPEN,
154       "Count messages matching the search terms." },
155     { "reply", notmuch_reply_command, NOTMUCH_CONFIG_OPEN,
156       "Construct a reply template for a set of messages." },
157     { "tag", notmuch_tag_command, NOTMUCH_CONFIG_OPEN,
158       "Add/remove tags for all messages matching the search terms." },
159     { "dump", notmuch_dump_command, NOTMUCH_CONFIG_OPEN,
160       "Create a plain-text dump of the tags for each message." },
161     { "restore", notmuch_restore_command, NOTMUCH_CONFIG_OPEN,
162       "Restore the tags from the given dump file (see 'dump')." },
163     { "compact", notmuch_compact_command, NOTMUCH_CONFIG_OPEN,
164       "Compact the notmuch database." },
165     { "reindex", notmuch_reindex_command, NOTMUCH_CONFIG_OPEN,
166       "Re-index all messages matching the search terms." },
167     { "config", notmuch_config_command, NOTMUCH_CONFIG_OPEN,
168       "Get or set settings in the notmuch configuration file." },
169 #if WITH_EMACS
170     { "emacs-mua", NULL, 0,
171       "send mail with notmuch and emacs." },
172 #endif
173     { "help", notmuch_help_command, NOTMUCH_CONFIG_CREATE, /* create but don't save config */
174       "This message, or more detailed help for the named command." }
175 };
176
177 typedef struct help_topic {
178     const char *name;
179     const char *summary;
180 } help_topic_t;
181
182 static help_topic_t help_topics[] = {
183     { "search-terms",
184       "Common search term syntax." },
185     { "hooks",
186       "Hooks that will be run before or after certain commands." },
187     { "properties",
188       "Message property conventions and documentation." },
189 };
190
191 static command_t *
192 find_command (const char *name)
193 {
194     size_t i;
195
196     for (i = 0; i < ARRAY_SIZE (commands); i++)
197         if ((! name && ! commands[i].name) ||
198             (name && commands[i].name && strcmp (name, commands[i].name) == 0))
199             return &commands[i];
200
201     return NULL;
202 }
203
204 int notmuch_format_version;
205
206 static void
207 usage (FILE *out)
208 {
209     command_t *command;
210     help_topic_t *topic;
211     unsigned int i;
212
213     fprintf (out,
214              "Usage: notmuch --help\n"
215              "       notmuch --version\n"
216              "       notmuch <command> [args...]\n");
217     fprintf (out, "\n");
218     fprintf (out, "The available commands are as follows:\n");
219     fprintf (out, "\n");
220
221     for (i = 0; i < ARRAY_SIZE (commands); i++) {
222         command = &commands[i];
223
224         if (command->name)
225             fprintf (out, "  %-12s  %s\n", command->name, command->summary);
226     }
227
228     fprintf (out, "\n");
229     fprintf (out, "Additional help topics are as follows:\n");
230     fprintf (out, "\n");
231
232     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
233         topic = &help_topics[i];
234         fprintf (out, "  %-12s  %s\n", topic->name, topic->summary);
235     }
236
237     fprintf (out, "\n");
238     fprintf (out,
239              "Use \"notmuch help <command or topic>\" for more details "
240              "on each command or topic.\n\n");
241 }
242
243 void
244 notmuch_exit_if_unsupported_format (void)
245 {
246     if (notmuch_format_version > NOTMUCH_FORMAT_CUR) {
247         fprintf (stderr, "\
248 A caller requested output format version %d, but the installed notmuch\n\
249 CLI only supports up to format version %d.  You may need to upgrade your\n\
250 notmuch CLI.\n",
251                  notmuch_format_version, NOTMUCH_FORMAT_CUR);
252         exit (NOTMUCH_EXIT_FORMAT_TOO_NEW);
253     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) {
254         fprintf (stderr, "\
255 A caller requested output format version %d, which is no longer supported\n\
256 by the notmuch CLI (it requires at least version %d).  You may need to\n\
257 upgrade your notmuch front-end.\n",
258                  notmuch_format_version, NOTMUCH_FORMAT_MIN);
259         exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
260     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN_ACTIVE) {
261         /* Warn about old version requests so compatibility issues are
262          * less likely when we drop support for a deprecated format
263          * versions. */
264         fprintf (stderr, "\
265 A caller requested deprecated output format version %d, which may not\n\
266 be supported in the future.\n", notmuch_format_version);
267     }
268 }
269
270 void
271 notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch)
272 {
273     const char *uuid = NULL;
274
275     if (! notmuch_requested_db_uuid)
276         return;
277     IGNORE_RESULT (notmuch_database_get_revision (notmuch, &uuid));
278
279     if (strcmp (notmuch_requested_db_uuid, uuid) != 0) {
280         fprintf (stderr, "Error: requested database revision %s does not match %s\n",
281                  notmuch_requested_db_uuid, uuid);
282         exit (1);
283     }
284 }
285
286 static void
287 exec_man (const char *page)
288 {
289     if (execlp ("man", "man", page, (char *) NULL)) {
290         perror ("exec man");
291         exit (1);
292     }
293 }
294
295 static int
296 _help_for (const char *topic_name)
297 {
298     command_t *command;
299     help_topic_t *topic;
300     unsigned int i;
301
302     if (! topic_name) {
303         printf ("The notmuch mail system.\n\n");
304         usage (stdout);
305         return EXIT_SUCCESS;
306     }
307
308     if (strcmp (topic_name, "help") == 0) {
309         printf ("The notmuch help system.\n\n"
310                 "\tNotmuch uses the man command to display help. In case\n"
311                 "\tof difficulties check that MANPATH includes the pages\n"
312                 "\tinstalled by notmuch.\n\n"
313                 "\tTry \"notmuch help\" for a list of topics.\n");
314         return EXIT_SUCCESS;
315     }
316
317     command = find_command (topic_name);
318     if (command) {
319         char *page = talloc_asprintf (NULL, "notmuch-%s", command->name);
320         exec_man (page);
321     }
322
323     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
324         topic = &help_topics[i];
325         if (strcmp (topic_name, topic->name) == 0) {
326             char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name);
327             exec_man (page);
328         }
329     }
330
331     fprintf (stderr,
332              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
333              topic_name);
334     return EXIT_FAILURE;
335 }
336
337 static int
338 notmuch_help_command (unused (notmuch_config_t *config), int argc, char *argv[])
339 {
340     int opt_index;
341
342     opt_index = notmuch_minimal_options ("help", argc, argv);
343     if (opt_index < 0)
344         return EXIT_FAILURE;
345
346     /* skip at least subcommand argument */
347     argc -= opt_index;
348     argv += opt_index;
349
350     if (argc == 0) {
351         return _help_for (NULL);
352     }
353
354     return _help_for (argv[0]);
355 }
356
357 /* Handle the case of "notmuch" being invoked with no command
358  * argument. For now we just call notmuch_setup_command, but we plan
359  * to be more clever about this in the future.
360  */
361 static int
362 notmuch_command (notmuch_config_t *config,
363                  unused(int argc), unused(char **argv))
364 {
365     char *db_path;
366     struct stat st;
367
368     /* If the user has never configured notmuch, then run
369      * notmuch_setup_command which will give a nice welcome message,
370      * and interactively guide the user through the configuration. */
371     if (notmuch_config_is_new (config))
372         return notmuch_setup_command (config, 0, NULL);
373
374     /* Notmuch is already configured, but is there a database? */
375     db_path = talloc_asprintf (config, "%s/%s",
376                                notmuch_config_get_database_path (config),
377                                ".notmuch");
378     if (stat (db_path, &st)) {
379         if (errno != ENOENT) {
380             fprintf (stderr, "Error looking for notmuch database at %s: %s\n",
381                      db_path, strerror (errno));
382             return EXIT_FAILURE;
383         }
384         printf ("Notmuch is configured, but there's not yet a database at\n\n\t%s\n\n",
385                 db_path);
386         printf ("You probably want to run \"notmuch new\" now to create that database.\n\n"
387                 "Note that the first run of \"notmuch new\" can take a very long time\n"
388                 "and that the resulting database will use roughly the same amount of\n"
389                 "storage space as the email being indexed.\n\n");
390         return EXIT_SUCCESS;
391     }
392
393     printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
394             "At this point you can start exploring the functionality of notmuch by\n"
395             "using commands such as:\n\n"
396             "\tnotmuch search tag:inbox\n\n"
397             "\tnotmuch search to:\"%s\"\n\n"
398             "\tnotmuch search from:\"%s\"\n\n"
399             "\tnotmuch search subject:\"my favorite things\"\n\n"
400             "See \"notmuch help search\" for more details.\n\n"
401             "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
402             "from a search. Finally, you may want to explore using a more sophisticated\n"
403             "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
404             "or any other interface described at https://notmuchmail.org\n\n"
405             "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
406             "Have fun, and may your inbox never have much mail.\n\n",
407             notmuch_config_get_user_name (config),
408             notmuch_config_get_user_primary_email (config));
409
410     return EXIT_SUCCESS;
411 }
412
413 /*
414  * Try to run subcommand in argv[0] as notmuch- prefixed external
415  * command. argv must be NULL terminated (argv passed to main always
416  * is).
417  *
418  * Does not return if the external command is found and
419  * executed. Return true if external command is not found. Return
420  * false on errors.
421  */
422 static bool
423 try_external_command (char *argv[])
424 {
425     char *old_argv0 = argv[0];
426     bool ret = true;
427
428     argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0);
429
430     /*
431      * This will only return on errors. Not finding an external
432      * command (ENOENT) is not an error from our perspective.
433      */
434     execvp (argv[0], argv);
435     if (errno != ENOENT) {
436         fprintf (stderr, "Error: Running external command '%s' failed: %s\n",
437                  argv[0], strerror (errno));
438         ret = false;
439     }
440
441     talloc_free (argv[0]);
442     argv[0] = old_argv0;
443
444     return ret;
445 }
446
447 int
448 main (int argc, char *argv[])
449 {
450     void *local;
451     char *talloc_report;
452     const char *command_name = NULL;
453     command_t *command;
454     const char *config_file_name = NULL;
455     notmuch_config_t *config = NULL;
456     int opt_index;
457     int ret;
458
459     notmuch_opt_desc_t options[] = {
460         { .opt_string = &config_file_name, .name = "config" },
461         { .opt_inherit = notmuch_shared_options },
462         { }
463     };
464
465     talloc_enable_null_tracking ();
466
467     local = talloc_new (NULL);
468
469     g_mime_init ();
470 #if ! GLIB_CHECK_VERSION (2, 35, 1)
471     g_type_init ();
472 #endif
473
474     /* Globally default to the current output format version. */
475     notmuch_format_version = NOTMUCH_FORMAT_CUR;
476
477     opt_index = parse_arguments (argc, argv, options, 1);
478     if (opt_index < 0) {
479         ret = EXIT_FAILURE;
480         goto DONE;
481     }
482
483     if (opt_index < argc)
484         command_name = argv[opt_index];
485
486     notmuch_process_shared_options (command_name);
487
488     command = find_command (command_name);
489     /* if command->function is NULL, try external command */
490     if (! command || ! command->function) {
491         /* This won't return if the external command is found. */
492         if (try_external_command (argv + opt_index))
493             fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
494                      command_name);
495         ret = EXIT_FAILURE;
496         goto DONE;
497     }
498
499     config = notmuch_config_open (local, config_file_name, command->config_mode);
500     if (! config) {
501         ret = EXIT_FAILURE;
502         goto DONE;
503     }
504
505     ret = (command->function)(config, argc - opt_index, argv + opt_index);
506
507   DONE:
508     if (config)
509         notmuch_config_close (config);
510
511     talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
512     if (talloc_report && strcmp (talloc_report, "") != 0) {
513         /* this relies on the previous call to
514          * talloc_enable_null_tracking
515          */
516
517         FILE *report = fopen (talloc_report, "w");
518         if (report) {
519             talloc_report_full (NULL, report);
520         } else {
521             ret = 1;
522             fprintf (stderr, "ERROR: unable to write talloc log. ");
523             perror (talloc_report);
524         }
525     }
526
527     talloc_free (local);
528
529     return ret;
530 }