]> git.notmuchmail.org Git - notmuch/blob - notmuch.c
NEWS: set date
[notmuch] / 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  * Each subcommand should be passed either a config object, or an open
32  * database
33  */
34 typedef int (*command_function_t) (notmuch_database_t *notmuch, int argc, char *argv[]);
35
36 typedef struct command {
37     const char *name;
38     command_function_t function;
39     notmuch_command_mode_t mode;
40     const char *summary;
41 } command_t;
42
43 static int
44 notmuch_help_command (notmuch_database_t *notmuch, int argc, char *argv[]);
45
46 static int
47 notmuch_command (notmuch_database_t *notmuch, int argc, char *argv[]);
48
49 static int
50 _help_for (const char *topic);
51
52 static void
53 notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch);
54
55 static bool print_version = false, print_help = false;
56 static const char *notmuch_requested_db_uuid = NULL;
57 static int query_syntax = NOTMUCH_QUERY_SYNTAX_XAPIAN;
58
59 const notmuch_opt_desc_t notmuch_shared_options [] = {
60     { .opt_bool = &print_version, .name = "version" },
61     { .opt_bool = &print_help, .name = "help" },
62     { .opt_string = &notmuch_requested_db_uuid, .name = "uuid" },
63     { .opt_keyword = &query_syntax, .name = "query", .keywords =
64           (notmuch_keyword_t []){ { "infix", NOTMUCH_QUERY_SYNTAX_XAPIAN },
65                                   { "sexp", NOTMUCH_QUERY_SYNTAX_SEXP },
66                                   { 0, 0 } } },
67
68     { }
69 };
70
71 notmuch_query_syntax_t
72 shared_option_query_syntax ()
73 {
74     return query_syntax;
75 }
76
77 /* any subcommand wanting to support these options should call
78  * inherit notmuch_shared_options and call
79  * notmuch_process_shared_options (notmuch, subcommand_name);
80  * with notmuch = an open database, or NULL.
81  */
82 void
83 notmuch_process_shared_options (notmuch_database_t *notmuch, const char *subcommand_name)
84 {
85     if (print_version) {
86         printf ("notmuch " STRINGIFY (NOTMUCH_VERSION) "\n");
87         exit (EXIT_SUCCESS);
88     }
89
90     if (print_help) {
91         int ret = _help_for (subcommand_name);
92         exit (ret);
93     }
94
95     if (notmuch) {
96         notmuch_exit_if_unmatched_db_uuid (notmuch);
97     } else {
98         if (notmuch_requested_db_uuid)
99             fprintf (stderr, "Warning: ignoring --uuid=%s\n",
100                      notmuch_requested_db_uuid);
101     }
102 }
103
104 /* This is suitable for subcommands that do not actually open the
105  * database.
106  */
107 int
108 notmuch_minimal_options (const char *subcommand_name,
109                          int argc, char **argv)
110 {
111     int opt_index;
112
113     notmuch_opt_desc_t options[] = {
114         { .opt_inherit = notmuch_shared_options },
115         { }
116     };
117
118     opt_index = parse_arguments (argc, argv, options, 1);
119
120     if (opt_index < 0)
121         return -1;
122
123     /* We can't use argv here as it is sometimes NULL */
124     notmuch_process_shared_options (NULL, subcommand_name);
125     return opt_index;
126 }
127
128
129 struct _notmuch_client_indexing_cli_choices indexing_cli_choices = { };
130 const notmuch_opt_desc_t notmuch_shared_indexing_options [] = {
131     { .opt_keyword = &indexing_cli_choices.decrypt_policy,
132       .present = &indexing_cli_choices.decrypt_policy_set, .keywords =
133           (notmuch_keyword_t []){ { "false", NOTMUCH_DECRYPT_FALSE },
134                                   { "true", NOTMUCH_DECRYPT_TRUE },
135                                   { "auto", NOTMUCH_DECRYPT_AUTO },
136                                   { "nostash", NOTMUCH_DECRYPT_NOSTASH },
137                                   { 0, 0 } },
138       .name = "decrypt" },
139     { }
140 };
141
142
143 notmuch_status_t
144 notmuch_process_shared_indexing_options (notmuch_database_t *notmuch)
145 {
146     if (indexing_cli_choices.opts == NULL)
147         indexing_cli_choices.opts = notmuch_database_get_default_indexopts (notmuch);
148     if (indexing_cli_choices.decrypt_policy_set) {
149         notmuch_status_t status;
150         if (indexing_cli_choices.opts == NULL)
151             return NOTMUCH_STATUS_OUT_OF_MEMORY;
152         status = notmuch_indexopts_set_decrypt_policy (indexing_cli_choices.opts,
153                                                        indexing_cli_choices.decrypt_policy);
154         if (status != NOTMUCH_STATUS_SUCCESS) {
155             fprintf (stderr, "Error: Failed to set index decryption policy to %d. (%s)\n",
156                      indexing_cli_choices.decrypt_policy, notmuch_status_to_string (status));
157             notmuch_indexopts_destroy (indexing_cli_choices.opts);
158             indexing_cli_choices.opts = NULL;
159             return status;
160         }
161     }
162     return NOTMUCH_STATUS_SUCCESS;
163 }
164
165
166 static const command_t commands[] = {
167     { NULL, notmuch_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD,
168       "Notmuch main command." },
169     { "setup", notmuch_setup_command, NOTMUCH_COMMAND_CONFIG_CREATE | NOTMUCH_COMMAND_CONFIG_LOAD,
170       "Interactively set up notmuch for first use." },
171     { "new", notmuch_new_command,
172       NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE |
173       NOTMUCH_COMMAND_DATABASE_CREATE,
174       "Find and import new messages to the notmuch database." },
175     { "insert", notmuch_insert_command, NOTMUCH_COMMAND_DATABASE_EARLY |
176       NOTMUCH_COMMAND_DATABASE_WRITE,
177       "Add a new message into the maildir and notmuch database." },
178     { "search", notmuch_search_command, NOTMUCH_COMMAND_DATABASE_EARLY,
179       "Search for messages matching the given search terms." },
180     { "address", notmuch_address_command, NOTMUCH_COMMAND_DATABASE_EARLY,
181       "Get addresses from messages matching the given search terms." },
182     { "show", notmuch_show_command, NOTMUCH_COMMAND_DATABASE_EARLY,
183       "Show all messages matching the search terms." },
184     { "count", notmuch_count_command, NOTMUCH_COMMAND_DATABASE_EARLY,
185       "Count messages matching the search terms." },
186     { "reply", notmuch_reply_command, NOTMUCH_COMMAND_DATABASE_EARLY,
187       "Construct a reply template for a set of messages." },
188     { "tag", notmuch_tag_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
189       "Add/remove tags for all messages matching the search terms." },
190     { "dump", notmuch_dump_command, NOTMUCH_COMMAND_DATABASE_EARLY | NOTMUCH_COMMAND_DATABASE_WRITE,
191       "Create a plain-text dump of the tags for each message." },
192     { "restore", notmuch_restore_command, NOTMUCH_COMMAND_DATABASE_EARLY |
193       NOTMUCH_COMMAND_DATABASE_WRITE,
194       "Restore the tags from the given dump file (see 'dump')." },
195     { "compact", notmuch_compact_command, NOTMUCH_COMMAND_DATABASE_EARLY |
196       NOTMUCH_COMMAND_DATABASE_WRITE,
197       "Compact the notmuch database." },
198     { "reindex", notmuch_reindex_command, NOTMUCH_COMMAND_DATABASE_EARLY |
199       NOTMUCH_COMMAND_DATABASE_WRITE,
200       "Re-index all messages matching the search terms." },
201     { "config", notmuch_config_command, NOTMUCH_COMMAND_CONFIG_LOAD,
202       "Get or set settings in the notmuch configuration file." },
203 #if WITH_EMACS
204     { "emacs-mua", NULL, 0,
205       "send mail with notmuch and emacs." },
206 #endif
207     { "help", notmuch_help_command, NOTMUCH_COMMAND_CONFIG_CREATE, /* create but don't save config */
208       "This message, or more detailed help for the named command." }
209 };
210
211 typedef struct help_topic {
212     const char *name;
213     const char *summary;
214 } help_topic_t;
215
216 static const help_topic_t help_topics[] = {
217     { "search-terms",
218       "Common search term syntax." },
219     { "hooks",
220       "Hooks that will be run before or after certain commands." },
221     { "properties",
222       "Message property conventions and documentation." },
223 };
224
225 static const command_t *
226 find_command (const char *name)
227 {
228     size_t i;
229
230     for (i = 0; i < ARRAY_SIZE (commands); i++)
231         if ((! name && ! commands[i].name) ||
232             (name && commands[i].name && strcmp (name, commands[i].name) == 0))
233             return &commands[i];
234
235     return NULL;
236 }
237
238 int notmuch_format_version;
239
240 static void
241 usage (FILE *out)
242 {
243     const command_t *command;
244     const help_topic_t *topic;
245     unsigned int i;
246
247     fprintf (out,
248              "Usage: notmuch --help\n"
249              "       notmuch --version\n"
250              "       notmuch <command> [args...]\n");
251     fprintf (out, "\n");
252     fprintf (out, "The available commands are as follows:\n");
253     fprintf (out, "\n");
254
255     for (i = 0; i < ARRAY_SIZE (commands); i++) {
256         command = &commands[i];
257
258         if (command->name)
259             fprintf (out, "  %-12s  %s\n", command->name, command->summary);
260     }
261
262     fprintf (out, "\n");
263     fprintf (out, "Additional help topics are as follows:\n");
264     fprintf (out, "\n");
265
266     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
267         topic = &help_topics[i];
268         fprintf (out, "  %-12s  %s\n", topic->name, topic->summary);
269     }
270
271     fprintf (out, "\n");
272     fprintf (out,
273              "Use \"notmuch help <command or topic>\" for more details "
274              "on each command or topic.\n\n");
275 }
276
277 void
278 notmuch_exit_if_unsupported_format (void)
279 {
280     if (notmuch_format_version > NOTMUCH_FORMAT_CUR) {
281         fprintf (stderr,
282                  "\
283 A caller requested output format version %d, but the installed notmuch\n\
284 CLI only supports up to format version %d.  You may need to upgrade your\n\
285 notmuch CLI.\n",
286                  notmuch_format_version, NOTMUCH_FORMAT_CUR);
287         exit (NOTMUCH_EXIT_FORMAT_TOO_NEW);
288     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) {
289         fprintf (stderr,
290                  "\
291 A caller requested output format version %d, which is no longer supported\n\
292 by the notmuch CLI (it requires at least version %d).  You may need to\n\
293 upgrade your notmuch front-end.\n",
294                  notmuch_format_version, NOTMUCH_FORMAT_MIN);
295         exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
296     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN_ACTIVE) {
297         /* Warn about old version requests so compatibility issues are
298          * less likely when we drop support for a deprecated format
299          * versions. */
300         fprintf (stderr,
301                  "\
302 A caller requested deprecated output format version %d, which may not\n\
303 be supported in the future.\n", notmuch_format_version);
304     }
305 }
306
307 static void
308 notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch)
309 {
310     const char *uuid = NULL;
311
312     if (! notmuch_requested_db_uuid)
313         return;
314     IGNORE_RESULT (notmuch_database_get_revision (notmuch, &uuid));
315
316     if (strcmp (notmuch_requested_db_uuid, uuid) != 0) {
317         fprintf (stderr, "Error: requested database revision %s does not match %s\n",
318                  notmuch_requested_db_uuid, uuid);
319         exit (1);
320     }
321 }
322
323 static void
324 exec_man (const char *page)
325 {
326     if (execlp ("man", "man", page, (char *) NULL)) {
327         perror ("exec man");
328         exit (1);
329     }
330 }
331
332 static int
333 _help_for (const char *topic_name)
334 {
335     const command_t *command;
336     const help_topic_t *topic;
337     unsigned int i;
338
339     if (! topic_name) {
340         printf ("The notmuch mail system.\n\n");
341         usage (stdout);
342         return EXIT_SUCCESS;
343     }
344
345     if (strcmp (topic_name, "help") == 0) {
346         printf ("The notmuch help system.\n\n"
347                 "\tNotmuch uses the man command to display help. In case\n"
348                 "\tof difficulties check that MANPATH includes the pages\n"
349                 "\tinstalled by notmuch.\n\n"
350                 "\tTry \"notmuch help\" for a list of topics.\n");
351         return EXIT_SUCCESS;
352     }
353
354     command = find_command (topic_name);
355     if (command) {
356         char *page = talloc_asprintf (NULL, "notmuch-%s", command->name);
357         exec_man (page);
358     }
359
360     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
361         topic = &help_topics[i];
362         if (strcmp (topic_name, topic->name) == 0) {
363             char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name);
364             exec_man (page);
365         }
366     }
367
368     fprintf (stderr,
369              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
370              topic_name);
371     return EXIT_FAILURE;
372 }
373
374 static int
375 notmuch_help_command (unused(notmuch_database_t *notmuch), int argc, char *argv[])
376 {
377     int opt_index;
378
379     opt_index = notmuch_minimal_options ("help", argc, argv);
380     if (opt_index < 0)
381         return EXIT_FAILURE;
382
383     /* skip at least subcommand argument */
384     argc -= opt_index;
385     argv += opt_index;
386
387     if (argc == 0) {
388         return _help_for (NULL);
389     }
390
391     return _help_for (argv[0]);
392 }
393
394 /* Handle the case of "notmuch" being invoked with no command
395  * argument. For now we just call notmuch_setup_command, but we plan
396  * to be more clever about this in the future.
397  */
398 static int
399 notmuch_command (notmuch_database_t *notmuch,
400                  unused(int argc), unused(char **argv))
401 {
402
403     const char *config_path;
404
405     /* If the user has not created a configuration file, then run
406      * notmuch_setup_command which will give a nice welcome message,
407      * and interactively guide the user through the configuration. */
408     config_path = notmuch_config_path (notmuch);
409     if (access (config_path, R_OK | F_OK) == -1) {
410         if (errno != ENOENT) {
411             fprintf (stderr, "Error: %s config file access failed: %s\n", config_path,
412                      strerror (errno));
413             return EXIT_FAILURE;
414         } else {
415             return notmuch_setup_command (notmuch, 0, NULL);
416         }
417     }
418
419     printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
420             "At this point you can start exploring the functionality of notmuch by\n"
421             "using commands such as:\n\n"
422             "\tnotmuch search tag:inbox\n\n"
423             "\tnotmuch search to:\"%s\"\n\n"
424             "\tnotmuch search from:\"%s\"\n\n"
425             "\tnotmuch search subject:\"my favorite things\"\n\n"
426             "See \"notmuch help search\" for more details.\n\n"
427             "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
428             "from a search. Finally, you may want to explore using a more sophisticated\n"
429             "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
430             "or any other interface described at https://notmuchmail.org\n\n"
431             "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
432             "Have fun, and may your inbox never have much mail.\n\n",
433             notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME),
434             notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL));
435
436     return EXIT_SUCCESS;
437 }
438
439 /*
440  * Try to run subcommand in argv[0] as notmuch- prefixed external
441  * command. argv must be NULL terminated (argv passed to main always
442  * is).
443  *
444  * Does not return if the external command is found and
445  * executed. Return true if external command is not found. Return
446  * false on errors.
447  */
448 static bool
449 try_external_command (char *argv[])
450 {
451     char *old_argv0 = argv[0];
452     bool ret = true;
453
454     argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0);
455
456     /*
457      * This will only return on errors. Not finding an external
458      * command (ENOENT) is not an error from our perspective.
459      */
460     execvp (argv[0], argv);
461     if (errno != ENOENT) {
462         fprintf (stderr, "Error: Running external command '%s' failed: %s\n",
463                  argv[0], strerror (errno));
464         ret = false;
465     }
466
467     talloc_free (argv[0]);
468     argv[0] = old_argv0;
469
470     return ret;
471 }
472
473 int
474 main (int argc, char *argv[])
475 {
476     void *local;
477     char *talloc_report;
478     const char *command_name = NULL;
479     const command_t *command;
480     const char *config_file_name = NULL;
481     notmuch_database_t *notmuch = NULL;
482     int opt_index;
483     int ret = EXIT_SUCCESS;
484
485     notmuch_opt_desc_t options[] = {
486         { .opt_string = &config_file_name, .name = "config", .allow_empty = TRUE },
487         { .opt_inherit = notmuch_shared_options },
488         { }
489     };
490
491     notmuch_client_init ();
492
493     local = talloc_new (NULL);
494
495     /* Globally default to the current output format version. */
496     notmuch_format_version = NOTMUCH_FORMAT_CUR;
497
498     opt_index = parse_arguments (argc, argv, options, 1);
499     if (opt_index < 0) {
500         ret = EXIT_FAILURE;
501         goto DONE;
502     }
503
504     if (opt_index < argc)
505         command_name = argv[opt_index];
506
507     notmuch_process_shared_options (NULL, command_name);
508
509     command = find_command (command_name);
510     /* if command->function is NULL, try external command */
511     if (! command || ! command->function) {
512         /* This won't return if the external command is found. */
513         if (try_external_command (argv + opt_index))
514             fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
515                      command_name);
516         ret = EXIT_FAILURE;
517         goto DONE;
518     }
519
520     if (command->mode & NOTMUCH_COMMAND_DATABASE_EARLY) {
521         char *status_string = NULL;
522         notmuch_database_mode_t mode;
523         notmuch_status_t status;
524
525         if (command->mode & NOTMUCH_COMMAND_DATABASE_WRITE ||
526             command->mode & NOTMUCH_COMMAND_DATABASE_CREATE)
527             mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
528         else
529             mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
530
531         if (command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) {
532             status = notmuch_database_create_with_config (NULL,
533                                                           config_file_name,
534                                                           NULL,
535                                                           &notmuch,
536                                                           &status_string);
537             if (status && status != NOTMUCH_STATUS_DATABASE_EXISTS) {
538                 if (status_string) {
539                     fputs (status_string, stderr);
540                     free (status_string);
541                 }
542
543                 if (status == NOTMUCH_STATUS_NO_CONFIG)
544                     fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
545
546                 return EXIT_FAILURE;
547             }
548         }
549
550         if (notmuch == NULL) {
551             status = notmuch_database_open_with_config (NULL,
552                                                         mode,
553                                                         config_file_name,
554                                                         NULL,
555                                                         &notmuch,
556                                                         &status_string);
557             if (status) {
558                 if (status_string) {
559                     fputs (status_string, stderr);
560                     free (status_string);
561                 }
562
563                 return EXIT_FAILURE;
564             }
565         }
566     }
567
568     if (command->mode & NOTMUCH_COMMAND_CONFIG_LOAD) {
569         char *status_string = NULL;
570         notmuch_status_t status;
571         status = notmuch_database_load_config (NULL,
572                                                config_file_name,
573                                                NULL,
574                                                &notmuch,
575                                                &status_string);
576
577         if (status == NOTMUCH_STATUS_NO_CONFIG && ! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
578             fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
579             goto DONE;
580         }
581         switch (status) {
582         case NOTMUCH_STATUS_NO_CONFIG:
583             if (! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
584                 fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
585                 goto DONE;
586             }
587             break;
588         case NOTMUCH_STATUS_NO_DATABASE:
589             if (! command_name) {
590                 printf ("Notmuch is configured, but no database was found.\n");
591                 printf ("You probably want to run \"notmuch new\" now to create a database.\n\n"
592                         "Note that the first run of \"notmuch new\" can take a very long time\n"
593                         "and that the resulting database will use roughly the same amount of\n"
594                         "storage space as the email being indexed.\n\n");
595                 status = NOTMUCH_STATUS_SUCCESS;
596                 goto DONE;
597             }
598             break;
599         case NOTMUCH_STATUS_SUCCESS:
600             break;
601         default:
602             goto DONE;
603         }
604
605     }
606
607     ret = (command->function)(notmuch, argc - opt_index, argv + opt_index);
608
609   DONE:
610     talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
611     if (talloc_report && strcmp (talloc_report, "") != 0) {
612         /* this relies on the previous call to
613          * talloc_enable_null_tracking
614          */
615
616         FILE *report = fopen (talloc_report, "a");
617         if (report) {
618             talloc_report_full (NULL, report);
619         } else {
620             ret = 1;
621             fprintf (stderr, "ERROR: unable to write talloc log. ");
622             perror (talloc_report);
623         }
624     }
625
626     talloc_free (local);
627
628     return ret;
629 }