]> git.notmuchmail.org Git - notmuch/blob - notmuch-insert.c
cli: add insert command
[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     notmuch_bool_t ret;
71     int fd;
72
73     fd = open (dir, O_RDONLY);
74     if (fd == -1) {
75         fprintf (stderr, "Error: open() dir failed: %s\n", strerror (errno));
76         return FALSE;
77     }
78     ret = (fsync (fd) == 0);
79     if (! ret) {
80         fprintf (stderr, "Error: fsync() dir failed: %s\n", strerror (errno));
81     }
82     close (fd);
83     return ret;
84 }
85
86 /* Open a unique file in the 'tmp' sub-directory of dir.
87  * Returns the file descriptor on success, or -1 on failure.
88  * On success, file paths for the message in the 'tmp' and 'new'
89  * directories are returned via tmppath and newpath,
90  * and the path of the 'new' directory itself in newdir. */
91 static int
92 maildir_open_tmp_file (void *ctx, const char *dir,
93                        char **tmppath, char **newpath, char **newdir)
94 {
95     pid_t pid;
96     char hostname[256];
97     struct timeval tv;
98     char *filename;
99     int fd = -1;
100
101     /* We follow the Dovecot file name generation algorithm. */
102     pid = getpid ();
103     safe_gethostname (hostname, sizeof (hostname));
104     do {
105         gettimeofday (&tv, NULL);
106         filename = talloc_asprintf (ctx, "%ld.M%ldP%d.%s",
107                                     tv.tv_sec, tv.tv_usec, pid, hostname);
108         if (! filename) {
109             fprintf (stderr, "Out of memory\n");
110             return -1;
111         }
112
113         *tmppath = talloc_asprintf (ctx, "%s/tmp/%s", dir, filename);
114         if (! *tmppath) {
115             fprintf (stderr, "Out of memory\n");
116             return -1;
117         }
118
119         fd = open (*tmppath, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0600);
120     } while (fd == -1 && errno == EEXIST);
121
122     if (fd == -1) {
123         fprintf (stderr, "Error: opening %s: %s\n", *tmppath, strerror (errno));
124         return -1;
125     }
126
127     *newdir = talloc_asprintf (ctx, "%s/new", dir);
128     *newpath = talloc_asprintf (ctx, "%s/new/%s", dir, filename);
129     if (! *newdir || ! *newpath) {
130         fprintf (stderr, "Out of memory\n");
131         close (fd);
132         unlink (*tmppath);
133         return -1;
134     }
135
136     talloc_free (filename);
137
138     return fd;
139 }
140
141 /* Copy the contents of standard input (fdin) into fdout.
142  * Returns TRUE if a non-empty file was written successfully.
143  * Otherwise, return FALSE. */
144 static notmuch_bool_t
145 copy_stdin (int fdin, int fdout)
146 {
147     notmuch_bool_t empty = TRUE;
148
149     while (! interrupted) {
150         ssize_t remain;
151         char buf[4096];
152         char *p;
153
154         remain = read (fdin, buf, sizeof (buf));
155         if (remain == 0)
156             break;
157         if (remain < 0) {
158             if (errno == EINTR)
159                 continue;
160             fprintf (stderr, "Error: reading from standard input: %s\n",
161                      strerror (errno));
162             return FALSE;
163         }
164
165         p = buf;
166         do {
167             ssize_t written = write (fdout, p, remain);
168             if (written < 0 && errno == EINTR)
169                 continue;
170             if (written <= 0) {
171                 fprintf (stderr, "Error: writing to temporary file: %s",
172                          strerror (errno));
173                 return FALSE;
174             }
175             p += written;
176             remain -= written;
177             empty = FALSE;
178         } while (remain > 0);
179     }
180
181     return (!interrupted && !empty);
182 }
183
184 /* Add the specified message file to the notmuch database, applying tags.
185  * The file is renamed to encode notmuch tags as maildir flags. */
186 static void
187 add_file_to_database (notmuch_database_t *notmuch, const char *path,
188                       tag_op_list_t *tag_ops)
189 {
190     notmuch_message_t *message;
191     notmuch_status_t status;
192
193     status = notmuch_database_add_message (notmuch, path, &message);
194     switch (status) {
195     case NOTMUCH_STATUS_SUCCESS:
196     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
197         break;
198     default:
199     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
200     case NOTMUCH_STATUS_READ_ONLY_DATABASE:
201     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
202     case NOTMUCH_STATUS_OUT_OF_MEMORY:
203     case NOTMUCH_STATUS_FILE_ERROR:
204     case NOTMUCH_STATUS_NULL_POINTER:
205     case NOTMUCH_STATUS_TAG_TOO_LONG:
206     case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
207     case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
208     case NOTMUCH_STATUS_LAST_STATUS:
209         fprintf (stderr, "Error: failed to add `%s' to notmuch database: %s\n",
210                  path, notmuch_status_to_string (status));
211         return;
212     }
213
214     if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
215         /* Don't change tags of an existing message. */
216         status = notmuch_message_tags_to_maildir_flags (message);
217         if (status != NOTMUCH_STATUS_SUCCESS)
218             fprintf (stderr, "Error: failed to sync tags to maildir flags\n");
219     } else {
220         tag_op_list_apply (message, tag_ops, TAG_FLAG_MAILDIR_SYNC);
221     }
222
223     notmuch_message_destroy (message);
224 }
225
226 static notmuch_bool_t
227 insert_message (void *ctx, notmuch_database_t *notmuch, int fdin,
228                 const char *dir, tag_op_list_t *tag_ops)
229 {
230     char *tmppath;
231     char *newpath;
232     char *newdir;
233     int fdout;
234     char *cleanup_path;
235
236     fdout = maildir_open_tmp_file (ctx, dir, &tmppath, &newpath, &newdir);
237     if (fdout < 0)
238         return FALSE;
239
240     cleanup_path = tmppath;
241
242     if (! copy_stdin (fdin, fdout))
243         goto FAIL;
244
245     if (fsync (fdout) != 0) {
246         fprintf (stderr, "Error: fsync failed: %s\n", strerror (errno));
247         goto FAIL;
248     }
249
250     close (fdout);
251     fdout = -1;
252
253     /* Atomically move the new message file from the Maildir 'tmp' directory
254      * to the 'new' directory.  We follow the Dovecot recommendation to
255      * simply use rename() instead of link() and unlink().
256      * See also: http://wiki.dovecot.org/MailboxFormat/Maildir#Mail_delivery
257      */
258     if (rename (tmppath, newpath) != 0) {
259         fprintf (stderr, "Error: rename() failed: %s\n", strerror (errno));
260         goto FAIL;
261     }
262
263     cleanup_path = newpath;
264
265     if (! sync_dir (newdir))
266         goto FAIL;
267
268     /* Even if adding the message to the notmuch database fails,
269      * the message is on disk and we consider the delivery completed. */
270     add_file_to_database (notmuch, newpath, tag_ops);
271
272     return TRUE;
273
274   FAIL:
275     if (fdout >= 0)
276         close (fdout);
277     unlink (cleanup_path);
278     return FALSE;
279 }
280
281 int
282 notmuch_insert_command (notmuch_config_t *config, int argc, char *argv[])
283 {
284     notmuch_database_t *notmuch;
285     struct sigaction action;
286     const char *db_path;
287     const char **new_tags;
288     size_t new_tags_length;
289     tag_op_list_t *tag_ops;
290     char *query_string = NULL;
291     const char *maildir;
292     int opt_index = 1;
293     unsigned int i;
294     notmuch_bool_t ret;
295
296     db_path = notmuch_config_get_database_path (config);
297     new_tags = notmuch_config_get_new_tags (config, &new_tags_length);
298
299     tag_ops = tag_op_list_create (config);
300     if (tag_ops == NULL) {
301         fprintf (stderr, "Out of memory.\n");
302         return 1;
303     }
304     for (i = 0; i < new_tags_length; i++) {
305         if (tag_op_list_append (tag_ops, new_tags[i], FALSE))
306             return 1;
307     }
308
309     if (parse_tag_command_line (config, argc - opt_index, argv + opt_index,
310                                 &query_string, tag_ops))
311         return 1;
312
313     if (*query_string != '\0') {
314         fprintf (stderr, "Error: unexpected query string: %s\n", query_string);
315         return 1;
316     }
317
318     maildir = db_path;
319
320     /* Setup our handler for SIGINT. We do not set SA_RESTART so that copying
321      * from standard input may be interrupted. */
322     memset (&action, 0, sizeof (struct sigaction));
323     action.sa_handler = handle_sigint;
324     sigemptyset (&action.sa_mask);
325     action.sa_flags = 0;
326     sigaction (SIGINT, &action, NULL);
327
328     if (notmuch_database_open (notmuch_config_get_database_path (config),
329                                NOTMUCH_DATABASE_MODE_READ_WRITE, &notmuch))
330         return 1;
331
332     ret = insert_message (config, notmuch, STDIN_FILENO, maildir, tag_ops);
333
334     notmuch_database_destroy (notmuch);
335
336     return (ret) ? 0 : 1;
337 }