Start a new top-level executable: notmuch.
[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 <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <sys/types.h>
25 #include <sys/stat.h>
26 #include <unistd.h>
27 #include <dirent.h>
28 #include <errno.h>
29
30 #include <glib.h>
31
32 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
33
34 typedef int (*command_function_t) (int argc, char *argv[]);
35
36 typedef struct command {
37     const char *name;
38     command_function_t function;
39     const char *usage;
40 } command_t;
41
42 /* Read a line from stdin, without any line-terminator character.  The
43  * return value is a newly allocated string. The caller should free()
44  * the string when finished with it.
45  *
46  * This function returns NULL if EOF is encountered before any
47  * characters are input (otherwise it returns those characters).
48  */
49 char *
50 read_line (void)
51 {
52     char *result = NULL;
53     GError *error = NULL;
54     GIOStatus g_io_status;
55     gsize length;
56
57     GIOChannel *channel = g_io_channel_unix_new (fileno (stdin));
58
59     g_io_status = g_io_channel_read_line (channel, &result,
60                                           &length, NULL, &error);
61
62     if (g_io_status == EOF)
63         goto DONE;
64
65     if (g_io_status != G_IO_STATUS_NORMAL) {
66         fprintf(stderr, "Read error: %s\n", error->message);
67         exit (1);
68     }
69
70     if (length && result[length - 1] == '\n')
71         result[length - 1] = '\0';
72
73   DONE:
74     g_io_channel_unref (channel);
75     return result;
76 }
77
78 /* Recursively count all regular files in path and all sub-direcotries
79  * of path.  The result is added to *count (which should be
80  * initialized to zero by the top-level caller before calling
81  * count_files). */
82 void
83 count_files (const char *path, int *count)
84 {
85     DIR *dir;
86     struct dirent *entry, *e;
87     int entry_length;
88     int err;
89     char *next;
90     struct stat st;
91
92     dir = opendir (path);
93
94     if (dir == NULL) {
95         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
96                  path, strerror (errno));
97         return;
98     }
99
100     entry_length = offsetof (struct dirent, d_name) +
101         pathconf (path, _PC_NAME_MAX) + 1;
102     entry = malloc (entry_length);
103
104     while (1) {
105         err = readdir_r (dir, entry, &e);
106         if (err) {
107             fprintf (stderr, "Error reading directory: %s\n",
108                      strerror (errno));
109             free (entry);
110             return;
111         }
112
113         if (e == NULL)
114             break;
115
116         /* Skip these special directories to avoid infinite recursion. */
117         if (strcmp (entry->d_name, ".") == 0 ||
118             strcmp (entry->d_name, "..") == 0)
119         {
120             continue;
121         }
122
123         next = g_strdup_printf ("%s/%s", path, entry->d_name);
124
125         stat (next, &st);
126
127         if (S_ISREG (st.st_mode))
128             *count = *count + 1;
129         else if (S_ISDIR (st.st_mode))
130             count_files (next, count);
131
132         if (*count % 1000 == 0) {
133             printf ("Found %d files so far.\r", *count);
134             fflush (stdout);
135         }
136
137         free (next);
138     }
139
140     free (entry);
141
142     closedir (dir);
143 }
144
145 int
146 setup_command (int argc, char *argv[])
147 {
148     char *mail_directory;
149     int count;
150
151     printf ("Welcome to notmuch!\n\n");
152
153     printf ("The goal of notmuch is to help you manage and search your collection of\n"
154             "email, and to efficiently keep up with the flow of email as it comes in.\n\n");
155
156     printf ("Notmuch needs to know the top-level directory of your email archive,\n"
157             "(where you already have mail stored and where messages will be delivered\n"
158             "in the future). This directory can contain any number of sub-directories\n"
159             "but the only files it contains should be individual email messages.\n"
160             "Either maildir or mh format directories are fine, but you will want to\n"
161             "move away any auxiliary files maintained by other email programs.\n\n");
162
163     printf ("Mail storage that uses mbox format, (where one mbox file contains many\n"
164             "messages), will not work with notmuch. If that's how your mail is currently\n"
165             "stored, we recommend you first convert it to maildir format with a utility\n"
166             "such as mb2md. In that case, press Control-C now and run notmuch again\n"
167             "once the conversion is complete.\n\n");
168
169     printf ("Top-level mail directory [~/mail]: ");
170     fflush (stdout);
171
172     mail_directory = read_line ();
173
174     if (mail_directory == NULL || strlen (mail_directory) == 0) {
175         char *home;
176
177         if (mail_directory)
178             free (mail_directory);
179
180         home = getenv ("HOME");
181         if (!home) {
182             fprintf (stderr, "Error: No mail directory provided HOME environment variable is not set.\n");
183             fprintf (stderr, "Cowardly refusing to just guess where your mail might be.\n");
184             exit (1);
185         }
186
187         mail_directory = g_strdup_printf ("%s/mail", home);
188     }
189
190     printf ("OK. Let's take a look at the mail we can find in the directory\n");
191     printf ("%s ...\n", mail_directory);
192
193     count = 0;
194     count_files (mail_directory, &count);
195
196     printf ("Found %d total files. That's not much mail.\n", count);
197
198     free (mail_directory);
199     
200     return 0;
201 }
202
203 int
204 search_command (int argc, char *argv[])
205 {
206     fprintf (stderr, "Error: search is not implemented yet.\n");
207     return 1;
208 }
209
210 int
211 show_command (int argc, char *argv[])
212 {
213     fprintf (stderr, "Error: show-thread is not implemented yet.\n");
214     return 1;
215 }
216
217 command_t commands[] = {
218     { "setup", setup_command,
219       "Interactively setup notmuch for first use (no arguments).\n"
220       "\t\tInvoking notmuch with no command argument will run setup if\n"
221       "\t\the setup command has not previously been completed." },
222     { "search", search_command,
223       "Search for threads matching the given search terms." },
224     { "show", show_command,
225       "Show the thread with the given thread ID (see 'search')." }
226 };
227
228 void
229 usage (void)
230 {
231     command_t *command;
232     int i;
233
234     fprintf (stderr, "Usage: notmuch <command> [args...]\n");
235     fprintf (stderr, "\n");
236     fprintf (stderr, "Where <command> is one of the following:\n");
237     fprintf (stderr, "\n");
238
239     for (i = 0; i < ARRAY_SIZE (commands); i++) {
240         command = &commands[i];
241
242         fprintf (stderr, "\t%s\t%s\n\n", command->name, command->usage);
243     }
244 }
245     
246 int
247 main (int argc, char *argv[])
248 {
249     command_t *command;
250     int i;
251
252     if (argc == 1)
253         return setup_command (0, NULL);
254
255     for (i = 0; i < ARRAY_SIZE (commands); i++) {
256         command = &commands[i];
257
258         if (strcmp (argv[1], command->name) == 0)
259             return (command->function) (argc - 2, &argv[2]);
260     }
261
262     fprintf (stderr, "Error: Unknown command '%s'\n\n", argv[1]);
263     usage ();
264     exit (1);
265
266     return 0;
267 }