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