]> git.cworth.org Git - notmuch/blob - notmuch.c
CLI: simplify help command
[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     char *page;
333
334     if (! topic_name) {
335         printf ("The notmuch mail system.\n\n");
336         usage (stdout);
337         return EXIT_SUCCESS;
338     }
339
340     if (strcmp (topic_name, "help") == 0) {
341         printf ("The notmuch help system.\n\n"
342                 "\tNotmuch uses the man command to display help. In case\n"
343                 "\tof difficulties check that MANPATH includes the pages\n"
344                 "\tinstalled by notmuch.\n\n"
345                 "\tTry \"notmuch help\" for a list of topics.\n");
346         return EXIT_SUCCESS;
347     }
348
349     page = talloc_asprintf (NULL, "notmuch-%s", topic_name);
350     exec_man (page);
351
352     return EXIT_FAILURE;
353 }
354
355 static int
356 notmuch_help_command (unused(notmuch_database_t *notmuch), int argc, char *argv[])
357 {
358     int opt_index;
359
360     opt_index = notmuch_minimal_options ("help", argc, argv);
361     if (opt_index < 0)
362         return EXIT_FAILURE;
363
364     /* skip at least subcommand argument */
365     argc -= opt_index;
366     argv += opt_index;
367
368     if (argc == 0) {
369         return _help_for (NULL);
370     }
371
372     return _help_for (argv[0]);
373 }
374
375 /* Handle the case of "notmuch" being invoked with no command
376  * argument. For now we just call notmuch_setup_command, but we plan
377  * to be more clever about this in the future.
378  */
379 static int
380 notmuch_command (notmuch_database_t *notmuch,
381                  unused(int argc), unused(char **argv))
382 {
383
384     const char *config_path;
385
386     /* If the user has not created a configuration file, then run
387      * notmuch_setup_command which will give a nice welcome message,
388      * and interactively guide the user through the configuration. */
389     config_path = notmuch_config_path (notmuch);
390     if (access (config_path, R_OK | F_OK) == -1) {
391         if (errno != ENOENT) {
392             fprintf (stderr, "Error: %s config file access failed: %s\n", config_path,
393                      strerror (errno));
394             return EXIT_FAILURE;
395         } else {
396             return notmuch_setup_command (notmuch, 0, NULL);
397         }
398     }
399
400     printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
401             "At this point you can start exploring the functionality of notmuch by\n"
402             "using commands such as:\n\n"
403             "\tnotmuch search tag:inbox\n\n"
404             "\tnotmuch search to:\"%s\"\n\n"
405             "\tnotmuch search from:\"%s\"\n\n"
406             "\tnotmuch search subject:\"my favorite things\"\n\n"
407             "See \"notmuch help search\" for more details.\n\n"
408             "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
409             "from a search. Finally, you may want to explore using a more sophisticated\n"
410             "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
411             "or any other interface described at https://notmuchmail.org\n\n"
412             "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
413             "Have fun, and may your inbox never have much mail.\n\n",
414             notmuch_config_get (notmuch, NOTMUCH_CONFIG_USER_NAME),
415             notmuch_config_get (notmuch, NOTMUCH_CONFIG_PRIMARY_EMAIL));
416
417     return EXIT_SUCCESS;
418 }
419
420 /*
421  * Try to run subcommand in argv[0] as notmuch- prefixed external
422  * command. argv must be NULL terminated (argv passed to main always
423  * is).
424  *
425  * Does not return if the external command is found and
426  * executed. Return true if external command is not found. Return
427  * false on errors.
428  */
429 static bool
430 try_external_command (char *argv[])
431 {
432     char *old_argv0 = argv[0];
433     bool ret = true;
434
435     argv[0] = talloc_asprintf (NULL, "notmuch-%s", old_argv0);
436
437     /*
438      * This will only return on errors. Not finding an external
439      * command (ENOENT) is not an error from our perspective.
440      */
441     execvp (argv[0], argv);
442     if (errno != ENOENT) {
443         fprintf (stderr, "Error: Running external command '%s' failed: %s\n",
444                  argv[0], strerror (errno));
445         ret = false;
446     }
447
448     talloc_free (argv[0]);
449     argv[0] = old_argv0;
450
451     return ret;
452 }
453
454 int
455 main (int argc, char *argv[])
456 {
457     void *local;
458     char *talloc_report;
459     const char *command_name = NULL;
460     const command_t *command;
461     const char *config_file_name = NULL;
462     notmuch_database_t *notmuch = NULL;
463     int opt_index;
464     int ret = EXIT_SUCCESS;
465
466     notmuch_opt_desc_t options[] = {
467         { .opt_string = &config_file_name, .name = "config", .allow_empty = TRUE },
468         { .opt_inherit = notmuch_shared_options },
469         { }
470     };
471
472     notmuch_client_init ();
473
474     local = talloc_new (NULL);
475
476     /* Globally default to the current output format version. */
477     notmuch_format_version = NOTMUCH_FORMAT_CUR;
478
479     opt_index = parse_arguments (argc, argv, options, 1);
480     if (opt_index < 0) {
481         ret = EXIT_FAILURE;
482         goto DONE;
483     }
484
485     if (opt_index < argc)
486         command_name = argv[opt_index];
487
488     notmuch_process_shared_options (NULL, command_name);
489
490     command = find_command (command_name);
491     /* if command->function is NULL, try external command */
492     if (! command || ! command->function) {
493         /* This won't return if the external command is found. */
494         if (try_external_command (argv + opt_index))
495             fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
496                      command_name);
497         ret = EXIT_FAILURE;
498         goto DONE;
499     }
500
501     if (command->mode & NOTMUCH_COMMAND_DATABASE_EARLY) {
502         char *status_string = NULL;
503         notmuch_database_mode_t mode;
504         notmuch_status_t status;
505
506         if (command->mode & NOTMUCH_COMMAND_DATABASE_WRITE ||
507             command->mode & NOTMUCH_COMMAND_DATABASE_CREATE)
508             mode = NOTMUCH_DATABASE_MODE_READ_WRITE;
509         else
510             mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
511
512         if (command->mode & NOTMUCH_COMMAND_DATABASE_CREATE) {
513             status = notmuch_database_create_with_config (NULL,
514                                                           config_file_name,
515                                                           NULL,
516                                                           &notmuch,
517                                                           &status_string);
518             if (status && status != NOTMUCH_STATUS_DATABASE_EXISTS) {
519                 if (status_string) {
520                     fputs (status_string, stderr);
521                     free (status_string);
522                 }
523
524                 if (status == NOTMUCH_STATUS_NO_CONFIG)
525                     fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
526
527                 return EXIT_FAILURE;
528             }
529         }
530
531         if (notmuch == NULL) {
532             status = notmuch_database_open_with_config (NULL,
533                                                         mode,
534                                                         config_file_name,
535                                                         NULL,
536                                                         &notmuch,
537                                                         &status_string);
538             if (status) {
539                 if (status_string) {
540                     fputs (status_string, stderr);
541                     free (status_string);
542                 }
543
544                 return EXIT_FAILURE;
545             }
546         }
547     }
548
549     if (command->mode & NOTMUCH_COMMAND_CONFIG_LOAD) {
550         char *status_string = NULL;
551         notmuch_status_t status;
552         status = notmuch_database_load_config (NULL,
553                                                config_file_name,
554                                                NULL,
555                                                &notmuch,
556                                                &status_string);
557
558         if (status == NOTMUCH_STATUS_NO_CONFIG && ! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
559             fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
560             goto DONE;
561         }
562         switch (status) {
563         case NOTMUCH_STATUS_NO_CONFIG:
564             if (! (command->mode & NOTMUCH_COMMAND_CONFIG_CREATE)) {
565                 fputs ("Try running 'notmuch setup' to create a configuration.", stderr);
566                 goto DONE;
567             }
568             break;
569         case NOTMUCH_STATUS_NO_DATABASE:
570             if (! command_name) {
571                 printf ("Notmuch is configured, but no database was found.\n");
572                 printf ("You probably want to run \"notmuch new\" now to create a database.\n\n"
573                         "Note that the first run of \"notmuch new\" can take a very long time\n"
574                         "and that the resulting database will use roughly the same amount of\n"
575                         "storage space as the email being indexed.\n\n");
576                 status = NOTMUCH_STATUS_SUCCESS;
577                 goto DONE;
578             }
579             break;
580         case NOTMUCH_STATUS_SUCCESS:
581             break;
582         default:
583             goto DONE;
584         }
585
586     }
587
588     ret = (command->function)(notmuch, argc - opt_index, argv + opt_index);
589
590   DONE:
591     talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
592     if (talloc_report && strcmp (talloc_report, "") != 0) {
593         /* this relies on the previous call to
594          * talloc_enable_null_tracking
595          */
596
597         FILE *report = fopen (talloc_report, "a");
598         if (report) {
599             talloc_report_full (NULL, report);
600         } else {
601             ret = 1;
602             fprintf (stderr, "ERROR: unable to write talloc log. ");
603             perror (talloc_report);
604         }
605     }
606
607     talloc_free (local);
608
609     return ret;
610 }