]> git.notmuchmail.org Git - notmuch/blob - notmuch.c
notmuch_database_create/open: Fix to handle NULL as documented.
[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     {
314         char *default_path = notmuch_database_default_path ();
315         printf ("Top-level mail directory [%s]: ", default_path);
316         free (default_path);
317         fflush (stdout);
318     }
319
320     mail_directory = read_line ();
321
322     if (mail_directory == NULL || strlen (mail_directory) == 0) {
323         if (mail_directory)
324             free (mail_directory);
325         mail_directory = notmuch_database_default_path ();
326     }
327
328     notmuch = notmuch_database_create (mail_directory);
329     if (notmuch == NULL) {
330         fprintf (stderr, "Failed to create new notmuch database at %s\n",
331                  mail_directory);
332         free (mail_directory);
333         return 1;
334     }
335
336     printf ("OK. Let's take a look at the mail we can find in the directory\n");
337     printf ("%s ...\n", mail_directory);
338
339     count = 0;
340     count_files (mail_directory, &count);
341
342     printf ("Found %d total files. That's not much mail.\n\n", count);
343
344     printf ("Next, we'll inspect the messages and create a database of threads:\n");
345
346     add_files_state.total_messages = count;
347     add_files_state.count = 0;
348     gettimeofday (&add_files_state.tv_start, NULL);
349
350     add_files (notmuch, mail_directory, &add_files_state);
351
352     gettimeofday (&tv_now, NULL);
353     elapsed = tv_elapsed (add_files_state.tv_start,
354                           tv_now);
355     printf ("Added %d total messages in ", add_files_state.count);
356     print_formatted_seconds (elapsed);
357     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
358
359     notmuch_database_close (notmuch);
360
361     free (mail_directory);
362     
363     return 0;
364 }
365
366 int
367 search_command (int argc, char *argv[])
368 {
369     fprintf (stderr, "Error: search is not implemented yet.\n");
370     return 1;
371 }
372
373 int
374 show_command (int argc, char *argv[])
375 {
376     fprintf (stderr, "Error: show is not implemented yet.\n");
377     return 1;
378 }
379
380 int
381 dump_command (int argc, char *argv[])
382 {
383     fprintf (stderr, "Error: dump is not implemented yet.\n");
384     return 1;
385 }
386
387 int
388 restore_command (int argc, char *argv[])
389 {
390     fprintf (stderr, "Error: restore is not implemented yet.\n");
391     return 1;
392 }
393
394 command_t commands[] = {
395     { "setup", setup_command,
396       "Interactively setup notmuch for first use.\n"
397       "\t\tInvoking notmuch with no command argument will run setup if\n"
398       "\t\tthe setup command has not previously been completed." },
399     { "search", search_command,
400       "<search-term> [...]\n\n"
401       "\t\tSearch for threads matching the given search terms.\n"
402       "\t\tOnce we actually implement search we'll document the\n"
403       "\t\tsyntax here." },
404     { "show", show_command,
405       "<thread-id>\n\n"
406       "\t\tShow the thread with the given thread ID (see 'search')." },
407     { "dump", dump_command,
408       "[<filename>]\n\n"
409       "\t\tCreate a plain-text dump of the tags for each message\n"
410       "\t\twriting to the given filename, if any, or to stdout.\n"
411       "\t\tThese tags are the only data in the notmuch database\n"
412       "\t\tthat can't be recreated from the messages themselves.\n"
413       "\t\tThe output of notmuch dump is therefore the only\n"
414       "\t\tcritical thing to backup (and much more friendly to\n"
415       "\t\tincremental backup than the native database files." },
416     { "restore", restore_command,
417       "<filename>\n\n"
418       "\t\tRestore the tags from the given dump file (see 'dump')." }
419 };
420
421 void
422 usage (void)
423 {
424     command_t *command;
425     int i;
426
427     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
428     fprintf (stderr, "\n");
429     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
430     fprintf (stderr, "\n");
431
432     for (i = 0; i < ARRAY_SIZE (commands); i++) {
433         command = &commands[i];
434
435         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
436     }
437 }
438     
439 int
440 main (int argc, char *argv[])
441 {
442     command_t *command;
443     int i;
444
445     if (argc == 1)
446         return setup_command (0, NULL);
447
448     for (i = 0; i < ARRAY_SIZE (commands); i++) {
449         command = &commands[i];
450
451         if (strcmp (argv[1], command->name) == 0)
452             return (command->function) (argc - 2, &argv[2]);
453     }
454
455     /* Don't complain about "help" being an unknown command when we're
456        about to provide exactly what's wanted anyway. */
457     if (strcmp (argv[1], "help") == 0 ||
458         strcmp (argv[1], "--help") == 0)
459     {
460         fprintf (stderr, "The notmuch mail system.\n\n");
461     } else {
462         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
463     }
464     usage ();
465     exit (1);
466
467     return 0;
468 }