notmuch: Ignore files that don't look like email messages.
[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-thread is not implemented yet.\n");
383     return 1;
384 }
385
386 command_t commands[] = {
387     { "setup", setup_command,
388       "Interactively setup notmuch for first use (no arguments).\n"
389       "\t\tInvoking notmuch with no command argument will run setup if\n"
390       "\t\the setup command has not previously been completed." },
391     { "search", search_command,
392       "Search for threads matching the given search terms." },
393     { "show", show_command,
394       "Show the thread with the given thread ID (see 'search')." }
395 };
396
397 void
398 usage (void)
399 {
400     command_t *command;
401     int i;
402
403     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
404     fprintf (stderr, "\n");
405     fprintf (stderr, "Where <command> is one of the following:\n");
406     fprintf (stderr, "\n");
407
408     for (i = 0; i < ARRAY_SIZE (commands); i++) {
409         command = &commands[i];
410
411         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
412     }
413 }
414     
415 int
416 main (int argc, char *argv[])
417 {
418     command_t *command;
419     int i;
420
421     if (argc == 1)
422         return setup_command (0, NULL);
423
424     for (i = 0; i < ARRAY_SIZE (commands); i++) {
425         command = &commands[i];
426
427         if (strcmp (argv[1], command->name) == 0)
428             return (command->function) (argc - 2, &argv[2]);
429     }
430
431     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
432     usage ();
433     exit (1);
434
435     return 0;
436 }