03583c8d0e43f122215bdbcdd6e7c9ca92242a79
[notmuch] / message.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 {
34     /* File object */
35     FILE *file;
36
37     /* Header storage */
38     int restrict_headers;
39     GHashTable *headers;
40
41     /* Parsing state */
42     char *line;
43     size_t line_size;
44     header_value_closure_t value;
45
46     int parsing_started;
47     int parsing_finished;
48 };
49
50 static int
51 strcase_equal (const void *a, const void *b)
52 {
53     return strcasecmp (a, b) == 0;
54 }
55
56 static unsigned int
57 strcase_hash (const void *ptr)
58 {
59     const char *s = ptr;
60
61     /* This is the djb2 hash. */
62     unsigned int hash = 5381;
63     while (s && *s) {
64         hash = ((hash << 5) + hash) + tolower (*s);
65         s++;
66     }
67
68     return hash;
69 }
70
71 notmuch_message_t *
72 notmuch_message_open (const char *filename)
73 {
74     notmuch_message_t *message;
75
76     message = xcalloc (1, sizeof (notmuch_message_t));
77
78     message->file = fopen (filename, "r");
79     if (message->file == NULL)
80         goto FAIL;
81
82     message->headers = g_hash_table_new_full (strcase_hash,
83                                               strcase_equal,
84                                               free,
85                                               free);
86
87     message->parsing_started = 0;
88     message->parsing_finished = 0;
89
90     return message;
91
92   FAIL:
93     fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
94     notmuch_message_close (message);
95
96     return NULL;
97 }
98
99 void
100 notmuch_message_close (notmuch_message_t *message)
101 {
102     if (message == NULL)
103         return;
104
105     if (message->headers)
106         g_hash_table_unref (message->headers);
107
108     if (message->file)
109         fclose (message->file);
110
111     free (message);
112 }
113
114 void
115 notmuch_message_restrict_headersv (notmuch_message_t *message,
116                                    va_list va_headers)
117 {
118     char *header;
119
120     if (message->parsing_started ) {
121         fprintf (stderr, "Error: notmuch_message_restrict_headers called after parsing has started\n");
122         exit (1);
123     }
124
125     while (1) {
126         header = va_arg (va_headers, char*);
127         if (header == NULL)
128             break;
129         g_hash_table_insert (message->headers,
130                              xstrdup (header), NULL);
131     }
132
133     message->restrict_headers = 1;
134 }
135
136 void
137 notmuch_message_restrict_headers (notmuch_message_t *message, ...)
138 {
139     va_list va_headers;
140
141     va_start (va_headers, message);
142
143     notmuch_message_restrict_headersv (message, va_headers);
144 }
145
146 void
147 copy_header_unfolding (header_value_closure_t *value,
148                        const char *chunk)
149 {
150     char *last;
151
152     if (chunk == NULL)
153         return;
154
155     while (*chunk == ' ' || *chunk == '\t')
156         chunk++;
157
158     if (value->len + 1 + strlen (chunk) + 1 > value->size) {
159         int new_size = value->size;
160         if (value->size == 0)
161             new_size = strlen (chunk) + 1;
162         else
163             while (value->len + 1 + strlen (chunk) + 1 > new_size)
164                 new_size *= 2;
165         value->str = xrealloc (value->str, new_size);
166         value->size = new_size;
167     }
168
169     last = value->str + value->len;
170     if (value->len) {
171         *last = ' ';
172         last++;
173         value->len++;
174     }
175
176     strcpy (last, chunk);
177     value->len += strlen (chunk);
178
179     last = value->str + value->len - 1;
180     if (*last == '\n') {
181         *last = '\0';
182         value->len--;
183     }
184 }
185
186 const char *
187 notmuch_message_get_header (notmuch_message_t *message,
188                             const char *header_desired)
189 {
190     int contains;
191     char *header, *value;
192     const char *s, *colon;
193     int match;
194
195     message->parsing_started = 1;
196
197     contains = g_hash_table_lookup_extended (message->headers,
198                                              header_desired, NULL,
199                                              (gpointer *) &value);
200     if (contains && value)
201         return value;
202
203     if (message->parsing_finished)
204         return NULL;
205
206 #define NEXT_HEADER_LINE(closure)                               \
207     do {                                                        \
208         ssize_t bytes_read = getline (&message->line,           \
209                                       &message->line_size,      \
210                                       message->file);           \
211         if (bytes_read == -1) {                                 \
212             message->parsing_finished = 1;                      \
213             break;                                              \
214         }                                                       \
215         if (*message->line == '\n') {                           \
216             message->parsing_finished = 1;                      \
217             break;                                              \
218         }                                                       \
219         if (closure &&                                          \
220             (*message->line == ' ' || *message->line == '\t'))  \
221         {                                                       \
222             copy_header_unfolding ((closure), message->line);   \
223         }                                                       \
224     } while (*message->line == ' ' || *message->line == '\t');
225
226     if (message->line == NULL)
227         NEXT_HEADER_LINE (NULL);
228
229     while (1) {
230
231         if (message->parsing_finished)
232             break;
233
234         colon = strchr (message->line, ':');
235
236         if (colon == NULL) {
237             fprintf (stderr, "Warning: Unexpected non-header line: %s\n",
238                      message->line);
239             NEXT_HEADER_LINE (NULL);
240             continue;
241         }
242
243         header = xstrndup (message->line, colon - message->line);
244
245         if (message->restrict_headers &&
246             ! g_hash_table_lookup_extended (message->headers,
247                                             header, NULL, NULL))
248         {
249             free (header);
250             NEXT_HEADER_LINE (NULL);
251             continue;
252         }
253
254         s = colon + 1;
255         while (*s == ' ' || *s == '\t')
256             s++;
257
258         message->value.len = 0;
259         copy_header_unfolding (&message->value, s);
260
261         NEXT_HEADER_LINE (&message->value);
262
263         match = (strcasecmp (header, header_desired) == 0);
264
265         g_hash_table_insert (message->headers, header,
266                              xstrdup (message->value.str));
267
268         if (match)
269             return value;
270     }
271
272     if (message->line)
273         free (message->line);
274     message->line = NULL;
275
276     if (message->value.size) {
277         free (message->value.str);
278         message->value.str = NULL;
279         message->value.size = 0;
280         message->value.len = 0;
281     }
282
283     return NULL;
284 }