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