25aa6c5f402a532b5558c928bdd2e40d107940da
[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>
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 messages_total;
83     int count;
84     int count_last;
85     struct timeval tv_start;
86     struct timeval tv_last;
87 } add_files_state_t;
88
89 /* Compute the number of seconds elapsed from start to end. */
90 double
91 tv_elapsed (struct timeval start, struct timeval end)
92 {
93     return ((end.tv_sec - start.tv_sec) +
94             (end.tv_usec - start.tv_usec) / 1e6);
95 }
96
97 void
98 print_formatted_seconds (double seconds)
99 {
100     int hours;
101     int minutes;
102
103     if (seconds > 3600) {
104         hours = (int) seconds / 3600;
105         printf ("%d:", hours);
106         seconds -= hours * 3600;
107     }
108
109     if (seconds > 60)
110         minutes = (int) seconds / 60;
111     else
112         minutes = 0;
113
114     printf ("%02d:", minutes);
115     seconds -= minutes * 60;
116
117     printf ("%02d", (int) seconds);
118 }
119
120 void
121 add_files_print_progress (add_files_state_t *state)
122 {
123     struct timeval tv_now;
124     double ratio_complete;
125     double elapsed_current, rate_current;
126     double elapsed_overall;
127
128     gettimeofday (&tv_now, NULL);
129
130     ratio_complete = (double) state->count / state->messages_total;
131     elapsed_current = tv_elapsed (state->tv_last, tv_now);
132     rate_current = (state->count - state->count_last) / elapsed_current;
133     elapsed_overall = tv_elapsed (state->tv_start, tv_now);
134
135     printf ("Added %d messages at %d messages/sec. ",
136             state->count, (int) rate_current);
137     print_formatted_seconds (elapsed_overall);
138     printf ("/");
139     print_formatted_seconds (elapsed_overall / ratio_complete);
140     printf (" elapsed (%.2f%%).     \r", 100 * ratio_complete);
141
142     fflush (stdout);
143
144     state->tv_last = tv_now;
145     state->count_last = state->count;
146 }
147
148 /* Recursively find all regular files in 'path' and add them to the
149  * database. */
150 void
151 add_files (notmuch_database_t *notmuch, const char *path,
152            add_files_state_t *state)
153 {
154     DIR *dir;
155     struct dirent *entry, *e;
156     int entry_length;
157     int err;
158     char *next;
159     struct stat st;
160
161     dir = opendir (path);
162
163     if (dir == NULL) {
164         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
165                  path, strerror (errno));
166         return;
167     }
168
169     entry_length = offsetof (struct dirent, d_name) +
170         pathconf (path, _PC_NAME_MAX) + 1;
171     entry = malloc (entry_length);
172
173     while (1) {
174         err = readdir_r (dir, entry, &e);
175         if (err) {
176             fprintf (stderr, "Error reading directory: %s\n",
177                      strerror (errno));
178             free (entry);
179             return;
180         }
181
182         if (e == NULL)
183             break;
184
185         /* Ignore special directories to avoid infinite recursion.
186          * Also ignore the .notmuch directory.
187          */
188         /* XXX: Eventually we'll want more sophistication to let the
189          * user specify files to be ignored. */
190         if (strcmp (entry->d_name, ".") == 0 ||
191             strcmp (entry->d_name, "..") == 0 ||
192             strcmp (entry->d_name, ".notmuch") ==0)
193         {
194             continue;
195         }
196
197         next = g_strdup_printf ("%s/%s", path, entry->d_name);
198
199         stat (next, &st);
200
201         if (S_ISREG (st.st_mode)) {
202             notmuch_database_add_message (notmuch, next);
203             state->count++;
204             if (state->count % 1000 == 0)
205                 add_files_print_progress (state);
206         } else if (S_ISDIR (st.st_mode)) {
207             add_files (notmuch, next, state);
208         }
209
210         free (next);
211     }
212
213     free (entry);
214
215     closedir (dir);
216 }
217
218 /* Recursively count all regular files in path and all sub-direcotries
219  * of path.  The result is added to *count (which should be
220  * initialized to zero by the top-level caller before calling
221  * count_files). */
222 void
223 count_files (const char *path, int *count)
224 {
225     DIR *dir;
226     struct dirent *entry, *e;
227     int entry_length;
228     int err;
229     char *next;
230     struct stat st;
231
232     dir = opendir (path);
233
234     if (dir == NULL) {
235         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
236                  path, strerror (errno));
237         return;
238     }
239
240     entry_length = offsetof (struct dirent, d_name) +
241         pathconf (path, _PC_NAME_MAX) + 1;
242     entry = malloc (entry_length);
243
244     while (1) {
245         err = readdir_r (dir, entry, &e);
246         if (err) {
247             fprintf (stderr, "Error reading directory: %s\n",
248                      strerror (errno));
249             free (entry);
250             return;
251         }
252
253         if (e == NULL)
254             break;
255
256         /* Skip these special directories to avoid infinite recursion. */
257         if (strcmp (entry->d_name, ".") == 0 ||
258             strcmp (entry->d_name, "..") == 0)
259         {
260             continue;
261         }
262
263         next = g_strdup_printf ("%s/%s", path, entry->d_name);
264
265         stat (next, &st);
266
267         if (S_ISREG (st.st_mode)) {
268             *count = *count + 1;
269             if (*count % 1000 == 0) {
270                 printf ("Found %d files so far.\r", *count);
271                 fflush (stdout);
272             }
273         } else if (S_ISDIR (st.st_mode)) {
274             count_files (next, count);
275         }
276
277         free (next);
278     }
279
280     free (entry);
281
282     closedir (dir);
283 }
284
285 int
286 setup_command (int argc, char *argv[])
287 {
288     notmuch_database_t *notmuch;
289     char *mail_directory;
290     int count;
291     add_files_state_t add_files_state;
292     double elapsed;
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             "but the only files it contains should be individual email messages.\n"
303             "Either maildir or mh format directories are fine, but you will want to\n"
304             "move away any auxiliary files maintained by other email programs.\n\n");
305
306     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
307             "messages), will not work with notmuch. If that's how your mail is currently\n"
308             "stored, we recommend you first convert it to maildir format with a utility\n"
309             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
310             "once the conversion is complete.\n\n");
311
312     printf ("Top-level mail directory [~/mail]: ");
313     fflush (stdout);
314
315     mail_directory = read_line ();
316
317     if (mail_directory == NULL || strlen (mail_directory) == 0) {
318         char *home;
319
320         if (mail_directory)
321             free (mail_directory);
322
323         home = getenv ("HOME");
324         if (!home) {
325             fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
326             fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
327             exit (1);
328         }
329
330         mail_directory = g_strdup_printf ("%s/mail", home);
331     }
332
333     notmuch = notmuch_database_create (mail_directory);
334     if (notmuch == NULL) {
335         fprintf (stderr, "Failed to create new notmuch database at %s\n",
336                  mail_directory);
337         free (mail_directory);
338         return 1;
339     }
340
341     printf ("OK. Let's take a look at the mail we can find in the directory\n");
342     printf ("%s ...\n", mail_directory);
343
344     count = 0;
345     count_files (mail_directory, &count);
346
347     printf ("Found %d total files. That's not much mail.\n\n", count);
348
349     printf ("Next, we'll inspect the messages and create a database of threads:\n");
350
351     add_files_state.messages_total = count;
352     add_files_state.count = 0;
353     add_files_state.count_last = 0;
354     gettimeofday (&add_files_state.tv_start, NULL);
355     add_files_state.tv_last = add_files_state.tv_start;
356
357     add_files (notmuch, mail_directory, &add_files_state);
358
359     gettimeofday (&add_files_state.tv_last, NULL);
360     elapsed = tv_elapsed (add_files_state.tv_start,
361                           add_files_state.tv_last);
362     printf ("Added %d total messages in ", add_files_state.count);
363     print_formatted_seconds (elapsed);
364     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
365
366     notmuch_database_close (notmuch);
367
368     free (mail_directory);
369     
370     return 0;
371 }
372
373 int
374 search_command (int argc, char *argv[])
375 {
376     fprintf (stderr, "Error: search is not implemented yet.\n");
377     return 1;
378 }
379
380 int
381 show_command (int argc, char *argv[])
382 {
383     fprintf (stderr, "Error: show-thread is not implemented yet.\n");
384     return 1;
385 }
386
387 command_t commands[] = {
388     { "setup", setup_command,
389       "Interactively setup notmuch for first use (no arguments).\n"
390       "\t\tInvoking notmuch with no command argument will run setup if\n"
391       "\t\the setup command has not previously been completed." },
392     { "search", search_command,
393       "Search for threads matching the given search terms." },
394     { "show", show_command,
395       "Show the thread with the given thread ID (see 'search')." }
396 };
397
398 void
399 usage (void)
400 {
401     command_t *command;
402     int i;
403
404     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
405     fprintf (stderr, "\n");
406     fprintf (stderr, "Where <command> is one of the following:\n");
407     fprintf (stderr, "\n");
408
409     for (i = 0; i < ARRAY_SIZE (commands); i++) {
410         command = &commands[i];
411
412         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
413     }
414 }
415     
416 int
417 main (int argc, char *argv[])
418 {
419     command_t *command;
420     int i;
421
422     if (argc == 1)
423         return setup_command (0, NULL);
424
425     for (i = 0; i < ARRAY_SIZE (commands); i++) {
426         command = &commands[i];
427
428         if (strcmp (argv[1], command->name) == 0)
429             return (command->function) (argc - 2, &argv[2]);
430     }
431
432     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
433     usage ();
434     exit (1);
435
436     return 0;
437 }