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