notmuch setup: Break the implementation up into manageable functions.
[notmuch] / notmuch-setup.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-client.h"
22
23 static notmuch_status_t
24 add_all_files (notmuch_database_t *notmuch,
25                const char *mail_directory,
26                int num_files)
27 {
28     add_files_state_t add_files_state;
29     double elapsed;
30     struct timeval tv_now;
31     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
32
33     add_files_state.ignore_read_only_directories = FALSE;
34     add_files_state.saw_read_only_directory = FALSE;
35     add_files_state.total_files = num_files;
36     add_files_state.processed_files = 0;
37     add_files_state.added_messages = 0;
38     add_files_state.callback = NULL;
39     gettimeofday (&add_files_state.tv_start, NULL);
40
41     ret = add_files (notmuch, mail_directory, &add_files_state);
42
43     gettimeofday (&tv_now, NULL);
44     elapsed = notmuch_time_elapsed (add_files_state.tv_start,
45                                     tv_now);
46     printf ("Processed %d %s in ", add_files_state.processed_files,
47             add_files_state.processed_files == 1 ?
48             "file" : "total files");
49     notmuch_time_print_formatted_seconds (elapsed);
50     if (elapsed > 1) {
51         printf (" (%d files/sec.).                 \n",
52                 (int) (add_files_state.processed_files / elapsed));
53     } else {
54         printf (".                    \n");
55     }
56     if (add_files_state.added_messages) {
57         printf ("Added %d %s to the database.\n\n",
58                 add_files_state.added_messages,
59                 add_files_state.added_messages == 1 ?
60                 "message" : "unique messages");
61     }
62
63     return ret;
64 }
65
66
67 /* XXX: This should be merged with the existing add_files function in
68  * add-files.c. */
69 /* Recursively count all regular files in path and all sub-direcotries
70  * of path.  The result is added to *count (which should be
71  * initialized to zero by the top-level caller before calling
72  * count_files). */
73 static void
74 count_files (const char *path, int *count)
75 {
76     DIR *dir;
77     struct dirent *e, *entry = NULL;
78     int entry_length;
79     int err;
80     char *next;
81     struct stat st;
82
83     dir = opendir (path);
84
85     if (dir == NULL) {
86         fprintf (stderr, "Warning: failed to open directory %s: %s\n",
87                  path, strerror (errno));
88         goto DONE;
89     }
90
91     entry_length = offsetof (struct dirent, d_name) +
92         pathconf (path, _PC_NAME_MAX) + 1;
93     entry = malloc (entry_length);
94
95     while (1) {
96         err = readdir_r (dir, entry, &e);
97         if (err) {
98             fprintf (stderr, "Error reading directory: %s\n",
99                      strerror (errno));
100             free (entry);
101             goto DONE;
102         }
103
104         if (e == NULL)
105             break;
106
107         /* Ignore special directories to avoid infinite recursion.
108          * Also ignore the .notmuch directory.
109          */
110         /* XXX: Eventually we'll want more sophistication to let the
111          * user specify files to be ignored. */
112         if (strcmp (entry->d_name, ".") == 0 ||
113             strcmp (entry->d_name, "..") == 0 ||
114             strcmp (entry->d_name, ".notmuch") == 0)
115         {
116             continue;
117         }
118
119         if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
120             next = NULL;
121             fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
122                      path, entry->d_name);
123             continue;
124         }
125
126         stat (next, &st);
127
128         if (S_ISREG (st.st_mode)) {
129             *count = *count + 1;
130             if (*count % 1000 == 0) {
131                 printf ("Found %d files so far.\r", *count);
132                 fflush (stdout);
133             }
134         } else if (S_ISDIR (st.st_mode)) {
135             count_files (next, count);
136         }
137
138         free (next);
139     }
140
141   DONE:
142     if (entry)
143         free (entry);
144
145     closedir (dir);
146 }
147
148 static void
149 welcome_message (void)
150 {
151     printf (
152 "Welcome to notmuch!\n\n"
153 "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 "Notmuch needs to know the top-level directory of your email archive,\n"
156 "(where you already have mail stored and where messages will be delivered\n"
157 "in the future). This directory can contain any number of sub-directories\n"
158 "and primarily just files with indvidual email messages (eg. maildir or mh\n"
159 "archives are perfect). If there are other, non-email files (such as\n"
160 "indexes maintained by other email programs) then notmuch will do its\n"
161 "best to detect those and ignore them.\n\n"
162 "Mail storage that uses mbox format, (where one mbox file contains many\n"
163 "messages), will not work with notmuch. If that's how your mail is currently\n"
164 "stored, we recommend you first convert it to maildir format with a utility\n"
165 "such as mb2md. In that case, press Control-C now and run notmuch again\n"
166 "once the conversion is complete.\n\n");
167 }
168
169 static char *
170 prompt_user_for_mail_directory ()
171 {
172     char *default_path, *mail_directory = NULL;
173     size_t line_size;
174
175     default_path = notmuch_database_default_path ();
176     printf ("Top-level mail directory [%s]: ", default_path);
177     fflush (stdout);
178
179     getline (&mail_directory, &line_size, stdin);
180     chomp_newline (mail_directory);
181
182     printf ("\n");
183
184     if (mail_directory == NULL || strlen (mail_directory) == 0) {
185         if (mail_directory)
186             free (mail_directory);
187         mail_directory = default_path;
188     } else {
189         /* XXX: Instead of telling the user to use an environment
190          * variable here, we should really be writing out a configuration
191          * file and loading that on the next run. */
192         if (strcmp (mail_directory, default_path)) {
193             printf ("Note: Since you are not using the default path, you will want to set\n"
194                     "the NOTMUCH_BASE environment variable to %s so that\n"
195                     "future calls to notmuch commands will know where to find your mail.\n",
196                     mail_directory);
197             printf ("For example, if you are using bash for your shell, add:\n\n");
198             printf ("\texport NOTMUCH_BASE=%s\n\n", mail_directory);
199             printf ("to your ~/.bashrc file.\n\n");
200         }
201         free (default_path);
202     }
203
204     /* Coerce the directory into an absolute directory name. */
205     if (*mail_directory != '/') {
206         char *cwd, *absolute_mail_directory;
207
208         cwd = getcwd (NULL, 0);
209         if (cwd == NULL) {
210             fprintf (stderr, "Out of memory.\n");
211             exit (1);
212         }
213
214         if (asprintf (&absolute_mail_directory, "%s/%s",
215                       cwd, mail_directory) < 0)
216         {
217             fprintf (stderr, "Out of memory.\n");
218             exit (1);
219         }
220
221         free (cwd);
222         free (mail_directory);
223         mail_directory = absolute_mail_directory;
224     }
225
226     return mail_directory;
227 }
228
229 int
230 notmuch_setup_command (unused (void *ctx),
231                        unused (int argc), unused (char *argv[]))
232 {
233     notmuch_database_t *notmuch = NULL;
234     char *mail_directory = NULL;
235     int count;
236     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
237
238     welcome_message ();
239
240     mail_directory = prompt_user_for_mail_directory ();
241
242     notmuch = notmuch_database_create (mail_directory);
243     if (notmuch == NULL) {
244         fprintf (stderr, "Failed to create new notmuch database at %s\n",
245                  mail_directory);
246         ret = NOTMUCH_STATUS_FILE_ERROR;
247         goto DONE;
248     }
249
250     printf ("OK. Let's take a look at the mail we can find in the directory\n");
251     printf ("%s ...\n", mail_directory);
252
253     count = 0;
254     count_files (mail_directory, &count);
255
256     printf ("Found %d total files. That's not much mail.\n\n", count);
257
258     printf ("Next, we'll inspect the messages and create a database of threads:\n");
259
260     ret = add_all_files (notmuch, mail_directory, count);
261
262     printf ("When new mail is delivered to %s in the future,\n"
263             "run \"notmuch new\" to add it to the database.\n\n",
264             mail_directory);
265
266     if (ret) {
267         printf ("Note: At least one error was encountered: %s\n",
268                 notmuch_status_to_string (ret));
269     }
270
271   DONE:
272     if (mail_directory)
273         free (mail_directory);
274     if (notmuch)
275         notmuch_database_close (notmuch);
276
277     return ret;
278 }