Add new "notmuch new" command.
[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 #ifndef _GNU_SOURCE
22 #define _GNU_SOURCE /* for getline */
23 #endif
24 #include <stdio.h>
25
26 #include "notmuch.h"
27
28 /* This is separate from notmuch-private.h because we're trying to
29  * keep notmuch.c from looking into any internals, (which helps us
30  * develop notmuch.h into a plausible library interface).
31  */
32 #include "xutil.h"
33
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_files;
58     int processed_files;
59     int added_messages;
60     struct timeval tv_start;
61 } add_files_state_t;
62
63 static void
64 chomp_newline (char *str)
65 {
66     if (str && str[strlen(str)-1] == '\n')
67         str[strlen(str)-1] = '\0';
68 }
69
70 /* Compute the number of seconds elapsed from start to end. */
71 double
72 tv_elapsed (struct timeval start, struct timeval end)
73 {
74     return ((end.tv_sec - start.tv_sec) +
75             (end.tv_usec - start.tv_usec) / 1e6);
76 }
77
78 void
79 print_formatted_seconds (double seconds)
80 {
81     int hours;
82     int minutes;
83
84     if (seconds > 3600) {
85         hours = (int) seconds / 3600;
86         printf ("%dh ", hours);
87         seconds -= hours * 3600;
88     }
89
90     if (seconds > 60) {
91         minutes = (int) seconds / 60;
92         printf ("%dm ", minutes);
93         seconds -= minutes * 60;
94     }
95
96     printf ("%ds", (int) seconds);
97 }
98
99 void
100 add_files_print_progress (add_files_state_t *state)
101 {
102     struct timeval tv_now;
103     double elapsed_overall, rate_overall;
104
105     gettimeofday (&tv_now, NULL);
106
107     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
108     rate_overall = (state->processed_files) / elapsed_overall;
109
110     printf ("Processed %d", state->processed_files);
111
112     if (state->total_files) {
113         printf (" of %d files (", state->total_files);
114         print_formatted_seconds ((state->total_files - state->processed_files) /
115                                  rate_overall);
116         printf (" remaining).      \r");
117     } else {
118         printf (" files (%d files/sec.)    \r", (int) rate_overall);
119     }
120
121     fflush (stdout);
122 }
123
124 /* Examine 'path' recursively as follows:
125  *
126  *   o Ask the filesystem for the mtime of 'path' (path_mtime)
127  *
128  *   o Ask the database for its timestamp of 'path' (path_dbtime)
129  *
130  *   o If 'path_mtime' > 'path_dbtime'
131  *
132  *       o For each regular file in 'path' with mtime newer than the
133  *         'path_dbtime' call add_message to add the file to the
134  *         database.
135  *
136  *       o For each sub-directory of path, recursively call into this
137  *         same function.
138  *
139  *   o Tell the database to update its time of 'path' to 'path_mtime'
140  */
141 notmuch_status_t
142 add_files (notmuch_database_t *notmuch, const char *path,
143            add_files_state_t *state)
144 {
145     DIR *dir = NULL;
146     struct dirent *e, *entry = NULL;
147     int entry_length;
148     int err;
149     char *next = NULL;
150     struct stat st;
151     time_t path_mtime, path_dbtime;
152     notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
153
154     if (stat (path, &st)) {
155         fprintf (stderr, "Error reading directory %s: %s\n",
156                  path, strerror (errno));
157         return NOTMUCH_STATUS_FILE_ERROR;
158     }
159
160     if (! S_ISDIR (st.st_mode)) {
161         fprintf (stderr, "Error: %s is not a directory.\n", path);
162         return NOTMUCH_STATUS_FILE_ERROR;
163     }
164
165     path_mtime = st.st_mtime;
166
167     path_dbtime = notmuch_database_get_timestamp (notmuch, path);
168
169     dir = opendir (path);
170     if (dir == NULL) {
171         fprintf (stderr, "Error opening directory %s: %s\n",
172                  path, strerror (errno));
173         ret = NOTMUCH_STATUS_FILE_ERROR;
174         goto DONE;
175     }
176
177     entry_length = offsetof (struct dirent, d_name) +
178         pathconf (path, _PC_NAME_MAX) + 1;
179     entry = malloc (entry_length);
180
181     while (1) {
182         err = readdir_r (dir, entry, &e);
183         if (err) {
184             fprintf (stderr, "Error reading directory: %s\n",
185                      strerror (errno));
186             ret = NOTMUCH_STATUS_FILE_ERROR;
187             goto DONE;
188         }
189
190         if (e == NULL)
191             break;
192
193         /* If this directory hasn't been modified since the last
194          * add_files, then we only need to look further for
195          * sub-directories. */
196         if (path_mtime <= path_dbtime && entry->d_type != DT_DIR)
197             continue;
198
199         /* Ignore special directories to avoid infinite recursion.
200          * Also ignore the .notmuch directory.
201          */
202         /* XXX: Eventually we'll want more sophistication to let the
203          * user specify files to be ignored. */
204         if (strcmp (entry->d_name, ".") == 0 ||
205             strcmp (entry->d_name, "..") == 0 ||
206             strcmp (entry->d_name, ".notmuch") ==0)
207         {
208             continue;
209         }
210
211         next = g_strdup_printf ("%s/%s", path, entry->d_name);
212
213         if (stat (next, &st)) {
214             fprintf (stderr, "Error reading %s: %s\n",
215                      next, strerror (errno));
216             ret = NOTMUCH_STATUS_FILE_ERROR;
217             continue;
218         }
219
220         if (S_ISREG (st.st_mode)) {
221             /* If the file hasn't been modified since the last
222              * add_files, then we need not look at it. */
223             if (st.st_mtime > path_dbtime) {
224                 state->processed_files++;
225
226                 status = notmuch_database_add_message (notmuch, next);
227                 switch (status) {
228                     /* success */
229                     case NOTMUCH_STATUS_SUCCESS:
230                         state->added_messages++;
231                         break;
232                     /* Non-fatal issues (go on to next file) */
233                     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
234                         /* Stay silent on this one. */
235                         break;
236                     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
237                         fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
238                                  next);
239                         break;
240                     /* Fatal issues. Don't process anymore. */
241                     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
242                         fprintf (stderr, "A Xapian error was encountered. Halting processing.\n");
243                         ret = status;
244                         goto DONE;
245                     default:
246                         fprintf (stderr, "Internal error: add_message returned unexpected value: %d\n",  status);
247                         ret = status;
248                         goto DONE;
249                 }
250                 if (state->processed_files % 1000 == 0)
251                     add_files_print_progress (state);
252             }
253         } else if (S_ISDIR (st.st_mode)) {
254             status = add_files (notmuch, next, state);
255             if (status && ret == NOTMUCH_STATUS_SUCCESS)
256                 ret = status;
257         }
258
259         free (next);
260         next = NULL;
261     }
262
263     notmuch_database_set_timestamp (notmuch, path, path_mtime);
264
265   DONE:
266     if (next)
267         free (next);
268     if (entry)
269         free (entry);
270     if (dir)
271         closedir (dir);
272
273     return ret;
274 }
275
276 /* Recursively count all regular files in path and all sub-direcotries
277  * of path.  The result is added to *count (which should be
278  * initialized to zero by the top-level caller before calling
279  * count_files). */
280 void
281 count_files (const char *path, int *count)
282 {
283     DIR *dir;
284     struct dirent *entry, *e;
285     int entry_length;
286     int err;
287     char *next;
288     struct stat st;
289
290     dir = opendir (path);
291
292     if (dir == NULL) {
293         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
294                  path, strerror (errno));
295         return;
296     }
297
298     entry_length = offsetof (struct dirent, d_name) +
299         pathconf (path, _PC_NAME_MAX) + 1;
300     entry = malloc (entry_length);
301
302     while (1) {
303         err = readdir_r (dir, entry, &e);
304         if (err) {
305             fprintf (stderr, "Error reading directory: %s\n",
306                      strerror (errno));
307             free (entry);
308             return;
309         }
310
311         if (e == NULL)
312             break;
313
314         /* Ignore special directories to avoid infinite recursion.
315          * Also ignore the .notmuch directory.
316          */
317         /* XXX: Eventually we'll want more sophistication to let the
318          * user specify files to be ignored. */
319         if (strcmp (entry->d_name, ".") == 0 ||
320             strcmp (entry->d_name, "..") == 0 ||
321             strcmp (entry->d_name, ".notmuch") == 0)
322         {
323             continue;
324         }
325
326         next = g_strdup_printf ("%s/%s", path, entry->d_name);
327
328         stat (next, &st);
329
330         if (S_ISREG (st.st_mode)) {
331             *count = *count + 1;
332             if (*count % 1000 == 0) {
333                 printf ("Found %d files so far.\r", *count);
334                 fflush (stdout);
335             }
336         } else if (S_ISDIR (st.st_mode)) {
337             count_files (next, count);
338         }
339
340         free (next);
341     }
342
343     free (entry);
344
345     closedir (dir);
346 }
347
348 int
349 setup_command (int argc, char *argv[])
350 {
351     notmuch_database_t *notmuch = NULL;
352     char *default_path, *mail_directory = NULL;
353     size_t line_size;
354     int count;
355     add_files_state_t add_files_state;
356     double elapsed;
357     struct timeval tv_now;
358     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
359
360     printf ("Welcome to notmuch!\n\n");
361
362     printf ("The goal of notmuch is to help you manage and search your collection of\n"
363             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
364
365     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
366             "(where you already have mail stored and where messages will be delivered\n"
367             "in the future). This directory can contain any number of sub-directories\n"
368             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
369             "archives are perfect). If there are other, non-email files (such as\n"
370             "indexes maintained by other email programs) then notmuch will do its\n"
371             "best to detect those and ignore them.\n\n");
372
373     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
374             "messages), will not work with notmuch. If that's how your mail is currently\n"
375             "stored, we recommend you first convert it to maildir format with a utility\n"
376             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
377             "once the conversion is complete.\n\n");
378
379
380     default_path = notmuch_database_default_path ();
381     printf ("Top-level mail directory [%s]: ", default_path);
382     fflush (stdout);
383
384     getline (&mail_directory, &line_size, stdin);
385     chomp_newline (mail_directory);
386
387     printf ("\n");
388
389     if (mail_directory == NULL || strlen (mail_directory) == 0) {
390         if (mail_directory)
391             free (mail_directory);
392         mail_directory = default_path;
393     } else {
394         /* XXX: Instead of telling the user to use an environment
395          * variable here, we should really be writing out a configuration
396          * file and loading that on the next run. */
397         if (strcmp (mail_directory, default_path)) {
398             printf ("Note: Since you are not using the default path, you will want to set\n"
399                     "the NOTMUCH_BASE environment variable to %s so that\n"
400                     "future calls to notmuch commands will know where to find your mail.\n",
401                     mail_directory);
402             printf ("For example, if you are using bash for your shell, add:\n\n");
403             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
404             printf ("to your ~/.bashrc file.\n\n");
405         }
406         free (default_path);
407     }
408
409     notmuch = notmuch_database_create (mail_directory);
410     if (notmuch == NULL) {
411         fprintf (stderr, "Failed to create new notmuch database at %s\n",
412                  mail_directory);
413         ret = NOTMUCH_STATUS_FILE_ERROR;
414         goto DONE;
415     }
416
417     printf ("OK. Let's take a look at the mail we can find in the directory\n");
418     printf ("%s ...\n", mail_directory);
419
420     count = 0;
421     count_files (mail_directory, &count);
422
423     printf ("Found %d total files. That's not much mail.\n\n", count);
424
425     printf ("Next, we'll inspect the messages and create a database of threads:\n");
426
427     add_files_state.total_files = count;
428     add_files_state.processed_files = 0;
429     add_files_state.added_messages = 0;
430     gettimeofday (&add_files_state.tv_start, NULL);
431
432     ret = add_files (notmuch, mail_directory, &add_files_state);
433
434     gettimeofday (&tv_now, NULL);
435     elapsed = tv_elapsed (add_files_state.tv_start,
436                           tv_now);
437     printf ("Processed %d total files in ", add_files_state.processed_files);
438     print_formatted_seconds (elapsed);
439     printf (" (%d files/sec.).                 \n",
440             (int) (add_files_state.processed_files / elapsed));
441     printf ("Added %d unique messages to the database.\n\n",
442             add_files_state.added_messages);
443
444     printf ("When new mail is delivered to %s in the future,\n"
445             "run \"notmuch new\" to add it to the database.\n",
446             mail_directory);
447
448     if (ret) {
449         printf ("Note: At least one error was encountered: %s\n",
450                 notmuch_status_to_string (ret));
451     }
452
453   DONE:
454     if (mail_directory)
455         free (mail_directory);
456     if (notmuch)
457         notmuch_database_close (notmuch);
458
459     return ret;
460 }
461
462 int
463 new_command (int argc, char *argv[])
464 {
465     notmuch_database_t *notmuch;
466     const char *mail_directory;
467     add_files_state_t add_files_state;
468     double elapsed;
469     struct timeval tv_now;
470     int ret = 0;
471
472     notmuch = notmuch_database_open (NULL);
473     if (notmuch == NULL) {
474         ret = 1;
475         goto DONE;
476     }
477
478     mail_directory = notmuch_database_get_path (notmuch);
479
480     add_files_state.total_files = 0;
481     add_files_state.processed_files = 0;
482     add_files_state.added_messages = 0;
483     gettimeofday (&add_files_state.tv_start, NULL);
484
485     ret = add_files (notmuch, mail_directory, &add_files_state);
486
487     gettimeofday (&tv_now, NULL);
488     elapsed = tv_elapsed (add_files_state.tv_start,
489                           tv_now);
490     if (add_files_state.processed_files) {
491         printf ("Processed %d total files in ", add_files_state.processed_files);
492         print_formatted_seconds (elapsed);
493         printf (" (%d files/sec.).                 \n",
494                 (int) (add_files_state.processed_files / elapsed));
495     }
496     if (add_files_state.added_messages) {
497         printf ("Added %d new messages to the database (not much, really).\n",
498                 add_files_state.added_messages);
499     } else {
500         printf ("No new mail---and that's not much!.\n");
501     }
502
503     if (ret) {
504         printf ("Note: At least one error was encountered: %s\n",
505                 notmuch_status_to_string (ret));
506     }
507
508   DONE:
509     if (notmuch)
510         notmuch_database_close (notmuch);
511
512     return ret;
513 }
514
515 int
516 search_command (int argc, char *argv[])
517 {
518     fprintf (stderr, "Error: search is not implemented yet.\n");
519     return 1;
520 }
521
522 int
523 show_command (int argc, char *argv[])
524 {
525     fprintf (stderr, "Error: show is not implemented yet.\n");
526     return 1;
527 }
528
529 int
530 dump_command (int argc, char *argv[])
531 {
532     FILE *output;
533     notmuch_database_t *notmuch;
534     notmuch_query_t *query;
535     notmuch_results_t *results;
536     notmuch_message_t *message;
537     notmuch_tags_t *tags;
538     int ret = 0;
539
540     if (argc) {
541         output = fopen (argv[0], "w");
542         if (output == NULL) {
543             fprintf (stderr, "Error opening %s for writing: %s\n",
544                      argv[0], strerror (errno));
545             ret = 1;
546             goto DONE;
547         }
548     } else {
549         output = stdout;
550     }
551
552     notmuch = notmuch_database_open (NULL);
553     if (notmuch == NULL) {
554         ret = 1;
555         goto DONE;
556     }
557
558     query = notmuch_query_create (notmuch, "");
559     if (query == NULL) {
560         fprintf (stderr, "Out of memory\n");
561         ret = 1;
562         goto DONE;
563     }
564
565     notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
566
567     for (results = notmuch_query_search (query);
568          notmuch_results_has_more (results);
569          notmuch_results_advance (results))
570     {
571         int first = 1;
572         message = notmuch_results_get (results);
573
574         fprintf (output,
575                  "%s (", notmuch_message_get_message_id (message));
576
577         for (tags = notmuch_message_get_tags (message);
578              notmuch_tags_has_more (tags);
579              notmuch_tags_advance (tags))
580         {
581             if (! first)
582                 fprintf (output, " ");
583
584             fprintf (output, "%s", notmuch_tags_get (tags));
585
586             first = 0;
587         }
588
589         fprintf (output, ")\n");
590
591         notmuch_message_destroy (message);
592     }
593
594     notmuch_query_destroy (query);
595
596   DONE:
597     if (notmuch)
598         notmuch_database_close (notmuch);
599     if (output != stdout)
600         fclose (output);
601
602     return ret;
603 }
604
605 int
606 restore_command (int argc, char *argv[])
607 {
608     FILE *input;
609     notmuch_database_t *notmuch;
610     char *line = NULL;
611     size_t line_size, line_len;
612     regex_t regex;
613     int rerr;
614     int ret = 0;
615
616     if (argc) {
617         input = fopen (argv[0], "r");
618         if (input == NULL) {
619             fprintf (stderr, "Error opening %s for reading: %s\n",
620                      argv[0], strerror (errno));
621             ret = 1;
622             goto DONE;
623         }
624     } else {
625         printf ("No filename given. Reading dump from stdin.\n");
626         input = stdin;
627     }
628
629     notmuch = notmuch_database_open (NULL);
630     if (notmuch == NULL) {
631         ret = 1;
632         goto DONE;
633     }
634
635     /* Dump output is one line per message. We match a sequence of
636      * non-space characters for the message-id, then one or more
637      * spaces, then a list of space-separated tags as a sequence of
638      * characters within literal '(' and ')'. */
639     xregcomp (&regex,
640               "^([^ ]+) \\(([^)]*)\\)$",
641               REG_EXTENDED);
642
643     while ((line_len = getline (&line, &line_size, input)) != -1) {
644         regmatch_t match[3];
645         char *message_id, *tags, *tag, *next;
646         notmuch_message_t *message;
647         notmuch_status_t status;
648
649         chomp_newline (line);
650
651         rerr = xregexec (&regex, line, 3, match, 0);
652         if (rerr == REG_NOMATCH)
653         {
654             fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
655                      line);
656             continue;
657         }
658
659         message_id = xstrndup (line + match[1].rm_so,
660                                match[1].rm_eo - match[1].rm_so);
661         tags = xstrndup (line + match[2].rm_so,
662                          match[2].rm_eo - match[2].rm_so);
663
664         if (strlen (tags)) {
665
666             message = notmuch_database_find_message (notmuch, message_id);
667             if (message == NULL) {
668                 fprintf (stderr, "Warning: Cannot apply tags to missing message: %s (",
669                          message_id);
670             }
671
672             next = tags;
673             while (next) {
674                 tag = strsep (&next, " ");
675                 if (*tag == '\0')
676                     continue;
677                 if (message) {
678                     status = notmuch_message_add_tag (message, tag);
679                     if (status) {
680                         fprintf (stderr,
681                                  "Error applying tag %s to message %s:\n",
682                                  tag, message_id);
683                         fprintf (stderr, "%s\n",
684                                  notmuch_status_to_string (status));
685                     }
686                 } else {
687                     fprintf (stderr, "%s ", tag);
688                 }
689             }
690
691             if (message)
692                 notmuch_message_destroy (message);
693             else
694                 fprintf (stderr, ")\n");
695         }
696         free (message_id);
697         free (tags);
698     }
699
700     regfree (&regex);
701
702   DONE:
703     if (line)
704         free (line);
705     if (notmuch)
706         notmuch_database_close (notmuch);
707
708     return ret;
709 }
710
711 command_t commands[] = {
712     { "setup", setup_command,
713       "Interactively setup notmuch for first use.\n"
714       "\t\tInvoking notmuch with no command argument will run setup if\n"
715       "\t\tthe setup command has not previously been completed." },
716     { "new", new_command,
717       "Find and import any new messages."},
718     { "search", search_command,
719       "<search-term> [...]\n\n"
720       "\t\tSearch for threads matching the given search terms.\n"
721       "\t\tOnce we actually implement search we'll document the\n"
722       "\t\tsyntax here." },
723     { "show", show_command,
724       "<thread-id>\n\n"
725       "\t\tShow the thread with the given thread ID (see 'search')." },
726     { "dump", dump_command,
727       "[<filename>]\n\n"
728       "\t\tCreate a plain-text dump of the tags for each message\n"
729       "\t\twriting to the given filename, if any, or to stdout.\n"
730       "\t\tThese tags are the only data in the notmuch database\n"
731       "\t\tthat can't be recreated from the messages themselves.\n"
732       "\t\tThe output of notmuch dump is therefore the only\n"
733       "\t\tcritical thing to backup (and much more friendly to\n"
734       "\t\tincremental backup than the native database files." },
735     { "restore", restore_command,
736       "<filename>\n\n"
737       "\t\tRestore the tags from the given dump file (see 'dump')." }
738 };
739
740 void
741 usage (void)
742 {
743     command_t *command;
744     int i;
745
746     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
747     fprintf (stderr, "\n");
748     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
749     fprintf (stderr, "\n");
750
751     for (i = 0; i < ARRAY_SIZE (commands); i++) {
752         command = &commands[i];
753
754         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
755     }
756 }
757     
758 int
759 main (int argc, char *argv[])
760 {
761     command_t *command;
762     int i;
763
764     if (argc == 1)
765         return setup_command (0, NULL);
766
767     for (i = 0; i < ARRAY_SIZE (commands); i++) {
768         command = &commands[i];
769
770         if (strcmp (argv[1], command->name) == 0)
771             return (command->function) (argc - 2, &argv[2]);
772     }
773
774     /* Don't complain about "help" being an unknown command when we're
775        about to provide exactly what's wanted anyway. */
776     if (strcmp (argv[1], "help") == 0 ||
777         strcmp (argv[1], "--help") == 0)
778     {
779         fprintf (stderr, "The notmuch mail system.\n\n");
780     } else {
781         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
782     }
783     usage ();
784     exit (1);
785
786     return 0;
787 }