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