05aa52dce1d5b4e57949635b5bf2bc293f6c5174
[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 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <sys/types.h>
27 #include <sys/stat.h>
28 #include <sys/time.h>
29 #include <unistd.h>
30 #include <dirent.h>
31 #include <errno.h>
32
33 #include <glib.h> /* GIOChannel */
34
35 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
36
37 typedef int (*command_function_t) (int argc, char *argv[]);
38
39 typedef struct command {
40     const char *name;
41     command_function_t function;
42     const char *usage;
43 } command_t;
44
45 /* Read a line from stdin, without any line-terminator character.  The
46  * return value is a newly allocated string. The caller should free()
47  * the string when finished with it.
48  *
49  * This function returns NULL if EOF is encountered before any
50  * characters are input (otherwise it returns those characters).
51  */
52 char *
53 read_line (void)
54 {
55     char *result = NULL;
56     GError *error = NULL;
57     GIOStatus g_io_status;
58     gsize length;
59
60     GIOChannel *channel = g_io_channel_unix_new (fileno (stdin));
61
62     g_io_status = g_io_channel_read_line (channel, &result,
63                                           &length, NULL, &error);
64
65     if (g_io_status == EOF)
66         goto DONE;
67
68     if (g_io_status != G_IO_STATUS_NORMAL) {
69         fprintf(stderr, "Read error: %s\n", error->message);
70         exit (1);
71     }
72
73     if (length && result[length - 1] == '\n')
74         result[length - 1] = '\0';
75
76   DONE:
77     g_io_channel_unref (channel);
78     return result;
79 }
80
81 typedef struct {
82     int total_messages;
83     int count;
84     struct timeval tv_start;
85 } add_files_state_t;
86
87 /* Compute the number of seconds elapsed from start to end. */
88 double
89 tv_elapsed (struct timeval start, struct timeval end)
90 {
91     return ((end.tv_sec - start.tv_sec) +
92             (end.tv_usec - start.tv_usec) / 1e6);
93 }
94
95 void
96 print_formatted_seconds (double seconds)
97 {
98     int hours;
99     int minutes;
100
101     if (seconds > 3600) {
102         hours = (int) seconds / 3600;
103         printf ("%dh ", hours);
104         seconds -= hours * 3600;
105     }
106
107     if (seconds > 60) {
108         minutes = (int) seconds / 60;
109         printf ("%dm ", minutes);
110         seconds -= minutes * 60;
111     }
112
113     printf ("%02ds", (int) seconds);
114 }
115
116 void
117 add_files_print_progress (add_files_state_t *state)
118 {
119     struct timeval tv_now;
120     double elapsed_overall, rate_overall;
121
122     gettimeofday (&tv_now, NULL);
123
124     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
125     rate_overall = (state->count) / elapsed_overall;
126
127     printf ("Added %d of %d messages (",
128             state->count, state->total_messages);
129     print_formatted_seconds ((state->total_messages - state->count) /
130                              rate_overall);
131     printf (" remaining).\r");
132
133     fflush (stdout);
134 }
135
136 /* Recursively find all regular files in 'path' and add them to the
137  * database. */
138 void
139 add_files (notmuch_database_t *notmuch, const char *path,
140            add_files_state_t *state)
141 {
142     DIR *dir;
143     struct dirent *entry, *e;
144     int entry_length;
145     int err;
146     char *next;
147     struct stat st;
148     notmuch_status_t status;
149
150     dir = opendir (path);
151
152     if (dir == NULL) {
153         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
154                  path, strerror (errno));
155         return;
156     }
157
158     entry_length = offsetof (struct dirent, d_name) +
159         pathconf (path, _PC_NAME_MAX) + 1;
160     entry = malloc (entry_length);
161
162     while (1) {
163         err = readdir_r (dir, entry, &e);
164         if (err) {
165             fprintf (stderr, "Error reading directory: %s\n",
166                      strerror (errno));
167             free (entry);
168             return;
169         }
170
171         if (e == NULL)
172             break;
173
174         /* Ignore special directories to avoid infinite recursion.
175          * Also ignore the .notmuch directory.
176          */
177         /* XXX: Eventually we'll want more sophistication to let the
178          * user specify files to be ignored. */
179         if (strcmp (entry->d_name, ".") == 0 ||
180             strcmp (entry->d_name, "..") == 0 ||
181             strcmp (entry->d_name, ".notmuch") ==0)
182         {
183             continue;
184         }
185
186         next = g_strdup_printf ("%s/%s", path, entry->d_name);
187
188         stat (next, &st);
189
190         if (S_ISREG (st.st_mode)) {
191             status = notmuch_database_add_message (notmuch, next);
192             if (status == NOTMUCH_STATUS_FILE_NOT_EMAIL) {
193                 fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
194                          next);
195             } else {
196                 state->count++;
197             }
198             if (state->count % 1000 == 0)
199                 add_files_print_progress (state);
200         } else if (S_ISDIR (st.st_mode)) {
201             add_files (notmuch, next, state);
202         }
203
204         free (next);
205     }
206
207     free (entry);
208
209     closedir (dir);
210 }
211
212 /* Recursively count all regular files in path and all sub-direcotries
213  * of path.  The result is added to *count (which should be
214  * initialized to zero by the top-level caller before calling
215  * count_files). */
216 void
217 count_files (const char *path, int *count)
218 {
219     DIR *dir;
220     struct dirent *entry, *e;
221     int entry_length;
222     int err;
223     char *next;
224     struct stat st;
225
226     dir = opendir (path);
227
228     if (dir == NULL) {
229         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
230                  path, strerror (errno));
231         return;
232     }
233
234     entry_length = offsetof (struct dirent, d_name) +
235         pathconf (path, _PC_NAME_MAX) + 1;
236     entry = malloc (entry_length);
237
238     while (1) {
239         err = readdir_r (dir, entry, &e);
240         if (err) {
241             fprintf (stderr, "Error reading directory: %s\n",
242                      strerror (errno));
243             free (entry);
244             return;
245         }
246
247         if (e == NULL)
248             break;
249
250         /* Ignore special directories to avoid infinite recursion.
251          * Also ignore the .notmuch directory.
252          */
253         /* XXX: Eventually we'll want more sophistication to let the
254          * user specify files to be ignored. */
255         if (strcmp (entry->d_name, ".") == 0 ||
256             strcmp (entry->d_name, "..") == 0 ||
257             strcmp (entry->d_name, ".notmuch") == 0)
258         {
259             continue;
260         }
261
262         next = g_strdup_printf ("%s/%s", path, entry->d_name);
263
264         stat (next, &st);
265
266         if (S_ISREG (st.st_mode)) {
267             *count = *count + 1;
268             if (*count % 1000 == 0) {
269                 printf ("Found %d files so far.\r", *count);
270                 fflush (stdout);
271             }
272         } else if (S_ISDIR (st.st_mode)) {
273             count_files (next, count);
274         }
275
276         free (next);
277     }
278
279     free (entry);
280
281     closedir (dir);
282 }
283
284 int
285 setup_command (int argc, char *argv[])
286 {
287     notmuch_database_t *notmuch;
288     char *mail_directory;
289     int count;
290     add_files_state_t add_files_state;
291     double elapsed;
292     struct timeval tv_now;
293
294     printf ("Welcome to notmuch!\n\n");
295
296     printf ("The goal of notmuch is to help you manage and search your collection of\n"
297             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
298
299     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
300             "(where you already have mail stored and where messages will be delivered\n"
301             "in the future). This directory can contain any number of sub-directories\n"
302             "and primarily just files with indvidual email messages (eg. maildir or mh\n"
303             "archives are perfect). If there are other, non-email files (such as\n"
304             "indexes maintained by other email programs) then notmuch will do its\n"
305             "best to detect those and ignore them.\n\n");
306
307     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
308             "messages), will not work with notmuch. If that's how your mail is currently\n"
309             "stored, we recommend you first convert it to maildir format with a utility\n"
310             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
311             "once the conversion is complete.\n\n");
312
313     printf ("Top-level mail directory [~/mail]: ");
314     fflush (stdout);
315
316     mail_directory = read_line ();
317
318     if (mail_directory == NULL || strlen (mail_directory) == 0) {
319         char *home;
320
321         if (mail_directory)
322             free (mail_directory);
323
324         home = getenv ("HOME");
325         if (!home) {
326             fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
327             fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
328             exit (1);
329         }
330
331         mail_directory = g_strdup_printf ("%s/mail", home);
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         free (mail_directory);
339         return 1;
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_messages = count;
353     add_files_state.count = 0;
354     gettimeofday (&add_files_state.tv_start, NULL);
355
356     add_files (notmuch, mail_directory, &add_files_state);
357
358     gettimeofday (&tv_now, NULL);
359     elapsed = tv_elapsed (add_files_state.tv_start,
360                           tv_now);
361     printf ("Added %d total messages in ", add_files_state.count);
362     print_formatted_seconds (elapsed);
363     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
364
365     notmuch_database_close (notmuch);
366
367     free (mail_directory);
368     
369     return 0;
370 }
371
372 int
373 search_command (int argc, char *argv[])
374 {
375     fprintf (stderr, "Error: search is not implemented yet.\n");
376     return 1;
377 }
378
379 int
380 show_command (int argc, char *argv[])
381 {
382     fprintf (stderr, "Error: show is not implemented yet.\n");
383     return 1;
384 }
385
386 int
387 dump_command (int argc, char *argv[])
388 {
389     fprintf (stderr, "Error: dump is not implemented yet.\n");
390     return 1;
391 }
392
393 int
394 restore_command (int argc, char *argv[])
395 {
396     fprintf (stderr, "Error: restore is not implemented yet.\n");
397     return 1;
398 }
399
400 command_t commands[] = {
401     { "setup", setup_command,
402       "Interactively setup notmuch for first use.\n"
403       "\t\tInvoking notmuch with no command argument will run setup if\n"
404       "\t\tthe setup command has not previously been completed." },
405     { "search", search_command,
406       "<search-term> [...]\n\n"
407       "\t\tSearch for threads matching the given search terms.\n"
408       "\t\tOnce we actually implement search we'll document the\n"
409       "\t\tsyntax here." },
410     { "show", show_command,
411       "<thread-id>\n\n"
412       "\t\tShow the thread with the given thread ID (see 'search')." },
413     { "dump", dump_command,
414       "[<filename>]\n\n"
415       "\t\tCreate a plain-text dump of the tags for each message\n"
416       "\t\twriting to the given filename, if any, or to stdout.\n"
417       "\t\tThese tags are the only data in the notmuch database\n"
418       "\t\tthat can't be recreated from the messages themselves.\n"
419       "\t\tThe output of notmuch dump is therefore the only\n"
420       "\t\tcritical thing to backup (and much more friendly to\n"
421       "\t\tincremental backup than the native database files." },
422     { "restore", restore_command,
423       "<filename>\n\n"
424       "\t\tRestore the tags from the given dump file (see 'dump')." }
425 };
426
427 void
428 usage (void)
429 {
430     command_t *command;
431     int i;
432
433     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
434     fprintf (stderr, "\n");
435     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
436     fprintf (stderr, "\n");
437
438     for (i = 0; i < ARRAY_SIZE (commands); i++) {
439         command = &commands[i];
440
441         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
442     }
443 }
444     
445 int
446 main (int argc, char *argv[])
447 {
448     command_t *command;
449     int i;
450
451     if (argc == 1)
452         return setup_command (0, NULL);
453
454     for (i = 0; i < ARRAY_SIZE (commands); i++) {
455         command = &commands[i];
456
457         if (strcmp (argv[1], command->name) == 0)
458             return (command->function) (argc - 2, &argv[2]);
459     }
460
461     /* Don't complain about "help" being an unknown command when we're
462        about to provide exactly what's wanted anyway. */
463     if (strcmp (argv[1], "help") == 0 ||
464         strcmp (argv[1], "--help") == 0)
465     {
466         fprintf (stderr, "The notmuch mail system.\n\n");
467     } else {
468         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
469     }
470     usage ();
471     exit (1);
472
473     return 0;
474 }