]> git.notmuchmail.org Git - notmuch/blob - notmuch-insert.c
cli/insert: clean up sync_dir
[notmuch] / notmuch-insert.c
1 /* notmuch - Not much of an email program, (just index and search)
2  *
3  * Copyright © 2013 Peter Wang
4  *
5  * Based in part on notmuch-deliver
6  * Copyright © 2010 Ali Polatel
7  *
8  * This program is free software: you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation, either version 3 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program.  If not, see http://www.gnu.org/licenses/ .
20  *
21  * Author: Peter Wang <novalazy@gmail.com>
22  */
23
24 #include "notmuch-client.h"
25 #include "tag-util.h"
26
27 #include <sys/types.h>
28 #include <sys/stat.h>
29 #include <fcntl.h>
30
31 static volatile sig_atomic_t interrupted;
32
33 static void
34 handle_sigint (unused (int sig))
35 {
36     static char msg[] = "Stopping...         \n";
37
38     /* This write is "opportunistic", so it's okay to ignore the
39      * result.  It is not required for correctness, and if it does
40      * fail or produce a short write, we want to get out of the signal
41      * handler as quickly as possible, not retry it. */
42     IGNORE_RESULT (write (2, msg, sizeof (msg) - 1));
43     interrupted = 1;
44 }
45
46 /* Like gethostname but guarantees that a null-terminated hostname is
47  * returned, even if it has to make one up. Invalid characters are
48  * substituted such that the hostname can be used within a filename.
49  */
50 static void
51 safe_gethostname (char *hostname, size_t len)
52 {
53     char *p;
54
55     if (gethostname (hostname, len) == -1) {
56         strncpy (hostname, "unknown", len);
57     }
58     hostname[len - 1] = '\0';
59
60     for (p = hostname; *p != '\0'; p++) {
61         if (*p == '/' || *p == ':')
62             *p = '_';
63     }
64 }
65
66 /* Call fsync() on a directory path. */
67 static notmuch_bool_t
68 sync_dir (const char *dir)
69 {
70     int fd, r;
71
72     fd = open (dir, O_RDONLY);
73     if (fd == -1) {
74         fprintf (stderr, "Error: open %s: %s\n", dir, strerror (errno));
75         return FALSE;
76     }
77
78     r = fsync (fd);
79     if (r)
80         fprintf (stderr, "Error: fsync %s: %s\n", dir, strerror (errno));
81
82     close (fd);
83
84     return r == 0;
85 }
86
87 /*
88  * Check the specified folder name does not contain a directory
89  * component ".." to prevent writes outside of the Maildir
90  * hierarchy. Return TRUE on valid folder name, FALSE otherwise.
91  */
92 static notmuch_bool_t
93 is_valid_folder_name (const char *folder)
94 {
95     const char *p = folder;
96
97     for (;;) {
98         if ((p[0] == '.') && (p[1] == '.') && (p[2] == '\0' || p[2] == '/'))
99             return FALSE;
100         p = strchr (p, '/');
101         if (!p)
102             return TRUE;
103         p++;
104     }
105 }
106
107 /* Make the given directory, succeeding if it already exists. */
108 static notmuch_bool_t
109 make_directory (char *path, int mode)
110 {
111     notmuch_bool_t ret;
112     char *slash;
113
114     if (mkdir (path, mode) != 0)
115         return (errno == EEXIST);
116
117     /* Sync the parent directory for durability. */
118     ret = TRUE;
119     slash = strrchr (path, '/');
120     if (slash) {
121         *slash = '\0';
122         ret = sync_dir (path);
123         *slash = '/';
124     }
125     return ret;
126 }
127
128 /* Make the given directory including its parent directories as necessary.
129  * Return TRUE on success, FALSE on error. */
130 static notmuch_bool_t
131 make_directory_and_parents (char *path, int mode)
132 {
133     struct stat st;
134     char *start;
135     char *end;
136     notmuch_bool_t ret;
137
138     /* First check the common case: directory already exists. */
139     if (stat (path, &st) == 0)
140         return S_ISDIR (st.st_mode) ? TRUE : FALSE;
141
142     for (start = path; *start != '\0'; start = end + 1) {
143         /* start points to the first unprocessed character.
144          * Find the next slash from start onwards. */
145         end = strchr (start, '/');
146
147         /* If there are no more slashes then all the parent directories
148          * have been made.  Now attempt to make the whole path. */
149         if (end == NULL)
150             return make_directory (path, mode);
151
152         /* Make the path up to the next slash, unless the current
153          * directory component is actually empty. */
154         if (end > start) {
155             *end = '\0';
156             ret = make_directory (path, mode);
157             *end = '/';
158             if (! ret)
159                 return FALSE;
160         }
161     }
162
163     return TRUE;
164 }
165
166 /* Create the given maildir folder, i.e. dir and its subdirectories
167  * 'cur', 'new', 'tmp'. */
168 static notmuch_bool_t
169 maildir_create_folder (void *ctx, const char *dir)
170 {
171     const int mode = 0700;
172     char *subdir;
173     char *tail;
174
175     /* Create 'cur' directory, including parent directories. */
176     subdir = talloc_asprintf (ctx, "%s/cur", dir);
177     if (! subdir) {
178         fprintf (stderr, "Out of memory.\n");
179         return FALSE;
180     }
181     if (! make_directory_and_parents (subdir, mode))
182         return FALSE;
183
184     tail = subdir + strlen (subdir) - 3;
185
186     /* Create 'new' directory. */
187     strcpy (tail, "new");
188     if (! make_directory (subdir, mode))
189         return FALSE;
190
191     /* Create 'tmp' directory. */
192     strcpy (tail, "tmp");
193     if (! make_directory (subdir, mode))
194         return FALSE;
195
196     talloc_free (subdir);
197     return TRUE;
198 }
199
200 /* Open a unique file in the 'tmp' sub-directory of dir.
201  * Returns the file descriptor on success, or -1 on failure.
202  * On success, file paths for the message in the 'tmp' and 'new'
203  * directories are returned via tmppath and newpath,
204  * and the path of the 'new' directory itself in newdir. */
205 static int
206 maildir_open_tmp_file (void *ctx, const char *dir,
207                        char **tmppath, char **newpath, char **newdir)
208 {
209     pid_t pid;
210     char hostname[256];
211     struct timeval tv;
212     char *filename;
213     int fd = -1;
214
215     /* We follow the Dovecot file name generation algorithm. */
216     pid = getpid ();
217     safe_gethostname (hostname, sizeof (hostname));
218     do {
219         gettimeofday (&tv, NULL);
220         filename = talloc_asprintf (ctx, "%ld.M%ldP%d.%s",
221                                     tv.tv_sec, tv.tv_usec, pid, hostname);
222         if (! filename) {
223             fprintf (stderr, "Out of memory\n");
224             return -1;
225         }
226
227         *tmppath = talloc_asprintf (ctx, "%s/tmp/%s", dir, filename);
228         if (! *tmppath) {
229             fprintf (stderr, "Out of memory\n");
230             return -1;
231         }
232
233         fd = open (*tmppath, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600);
234     } while (fd == -1 && errno == EEXIST);
235
236     if (fd == -1) {
237         fprintf (stderr, "Error: opening %s: %s\n", *tmppath, strerror (errno));
238         return -1;
239     }
240
241     *newdir = talloc_asprintf (ctx, "%s/new", dir);
242     *newpath = talloc_asprintf (ctx, "%s/new/%s", dir, filename);
243     if (! *newdir || ! *newpath) {
244         fprintf (stderr, "Out of memory\n");
245         close (fd);
246         unlink (*tmppath);
247         return -1;
248     }
249
250     talloc_free (filename);
251
252     return fd;
253 }
254
255 /*
256  * Copy fdin to fdout, return TRUE on success, and FALSE on errors and
257  * empty input.
258  */
259 static notmuch_bool_t
260 copy_fd (int fdout, int fdin)
261 {
262     notmuch_bool_t empty = TRUE;
263
264     while (! interrupted) {
265         ssize_t remain;
266         char buf[4096];
267         char *p;
268
269         remain = read (fdin, buf, sizeof (buf));
270         if (remain == 0)
271             break;
272         if (remain < 0) {
273             if (errno == EINTR)
274                 continue;
275             fprintf (stderr, "Error: reading from standard input: %s\n",
276                      strerror (errno));
277             return FALSE;
278         }
279
280         p = buf;
281         do {
282             ssize_t written = write (fdout, p, remain);
283             if (written < 0 && errno == EINTR)
284                 continue;
285             if (written <= 0) {
286                 fprintf (stderr, "Error: writing to temporary file: %s",
287                          strerror (errno));
288                 return FALSE;
289             }
290             p += written;
291             remain -= written;
292             empty = FALSE;
293         } while (remain > 0);
294     }
295
296     return (!interrupted && !empty);
297 }
298
299 static notmuch_bool_t
300 write_message (void *ctx, int fdin, const char *dir, char **newpath)
301 {
302     char *tmppath;
303     char *newdir;
304     char *cleanup_path;
305     int fdout;
306
307     fdout = maildir_open_tmp_file (ctx, dir, &tmppath, newpath, &newdir);
308     if (fdout < 0)
309         return FALSE;
310
311     cleanup_path = tmppath;
312
313     if (! copy_fd (fdout, fdin))
314         goto FAIL;
315
316     if (fsync (fdout) != 0) {
317         fprintf (stderr, "Error: fsync failed: %s\n", strerror (errno));
318         goto FAIL;
319     }
320
321     close (fdout);
322     fdout = -1;
323
324     /* Atomically move the new message file from the Maildir 'tmp' directory
325      * to the 'new' directory.  We follow the Dovecot recommendation to
326      * simply use rename() instead of link() and unlink().
327      * See also: http://wiki.dovecot.org/MailboxFormat/Maildir#Mail_delivery
328      */
329     if (rename (tmppath, *newpath) != 0) {
330         fprintf (stderr, "Error: rename() failed: %s\n", strerror (errno));
331         goto FAIL;
332     }
333
334     cleanup_path = *newpath;
335
336     if (! sync_dir (newdir))
337         goto FAIL;
338
339     return TRUE;
340
341   FAIL:
342     if (fdout >= 0)
343         close (fdout);
344     unlink (cleanup_path);
345     return FALSE;
346 }
347
348 /* Add the specified message file to the notmuch database, applying tags.
349  * The file is renamed to encode notmuch tags as maildir flags. */
350 static void
351 add_file_to_database (notmuch_database_t *notmuch, const char *path,
352                       tag_op_list_t *tag_ops, notmuch_bool_t synchronize_flags)
353 {
354     notmuch_message_t *message;
355     notmuch_status_t status;
356
357     status = notmuch_database_add_message (notmuch, path, &message);
358     switch (status) {
359     case NOTMUCH_STATUS_SUCCESS:
360     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
361         break;
362     default:
363     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
364     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
365     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
366     case NOTMUCH_STATUS_OUT_OF_MEMORY:
367     case NOTMUCH_STATUS_FILE_ERROR:
368     case NOTMUCH_STATUS_NULL_POINTER:
369     case NOTMUCH_STATUS_TAG_TOO_LONG:
370     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
371     case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
372     case NOTMUCH_STATUS_LAST_STATUS:
373         fprintf (stderr, "Error: failed to add `%s' to notmuch database: %s\n",
374                  path, notmuch_status_to_string (status));
375         return;
376     }
377
378     if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
379         /* Don't change tags of an existing message. */
380         if (synchronize_flags) {
381             status = notmuch_message_tags_to_maildir_flags (message);
382             if (status != NOTMUCH_STATUS_SUCCESS)
383                 fprintf (stderr, "Error: failed to sync tags to maildir flags\n");
384         }
385     } else {
386         tag_op_flag_t flags = synchronize_flags ? TAG_FLAG_MAILDIR_SYNC : 0;
387
388         tag_op_list_apply (message, tag_ops, flags);
389     }
390
391     notmuch_message_destroy (message);
392 }
393
394 int
395 notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[])
396 {
397     notmuch_database_t *notmuch;
398     struct sigaction action;
399     const char *db_path;
400     const char **new_tags;
401     size_t new_tags_length;
402     tag_op_list_t *tag_ops;
403     char *query_string = NULL;
404     const char *folder = NULL;
405     notmuch_bool_t create_folder = FALSE;
406     notmuch_bool_t synchronize_flags;
407     const char *maildir;
408     char *newpath;
409     int opt_index;
410     unsigned int i;
411
412     notmuch_opt_desc_t options[] = {
413         { NOTMUCH_OPT_STRING, &folder, "folder", 0, 0 },
414         { NOTMUCH_OPT_BOOLEAN, &create_folder, "create-folder", 0, 0 },
415         { NOTMUCH_OPT_END, 0, 0, 0, 0 }
416     };
417
418     opt_index = parse_arguments (argc, argv, options, 1);
419     if (opt_index < 0)
420         return EXIT_FAILURE;
421
422     db_path = notmuch_config_get_database_path (config);
423     new_tags = notmuch_config_get_new_tags (config, &new_tags_length);
424     synchronize_flags = notmuch_config_get_maildir_synchronize_flags (config);
425
426     tag_ops = tag_op_list_create (config);
427     if (tag_ops == NULL) {
428         fprintf (stderr, "Out of memory.\n");
429         return EXIT_FAILURE;
430     }
431     for (i = 0; i < new_tags_length; i++) {
432         const char *error_msg;
433
434         error_msg = illegal_tag (new_tags[i], FALSE);
435         if (error_msg) {
436             fprintf (stderr, "Error: tag '%s' in new.tags: %s\n",
437                      new_tags[i],  error_msg);
438             return EXIT_FAILURE;
439         }
440
441         if (tag_op_list_append (tag_ops, new_tags[i], FALSE))
442             return EXIT_FAILURE;
443     }
444
445     if (parse_tag_command_line (config, argc - opt_index, argv + opt_index,
446                                 &query_string, tag_ops))
447         return EXIT_FAILURE;
448
449     if (*query_string != '\0') {
450         fprintf (stderr, "Error: unexpected query string: %s\n", query_string);
451         return EXIT_FAILURE;
452     }
453
454     if (folder == NULL) {
455         maildir = db_path;
456     } else {
457         if (! is_valid_folder_name (folder)) {
458             fprintf (stderr, "Error: invalid folder name: '%s'\n", folder);
459             return EXIT_FAILURE;
460         }
461         maildir = talloc_asprintf (config, "%s/%s", db_path, folder);
462         if (! maildir) {
463             fprintf (stderr, "Out of memory\n");
464             return EXIT_FAILURE;
465         }
466         if (create_folder && ! maildir_create_folder (config, maildir)) {
467             fprintf (stderr, "Error: creating maildir %s: %s\n",
468                      maildir, strerror (errno));
469             return EXIT_FAILURE;
470         }
471     }
472
473     /* Setup our handler for SIGINT. We do not set SA_RESTART so that copying
474      * from standard input may be interrupted. */
475     memset (&action, 0, sizeof (struct sigaction));
476     action.sa_handler = handle_sigint;
477     sigemptyset (&action.sa_mask);
478     action.sa_flags = 0;
479     sigaction (SIGINT, &action, NULL);
480
481     if (notmuch_database_open (notmuch_config_get_database_path (config),
482                                NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
483         return EXIT_FAILURE;
484
485     /* Write the message to the Maildir new directory. */
486     if (! write_message (config, STDIN_FILENO, maildir, &newpath)) {
487         notmuch_database_destroy (notmuch);
488         return EXIT_FAILURE;
489     }
490
491     /* Add the message to the index.
492      * Even if adding the message to the notmuch database fails,
493      * the message is on disk and we consider the delivery completed. */
494     add_file_to_database (notmuch, newpath, tag_ops,
495                                     synchronize_flags);
496
497     notmuch_database_destroy (notmuch);
498     return EXIT_SUCCESS;
499 }