1 /* $OpenBSD: mailbox.c,v 1.6 2003/05/12 19:28:22 camield Exp $ */
2 
3 /*
4  * Mailbox access.
5  */
6 
7 #include <stdio.h>
8 #include <unistd.h>
9 #include <fcntl.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <signal.h>
13 #include <sys/stat.h>
14 #include <sys/types.h>
15 #include <time.h>
16 #include <errno.h>
17 
18 #include <md5.h>
19 
20 #include "misc.h"
21 #include "params.h"
22 #include "protocol.h"
23 #include "database.h"
24 
25 static int mailbox_fd;			/* fd for the mailbox, or -1 */
26 static time_t mailbox_mtime;		/* mtime, as of the last check */
27 static unsigned long mailbox_size;	/* Its original size */
28 
29 static struct db_message *cmp;
30 
31 /*
32  * If a message has changed since the database was filled in, then we
33  * consider the database stale. This is called for every message when
34  * the mailbox is being re-parsed (because of its mtime change).
35  */
db_compare(struct db_message * msg)36 static int db_compare(struct db_message *msg)
37 {
38 	if (!cmp) return 1;
39 
40 	if (msg->raw_size != cmp->raw_size || msg->size != cmp->size ||
41 	    memcmp(msg->hash, cmp->hash, sizeof(msg->hash))) {
42 		db.flags |= DB_STALE;
43 		return 1;
44 	}
45 
46 	cmp = cmp->next;
47 
48 	return 0;
49 }
50 
51 /*
52  * Checks if the buffer pointed to by s1, of n1 chars, starts with the
53  * string s2, of n2 chars.
54  */
55 #ifdef __GNUC__
56 __inline__
57 #endif
eq(char * s1,int n1,char * s2,int n2)58 static int eq(char *s1, int n1, char *s2, int n2)
59 {
60 	if (n1 < n2) return 0;
61 	if (!memcmp(s1, s2, n2)) return 1;
62 	return !strncasecmp(s1, s2, n2);
63 }
64 
65 /*
66  * The mailbox parsing routine: first called to fill the database in,
67  * then to check if the database is still up to date. We implement a
68  * state machine at the line fragment level (that is, full or partial
69  * lines). This is faster than dealing with individual characters (we
70  * leave that job for libc), and doesn't require ever loading entire
71  * lines into memory.
72  */
mailbox_parse(int init)73 static int mailbox_parse(int init)
74 {
75 	struct stat stat;			/* File information */
76 	struct db_message msg;			/* Message being parsed */
77 	MD5_CTX hash;				/* Its hash being computed */
78 	int (*db_op)(struct db_message *msg);	/* db_add or db_compare */
79 	char *file_buffer, *line_buffer;	/* Our internal buffers */
80 	unsigned long file_offset, line_offset;	/* Their offsets in the file */
81 	unsigned long offset;			/* A line fragment's offset */
82 	char *current, *next, *line;		/* Line pointers */
83 	int block, saved, extra, length;	/* Internal block sizes */
84 	int done, start, end;			/* Various boolean flags: */
85 	int blank, header, body;		/* the state information */
86 	int fixed, received;			/* ...and more of it */
87 
88 	if (fstat(mailbox_fd, &stat)) return 1;
89 
90 	if (init) {
91 /* Prepare for the database initialization */
92 		if (!S_ISREG(stat.st_mode)) return 1;
93 		mailbox_mtime = stat.st_mtime;
94 		if (stat.st_size > MAX_MAILBOX_OPEN_BYTES ||
95 		    stat.st_size > ~0UL) return 1;
96 		mailbox_size = stat.st_size;
97 		if (!mailbox_size) return 0;
98 		db_op = db_add;
99 	} else {
100 /* Prepare for checking against the database */
101 		if (mailbox_mtime == stat.st_mtime) return 0;
102 		if (!mailbox_size) return 0;
103 		if (stat.st_size < mailbox_size) {
104 			db.flags |= DB_STALE;
105 			return 1;
106 		}
107 		if (stat.st_size > MAX_MAILBOX_WORK_BYTES ||
108 		    stat.st_size > ~0UL) return 1;
109 		if (lseek(mailbox_fd, 0, SEEK_SET) < 0) return 1;
110 		db_op = db_compare; cmp = db.head;
111 	}
112 
113 	memset(&msg, 0, sizeof(msg));
114 	MD5Init(&hash);
115 
116 	file_buffer = malloc(FILE_BUFFER_SIZE + LINE_BUFFER_SIZE);
117 	if (!file_buffer) return 1;
118 	line_buffer = &file_buffer[FILE_BUFFER_SIZE];
119 
120 	file_offset = 0; line_offset = 0; offset = 0;	/* Start at 0, with */
121 	current = file_buffer; block = 0; saved = 0;	/* empty buffers */
122 
123 	done = 0;	/* Haven't reached EOF or the original size yet */
124 	end = 1;	/* Assume we've just seen a LF: parse a new line */
125 	blank = 1;	/* Assume we've seen a blank line: look for "From " */
126 	header = 0;	/* Not in message headers, */
127 	body = 0;	/* and not in message body */
128 	fixed = 0;	/* Not in a "fixed" part of a message, */
129 	received = 0;	/* and haven't got a Received: header yet */
130 
131 /*
132  * The main loop. Its first part extracts the line fragments, while the
133  * second one manages the state flags and performs whatever is required
134  * based on the state. Unfortunately, splitting this into two functions
135  * didn't seem to simplify the code.
136  */
137 	do {
138 /*
139  * Part 1.
140  * The line fragment extraction.
141  */
142 
143 /* Look for the next LF in the file buffer */
144 		if ((next = memchr(current, '\n', block))) {
145 /* Found it: get the length of this piece, and check for buffered data */
146 			length = ++next - current;
147 			if (saved) {
148 /* Have this line's beginning in the line buffer: combine them */
149 				extra = LINE_BUFFER_SIZE - saved;
150 				if (extra > length) extra = length;
151 				memcpy(&line_buffer[saved], current, extra);
152 				current += extra; block -= extra;
153 				length = saved + extra;
154 				line = line_buffer;
155 				offset = line_offset;
156 				start = end; end = current == next;
157 				saved = 0;
158 			} else {
159 /* Nothing in the line buffer: just process what we've got now */
160 				line = current;
161 				offset = file_offset - block;
162 				start = end; end = 1;
163 				current = next; block -= length;
164 			}
165 		} else {
166 /* No more LFs in the file buffer */
167 			if (saved || block <= LINE_BUFFER_SIZE) {
168 /* Have this line's beginning in the line buffer: combine them */
169 /* Not enough data to process right now: buffer it */
170 				extra = LINE_BUFFER_SIZE - saved;
171 				if (extra > block) extra = block;
172 				if (!saved) line_offset = file_offset - block;
173 				memcpy(&line_buffer[saved], current, extra);
174 				current += extra; block -= extra;
175 				saved += extra;
176 				length = saved;
177 				line = line_buffer;
178 				offset = line_offset;
179 			} else {
180 /* Nothing in the line buffer and we've got enough data: just process it */
181 				length = block - 1;
182 				line = current;
183 				offset = file_offset - block;
184 				current += length;
185 				block = 1;
186 			}
187 			if (!block) {
188 /* We've emptied the file buffer: fetch some more data */
189 				current = file_buffer;
190 				block = FILE_BUFFER_SIZE;
191 				if (!init &&
192 				    block > mailbox_size - file_offset)
193 					block = mailbox_size - file_offset;
194 				block = read(mailbox_fd, file_buffer, block);
195 				if (block < 0) break;
196 				file_offset += block;
197 				if (block > 0 && saved < LINE_BUFFER_SIZE)
198 					continue;
199 				if (!saved) {
200 /* Nothing in the line buffer, and read(2) returned 0: we're done */
201 					offset = file_offset;
202 					done = 1;
203 					break;
204 				}
205 			}
206 			start = end; end = !block;
207 			saved = 0;
208 		}
209 
210 /*
211  * Part 2.
212  * The following variables are set when we get here:
213  * -- line	the line fragment, not NUL terminated;
214  * -- length	its length;
215  * -- offset	its offset in the file;
216  * -- start	whether it's at the start of the line;
217  * -- end	whether it's at the end of the line
218  * (all four combinations of "start" and "end" are possible).
219  */
220 
221 /* Check for a new message if we've just seen a blank line */
222 		if (blank && start)
223 		if (line[0] == 'F' && length >= 5 &&
224 		    line[1] == 'r' && line[2] == 'o' && line[3] == 'm' &&
225 		    line[4] == ' ') {
226 /* Process the previous one first, if exists */
227 			if (offset) {
228 /* If we aren't at the very beginning, there must have been a message */
229 				if (!msg.data_offset) break;
230 				msg.raw_size = offset - msg.raw_offset;
231 				msg.data_size = offset - body - msg.data_offset;
232 				msg.size -= body << 1;
233 				MD5Final(msg.hash, &hash);
234 				if (db_op(&msg)) break;
235 			}
236 /* Now prepare for parsing the new one */
237 			msg.raw_offset = offset;
238 			msg.data_offset = 0;
239 			MD5Init(&hash);
240 			header = 1; body = 0;
241 			fixed = received = 0;
242 			continue;
243 		}
244 
245 /* Memorize file offset of the message data (the line next to "From ") */
246 		if (header && start && !msg.data_offset) {
247 			msg.data_offset = offset;
248 			msg.data_size = 0;
249 			msg.size = 0;
250 		}
251 
252 /* Count this fragment, with LFs as CRLF, into the message size */
253 		if (msg.data_offset)
254 			msg.size += length + end;
255 
256 /* If we see LF at start of line, then this is a blank line :-) */
257 		blank = start && line[0] == '\n';
258 
259 		if (!header) {
260 /* If we're no longer in message headers and we see more data, then it's
261  * the body. */
262 			if (msg.data_offset)
263 				body = 1;
264 /* The rest of actions in this loop are for header lines only */
265 			continue;
266 		}
267 
268 /* Blank line ends message headers */
269 		if (blank) {
270 			header = 0;
271 			continue;
272 		}
273 
274 /* Some header lines are known to remain fixed over MUA runs */
275 		if (start)
276 		switch (line[0]) {
277 		case '\t':
278 		case ' ':
279 /* Inherit "fixed" from the previous line */
280 			break;
281 
282 		case 'R':
283 		case 'r':
284 /* One Received: header from the local MTA should be sufficient */
285 			fixed = !received &&
286 				(received = eq(line, length, "Received:", 9));
287 			break;
288 
289 		case 'D':
290 		case 'd':
291 			fixed = eq(line, length, "Delivered-To:", 13) ||
292 				(!received && eq(line, length, "Date:", 5));
293 			break;
294 
295 		case 'M':
296 		case 'm':
297 			fixed = !received &&
298 				eq(line, length, "Message-ID:", 11);
299 			break;
300 
301 		case 'X':
302 /* Let the local delivery agent help generate unique IDs but don't blindly
303  * trust this header alone as it could just as easily come from the remote. */
304 			fixed = eq(line, length, "X-Delivery-ID:", 14);
305 			break;
306 
307 		default:
308 			fixed = 0;
309 			continue;
310 		}
311 
312 /* We can hash all fragments of those lines, for UIDL */
313 		if (fixed)
314 			MD5Update(&hash, (u_char *)line, length);
315 	} while (1);
316 
317 	free(file_buffer);
318 
319 	if (done) {
320 /* Process the last message */
321 		if (offset != mailbox_size) return 1;
322 		if (!msg.data_offset) return 1;
323 		msg.raw_size = offset - msg.raw_offset;
324 		msg.data_size = offset - (blank & body) - msg.data_offset;
325 		msg.size -= (blank & body) << 1;
326 		MD5Final(msg.hash, &hash);
327 		if (db_op(&msg)) return 1;
328 
329 /* Everything went well, update our timestamp if we were checking */
330 		if (!init) mailbox_mtime = stat.st_mtime;
331 	}
332 
333 	return !done;
334 }
335 
mailbox_open(char * spool,char * mailbox)336 int mailbox_open(char *spool, char *mailbox)
337 {
338 	char *pathname;
339 	struct stat stat;
340 	int result;
341 
342 	mailbox_fd = -1;
343 
344 	if (asprintf(&pathname, "%s/%s", spool, mailbox) == -1)
345 		return 1;
346 
347 	if (lstat(pathname, &stat)) {
348 		free(pathname);
349 		return errno != ENOENT;
350 	}
351 
352 	if (!S_ISREG(stat.st_mode)) {
353 		free(pathname);
354 		return 1;
355 	}
356 
357 	if (!stat.st_size) {
358 		free(pathname);
359 		return 0;
360 	}
361 
362 	mailbox_fd = open(pathname, O_RDWR | O_NOCTTY | O_NONBLOCK);
363 
364 	free(pathname);
365 
366 	if (mailbox_fd < 0)
367 		return errno != ENOENT;
368 
369 	if (lock_fd(mailbox_fd, 1)) return 1;
370 
371 	result = mailbox_parse(1);
372 
373 	if (!result && time(NULL) == mailbox_mtime)
374 	if (sleep_select(1, 0)) result = 1;
375 
376 	if (unlock_fd(mailbox_fd)) return 1;
377 
378 	return result;
379 }
380 
mailbox_changed(void)381 static int mailbox_changed(void)
382 {
383 	struct stat stat;
384 	int result;
385 
386 	if (fstat(mailbox_fd, &stat)) return 1;
387 	if (mailbox_mtime == stat.st_mtime) return 0;
388 
389 	if (lock_fd(mailbox_fd, 1)) return 1;
390 
391 	result = mailbox_parse(0);
392 
393 	if (!result && time(NULL) == mailbox_mtime)
394 	if (sleep_select(1, 0)) result = 1;
395 
396 	if (unlock_fd(mailbox_fd)) return 1;
397 
398 	return result;
399 }
400 
mailbox_get(struct db_message * msg,int lines)401 int mailbox_get(struct db_message *msg, int lines)
402 {
403 	int event;
404 
405 	if (mailbox_changed()) return POP_CRASH_SERVER;
406 
407 /* The calls to mailbox_changed() will set DB_STALE if that is the case */
408 	if (lseek(mailbox_fd, msg->data_offset, SEEK_SET) < 0) {
409 		mailbox_changed();
410 		return POP_CRASH_SERVER;
411 	}
412 	if ((event = pop_reply_multiline(mailbox_fd, msg->data_size, lines))) {
413 		if (event == POP_CRASH_SERVER) mailbox_changed();
414 		return event;
415 	}
416 
417 	if (mailbox_changed()) return POP_CRASH_SERVER;
418 
419 	if (pop_reply_terminate()) return POP_CRASH_NETFAIL;
420 
421 	return POP_OK;
422 }
423 
mailbox_write(char * buffer)424 static int mailbox_write(char *buffer)
425 {
426 	struct db_message *msg;
427 	unsigned long old, new;
428 	unsigned long size;
429 	int block;
430 
431 	msg = db.head;
432 	old = new = 0;
433 	do {
434 		if (msg->flags & MSG_DELETED) continue;
435 		old = msg->raw_offset;
436 
437 		if (old == new) {
438 			old = (new += msg->raw_size);
439 			continue;
440 		}
441 
442 		while ((size = msg->raw_size - (old - msg->raw_offset))) {
443 			if (lseek(mailbox_fd, old, SEEK_SET) < 0) return 1;
444 			if (size > FILE_BUFFER_SIZE)
445 				size = FILE_BUFFER_SIZE;
446 			block = read(mailbox_fd, buffer, size);
447 			if (!block && old == mailbox_size) break;
448 			if (block <= 0) return 1;
449 
450 			if (lseek(mailbox_fd, new, SEEK_SET) < 0) return 1;
451 			if (write_loop(mailbox_fd, buffer, block) != block)
452 				return 1;
453 
454 			old += block; new += block;
455 		}
456 	} while ((msg = msg->next));
457 
458 	old = mailbox_size;
459 	while (1) {
460 		if (lseek(mailbox_fd, old, SEEK_SET) < 0) return 1;
461 		block = read(mailbox_fd, buffer, FILE_BUFFER_SIZE);
462 		if (!block) break;
463 		if (block < 0) return 1;
464 
465 		if (lseek(mailbox_fd, new, SEEK_SET) < 0) return 1;
466 		if (write_loop(mailbox_fd, buffer, block) != block) return 1;
467 
468 /* Cannot overflow unless locking is bypassed */
469 		if ((old += block) < block || (new += block) < block) return 1;
470 	}
471 
472 	if (ftruncate(mailbox_fd, new)) return 1;
473 
474 	return fsync(mailbox_fd);
475 }
476 
mailbox_write_blocked(void)477 static int mailbox_write_blocked(void)
478 {
479 	sigset_t blocked_set, old_set;
480 	char *buffer;
481 	int result;
482 
483 	if (sigfillset(&blocked_set)) return 1;
484 	if (sigprocmask(SIG_BLOCK, &blocked_set, &old_set)) return 1;
485 
486 	if ((buffer = malloc(FILE_BUFFER_SIZE))) {
487 		result = mailbox_write(buffer);
488 		free(buffer);
489 	} else
490 		result = 1;
491 
492 	if (sigprocmask(SIG_SETMASK, &old_set, NULL)) return 1;
493 
494 	return result;
495 }
496 
mailbox_update(void)497 int mailbox_update(void)
498 {
499 	int result;
500 
501 	if (mailbox_fd < 0 || !(db.flags & DB_DIRTY)) return 0;
502 
503 	if (lock_fd(mailbox_fd, 0)) return 1;
504 
505 	if (!(result = mailbox_parse(0)))
506 		result = mailbox_write_blocked();
507 
508 	if (unlock_fd(mailbox_fd)) return 1;
509 
510 	return result;
511 }
512 
mailbox_close(void)513 int mailbox_close(void)
514 {
515 	if (mailbox_fd < 0) return 0;
516 
517 	return close(mailbox_fd);
518 }
519