notmuch: Ignore .notmuch when counting files.
[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         /* Ignore special directories to avoid infinite recursion.
257          * Also ignore the .notmuch directory.
258          */
259         /* XXX: Eventually we'll want more sophistication to let the
260          * user specify files to be ignored. */
261         if (strcmp (entry->d_name, ".") == 0 ||
262             strcmp (entry->d_name, "..") == 0 ||
263             strcmp (entry->d_name, ".notmuch") == 0)
264         {
265             continue;
266         }
267
268         next = g_strdup_printf ("%s/%s", path, entry->d_name);
269
270         stat (next, &st);
271
272         if (S_ISREG (st.st_mode)) {
273             *count = *count + 1;
274             if (*count % 1000 == 0) {
275                 printf ("Found %d files so far.\r", *count);
276                 fflush (stdout);
277             }
278         } else if (S_ISDIR (st.st_mode)) {
279             count_files (next, count);
280         }
281
282         free (next);
283     }
284
285     free (entry);
286
287     closedir (dir);
288 }
289
290 int
291 setup_command (int argc, char *argv[])
292 {
293     notmuch_database_t *notmuch;
294     char *mail_directory;
295     int count;
296     add_files_state_t add_files_state;
297     double elapsed;
298
299     printf ("Welcome to notmuch!\n\n");
300
301     printf ("The goal of notmuch is to help you manage and search your collection of\n"
302             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
303
304     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
305             "(where you already have mail stored and where messages will be delivered\n"
306             "in the future). This directory can contain any number of sub-directories\n"
307             "but the only files it contains should be individual email messages.\n"
308             "Either maildir or mh format directories are fine, but you will want to\n"
309             "move away any auxiliary files maintained by other email programs.\n\n");
310
311     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
312             "messages), will not work with notmuch. If that's how your mail is currently\n"
313             "stored, we recommend you first convert it to maildir format with a utility\n"
314             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
315             "once the conversion is complete.\n\n");
316
317     printf ("Top-level mail directory [~/mail]: ");
318     fflush (stdout);
319
320     mail_directory = read_line ();
321
322     if (mail_directory == NULL || strlen (mail_directory) == 0) {
323         char *home;
324
325         if (mail_directory)
326             free (mail_directory);
327
328         home = getenv ("HOME");
329         if (!home) {
330             fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
331             fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
332             exit (1);
333         }
334
335         mail_directory = g_strdup_printf ("%s/mail", home);
336     }
337
338     notmuch = notmuch_database_create (mail_directory);
339     if (notmuch == NULL) {
340         fprintf (stderr, "Failed to create new notmuch database at %s\n",
341                  mail_directory);
342         free (mail_directory);
343         return 1;
344     }
345
346     printf ("OK. Let's take a look at the mail we can find in the directory\n");
347     printf ("%s ...\n", mail_directory);
348
349     count = 0;
350     count_files (mail_directory, &count);
351
352     printf ("Found %d total files. That's not much mail.\n\n", count);
353
354     printf ("Next, we'll inspect the messages and create a database of threads:\n");
355
356     add_files_state.messages_total = count;
357     add_files_state.count = 0;
358     add_files_state.count_last = 0;
359     gettimeofday (&add_files_state.tv_start, NULL);
360     add_files_state.tv_last = add_files_state.tv_start;
361
362     add_files (notmuch, mail_directory, &add_files_state);
363
364     gettimeofday (&add_files_state.tv_last, NULL);
365     elapsed = tv_elapsed (add_files_state.tv_start,
366                           add_files_state.tv_last);
367     printf ("Added %d total messages in ", add_files_state.count);
368     print_formatted_seconds (elapsed);
369     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
370
371     notmuch_database_close (notmuch);
372
373     free (mail_directory);
374     
375     return 0;
376 }
377
378 int
379 search_command (int argc, char *argv[])
380 {
381     fprintf (stderr, "Error: search is not implemented yet.\n");
382     return 1;
383 }
384
385 int
386 show_command (int argc, char *argv[])
387 {
388     fprintf (stderr, "Error: show-thread is not implemented yet.\n");
389     return 1;
390 }
391
392 command_t commands[] = {
393     { "setup", setup_command,
394       "Interactively setup notmuch for first use (no arguments).\n"
395       "\t\tInvoking notmuch with no command argument will run setup if\n"
396       "\t\the setup command has not previously been completed." },
397     { "search", search_command,
398       "Search for threads matching the given search terms." },
399     { "show", show_command,
400       "Show the thread with the given thread ID (see 'search')." }
401 };
402
403 void
404 usage (void)
405 {
406     command_t *command;
407     int i;
408
409     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
410     fprintf (stderr, "\n");
411     fprintf (stderr, "Where <command> is one of the following:\n");
412     fprintf (stderr, "\n");
413
414     for (i = 0; i < ARRAY_SIZE (commands); i++) {
415         command = &commands[i];
416
417         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
418     }
419 }
420     
421 int
422 main (int argc, char *argv[])
423 {
424     command_t *command;
425     int i;
426
427     if (argc == 1)
428         return setup_command (0, NULL);
429
430     for (i = 0; i < ARRAY_SIZE (commands); i++) {
431         command = &commands[i];
432
433         if (strcmp (argv[1], command->name) == 0)
434             return (command->function) (argc - 2, &argv[2]);
435     }
436
437     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
438     usage ();
439     exit (1);
440
441     return 0;
442 }