notmuch: Use GNU libc getline() instead of glib GIOChannel
[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 == NULL || strlen (mail_directory) == 0) {
293         if (mail_directory)
294             free (mail_directory);
295         mail_directory = default_path;
296     } else {
297         if (mail_directory[strlen(mail_directory)-1] == '\n')
298             mail_directory[strlen(mail_directory)-1] = '\0';
299         /* XXX: Instead of telling the user to use an environment
300          * variable here, we should really be writing out a configuration
301          * file and loading that on the next run. */
302         if (strcmp (mail_directory, default_path)) {
303             printf ("Note: Since you are not using the default path, you will want to set\n"
304                     "the NOTMUCH_BASE environment variable to %s so that\n"
305                     "future calls to notmuch commands will know where to find your mail.\n",
306                     mail_directory);
307             printf ("For example, if you are using bash for your shell, add:\n\n");
308             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
309             printf ("to your ~/.bashrc file.\n\n");
310         }
311         free (default_path);
312     }
313
314     notmuch = notmuch_database_create (mail_directory);
315     if (notmuch == NULL) {
316         fprintf (stderr, "Failed to create new notmuch database at %s\n",
317                  mail_directory);
318         free (mail_directory);
319         return 1;
320     }
321
322     printf ("OK. Let's take a look at the mail we can find in the directory\n");
323     printf ("%s ...\n", mail_directory);
324
325     count = 0;
326     count_files (mail_directory, &count);
327
328     printf ("Found %d total files. That's not much mail.\n\n", count);
329
330     printf ("Next, we'll inspect the messages and create a database of threads:\n");
331
332     add_files_state.total_messages = count;
333     add_files_state.count = 0;
334     gettimeofday (&add_files_state.tv_start, NULL);
335
336     add_files (notmuch, mail_directory, &add_files_state);
337
338     gettimeofday (&tv_now, NULL);
339     elapsed = tv_elapsed (add_files_state.tv_start,
340                           tv_now);
341     printf ("Added %d total messages in ", add_files_state.count);
342     print_formatted_seconds (elapsed);
343     printf (" (%d messages/sec.).                 \n", (int) (add_files_state.count / elapsed));
344
345     notmuch_database_close (notmuch);
346
347     free (mail_directory);
348     
349     return 0;
350 }
351
352 int
353 search_command (int argc, char *argv[])
354 {
355     fprintf (stderr, "Error: search is not implemented yet.\n");
356     return 1;
357 }
358
359 int
360 show_command (int argc, char *argv[])
361 {
362     fprintf (stderr, "Error: show is not implemented yet.\n");
363     return 1;
364 }
365
366 int
367 dump_command (int argc, char *argv[])
368 {
369     fprintf (stderr, "Error: dump is not implemented yet.\n");
370     return 1;
371 }
372
373 int
374 restore_command (int argc, char *argv[])
375 {
376     fprintf (stderr, "Error: restore is not implemented yet.\n");
377     return 1;
378 }
379
380 command_t commands[] = {
381     { "setup", setup_command,
382       "Interactively setup notmuch for first use.\n"
383       "\t\tInvoking notmuch with no command argument will run setup if\n"
384       "\t\tthe setup command has not previously been completed." },
385     { "search", search_command,
386       "<search-term> [...]\n\n"
387       "\t\tSearch for threads matching the given search terms.\n"
388       "\t\tOnce we actually implement search we'll document the\n"
389       "\t\tsyntax here." },
390     { "show", show_command,
391       "<thread-id>\n\n"
392       "\t\tShow the thread with the given thread ID (see 'search')." },
393     { "dump", dump_command,
394       "[<filename>]\n\n"
395       "\t\tCreate a plain-text dump of the tags for each message\n"
396       "\t\twriting to the given filename, if any, or to stdout.\n"
397       "\t\tThese tags are the only data in the notmuch database\n"
398       "\t\tthat can't be recreated from the messages themselves.\n"
399       "\t\tThe output of notmuch dump is therefore the only\n"
400       "\t\tcritical thing to backup (and much more friendly to\n"
401       "\t\tincremental backup than the native database files." },
402     { "restore", restore_command,
403       "<filename>\n\n"
404       "\t\tRestore the tags from the given dump file (see 'dump')." }
405 };
406
407 void
408 usage (void)
409 {
410     command_t *command;
411     int i;
412
413     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
414     fprintf (stderr, "\n");
415     fprintf (stderr, "Where <command> and [args...] are as follows:\n");
416     fprintf (stderr, "\n");
417
418     for (i = 0; i < ARRAY_SIZE (commands); i++) {
419         command = &commands[i];
420
421         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
422     }
423 }
424     
425 int
426 main (int argc, char *argv[])
427 {
428     command_t *command;
429     int i;
430
431     if (argc == 1)
432         return setup_command (0, NULL);
433
434     for (i = 0; i < ARRAY_SIZE (commands); i++) {
435         command = &commands[i];
436
437         if (strcmp (argv[1], command->name) == 0)
438             return (command->function) (argc - 2, &argv[2]);
439     }
440
441     /* Don't complain about "help" being an unknown command when we're
442        about to provide exactly what's wanted anyway. */
443     if (strcmp (argv[1], "help") == 0 ||
444         strcmp (argv[1], "--help") == 0)
445     {
446         fprintf (stderr, "The notmuch mail system.\n\n");
447     } else {
448         fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
449     }
450     usage ();
451     exit (1);
452
453     return 0;
454 }