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