xref: /freebsd/contrib/dma/mail.c (revision 81ad6265)
1 /*
2  * Copyright (c) 2008-2014, Simon Schubert <2@0x2c.org>.
3  * Copyright (c) 2008 The DragonFly Project.  All rights reserved.
4  *
5  * This code is derived from software contributed to The DragonFly Project
6  * by Simon Schubert <2@0x2c.org>.
7  *
8  * Redistribution and use in source and binary forms, with or without
9  * modification, are permitted provided that the following conditions
10  * are met:
11  *
12  * 1. Redistributions of source code must retain the above copyright
13  *    notice, this list of conditions and the following disclaimer.
14  * 2. Redistributions in binary form must reproduce the above copyright
15  *    notice, this list of conditions and the following disclaimer in
16  *    the documentation and/or other materials provided with the
17  *    distribution.
18  * 3. Neither the name of The DragonFly Project nor the names of its
19  *    contributors may be used to endorse or promote products derived
20  *    from this software without specific, prior written permission.
21  *
22  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
26  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
28  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
30  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33  * SUCH DAMAGE.
34  */
35 
36 #include <errno.h>
37 #include <inttypes.h>
38 #include <malloc_np.h>
39 #include <signal.h>
40 #include <strings.h>
41 #include <string.h>
42 #include <syslog.h>
43 #include <unistd.h>
44 
45 #include "dma.h"
46 
47 #define MAX_LINE_RFC822	999 /* 998 characters plus \n */
48 
49 void
50 bounce(struct qitem *it, const char *reason)
51 {
52 	struct queue bounceq;
53 	char line[1000];
54 	size_t pos;
55 	int error;
56 
57 	/* Don't bounce bounced mails */
58 	if (it->sender[0] == 0) {
59 		syslog(LOG_INFO, "can not bounce a bounce message, discarding");
60 		exit(EX_SOFTWARE);
61 	}
62 
63 	bzero(&bounceq, sizeof(bounceq));
64 	LIST_INIT(&bounceq.queue);
65 	bounceq.sender = "";
66 	if (add_recp(&bounceq, it->sender, EXPAND_WILDCARD) != 0)
67 		goto fail;
68 
69 	if (newspoolf(&bounceq) != 0)
70 		goto fail;
71 
72 	syslog(LOG_ERR, "delivery failed, bouncing as %s", bounceq.id);
73 	setlogident("%s", bounceq.id);
74 
75 	error = fprintf(bounceq.mailf,
76 		"Received: from MAILER-DAEMON\n"
77 		"\tid %s\n"
78 		"\tby %s (%s on %s);\n"
79 		"\t%s\n"
80 		"X-Original-To: <%s>\n"
81 		"From: MAILER-DAEMON <>\n"
82 		"To: %s\n"
83 		"Subject: Mail delivery failed\n"
84 		"Message-Id: <%s@%s>\n"
85 		"Date: %s\n"
86 		"\n"
87 		"This is the %s at %s.\n"
88 		"\n"
89 		"There was an error delivering your mail to <%s>.\n"
90 		"\n"
91 		"%s\n"
92 		"\n"
93 		"%s\n"
94 		"\n",
95 		bounceq.id,
96 		hostname(), VERSION, systemhostname(),
97 		rfc822date(),
98 		it->addr,
99 		it->sender,
100 		bounceq.id, hostname(),
101 		rfc822date(),
102 		VERSION, hostname(),
103 		it->addr,
104 		reason,
105 		config.features & FULLBOUNCE ?
106 		    "Original message follows." :
107 		    "Message headers follow.");
108 	if (error < 0)
109 		goto fail;
110 
111 	if (fseek(it->mailf, 0, SEEK_SET) != 0)
112 		goto fail;
113 	if (config.features & FULLBOUNCE) {
114 		while ((pos = fread(line, 1, sizeof(line), it->mailf)) > 0) {
115 			if (fwrite(line, 1, pos, bounceq.mailf) != pos)
116 				goto fail;
117 		}
118 	} else {
119 		while (!feof(it->mailf)) {
120 			if (fgets(line, sizeof(line), it->mailf) == NULL)
121 				break;
122 			if (line[0] == '\n')
123 				break;
124 			if (fwrite(line, strlen(line), 1, bounceq.mailf) != 1)
125 				goto fail;
126 		}
127 	}
128 
129 	if (linkspool(&bounceq) != 0)
130 		goto fail;
131 	/* bounce is safe */
132 
133 	delqueue(it);
134 
135 	run_queue(&bounceq);
136 	/* NOTREACHED */
137 
138 fail:
139 	syslog(LOG_CRIT, "error creating bounce: %m");
140 	delqueue(it);
141 	exit(EX_IOERR);
142 }
143 
144 struct parse_state {
145 	char addr[1000];
146 	int pos;
147 
148 	enum {
149 		NONE = 0,
150 		START,
151 		MAIN,
152 		EOL,
153 		QUIT
154 	} state;
155 	int comment;
156 	int quote;
157 	int brackets;
158 	int esc;
159 };
160 
161 /*
162  * Simplified RFC2822 header/address parsing.
163  * We copy escapes and quoted strings directly, since
164  * we have to pass them like this to the mail server anyways.
165  * XXX local addresses will need treatment
166  */
167 static int
168 parse_addrs(struct parse_state *ps, char *s, struct queue *queue)
169 {
170 	char *addr;
171 
172 again:
173 	switch (ps->state) {
174 	case NONE:
175 		return (-1);
176 
177 	case START:
178 		/* init our data */
179 		bzero(ps, sizeof(*ps));
180 
181 		/* skip over header name */
182 		while (*s != ':')
183 			s++;
184 		s++;
185 		ps->state = MAIN;
186 		break;
187 
188 	case MAIN:
189 		/* all fine */
190 		break;
191 
192 	case EOL:
193 		switch (*s) {
194 		case ' ':
195 		case '\t':
196 			ps->state = MAIN;
197 			break;
198 
199 		default:
200 			ps->state = QUIT;
201 			if (ps->pos != 0)
202 				goto newaddr;
203 			return (0);
204 		}
205 		break;
206 
207 	case QUIT:
208 		return (0);
209 	}
210 
211 	for (; *s != 0; s++) {
212 		if (ps->esc) {
213 			ps->esc = 0;
214 
215 			switch (*s) {
216 			case '\r':
217 			case '\n':
218 				goto err;
219 
220 			default:
221 				goto copy;
222 			}
223 		}
224 
225 		if (ps->quote) {
226 			switch (*s) {
227 			case '"':
228 				ps->quote = 0;
229 				goto copy;
230 
231 			case '\\':
232 				ps->esc = 1;
233 				goto copy;
234 
235 			case '\r':
236 			case '\n':
237 				goto eol;
238 
239 			default:
240 				goto copy;
241 			}
242 		}
243 
244 		switch (*s) {
245 		case '(':
246 			ps->comment++;
247 			break;
248 
249 		case ')':
250 			if (ps->comment)
251 				ps->comment--;
252 			else
253 				goto err;
254 			goto skip;
255 
256 		case '"':
257 			ps->quote = 1;
258 			goto copy;
259 
260 		case '\\':
261 			ps->esc = 1;
262 			goto copy;
263 
264 		case '\r':
265 		case '\n':
266 			goto eol;
267 		}
268 
269 		if (ps->comment)
270 			goto skip;
271 
272 		switch (*s) {
273 		case ' ':
274 		case '\t':
275 			/* ignore whitespace */
276 			goto skip;
277 
278 		case '<':
279 			/* this is the real address now */
280 			ps->brackets = 1;
281 			ps->pos = 0;
282 			goto skip;
283 
284 		case '>':
285 			if (!ps->brackets)
286 				goto err;
287 			ps->brackets = 0;
288 
289 			s++;
290 			goto newaddr;
291 
292 		case ':':
293 			/* group - ignore */
294 			ps->pos = 0;
295 			goto skip;
296 
297 		case ',':
298 		case ';':
299 			/*
300 			 * Next address, copy previous one.
301 			 * However, we might be directly after
302 			 * a <address>, or have two consecutive
303 			 * commas.
304 			 * Skip the comma unless there is
305 			 * really something to copy.
306 			 */
307 			if (ps->pos == 0)
308 				goto skip;
309 			s++;
310 			goto newaddr;
311 
312 		default:
313 			goto copy;
314 		}
315 
316 copy:
317 		if (ps->comment)
318 			goto skip;
319 
320 		if (ps->pos + 1 == sizeof(ps->addr))
321 			goto err;
322 		ps->addr[ps->pos++] = *s;
323 
324 skip:
325 		;
326 	}
327 
328 eol:
329 	ps->state = EOL;
330 	return (0);
331 
332 err:
333 	ps->state = QUIT;
334 	return (-1);
335 
336 newaddr:
337 	ps->addr[ps->pos] = 0;
338 	ps->pos = 0;
339 	addr = strdup(ps->addr);
340 	if (addr == NULL)
341 		errlog(EX_SOFTWARE, "strdup");
342 
343 	if (add_recp(queue, addr, EXPAND_WILDCARD) != 0)
344 		errlogx(EX_DATAERR, "invalid recipient `%s'", addr);
345 
346 	goto again;
347 }
348 
349 static int
350 writeline(struct queue *queue, const char *line, ssize_t linelen)
351 {
352 	ssize_t len;
353 
354 	while (linelen > 0) {
355 		len = linelen;
356 		if (linelen > MAX_LINE_RFC822) {
357 			len = MAX_LINE_RFC822 - 10;
358 		}
359 
360 		if (fwrite(line, len, 1, queue->mailf) != 1)
361 			return (-1);
362 
363 		if (linelen <= MAX_LINE_RFC822)
364 			break;
365 
366 		if (fwrite("\n", 1, 1, queue->mailf) != 1)
367 			return (-1);
368 
369 		line += MAX_LINE_RFC822 - 10;
370 		linelen = strlen(line);
371 	}
372 	return (0);
373 }
374 
375 int
376 readmail(struct queue *queue, int nodot, int recp_from_header)
377 {
378 	struct parse_state parse_state;
379 	char *line = NULL;
380 	ssize_t linelen;
381 	size_t linecap = 0;
382 	char newline[MAX_LINE_RFC822 + 1];
383 	size_t error;
384 	int had_headers = 0;
385 	int had_from = 0;
386 	int had_messagid = 0;
387 	int had_date = 0;
388 	int had_first_line = 0;
389 	int had_last_line = 0;
390 	int nocopy = 0;
391 	int ret = -1;
392 
393 	parse_state.state = NONE;
394 
395 	error = fprintf(queue->mailf,
396 		"Received: from %s (uid %d)\n"
397 		"\t(envelope-from %s)\n"
398 		"\tid %s\n"
399 		"\tby %s (%s on %s);\n"
400 		"\t%s\n",
401 		username, useruid,
402 		queue->sender,
403 		queue->id,
404 		hostname(), VERSION, systemhostname(),
405 		rfc822date());
406 	if ((ssize_t)error < 0)
407 		return (-1);
408 
409 	while ((linelen = getline(&line, &linecap, stdin)) > 0) {
410 		newline[0] = '\0';
411 		if (had_last_line)
412 			errlogx(EX_DATAERR, "bad mail input format:"
413 				" from %s (uid %d) (envelope-from %s)",
414 				username, useruid, queue->sender);
415 		linelen = strlen(line);
416 		if (linelen == 0 || line[linelen - 1] != '\n') {
417 			/*
418 			 * This line did not end with a newline character.
419 			 * If we fix it, it better be the last line of
420 			 * the file.
421 			 */
422 			if ((size_t)linelen + 1 > linecap) {
423 				line = realloc(line, linelen + 2);
424 				if (line == NULL)
425 					errlogx(EX_SOFTWARE, "realloc");
426 				linecap = malloc_usable_size(line);
427 			}
428 			line[linelen++] = '\n';
429 			line[linelen] = 0;
430 			had_last_line = 1;
431 		}
432 		if (!had_first_line) {
433 			/*
434 			 * Ignore a leading RFC-976 From_ or >From_ line mistakenly
435 			 * inserted by some programs.
436 			 */
437 			if (strprefixcmp(line, "From ") == 0 || strprefixcmp(line, ">From ") == 0)
438 				continue;
439 			had_first_line = 1;
440 		}
441 		if (!had_headers) {
442 			if (linelen > MAX_LINE_RFC822) {
443 				/* XXX also split headers */
444 				errlogx(EX_DATAERR, "bad mail input format:"
445 				    " from %s (uid %d) (envelope-from %s)",
446 				    username, useruid, queue->sender);
447 			}
448 			/*
449 			 * Unless this is a continuation, switch of
450 			 * the Bcc: nocopy flag.
451 			 */
452 			if (!(line[0] == ' ' || line[0] == '\t'))
453 				nocopy = 0;
454 
455 			if (strprefixcmp(line, "Date:") == 0)
456 				had_date = 1;
457 			else if (strprefixcmp(line, "Message-Id:") == 0)
458 				had_messagid = 1;
459 			else if (strprefixcmp(line, "From:") == 0)
460 				had_from = 1;
461 			else if (strprefixcmp(line, "Bcc:") == 0)
462 				nocopy = 1;
463 
464 			if (parse_state.state != NONE) {
465 				if (parse_addrs(&parse_state, line, queue) < 0) {
466 					errlogx(EX_DATAERR, "invalid address in header\n");
467 					/* NOTREACHED */
468 				}
469 			}
470 
471 			if (recp_from_header && (
472 					strprefixcmp(line, "To:") == 0 ||
473 					strprefixcmp(line, "Cc:") == 0 ||
474 					strprefixcmp(line, "Bcc:") == 0)) {
475 				parse_state.state = START;
476 				if (parse_addrs(&parse_state, line, queue) < 0) {
477 					errlogx(EX_DATAERR, "invalid address in header\n");
478 					/* NOTREACHED */
479 				}
480 			}
481 		}
482 
483 		if (strcmp(line, "\n") == 0 && !had_headers) {
484 			had_headers = 1;
485 			while (!had_date || !had_messagid || !had_from) {
486 				if (!had_date) {
487 					had_date = 1;
488 					snprintf(newline, sizeof(newline), "Date: %s\n", rfc822date());
489 				} else if (!had_messagid) {
490 					/* XXX msgid, assign earlier and log? */
491 					had_messagid = 1;
492 					snprintf(newline, sizeof(newline), "Message-Id: <%"PRIxMAX".%s.%"PRIxMAX"@%s>\n",
493 						 (uintmax_t)time(NULL),
494 						 queue->id,
495 						 (uintmax_t)random(),
496 						 hostname());
497 				} else if (!had_from) {
498 					had_from = 1;
499 					snprintf(newline, sizeof(newline), "From: <%s>\n", queue->sender);
500 				}
501 				if (fwrite(newline, strlen(newline), 1, queue->mailf) != 1)
502 					goto fail;
503 			}
504 			strlcpy(newline, "\n", sizeof(newline));
505 		}
506 		if (!nodot && linelen == 2 && line[0] == '.')
507 			break;
508 		if (!nocopy) {
509 			if (newline[0]) {
510 				if (fwrite(newline, strlen(newline), 1, queue->mailf) != 1)
511 					goto fail;
512 			} else {
513 				if (writeline(queue, line, linelen) != 0)
514 					goto fail;
515 			}
516 		}
517 	}
518 	if (ferror(stdin) == 0)
519 		ret = 0;
520 fail:
521 	free(line);
522 	return (ret);
523 }
524