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