util: move strcase_equal and strcase_hash to util
[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 http://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     /* File object */
31     FILE *file;
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     if (message->file)
50         fclose (message->file);
51
52     return 0;
53 }
54
55 /* Create a new notmuch_message_file_t for 'filename' with 'ctx' as
56  * the talloc owner. */
57 notmuch_message_file_t *
58 _notmuch_message_file_open_ctx (notmuch_database_t *notmuch,
59                                 void *ctx, const char *filename)
60 {
61     notmuch_message_file_t *message;
62
63     message = talloc_zero (ctx, notmuch_message_file_t);
64     if (unlikely (message == NULL))
65         return NULL;
66
67     /* Only needed for error messages during parsing. */
68     message->filename = talloc_strdup (message, filename);
69     if (message->filename == NULL)
70         goto FAIL;
71
72     talloc_set_destructor (message, _notmuch_message_file_destructor);
73
74     message->file = fopen (filename, "r");
75     if (message->file == NULL)
76         goto FAIL;
77
78     return message;
79
80   FAIL:
81     _notmuch_database_log (notmuch, "Error opening %s: %s\n",
82                           filename, strerror (errno));
83     _notmuch_message_file_close (message);
84
85     return NULL;
86 }
87
88 notmuch_message_file_t *
89 _notmuch_message_file_open (notmuch_database_t *notmuch,
90                             const char *filename)
91 {
92     return _notmuch_message_file_open_ctx (notmuch, NULL, filename);
93 }
94
95 void
96 _notmuch_message_file_close (notmuch_message_file_t *message)
97 {
98     talloc_free (message);
99 }
100
101 static notmuch_bool_t
102 _is_mbox (FILE *file)
103 {
104     char from_buf[5];
105     notmuch_bool_t ret = FALSE;
106
107     /* Is this mbox? */
108     if (fread (from_buf, sizeof (from_buf), 1, file) == 1 &&
109         strncmp (from_buf, "From ", 5) == 0)
110         ret = TRUE;
111
112     rewind (file);
113
114     return ret;
115 }
116
117 notmuch_status_t
118 _notmuch_message_file_parse (notmuch_message_file_t *message)
119 {
120     GMimeStream *stream;
121     GMimeParser *parser;
122     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
123     static int initialized = 0;
124     notmuch_bool_t is_mbox;
125
126     if (message->message)
127         return NOTMUCH_STATUS_SUCCESS;
128
129     is_mbox = _is_mbox (message->file);
130
131     if (! initialized) {
132         g_mime_init (GMIME_ENABLE_RFC2047_WORKAROUNDS);
133         initialized = 1;
134     }
135
136     message->headers = g_hash_table_new_full (strcase_hash, strcase_equal,
137                                               free, g_free);
138     if (! message->headers)
139         return NOTMUCH_STATUS_OUT_OF_MEMORY;
140
141     stream = g_mime_stream_file_new (message->file);
142
143     /* We'll own and fclose the FILE* ourselves. */
144     g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
145
146     parser = g_mime_parser_new_with_stream (stream);
147     g_mime_parser_set_scan_from (parser, is_mbox);
148
149     message->message = g_mime_parser_construct_message (parser);
150     if (! message->message) {
151         status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
152         goto DONE;
153     }
154
155     if (is_mbox && ! g_mime_parser_eos (parser)) {
156         /*
157          * This is a multi-message mbox. (For historical reasons, we
158          * do support single-message mboxes.)
159          */
160         status = NOTMUCH_STATUS_FILE_NOT_EMAIL;
161     }
162
163   DONE:
164     g_object_unref (stream);
165     g_object_unref (parser);
166
167     if (status) {
168         g_hash_table_destroy (message->headers);
169         message->headers = NULL;
170
171         if (message->message) {
172             g_object_unref (message->message);
173             message->message = NULL;
174         }
175
176         rewind (message->file);
177     }
178
179     return status;
180 }
181
182 notmuch_status_t
183 _notmuch_message_file_get_mime_message (notmuch_message_file_t *message,
184                                         GMimeMessage **mime_message)
185 {
186     notmuch_status_t status;
187
188     status = _notmuch_message_file_parse (message);
189     if (status)
190         return status;
191
192     *mime_message = message->message;
193
194     return NOTMUCH_STATUS_SUCCESS;
195 }
196
197 /*
198  * Get all instances of a header decoded and concatenated.
199  *
200  * The result must be freed using g_free().
201  *
202  * Return NULL on errors, empty string for non-existing headers.
203  */
204 static char *
205 _notmuch_message_file_get_combined_header (notmuch_message_file_t *message,
206                                            const char *header)
207 {
208     GMimeHeaderList *headers;
209     GMimeHeaderIter *iter;
210     char *combined = NULL;
211
212     headers = g_mime_object_get_header_list (GMIME_OBJECT (message->message));
213     if (! headers)
214         return NULL;
215
216     iter = g_mime_header_iter_new ();
217     if (! iter)
218         return NULL;
219
220     if (! g_mime_header_list_get_iter (headers, iter))
221         goto DONE;
222
223     do {
224         const char *value;
225         char *decoded;
226
227         if (strcasecmp (g_mime_header_iter_get_name (iter), header) != 0)
228             continue;
229
230         /* Note that GMime retains ownership of value... */
231         value = g_mime_header_iter_get_value (iter);
232
233         /* ... while decoded needs to be freed with g_free(). */
234         decoded = g_mime_utils_header_decode_text (value);
235         if (! decoded) {
236             if (combined) {
237                 g_free (combined);
238                 combined = NULL;
239             }
240             goto DONE;
241         }
242
243         if (combined) {
244             char *tmp = g_strdup_printf ("%s %s", combined, decoded);
245             g_free (decoded);
246             g_free (combined);
247             if (! tmp) {
248                 combined = NULL;
249                 goto DONE;
250             }
251
252             combined = tmp;
253         } else {
254             combined = decoded;
255         }
256     } while (g_mime_header_iter_next (iter));
257
258     /* Return empty string for non-existing headers. */
259     if (! combined)
260         combined = g_strdup ("");
261
262   DONE:
263     g_mime_header_iter_free (iter);
264
265     return combined;
266 }
267
268 const char *
269 _notmuch_message_file_get_header (notmuch_message_file_t *message,
270                                  const char *header)
271 {
272     const char *value;
273     char *decoded;
274
275     if (_notmuch_message_file_parse (message))
276         return NULL;
277
278     /* If we have a cached decoded value, use it. */
279     value = g_hash_table_lookup (message->headers, header);
280     if (value)
281         return value;
282
283     if (strcasecmp (header, "received") == 0) {
284         /*
285          * The Received: header is special. We concatenate all
286          * instances of the header as we use this when analyzing the
287          * path the mail has taken from sender to recipient.
288          */
289         decoded = _notmuch_message_file_get_combined_header (message, header);
290     } else {
291         value = g_mime_object_get_header (GMIME_OBJECT (message->message),
292                                           header);
293         if (value)
294             decoded = g_mime_utils_header_decode_text (value);
295         else
296             decoded = g_strdup ("");
297     }
298
299     if (! decoded)
300         return NULL;
301
302     /* Cache the decoded value. We also own the strings. */
303     g_hash_table_insert (message->headers, xstrdup (header), decoded);
304
305     return decoded;
306 }