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