]> git.notmuchmail.org Git - notmuch/blob - message-file.c
a4c0860698d535eac3f9f5d1c2883e9e452314be
[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 notmuch_message_file_t *
74 notmuch_message_file_open (const char *filename)
75 {
76     notmuch_message_file_t *message;
77
78     message = talloc_zero (NULL, notmuch_message_file_t);
79
80     message->file = fopen (filename, "r");
81     if (message->file == NULL)
82         goto FAIL;
83
84     message->headers = g_hash_table_new_full (strcase_hash,
85                                               strcase_equal,
86                                               free,
87                                               free);
88
89     message->parsing_started = 0;
90     message->parsing_finished = 0;
91
92     return message;
93
94   FAIL:
95     fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
96     notmuch_message_file_close (message);
97
98     return NULL;
99 }
100
101 void
102 notmuch_message_file_close (notmuch_message_file_t *message)
103 {
104     if (message == NULL)
105         return;
106
107     if (message->line)
108         free (message->line);
109
110     if (message->value.size)
111         free (message->value.str);
112
113     if (message->headers)
114         g_hash_table_destroy (message->headers);
115
116     if (message->file)
117         fclose (message->file);
118
119     talloc_free (message);
120 }
121
122 void
123 notmuch_message_file_restrict_headersv (notmuch_message_file_t *message,
124                                         va_list va_headers)
125 {
126     char *header;
127
128     if (message->parsing_started)
129         INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started");
130
131     while (1) {
132         header = va_arg (va_headers, char*);
133         if (header == NULL)
134             break;
135         g_hash_table_insert (message->headers,
136                              xstrdup (header), NULL);
137     }
138
139     message->restrict_headers = 1;
140 }
141
142 void
143 notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...)
144 {
145     va_list va_headers;
146
147     va_start (va_headers, message);
148
149     notmuch_message_file_restrict_headersv (message, va_headers);
150 }
151
152 static void
153 copy_header_unfolding (header_value_closure_t *value,
154                        const char *chunk)
155 {
156     char *last;
157
158     if (chunk == NULL)
159         return;
160
161     while (*chunk == ' ' || *chunk == '\t')
162         chunk++;
163
164     if (value->len + 1 + strlen (chunk) + 1 > value->size) {
165         unsigned int new_size = value->size;
166         if (value->size == 0)
167             new_size = strlen (chunk) + 1;
168         else
169             while (value->len + 1 + strlen (chunk) + 1 > new_size)
170                 new_size *= 2;
171         value->str = xrealloc (value->str, new_size);
172         value->size = new_size;
173     }
174
175     last = value->str + value->len;
176     if (value->len) {
177         *last = ' ';
178         last++;
179         value->len++;
180     }
181
182     strcpy (last, chunk);
183     value->len += strlen (chunk);
184
185     last = value->str + value->len - 1;
186     if (*last == '\n') {
187         *last = '\0';
188         value->len--;
189     }
190 }
191
192 const char *
193 notmuch_message_file_get_header (notmuch_message_file_t *message,
194                                  const char *header_desired)
195 {
196     int contains;
197     char *header, *value;
198     const char *s, *colon;
199     int match;
200
201     message->parsing_started = 1;
202
203     contains = g_hash_table_lookup_extended (message->headers,
204                                              header_desired, NULL,
205                                              (gpointer *) &value);
206     if (contains && value)
207         return value;
208
209     if (message->parsing_finished)
210         return NULL;
211
212 #define NEXT_HEADER_LINE(closure)                               \
213     do {                                                        \
214         ssize_t bytes_read = getline (&message->line,           \
215                                       &message->line_size,      \
216                                       message->file);           \
217         if (bytes_read == -1) {                                 \
218             message->parsing_finished = 1;                      \
219             break;                                              \
220         }                                                       \
221         if (*message->line == '\n') {                           \
222             message->parsing_finished = 1;                      \
223             break;                                              \
224         }                                                       \
225         if (closure &&                                          \
226             (*message->line == ' ' || *message->line == '\t'))  \
227         {                                                       \
228             copy_header_unfolding ((closure), message->line);   \
229         }                                                       \
230     } while (*message->line == ' ' || *message->line == '\t');
231
232     if (message->line == NULL)
233         NEXT_HEADER_LINE (NULL);
234
235     while (1) {
236
237         if (message->parsing_finished)
238             break;
239
240         colon = strchr (message->line, ':');
241
242         if (colon == NULL) {
243             message->broken_headers++;
244             /* A simple heuristic for giving up on things that just
245              * don't look like mail messages. */
246             if (message->broken_headers >= 10 &&
247                 message->good_headers < 5)
248             {
249                 message->parsing_finished = 1;
250                 continue;
251             }
252             NEXT_HEADER_LINE (NULL);
253             continue;
254         }
255
256         message->good_headers++;
257
258         header = xstrndup (message->line, colon - message->line);
259
260         if (message->restrict_headers &&
261             ! g_hash_table_lookup_extended (message->headers,
262                                             header, NULL, NULL))
263         {
264             free (header);
265             NEXT_HEADER_LINE (NULL);
266             continue;
267         }
268
269         s = colon + 1;
270         while (*s == ' ' || *s == '\t')
271             s++;
272
273         message->value.len = 0;
274         copy_header_unfolding (&message->value, s);
275
276         NEXT_HEADER_LINE (&message->value);
277
278         match = (strcasecmp (header, header_desired) == 0);
279
280         value = xstrdup (message->value.str);
281
282         g_hash_table_insert (message->headers, header, value);
283
284         if (match)
285             return value;
286     }
287
288     if (message->line)
289         free (message->line);
290     message->line = NULL;
291
292     if (message->value.size) {
293         free (message->value.str);
294         message->value.str = NULL;
295         message->value.size = 0;
296         message->value.len = 0;
297     }
298
299     /* We've parsed all headers and never found the one we're looking
300      * for. It's probably just not there, but let's check that we
301      * didn't make a mistake preventing us from seeing it. */
302     if (message->restrict_headers &&
303         ! g_hash_table_lookup_extended (message->headers,
304                                         header_desired, NULL, NULL))
305     {
306         INTERNAL_ERROR ("Attempt to get header \"%s\" which was not\n"
307                         "included in call to notmuch_message_file_restrict_headers\n",
308                         header_desired);
309     }
310
311     return NULL;
312 }