Merge tag '0.23.1'
[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_bool_t create_config;
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 notmuch_bool_t print_version = FALSE, print_help = FALSE;
50 char *notmuch_requested_db_uuid = NULL;
51
52 const notmuch_opt_desc_t notmuch_shared_options [] = {
53     { NOTMUCH_OPT_BOOLEAN, &print_version, "version", 'v', 0 },
54     { NOTMUCH_OPT_BOOLEAN, &print_help, "help", 'h', 0 },
55     { NOTMUCH_OPT_STRING, &notmuch_requested_db_uuid, "uuid", 'u', 0 },
56     {0, 0, 0, 0, 0}
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         { NOTMUCH_OPT_INHERIT, (void *) &notmuch_shared_options, NULL, 0, 0 },
86         { 0, 0, 0, 0, 0 }
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 static command_t commands[] = {
100     { NULL, notmuch_command, TRUE,
101       "Notmuch main command." },
102     { "setup", notmuch_setup_command, TRUE,
103       "Interactively set up notmuch for first use." },
104     { "new", notmuch_new_command, FALSE,
105       "Find and import new messages to the notmuch database." },
106     { "insert", notmuch_insert_command, FALSE,
107       "Add a new message into the maildir and notmuch database." },
108     { "search", notmuch_search_command, FALSE,
109       "Search for messages matching the given search terms." },
110     { "address", notmuch_address_command, FALSE,
111       "Get addresses from messages matching the given search terms." },
112     { "show", notmuch_show_command, FALSE,
113       "Show all messages matching the search terms." },
114     { "count", notmuch_count_command, FALSE,
115       "Count messages matching the search terms." },
116     { "reply", notmuch_reply_command, FALSE,
117       "Construct a reply template for a set of messages." },
118     { "tag", notmuch_tag_command, FALSE,
119       "Add/remove tags for all messages matching the search terms." },
120     { "dump", notmuch_dump_command, FALSE,
121       "Create a plain-text dump of the tags for each message." },
122     { "restore", notmuch_restore_command, FALSE,
123       "Restore the tags from the given dump file (see 'dump')." },
124     { "compact", notmuch_compact_command, FALSE,
125       "Compact the notmuch database." },
126     { "config", notmuch_config_command, FALSE,
127       "Get or set settings in the notmuch configuration file." },
128     { "help", notmuch_help_command, TRUE, /* create but don't save config */
129       "This message, or more detailed help for the named command." }
130 };
131
132 typedef struct help_topic {
133     const char *name;
134     const char *summary;
135 } help_topic_t;
136
137 static help_topic_t help_topics[] = {
138     { "search-terms",
139       "Common search term syntax." },
140     { "hooks",
141       "Hooks that will be run before or after certain commands." },
142 };
143
144 static command_t *
145 find_command (const char *name)
146 {
147     size_t i;
148
149     for (i = 0; i < ARRAY_SIZE (commands); i++)
150         if ((!name && !commands[i].name) ||
151             (name && commands[i].name && strcmp (name, commands[i].name) == 0))
152             return &commands[i];
153
154     return NULL;
155 }
156
157 int notmuch_format_version;
158
159 static void
160 usage (FILE *out)
161 {
162     command_t *command;
163     help_topic_t *topic;
164     unsigned int i;
165
166     fprintf (out,
167              "Usage: notmuch --help\n"
168              "       notmuch --version\n"
169              "       notmuch <command> [args...]\n");
170     fprintf (out, "\n");
171     fprintf (out, "The available commands are as follows:\n");
172     fprintf (out, "\n");
173
174     for (i = 0; i < ARRAY_SIZE (commands); i++) {
175         command = &commands[i];
176
177         if (command->name)
178             fprintf (out, "  %-12s  %s\n", command->name, command->summary);
179     }
180
181     fprintf (out, "\n");
182     fprintf (out, "Additional help topics are as follows:\n");
183     fprintf (out, "\n");
184
185     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
186         topic = &help_topics[i];
187         fprintf (out, "  %-12s  %s\n", topic->name, topic->summary);
188     }
189
190     fprintf (out, "\n");
191     fprintf (out,
192              "Use \"notmuch help <command or topic>\" for more details "
193              "on each command or topic.\n\n");
194 }
195
196 void
197 notmuch_exit_if_unsupported_format (void)
198 {
199     if (notmuch_format_version > NOTMUCH_FORMAT_CUR) {
200         fprintf (stderr, "\
201 A caller requested output format version %d, but the installed notmuch\n\
202 CLI only supports up to format version %d.  You may need to upgrade your\n\
203 notmuch CLI.\n",
204                  notmuch_format_version, NOTMUCH_FORMAT_CUR);
205         exit (NOTMUCH_EXIT_FORMAT_TOO_NEW);
206     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN) {
207         fprintf (stderr, "\
208 A caller requested output format version %d, which is no longer supported\n\
209 by the notmuch CLI (it requires at least version %d).  You may need to\n\
210 upgrade your notmuch front-end.\n",
211                  notmuch_format_version, NOTMUCH_FORMAT_MIN);
212         exit (NOTMUCH_EXIT_FORMAT_TOO_OLD);
213     } else if (notmuch_format_version < NOTMUCH_FORMAT_MIN_ACTIVE) {
214         /* Warn about old version requests so compatibility issues are
215          * less likely when we drop support for a deprecated format
216          * versions. */
217         fprintf (stderr, "\
218 A caller requested deprecated output format version %d, which may not\n\
219 be supported in the future.\n", notmuch_format_version);
220     }
221 }
222
223 void
224 notmuch_exit_if_unmatched_db_uuid (notmuch_database_t *notmuch)
225 {
226     const char *uuid = NULL;
227
228     if (!notmuch_requested_db_uuid)
229         return;
230     IGNORE_RESULT (notmuch_database_get_revision (notmuch, &uuid));
231
232     if (strcmp (notmuch_requested_db_uuid, uuid) != 0){
233         fprintf (stderr, "Error: requested database revision %s does not match %s\n",
234                  notmuch_requested_db_uuid, uuid);
235         exit (1);
236     }
237 }
238
239 static void
240 exec_man (const char *page)
241 {
242     if (execlp ("man", "man", page, (char *) NULL)) {
243         perror ("exec man");
244         exit (1);
245     }
246 }
247
248 static int
249 _help_for (const char *topic_name)
250 {
251     command_t *command;
252     help_topic_t *topic;
253     unsigned int i;
254
255     if (!topic_name) {
256         printf ("The notmuch mail system.\n\n");
257         usage (stdout);
258         return EXIT_SUCCESS;
259     }
260
261     if (strcmp (topic_name, "help") == 0) {
262         printf ("The notmuch help system.\n\n"
263                 "\tNotmuch uses the man command to display help. In case\n"
264                 "\tof difficulties check that MANPATH includes the pages\n"
265                 "\tinstalled by notmuch.\n\n"
266                 "\tTry \"notmuch help\" for a list of topics.\n");
267         return EXIT_SUCCESS;
268     }
269
270     command = find_command (topic_name);
271     if (command) {
272         char *page = talloc_asprintf (NULL, "notmuch-%s", command->name);
273         exec_man (page);
274     }
275
276     for (i = 0; i < ARRAY_SIZE (help_topics); i++) {
277         topic = &help_topics[i];
278         if (strcmp (topic_name, topic->name) == 0) {
279             char *page = talloc_asprintf (NULL, "notmuch-%s", topic->name);
280             exec_man (page);
281         }
282     }
283
284     fprintf (stderr,
285              "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
286              topic_name);
287     return EXIT_FAILURE;
288 }
289
290 static int
291 notmuch_help_command (unused (notmuch_config_t * config), int argc, char *argv[])
292 {
293     int opt_index;
294
295     opt_index = notmuch_minimal_options ("help", argc, argv);
296     if (opt_index < 0)
297         return EXIT_FAILURE;
298
299     /* skip at least subcommand argument */
300     argc-= opt_index;
301     argv+= opt_index;
302
303     if (argc == 0) {
304         return _help_for (NULL);
305     }
306
307     return _help_for (argv[0]);
308 }
309
310 /* Handle the case of "notmuch" being invoked with no command
311  * argument. For now we just call notmuch_setup_command, but we plan
312  * to be more clever about this in the future.
313  */
314 static int
315 notmuch_command (notmuch_config_t *config,
316                  unused(int argc), unused(char *argv[]))
317 {
318     char *db_path;
319     struct stat st;
320
321     /* If the user has never configured notmuch, then run
322      * notmuch_setup_command which will give a nice welcome message,
323      * and interactively guide the user through the configuration. */
324     if (notmuch_config_is_new (config))
325         return notmuch_setup_command (config, 0, NULL);
326
327     /* Notmuch is already configured, but is there a database? */
328     db_path = talloc_asprintf (config, "%s/%s",
329                                notmuch_config_get_database_path (config),
330                                ".notmuch");
331     if (stat (db_path, &st)) {
332         if (errno != ENOENT) {
333             fprintf (stderr, "Error looking for notmuch database at %s: %s\n",
334                      db_path, strerror (errno));
335             return EXIT_FAILURE;
336         }
337         printf ("Notmuch is configured, but there's not yet a database at\n\n\t%s\n\n",
338                 db_path);
339         printf ("You probably want to run \"notmuch new\" now to create that database.\n\n"
340                 "Note that the first run of \"notmuch new\" can take a very long time\n"
341                 "and that the resulting database will use roughly the same amount of\n"
342                 "storage space as the email being indexed.\n\n");
343         return EXIT_SUCCESS;
344     }
345
346     printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
347             "At this point you can start exploring the functionality of notmuch by\n"
348             "using commands such as:\n\n"
349             "\tnotmuch search tag:inbox\n\n"
350             "\tnotmuch search to:\"%s\"\n\n"
351             "\tnotmuch search from:\"%s\"\n\n"
352             "\tnotmuch search subject:\"my favorite things\"\n\n"
353             "See \"notmuch help search\" for more details.\n\n"
354             "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
355             "from a search. Finally, you may want to explore using a more sophisticated\n"
356             "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
357             "or any other interface described at https://notmuchmail.org\n\n"
358             "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
359             "Have fun, and may your inbox never have much mail.\n\n",
360             notmuch_config_get_user_name (config),
361             notmuch_config_get_user_primary_email (config));
362
363     return EXIT_SUCCESS;
364 }
365
366 int
367 main (int argc, char *argv[])
368 {
369     void *local;
370     char *talloc_report;
371     const char *command_name = NULL;
372     command_t *command;
373     char *config_file_name = NULL;
374     notmuch_config_t *config = NULL;
375     int opt_index;
376     int ret;
377
378     notmuch_opt_desc_t options[] = {
379         { NOTMUCH_OPT_STRING, &config_file_name, "config", 'c', 0 },
380         { NOTMUCH_OPT_INHERIT, (void *) &notmuch_shared_options, NULL, 0, 0 },
381         { 0, 0, 0, 0, 0 }
382     };
383
384     talloc_enable_null_tracking ();
385
386     local = talloc_new (NULL);
387
388     g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
389 #if !GLIB_CHECK_VERSION(2, 35, 1)
390     g_type_init ();
391 #endif
392
393     /* Globally default to the current output format version. */
394     notmuch_format_version = NOTMUCH_FORMAT_CUR;
395
396     opt_index = parse_arguments (argc, argv, options, 1);
397     if (opt_index < 0) {
398         ret = EXIT_FAILURE;
399         goto DONE;
400     }
401
402     if (opt_index < argc)
403         command_name = argv[opt_index];
404
405     notmuch_process_shared_options (command_name);
406
407     command = find_command (command_name);
408     if (!command) {
409         fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
410                  command_name);
411         ret = EXIT_FAILURE;
412         goto DONE;
413     }
414
415     config = notmuch_config_open (local, config_file_name, command->create_config);
416     if (!config) {
417         ret = EXIT_FAILURE;
418         goto DONE;
419     }
420
421     ret = (command->function)(config, argc - opt_index, argv + opt_index);
422
423   DONE:
424     if (config)
425         notmuch_config_close (config);
426
427     talloc_report = getenv ("NOTMUCH_TALLOC_REPORT");
428     if (talloc_report && strcmp (talloc_report, "") != 0) {
429         /* this relies on the previous call to
430          * talloc_enable_null_tracking
431          */
432
433         FILE *report = fopen (talloc_report, "w");
434         if (report) {
435             talloc_report_full (NULL, report);
436         } else {
437             ret = 1;
438             fprintf (stderr, "ERROR: unable to write talloc log. ");
439             perror (talloc_report);
440         }
441     }
442
443     talloc_free (local);
444
445     return ret;
446 }