]> git.notmuchmail.org Git - notmuch/blob - lib/message-file.c
lib/message_file: open gzipped files
[notmuch] / lib / message-file.c
1 /* message.c - Utility functions for parsing an email message for notmuch.
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 https://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include <stdarg.h>
22
23 #include "notmuch-private.h"
24
25 #include <gmime/gmime.h>
26
27 #include <glib.h> /* GHashTable */
28
29 struct _notmuch_message_file {
30     /* open stream to (possibly gzipped) file */
31     GMimeStream *stream;
32     char *filename;
33
34     /* Cache for decoded headers */
35     GHashTable *headers;
36
37     GMimeMessage *message;
38 };
39
40 static int
41 _notmuch_message_file_destructor (notmuch_message_file_t *message)
42 {
43     if (message->headers)
44         g_hash_table_destroy (message->headers);
45
46     if (message->message)
47         g_object_unref (message->message);
48
49     return 0;
50 }
51
52 /* Create a new notmuch_message_file_t for 'filename' with 'ctx' as
53  * the talloc owner. */
54 notmuch_message_file_t *
55 _notmuch_message_file_open_ctx (notmuch_database_t *notmuch,
56                                 void *ctx, const char *filename)
57 {
58     notmuch_message_file_t *message;
59
60     message = talloc_zero (ctx, notmuch_message_file_t);
61     if (unlikely (message == NULL))
62         return NULL;
63
64     message->filename = talloc_strdup (message, filename);
65     if (message->filename == NULL)
66         goto FAIL;
67
68     talloc_set_destructor (message, _notmuch_message_file_destructor);
69
70     message->stream = g_mime_stream_gzfile_open (filename);
71     if (message->stream == NULL)
72         goto FAIL;
73
74     return message;
75
76   FAIL:
77     _notmuch_database_log (notmuch, "Error opening %s: %s\n",
78                           filename, strerror (errno));
79     _notmuch_message_file_close (message);
80
81     return NULL;
82 }
83
84 notmuch_message_file_t *
85 _notmuch_message_file_open (notmuch_database_t *notmuch,
86                             const char *filename)
87 {
88     return _notmuch_message_file_open_ctx (notmuch, NULL, filename);
89 }
90
91 const char *
92 _notmuch_message_file_get_filename (notmuch_message_file_t *message_file)
93 {
94     return message_file->filename;
95 }
96
97 void
98 _notmuch_message_file_close (notmuch_message_file_t *message)
99 {
100     talloc_free (message);
101 }
102
103 static bool
104 _is_mbox (GMimeStream *stream)
105 {
106     char from_buf[5];
107     bool ret = false;
108
109     /* Is this mbox? */
110     if (g_mime_stream_read (stream, from_buf, sizeof (from_buf)) == sizeof(from_buf) &&
111         strncmp (from_buf, "From ", 5) == 0)
112         ret = true;
113
114     g_mime_stream_reset (stream);
115
116     return ret;
117 }
118
119 notmuch_status_t
120 _notmuch_message_file_parse (notmuch_message_file_t *message)
121 {
122     GMimeParser *parser;
123     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
124     static int initialized = 0;
125     bool is_mbox;
126
127     if (message->message)
128         return NOTMUCH_STATUS_SUCCESS;
129
130     is_mbox = _is_mbox (message->stream);
131
132     if (! initialized) {
133         g_mime_init ();
134         initialized = 1;
135     }
136
137     message->headers = g_hash_table_new_full (strcase_hash, strcase_equal,
138                                               free, g_free);
139     if (! message->headers)
140         return NOTMUCH_STATUS_OUT_OF_MEMORY;
141
142     parser = g_mime_parser_new_with_stream (message->stream);
143     g_mime_parser_set_scan_from (parser, is_mbox);
144
145     message->message = g_mime_parser_construct_message (parser, NULL);
146     if (! message->message) {
147         status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
148         goto DONE;
149     }
150
151     if (is_mbox && ! g_mime_parser_eos (parser)) {
152         /*
153          * This is a multi-message mbox. (For historical reasons, we
154          * do support single-message mboxes.)
155          */
156         status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
157     }
158
159   DONE:
160     g_mime_stream_reset (message->stream);
161     g_object_unref (parser);
162
163     if (status) {
164         g_hash_table_destroy (message->headers);
165         message->headers = NULL;
166
167         if (message->message) {
168             g_object_unref (message->message);
169             message->message = NULL;
170         }
171
172     }
173
174     return status;
175 }
176
177 notmuch_status_t
178 _notmuch_message_file_get_mime_message (notmuch_message_file_t *message,
179                                         GMimeMessage **mime_message)
180 {
181     notmuch_status_t status;
182
183     status = _notmuch_message_file_parse (message);
184     if (status)
185         return status;
186
187     *mime_message = message->message;
188
189     return NOTMUCH_STATUS_SUCCESS;
190 }
191
192 /*
193  * Get all instances of a header decoded and concatenated.
194  *
195  * The result must be freed using g_free().
196  *
197  * Return NULL on errors, empty string for non-existing headers.
198  */
199
200 static char *
201 _extend_header (char *combined, const char *value) {
202     char *decoded;
203
204     decoded = g_mime_utils_header_decode_text (NULL, value);
205     if (! decoded) {
206         if (combined) {
207             g_free (combined);
208             combined = NULL;
209         }
210         goto DONE;
211     }
212
213     if (combined) {
214         char *tmp = g_strdup_printf ("%s %s", combined, decoded);
215         g_free (decoded);
216         g_free (combined);
217         if (! tmp) {
218             combined = NULL;
219             goto DONE;
220         }
221
222         combined = tmp;
223     } else {
224         combined = decoded;
225     }
226  DONE:
227     return combined;
228 }
229
230 static char *
231 _notmuch_message_file_get_combined_header (notmuch_message_file_t *message,
232                                            const char *header)
233 {
234     char *combined = NULL;
235     GMimeHeaderList *headers;
236
237     headers = g_mime_object_get_header_list (GMIME_OBJECT (message->message));
238     if (! headers)
239         return NULL;
240
241
242     for (int i=0; i < g_mime_header_list_get_count (headers); i++) {
243         const char *value;
244         GMimeHeader *g_header = g_mime_header_list_get_header_at (headers, i);
245
246         if (strcasecmp (g_mime_header_get_name (g_header), header) != 0)
247             continue;
248
249         /* GMime retains ownership of value, we hope */
250         value = g_mime_header_get_value (g_header);
251
252         combined = _extend_header (combined, value);
253     }
254
255     /* Return empty string for non-existing headers. */
256     if (! combined)
257         combined = g_strdup ("");
258
259     return combined;
260 }
261
262 const char *
263 _notmuch_message_file_get_header (notmuch_message_file_t *message,
264                                  const char *header)
265 {
266     const char *value;
267     char *decoded;
268
269     if (_notmuch_message_file_parse (message))
270         return NULL;
271
272     /* If we have a cached decoded value, use it. */
273     value = g_hash_table_lookup (message->headers, header);
274     if (value)
275         return value;
276
277     if (strcasecmp (header, "received") == 0) {
278         /*
279          * The Received: header is special. We concatenate all
280          * instances of the header as we use this when analyzing the
281          * path the mail has taken from sender to recipient.
282          */
283         decoded = _notmuch_message_file_get_combined_header (message, header);
284     } else {
285         value = g_mime_object_get_header (GMIME_OBJECT (message->message),
286                                           header);
287         if (value)
288             decoded = g_mime_utils_header_decode_text (NULL, value);
289         else
290             decoded = g_strdup ("");
291     }
292
293     if (! decoded)
294         return NULL;
295
296     /* Cache the decoded value. We also own the strings. */
297     g_hash_table_insert (message->headers, xstrdup (header), decoded);
298
299     return decoded;
300 }
301
302 notmuch_status_t
303 _notmuch_message_file_get_headers (notmuch_message_file_t *message_file,
304                                    const char **from_out,
305                                    const char **subject_out,
306                                    const char **to_out,
307                                    const char **date_out,
308                                    char **message_id_out)
309 {
310     notmuch_status_t ret;
311     const char *header;
312     const char *from, *to, *subject, *date;
313     char *message_id = NULL;
314
315     /* Parse message up front to get better error status. */
316     ret = _notmuch_message_file_parse (message_file);
317     if (ret)
318         goto DONE;
319
320     /* Before we do any real work, (especially before doing a
321      * potential SHA-1 computation on the entire file's contents),
322      * let's make sure that what we're looking at looks like an
323      * actual email message.
324      */
325     from = _notmuch_message_file_get_header (message_file, "from");
326     subject = _notmuch_message_file_get_header (message_file, "subject");
327     to = _notmuch_message_file_get_header (message_file, "to");
328     date = _notmuch_message_file_get_header (message_file, "date");
329
330     if ((from == NULL || *from == '\0') &&
331         (subject == NULL || *subject == '\0') &&
332         (to == NULL || *to == '\0')) {
333         ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
334         goto DONE;
335     }
336
337     /* Now that we're sure it's mail, the first order of business
338      * is to find a message ID (or else create one ourselves).
339      */
340     header = _notmuch_message_file_get_header (message_file, "message-id");
341     if (header && *header != '\0') {
342         message_id = _notmuch_message_id_parse (message_file, header, NULL);
343
344         /* So the header value isn't RFC-compliant, but it's
345          * better than no message-id at all.
346          */
347         if (message_id == NULL)
348             message_id = talloc_strdup (message_file, header);
349     }
350
351     if (message_id == NULL ) {
352         /* No message-id at all, let's generate one by taking a
353          * hash over the file's contents.
354          */
355         char *sha1 = _notmuch_sha1_of_file (_notmuch_message_file_get_filename (message_file));
356
357         /* If that failed too, something is really wrong. Give up. */
358         if (sha1 == NULL) {
359             ret = NOTMUCH_STATUS_FILE_ERROR;
360             goto DONE;
361         }
362
363         message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
364         free (sha1);
365     }
366  DONE:
367     if (ret == NOTMUCH_STATUS_SUCCESS) {
368         if (from_out)
369             *from_out = from;
370         if (subject_out)
371             *subject_out = subject;
372         if (to_out)
373             *to_out = to;
374         if (date_out)
375             *date_out = date;
376         if (message_id_out)
377             *message_id_out = message_id;
378     }
379     return ret;
380 }