notmuch dump: Fix buffer overrun in error message.
[notmuch] / notmuch.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch.h"
22
23 #ifndef _GNU_SOURCE
24 #define _GNU_SOURCE /* for getline */
25 #endif
26
27 #include <stdio.h>
28 #include <stdlib.h>
29 #include <stddef.h>
30 #include <string.h>
31 #include <sys/types.h>
32 #include <sys/stat.h>
33 #include <sys/time.h>
34 #include <unistd.h>
35 #include <dirent.h>
36 #include <errno.h>
37
38 #include <talloc.h>
39
40 #include <glib.h> /* g_strdup_printf */
41
42 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
43
44 typedef int (*command_function_t) (int argc, char *argv[]);
45
46 typedef struct command {
47     const char *name;
48     command_function_t function;
49     const char *usage;
50 } command_t;
51
52 typedef struct {
53     int total_messages;
54     int count;
55     struct timeval tv_start;
56 } add_files_state_t;
57
58 /* Compute the number of seconds elapsed from start to end. */
59 double
60 tv_elapsed (struct timeval start, struct timeval end)
61 {
62     return ((end.tv_sec - start.tv_sec) +
63             (end.tv_usec - start.tv_usec) / 1e6);
64 }
65
66 void
67 print_formatted_seconds (double seconds)
68 {
69     int hours;
70     int minutes;
71
72     if (seconds > 3600) {
73         hours = (int) seconds / 3600;
74         printf ("%dh ", hours);
75         seconds -= hours * 3600;
76     }
77
78     if (seconds > 60) {
79         minutes = (int) seconds / 60;
80         printf ("%dm ", minutes);
81         seconds -= minutes * 60;
82     }
83
84     printf ("%02ds", (int) seconds);
85 }
86
87 void
88 add_files_print_progress (add_files_state_t *state)
89 {
90     struct timeval tv_now;
91     double elapsed_overall, rate_overall;
92
93     gettimeofday (&tv_now, NULL);
94
95     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
96     rate_overall = (state->count) / elapsed_overall;
97
98     printf ("Added %d of %d messages (",
99             state->count, state->total_messages);
100     print_formatted_seconds ((state->total_messages - state->count) /
101                              rate_overall);
102     printf (" remaining).      \r");
103
104     fflush (stdout);
105 }
106
107 /* Recursively find all regular files in 'path' and add them to the
108  * database. */
109 void
110 add_files (notmuch_database_t *notmuch, const char *path,
111            add_files_state_t *state)
112 {
113     DIR *dir;
114     struct dirent *entry, *e;
115     int entry_length;
116     int err;
117     char *next;
118     struct stat st;
119     notmuch_status_t status;
120
121     dir = opendir (path);
122
123     if (dir == NULL) {
124         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
125                  path, strerror (errno));
126         return;
127     }
128
129     entry_length = offsetof (struct dirent, d_name) +
130         pathconf (path, _PC_NAME_MAX) + 1;
131     entry = malloc (entry_length);
132
133     while (1) {
134         err = readdir_r (dir, entry, &e);
135         if (err) {
136             fprintf (stderr, "Error reading directory: %s\n",
137                      strerror (errno));
138             free (entry);
139             return;
140         }
141
142         if (e == NULL)
143             break;
144
145         /* Ignore special directories to avoid infinite recursion.
146          * Also ignore the .notmuch directory.
147          */
148         /* XXX: Eventually we'll want more sophistication to let the
149          * user specify files to be ignored. */
150         if (strcmp (entry->d_name, ".") == 0 ||
151             strcmp (entry->d_name, "..") == 0 ||
152             strcmp (entry->d_name, ".notmuch") ==0)
153         {
154             continue;
155         }
156
157         next = g_strdup_printf ("%s/%s", path, entry->d_name);
158
159         stat (next, &st);
160
161         if (S_ISREG (st.st_mode)) {
162             status = notmuch_database_add_message (notmuch, next);
163             if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
164                 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
165                          next);
166             } else {
167                 state->count++;
168             }
169             if (state->count % 1000 == 0)
170                 add_files_print_progress (state);
171         } else if (S_ISDIR (st.st_mode)) {
172             add_files (notmuch, next, state);
173         }
174
175         free (next);
176     }
177
178     free (entry);
179
180     closedir (dir);
181 }
182
183 /* Recursively count all regular files in path and all sub-direcotries
184  * of path.  The result is added to *count (which should be
185  * initialized to zero by the top-level caller before calling
186  * count_files). */
187 void
188 count_files (const char *path, int *count)
189 {
190     DIR *dir;
191     struct dirent *entry, *e;
192     int entry_length;
193     int err;
194     char *next;
195     struct stat st;
196
197     dir = opendir (path);
198
199     if (dir == NULL) {
200         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
201                  path, strerror (errno));
202         return;
203     }
204
205     entry_length = offsetof (struct dirent, d_name) +
206         pathconf (path, _PC_NAME_MAX) + 1;
207     entry = malloc (entry_length);
208
209     while (1) {
210         err = readdir_r (dir, entry, &e);
211         if (err) {
212             fprintf (stderr, "Error reading directory: %s\n",
213                      strerror (errno));
214             free (entry);
215             return;
216         }
217
218         if (e == NULL)
219             break;
220
221         /* Ignore special directories to avoid infinite recursion.
222          * Also ignore the .notmuch directory.
223          */
224         /* XXX: Eventually we'll want more sophistication to let the
225          * user specify files to be ignored. */
226         if (strcmp (entry->d_name, ".") == 0 ||
227             strcmp (entry->d_name, "..") == 0 ||
228             strcmp (entry->d_name, ".notmuch") == 0)
229         {
230             continue;
231         }
232
233         next = g_strdup_printf ("%s/%s", path, entry->d_name);
234
235         stat (next, &st);
236
237         if (S_ISREG (st.st_mode)) {
238             *count = *count + 1;
239             if (*count % 1000 == 0) {
240                 printf ("Found %d files so far.\r", *count);
241                 fflush (stdout);
242             }
243         } else if (S_ISDIR (st.st_mode)) {
244             count_files (next, count);
245         }
246
247         free (next);
248     }
249
250     free (entry);
251
252     closedir (dir);
253 }
254
255 int
256 setup_command (int argc, char *argv[])
257 {
258     notmuch_database_t *notmuch;
259     char *mail_directory, *default_path;
260     size_t line_size;
261     int count;
262     add_files_state_t add_files_state;
263     double elapsed;
264     struct timeval tv_now;
265
266     printf ("Welcome to notmuch!\n\n");
267
268     printf ("The goal of notmuch is to help you manage and search your collection of\n"
269             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
270
271     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
272             "(where you already have mail stored and where messages will be delivered\n"
273             "in the future). This directory can contain any number of sub-directories\n"
274             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
275             "archives are perfect). If there are other, non-email files (such as\n"
276             "indexes maintained by other email programs) then notmuch will do its\n"
277             "best to detect those and ignore them.\n\n");
278
279     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
280             "messages), will not work with notmuch. If that's how your mail is currently\n"
281             "stored, we recommend you first convert it to maildir format with a utility\n"
282             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
283             "once the conversion is complete.\n\n");
284
285
286     default_path = notmuch_database_default_path ();
287     printf ("Top-level mail directory [%s]: ", default_path);
288     fflush (stdout);
289
290     mail_directory = NULL;
291     getline (&mail_directory, &line_size, stdin);
292     printf ("\n");
293
294     if (mail_directory &&
295         mail_directory[strlen(mail_directory)-1] == '\n')
296     {
297         mail_directory[strlen(mail_directory)-1] = '\0';
298     }
299
300     if (mail_directory == NULL || strlen (mail_directory) == 0) {
301         if (mail_directory)
302             free (mail_directory);
303         mail_directory = default_path;
304     } else {
305         /* XXX: Instead of telling the user to use an environment
306          * variable here, we should really be writing out a configuration
307          * file and loading that on the next run. */
308         if (strcmp (mail_directory, default_path)) {
309             printf ("Note: Since you are not using the default path, you will want to set\n"
310                     "the NOTMUCH_BASE environment variable to %s so that\n"
311                     "future calls to notmuch commands will know where to find your mail.\n",
312                     mail_directory);
313             printf ("For example, if you are using bash for your shell, add:\n\n");
314             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
315             printf ("to your ~/.bashrc file.\n\n");
316         }
317         free (default_path);
318     }
319
320     notmuch = notmuch_database_create (mail_directory);
321     if (notmuch == NULL) {
322         fprintf (stderr, "Failed to create new notmuch database at %s\n",
323                  mail_directory);
324         free (mail_directory);
325         return 1;
326     }
327
328     printf ("OK. Let's take a look at the mail we can find in the directory\n");
329     printf ("%s ...\n", mail_directory);
330
331     count = 0;
332     count_files (mail_directory, &count);
333
334     printf ("Found %d total files. That's not much mail.\n\n", count);
335
336     printf ("Next, we'll inspect the messages and create a database of threads:\n");
337
338     add_files_state.total_messages = count;
339     add_files_state.count = 0;
340     gettimeofday (&add_files_state.tv_start, NULL);
341
342     add_files (notmuch, mail_directory, &add_files_state);
343
344     gettimeofday (&tv_now, NULL);
345     elapsed = tv_elapsed (add_files_state.tv_start,
346                           tv_now);
347     printf ("Added %d total messages in ", add_files_state.count);
348     print_formatted_seconds (elapsed);
349     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
350
351     notmuch_database_close (notmuch);
352
353     free (mail_directory);
354     
355     return 0;
356 }
357
358 int
359 search_command (int argc, char *argv[])
360 {
361     fprintf (stderr, "Error: search is not implemented yet.\n");
362     return 1;
363 }
364
365 int
366 show_command (int argc, char *argv[])
367 {
368     fprintf (stderr, "Error: show is not implemented yet.\n");
369     return 1;
370 }
371
372 int
373 dump_command (int argc, char *argv[])
374 {
375     FILE *output;
376     notmuch_database_t *notmuch = NULL;
377     notmuch_query_t *query;
378     notmuch_results_t *results;
379     notmuch_message_t *message;
380     notmuch_tags_t *tags;
381     int ret = 0;
382
383     if (argc) {
384         output = fopen (argv[0], "w");
385         if (output == NULL) {
386             fprintf (stderr, "Error opening %s for writing: %s\n",
387                      argv[0], strerror (errno));
388             ret = 1;
389             goto DONE;
390         }
391     } else {
392         output = stdout;
393     }
394
395     notmuch = notmuch_database_open (NULL);
396     if (notmuch == NULL) {
397         ret = 1;
398         goto DONE;
399     }
400
401     query = notmuch_query_create (notmuch, "");
402     if (query == NULL) {
403         fprintf (stderr, "Out of memory\n");
404         ret = 1;
405         goto DONE;
406     }
407
408     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
409
410     for (results = notmuch_query_search (query);
411          notmuch_results_has_more (results);
412          notmuch_results_advance (results))
413     {
414         message = notmuch_results_get (results);
415
416         fprintf (output,
417                  "%s (", notmuch_message_get_message_id (message));
418
419         for (tags = notmuch_message_get_tags (message);
420              notmuch_tags_has_more (tags);
421              notmuch_tags_advance (tags))
422         {
423             int first = 1;
424
425             if (! first)
426                 fprintf (output, " ");
427
428             fprintf (output, "%s", notmuch_tags_get (tags));
429
430             first = 0;
431         }
432
433         fprintf (output, ")\n");
434
435         notmuch_message_destroy (message);
436     }
437
438     notmuch_query_destroy (query);
439
440   DONE:
441     if (notmuch)
442         notmuch_database_close (notmuch);
443     if (output != stdout)
444         fclose (output);
445
446     return ret;
447 }
448
449 int
450 restore_command (int argc, char *argv[])
451 {
452     fprintf (stderr, "Error: restore is not implemented yet.\n");
453     return 1;
454 }
455
456 command_t commands[] = {
457     { "setup", setup_command,
458       "Interactively setup notmuch for first use.\n"
459       "\t\tInvoking notmuch with no command argument will run setup if\n"
460       "\t\tthe setup command has not previously been completed." },
461     { "search", search_command,
462       "<search-term> [...]\n\n"
463       "\t\tSearch for threads matching the given search terms.\n"
464       "\t\tOnce we actually implement search we'll document the\n"
465       "\t\tsyntax here." },
466     { "show", show_command,
467       "<thread-id>\n\n"
468       "\t\tShow the thread with the given thread ID (see 'search')." },
469     { "dump", dump_command,
470       "[<filename>]\n\n"
471       "\t\tCreate a plain-text dump of the tags for each message\n"
472       "\t\twriting to the given filename, if any, or to stdout.\n"
473       "\t\tThese tags are the only data in the notmuch database\n"
474       "\t\tthat can't be recreated from the messages themselves.\n"
475       "\t\tThe output of notmuch dump is therefore the only\n"
476       "\t\tcritical thing to backup (and much more friendly to\n"
477       "\t\tincremental backup than the native database files." },
478     { "restore", restore_command,
479       "<filename>\n\n"
480       "\t\tRestore the tags from the given dump file (see 'dump')." }
481 };
482
483 void
484 usage (void)
485 {
486     command_t *command;
487     int i;
488
489     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
490     fprintf (stderr, "\n");
491     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
492     fprintf (stderr, "\n");
493
494     for (i = 0; i < ARRAY_SIZE (commands); i++) {
495         command = &commands[i];
496
497         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
498     }
499 }
500     
501 int
502 main (int argc, char *argv[])
503 {
504     command_t *command;
505     int i;
506
507     if (argc == 1)
508         return setup_command (0, NULL);
509
510     for (i = 0; i < ARRAY_SIZE (commands); i++) {
511         command = &commands[i];
512
513         if (strcmp (argv[1], command->name) == 0)
514             return (command->function) (argc - 2, &argv[2]);
515     }
516
517     /* Don't complain about "help" being an unknown command when we're
518        about to provide exactly what's wanted anyway. */
519     if (strcmp (argv[1], "help") == 0 ||
520         strcmp (argv[1], "--help") == 0)
521     {
522         fprintf (stderr, "The notmuch mail system.\n\n");
523     } else {
524         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
525     }
526     usage ();
527     exit (1);
528
529     return 0;
530 }