Add public notmuch_thread_get_subject
[notmuch] / 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 <glib.h> /* GHashTable */
26
27 typedef struct {
28     char *str;
29     size_t size;
30     size_t len;
31 } header_value_closure_t;
32
33 struct _notmuch_message_file {
34     /* File object */
35     FILE *file;
36
37     /* Header storage */
38     int restrict_headers;
39     GHashTable *headers;
40     int broken_headers;
41     int good_headers;
42
43     /* Parsing state */
44     char *line;
45     size_t line_size;
46     header_value_closure_t value;
47
48     int parsing_started;
49     int parsing_finished;
50 };
51
52 static int
53 strcase_equal (const void *a, const void *b)
54 {
55     return strcasecmp (a, b) == 0;
56 }
57
58 static unsigned int
59 strcase_hash (const void *ptr)
60 {
61     const char *s = ptr;
62
63     /* This is the djb2 hash. */
64     unsigned int hash = 5381;
65     while (s && *s) {
66         hash = ((hash << 5) + hash) + tolower (*s);
67         s++;
68     }
69
70     return hash;
71 }
72
73 static int
74 _notmuch_message_file_destructor (notmuch_message_file_t *message)
75 {
76     if (message->line)
77         free (message->line);
78
79     if (message->value.size)
80         free (message->value.str);
81
82     if (message->headers)
83         g_hash_table_destroy (message->headers);
84
85     if (message->file)
86         fclose (message->file);
87
88     return 0;
89 }
90
91 /* Create a new notmuch_message_file_t for 'filename' with 'ctx' as
92  * the talloc owner. */
93 notmuch_message_file_t *
94 _notmuch_message_file_open_ctx (void *ctx, const char *filename)
95 {
96     notmuch_message_file_t *message;
97
98     message = talloc_zero (ctx, notmuch_message_file_t);
99     if (unlikely (message == NULL))
100         return NULL;
101
102     talloc_set_destructor (message, _notmuch_message_file_destructor);
103
104     message->file = fopen (filename, "r");
105     if (message->file == NULL)
106         goto FAIL;
107
108     message->headers = g_hash_table_new_full (strcase_hash,
109                                               strcase_equal,
110                                               free,
111                                               free);
112
113     message->parsing_started = 0;
114     message->parsing_finished = 0;
115
116     return message;
117
118   FAIL:
119     fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
120     notmuch_message_file_close (message);
121
122     return NULL;
123 }
124
125 notmuch_message_file_t *
126 notmuch_message_file_open (const char *filename)
127 {
128     return _notmuch_message_file_open_ctx (NULL, filename);
129 }
130
131 void
132 notmuch_message_file_close (notmuch_message_file_t *message)
133 {
134     talloc_free (message);
135 }
136
137 void
138 notmuch_message_file_restrict_headersv (notmuch_message_file_t *message,
139                                         va_list va_headers)
140 {
141     char *header;
142
143     if (message->parsing_started)
144         INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started");
145
146     while (1) {
147         header = va_arg (va_headers, char*);
148         if (header == NULL)
149             break;
150         g_hash_table_insert (message->headers,
151                              xstrdup (header), NULL);
152     }
153
154     message->restrict_headers = 1;
155 }
156
157 void
158 notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...)
159 {
160     va_list va_headers;
161
162     va_start (va_headers, message);
163
164     notmuch_message_file_restrict_headersv (message, va_headers);
165 }
166
167 static void
168 copy_header_unfolding (header_value_closure_t *value,
169                        const char *chunk)
170 {
171     char *last;
172
173     if (chunk == NULL)
174         return;
175
176     while (*chunk == ' ' || *chunk == '\t')
177         chunk++;
178
179     if (value->len + 1 + strlen (chunk) + 1 > value->size) {
180         unsigned int new_size = value->size;
181         if (value->size == 0)
182             new_size = strlen (chunk) + 1;
183         else
184             while (value->len + 1 + strlen (chunk) + 1 > new_size)
185                 new_size *= 2;
186         value->str = xrealloc (value->str, new_size);
187         value->size = new_size;
188     }
189
190     last = value->str + value->len;
191     if (value->len) {
192         *last = ' ';
193         last++;
194         value->len++;
195     }
196
197     strcpy (last, chunk);
198     value->len += strlen (chunk);
199
200     last = value->str + value->len - 1;
201     if (*last == '\n') {
202         *last = '\0';
203         value->len--;
204     }
205 }
206
207 const char *
208 notmuch_message_file_get_header (notmuch_message_file_t *message,
209                                  const char *header_desired)
210 {
211     int contains;
212     char *header, *value;
213     const char *s, *colon;
214     int match;
215
216     message->parsing_started = 1;
217
218     contains = g_hash_table_lookup_extended (message->headers,
219                                              header_desired, NULL,
220                                              (gpointer *) &value);
221     if (contains && value)
222         return value;
223
224     if (message->parsing_finished)
225         return NULL;
226
227 #define NEXT_HEADER_LINE(closure)                               \
228     do {                                                        \
229         ssize_t bytes_read = getline (&message->line,           \
230                                       &message->line_size,      \
231                                       message->file);           \
232         if (bytes_read == -1) {                                 \
233             message->parsing_finished = 1;                      \
234             break;                                              \
235         }                                                       \
236         if (*message->line == '\n') {                           \
237             message->parsing_finished = 1;                      \
238             break;                                              \
239         }                                                       \
240         if (closure &&                                          \
241             (*message->line == ' ' || *message->line == '\t'))  \
242         {                                                       \
243             copy_header_unfolding ((closure), message->line);   \
244         }                                                       \
245     } while (*message->line == ' ' || *message->line == '\t');
246
247     if (message->line == NULL)
248         NEXT_HEADER_LINE (NULL);
249
250     while (1) {
251
252         if (message->parsing_finished)
253             break;
254
255         colon = strchr (message->line, ':');
256
257         if (colon == NULL) {
258             message->broken_headers++;
259             /* A simple heuristic for giving up on things that just
260              * don't look like mail messages. */
261             if (message->broken_headers >= 10 &&
262                 message->good_headers < 5)
263             {
264                 message->parsing_finished = 1;
265                 continue;
266             }
267             NEXT_HEADER_LINE (NULL);
268             continue;
269         }
270
271         message->good_headers++;
272
273         header = xstrndup (message->line, colon - message->line);
274
275         if (message->restrict_headers &&
276             ! g_hash_table_lookup_extended (message->headers,
277                                             header, NULL, NULL))
278         {
279             free (header);
280             NEXT_HEADER_LINE (NULL);
281             continue;
282         }
283
284         s = colon + 1;
285         while (*s == ' ' || *s == '\t')
286             s++;
287
288         message->value.len = 0;
289         copy_header_unfolding (&message->value, s);
290
291         NEXT_HEADER_LINE (&message->value);
292
293         match = (strcasecmp (header, header_desired) == 0);
294
295         value = xstrdup (message->value.str);
296
297         g_hash_table_insert (message->headers, header, value);
298
299         if (match)
300             return value;
301     }
302
303     if (message->line)
304         free (message->line);
305     message->line = NULL;
306
307     if (message->value.size) {
308         free (message->value.str);
309         message->value.str = NULL;
310         message->value.size = 0;
311         message->value.len = 0;
312     }
313
314     /* We've parsed all headers and never found the one we're looking
315      * for. It's probably just not there, but let's check that we
316      * didn't make a mistake preventing us from seeing it. */
317     if (message->restrict_headers &&
318         ! g_hash_table_lookup_extended (message->headers,
319                                         header_desired, NULL, NULL))
320     {
321         INTERNAL_ERROR ("Attempt to get header \"%s\" which was not\n"
322                         "included in call to notmuch_message_file_restrict_headers\n",
323                         header_desired);
324     }
325
326     return NULL;
327 }