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