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