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