Merge branch from fixing up bugs after bisecting.
[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 /* This is separate from notmuch-private.h because we're trying to
28  * keep notmuch.c from looking into any internals, (which helps us
29  * develop notmuch.h into a plausible library interface).
30  */
31 #include "xutil.h"
32
33 #include <stdio.h>
34 #include <stddef.h>
35 #include <string.h>
36 #include <sys/stat.h>
37 #include <sys/time.h>
38 #include <unistd.h>
39 #include <dirent.h>
40 #include <errno.h>
41
42 #include <talloc.h>
43
44 #include <glib.h> /* g_strdup_printf */
45
46 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
47
48 typedef int (*command_function_t) (int argc, char *argv[]);
49
50 typedef struct command {
51     const char *name;
52     command_function_t function;
53     const char *usage;
54 } command_t;
55
56 typedef struct {
57     int total_messages;
58     int count;
59     struct timeval tv_start;
60 } add_files_state_t;
61
62 static void
63 chomp_newline (char *str)
64 {
65     if (str && str[strlen(str)-1] == '\n')
66         str[strlen(str)-1] = '\0';
67 }
68
69 /* Compute the number of seconds elapsed from start to end. */
70 double
71 tv_elapsed (struct timeval start, struct timeval end)
72 {
73     return ((end.tv_sec - start.tv_sec) +
74             (end.tv_usec - start.tv_usec) / 1e6);
75 }
76
77 void
78 print_formatted_seconds (double seconds)
79 {
80     int hours;
81     int minutes;
82
83     if (seconds > 3600) {
84         hours = (int) seconds / 3600;
85         printf ("%dh ", hours);
86         seconds -= hours * 3600;
87     }
88
89     if (seconds > 60) {
90         minutes = (int) seconds / 60;
91         printf ("%dm ", minutes);
92         seconds -= minutes * 60;
93     }
94
95     printf ("%02ds", (int) seconds);
96 }
97
98 void
99 add_files_print_progress (add_files_state_t *state)
100 {
101     struct timeval tv_now;
102     double elapsed_overall, rate_overall;
103
104     gettimeofday (&tv_now, NULL);
105
106     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
107     rate_overall = (state->count) / elapsed_overall;
108
109     printf ("Added %d of %d messages (",
110             state->count, state->total_messages);
111     print_formatted_seconds ((state->total_messages - state->count) /
112                              rate_overall);
113     printf (" remaining).      \r");
114
115     fflush (stdout);
116 }
117
118 /* Recursively find all regular files in 'path' and add them to the
119  * database. */
120 void
121 add_files (notmuch_database_t *notmuch, const char *path,
122            add_files_state_t *state)
123 {
124     DIR *dir;
125     struct dirent *entry, *e;
126     int entry_length;
127     int err;
128     char *next;
129     struct stat st;
130     notmuch_status_t status;
131
132     dir = opendir (path);
133
134     if (dir == NULL) {
135         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
136                  path, strerror (errno));
137         return;
138     }
139
140     entry_length = offsetof (struct dirent, d_name) +
141         pathconf (path, _PC_NAME_MAX) + 1;
142     entry = malloc (entry_length);
143
144     while (1) {
145         err = readdir_r (dir, entry, &e);
146         if (err) {
147             fprintf (stderr, "Error reading directory: %s\n",
148                      strerror (errno));
149             free (entry);
150             return;
151         }
152
153         if (e == NULL)
154             break;
155
156         /* Ignore special directories to avoid infinite recursion.
157          * Also ignore the .notmuch directory.
158          */
159         /* XXX: Eventually we'll want more sophistication to let the
160          * user specify files to be ignored. */
161         if (strcmp (entry->d_name, ".") == 0 ||
162             strcmp (entry->d_name, "..") == 0 ||
163             strcmp (entry->d_name, ".notmuch") ==0)
164         {
165             continue;
166         }
167
168         next = g_strdup_printf ("%s/%s", path, entry->d_name);
169
170         stat (next, &st);
171
172         if (S_ISREG (st.st_mode)) {
173             status = notmuch_database_add_message (notmuch, next);
174             if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
175                 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
176                          next);
177             } else {
178                 state->count++;
179             }
180             if (state->count % 1000 == 0)
181                 add_files_print_progress (state);
182         } else if (S_ISDIR (st.st_mode)) {
183             add_files (notmuch, next, state);
184         }
185
186         free (next);
187     }
188
189     free (entry);
190
191     closedir (dir);
192 }
193
194 /* Recursively count all regular files in path and all sub-direcotries
195  * of path.  The result is added to *count (which should be
196  * initialized to zero by the top-level caller before calling
197  * count_files). */
198 void
199 count_files (const char *path, int *count)
200 {
201     DIR *dir;
202     struct dirent *entry, *e;
203     int entry_length;
204     int err;
205     char *next;
206     struct stat st;
207
208     dir = opendir (path);
209
210     if (dir == NULL) {
211         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
212                  path, strerror (errno));
213         return;
214     }
215
216     entry_length = offsetof (struct dirent, d_name) +
217         pathconf (path, _PC_NAME_MAX) + 1;
218     entry = malloc (entry_length);
219
220     while (1) {
221         err = readdir_r (dir, entry, &e);
222         if (err) {
223             fprintf (stderr, "Error reading directory: %s\n",
224                      strerror (errno));
225             free (entry);
226             return;
227         }
228
229         if (e == NULL)
230             break;
231
232         /* Ignore special directories to avoid infinite recursion.
233          * Also ignore the .notmuch directory.
234          */
235         /* XXX: Eventually we'll want more sophistication to let the
236          * user specify files to be ignored. */
237         if (strcmp (entry->d_name, ".") == 0 ||
238             strcmp (entry->d_name, "..") == 0 ||
239             strcmp (entry->d_name, ".notmuch") == 0)
240         {
241             continue;
242         }
243
244         next = g_strdup_printf ("%s/%s", path, entry->d_name);
245
246         stat (next, &st);
247
248         if (S_ISREG (st.st_mode)) {
249             *count = *count + 1;
250             if (*count % 1000 == 0) {
251                 printf ("Found %d files so far.\r", *count);
252                 fflush (stdout);
253             }
254         } else if (S_ISDIR (st.st_mode)) {
255             count_files (next, count);
256         }
257
258         free (next);
259     }
260
261     free (entry);
262
263     closedir (dir);
264 }
265
266 int
267 setup_command (int argc, char *argv[])
268 {
269     notmuch_database_t *notmuch;
270     char *mail_directory, *default_path;
271     size_t line_size;
272     int count;
273     add_files_state_t add_files_state;
274     double elapsed;
275     struct timeval tv_now;
276
277     printf ("Welcome to notmuch!\n\n");
278
279     printf ("The goal of notmuch is to help you manage and search your collection of\n"
280             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
281
282     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
283             "(where you already have mail stored and where messages will be delivered\n"
284             "in the future). This directory can contain any number of sub-directories\n"
285             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
286             "archives are perfect). If there are other, non-email files (such as\n"
287             "indexes maintained by other email programs) then notmuch will do its\n"
288             "best to detect those and ignore them.\n\n");
289
290     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
291             "messages), will not work with notmuch. If that's how your mail is currently\n"
292             "stored, we recommend you first convert it to maildir format with a utility\n"
293             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
294             "once the conversion is complete.\n\n");
295
296
297     default_path = notmuch_database_default_path ();
298     printf ("Top-level mail directory [%s]: ", default_path);
299     fflush (stdout);
300
301     mail_directory = NULL;
302     getline (&mail_directory, &line_size, stdin);
303     chomp_newline (mail_directory);
304
305     printf ("\n");
306
307     if (mail_directory == NULL || strlen (mail_directory) == 0) {
308         if (mail_directory)
309             free (mail_directory);
310         mail_directory = default_path;
311     } else {
312         /* XXX: Instead of telling the user to use an environment
313          * variable here, we should really be writing out a configuration
314          * file and loading that on the next run. */
315         if (strcmp (mail_directory, default_path)) {
316             printf ("Note: Since you are not using the default path, you will want to set\n"
317                     "the NOTMUCH_BASE environment variable to %s so that\n"
318                     "future calls to notmuch commands will know where to find your mail.\n",
319                     mail_directory);
320             printf ("For example, if you are using bash for your shell, add:\n\n");
321             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
322             printf ("to your ~/.bashrc file.\n\n");
323         }
324         free (default_path);
325     }
326
327     notmuch = notmuch_database_create (mail_directory);
328     if (notmuch == NULL) {
329         fprintf (stderr, "Failed to create new notmuch database at %s\n",
330                  mail_directory);
331         free (mail_directory);
332         return 1;
333     }
334
335     printf ("OK. Let's take a look at the mail we can find in the directory\n");
336     printf ("%s ...\n", mail_directory);
337
338     count = 0;
339     count_files (mail_directory, &count);
340
341     printf ("Found %d total files. That's not much mail.\n\n", count);
342
343     printf ("Next, we'll inspect the messages and create a database of threads:\n");
344
345     add_files_state.total_messages = count;
346     add_files_state.count = 0;
347     gettimeofday (&add_files_state.tv_start, NULL);
348
349     add_files (notmuch, mail_directory, &add_files_state);
350
351     gettimeofday (&tv_now, NULL);
352     elapsed = tv_elapsed (add_files_state.tv_start,
353                           tv_now);
354     printf ("Added %d total messages in ", add_files_state.count);
355     print_formatted_seconds (elapsed);
356     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
357
358     notmuch_database_close (notmuch);
359
360     free (mail_directory);
361     
362     return 0;
363 }
364
365 int
366 search_command (int argc, char *argv[])
367 {
368     fprintf (stderr, "Error: search is not implemented yet.\n");
369     return 1;
370 }
371
372 int
373 show_command (int argc, char *argv[])
374 {
375     fprintf (stderr, "Error: show is not implemented yet.\n");
376     return 1;
377 }
378
379 int
380 dump_command (int argc, char *argv[])
381 {
382     FILE *output;
383     notmuch_database_t *notmuch = NULL;
384     notmuch_query_t *query;
385     notmuch_results_t *results;
386     notmuch_message_t *message;
387     notmuch_tags_t *tags;
388     int ret = 0;
389
390     if (argc) {
391         output = fopen (argv[0], "w");
392         if (output == NULL) {
393             fprintf (stderr, "Error opening %s for writing: %s\n",
394                      argv[0], strerror (errno));
395             ret = 1;
396             goto DONE;
397         }
398     } else {
399         output = stdout;
400     }
401
402     notmuch = notmuch_database_open (NULL);
403     if (notmuch == NULL) {
404         ret = 1;
405         goto DONE;
406     }
407
408     query = notmuch_query_create (notmuch, "");
409     if (query == NULL) {
410         fprintf (stderr, "Out of memory\n");
411         ret = 1;
412         goto DONE;
413     }
414
415     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
416
417     for (results = notmuch_query_search (query);
418          notmuch_results_has_more (results);
419          notmuch_results_advance (results))
420     {
421         int first = 1;
422         message = notmuch_results_get (results);
423
424         fprintf (output,
425                  "%s (", notmuch_message_get_message_id (message));
426
427         for (tags = notmuch_message_get_tags (message);
428              notmuch_tags_has_more (tags);
429              notmuch_tags_advance (tags))
430         {
431             if (! first)
432                 fprintf (output, " ");
433
434             fprintf (output, "%s", notmuch_tags_get (tags));
435
436             first = 0;
437         }
438
439         fprintf (output, ")\n");
440
441         notmuch_message_destroy (message);
442     }
443
444     notmuch_query_destroy (query);
445
446   DONE:
447     if (notmuch)
448         notmuch_database_close (notmuch);
449     if (output != stdout)
450         fclose (output);
451
452     return ret;
453 }
454
455 int
456 restore_command (int argc, char *argv[])
457 {
458     FILE *input;
459     notmuch_database_t *notmuch = NULL;
460     char *line = NULL;
461     size_t line_size, line_len;
462     regex_t regex;
463     int rerr;
464     int ret = 0;
465
466     if (argc) {
467         input = fopen (argv[0], "r");
468         if (input == NULL) {
469             fprintf (stderr, "Error opening %s for reading: %s\n",
470                      argv[0], strerror (errno));
471             ret = 1;
472             goto DONE;
473         }
474     } else {
475         printf ("No filename given. Reading dump from stdin.\n");
476         input = stdin;
477     }
478
479     notmuch = notmuch_database_open (NULL);
480     if (notmuch == NULL) {
481         ret = 1;
482         goto DONE;
483     }
484
485     /* Dump output is one line per message. We match a sequence of
486      * non-space characters for the message-id, then one or more
487      * spaces, then a list of space-separated tags as a sequence of
488      * characters within literal '(' and ')'. */
489     xregcomp (&regex,
490               "^([^ ]+) \\(([^)]*)\\)$",
491               REG_EXTENDED);
492
493     while ((line_len = getline (&line, &line_size, input)) != -1) {
494         regmatch_t match[3];
495         char *message_id, *tags, *tag, *next;
496         notmuch_message_t *message;
497         notmuch_status_t status;
498
499         chomp_newline (line);
500
501         rerr = xregexec (&regex, line, 3, match, 0);
502         if (rerr == REG_NOMATCH)
503         {
504             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
505                      line);
506             continue;
507         }
508
509         message_id = xstrndup (line + match[1].rm_so,
510                                match[1].rm_eo - match[1].rm_so);
511         tags = xstrndup (line + match[2].rm_so,
512                          match[2].rm_eo - match[2].rm_so);
513
514         if (strlen (tags)) {
515
516             message = notmuch_database_find_message (notmuch, message_id);
517             if (message == NULL) {
518                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
519                          message_id);
520                 goto NEXT_LINE;
521             }
522
523             next = tags;
524             while (next) {
525                 tag = strsep (&next, " ");
526                 if (*tag == '\0')
527                     continue;
528                 status = notmuch_message_add_tag (message, tag);
529                 if (status) {
530                     fprintf (stderr,
531                              "Error applying tag %s to message %s:\n",
532                              tag, message_id);
533                     fprintf (stderr, "%s\n",
534                              notmuch_status_to_string (status));
535                 }
536             }
537
538             notmuch_message_destroy (message);
539         }
540       NEXT_LINE:
541         free (message_id);
542         free (tags);
543     }
544
545     regfree (&regex);
546
547   DONE:
548     if (line)
549         free (line);
550     if (notmuch)
551         notmuch_database_close (notmuch);
552
553     return ret;
554 }
555
556 command_t commands[] = {
557     { "setup", setup_command,
558       "Interactively setup notmuch for first use.\n"
559       "\t\tInvoking notmuch with no command argument will run setup if\n"
560       "\t\tthe setup command has not previously been completed." },
561     { "search", search_command,
562       "<search-term> [...]\n\n"
563       "\t\tSearch for threads matching the given search terms.\n"
564       "\t\tOnce we actually implement search we'll document the\n"
565       "\t\tsyntax here." },
566     { "show", show_command,
567       "<thread-id>\n\n"
568       "\t\tShow the thread with the given thread ID (see 'search')." },
569     { "dump", dump_command,
570       "[<filename>]\n\n"
571       "\t\tCreate a plain-text dump of the tags for each message\n"
572       "\t\twriting to the given filename, if any, or to stdout.\n"
573       "\t\tThese tags are the only data in the notmuch database\n"
574       "\t\tthat can't be recreated from the messages themselves.\n"
575       "\t\tThe output of notmuch dump is therefore the only\n"
576       "\t\tcritical thing to backup (and much more friendly to\n"
577       "\t\tincremental backup than the native database files." },
578     { "restore", restore_command,
579       "<filename>\n\n"
580       "\t\tRestore the tags from the given dump file (see 'dump')." }
581 };
582
583 void
584 usage (void)
585 {
586     command_t *command;
587     int i;
588
589     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
590     fprintf (stderr, "\n");
591     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
592     fprintf (stderr, "\n");
593
594     for (i = 0; i < ARRAY_SIZE (commands); i++) {
595         command = &commands[i];
596
597         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
598     }
599 }
600     
601 int
602 main (int argc, char *argv[])
603 {
604     command_t *command;
605     int i;
606
607     if (argc == 1)
608         return setup_command (0, NULL);
609
610     for (i = 0; i < ARRAY_SIZE (commands); i++) {
611         command = &commands[i];
612
613         if (strcmp (argv[1], command->name) == 0)
614             return (command->function) (argc - 2, &argv[2]);
615     }
616
617     /* Don't complain about "help" being an unknown command when we're
618        about to provide exactly what's wanted anyway. */
619     if (strcmp (argv[1], "help") == 0 ||
620         strcmp (argv[1], "--help") == 0)
621     {
622         fprintf (stderr, "The notmuch mail system.\n\n");
623     } else {
624         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
625     }
626     usage ();
627     exit (1);
628
629     return 0;
630 }