xref: /openbsd-src/libexec/spamd-setup/spamd-setup.c (revision f2da64fbbbf1b03f09f390ab01267c93dfd77c4c)
1 /*	$OpenBSD: spamd-setup.c,v 1.48 2016/01/04 09:15:24 mestre Exp $ */
2 
3 /*
4  * Copyright (c) 2003 Bob Beck.  All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16  * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18  * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */
26 
27 #include <arpa/inet.h>
28 #include <sys/socket.h>
29 
30 #include <err.h>
31 #include <errno.h>
32 #include <fcntl.h>
33 #include <netdb.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <unistd.h>
38 #include <zlib.h>
39 
40 #define PATH_FTP		"/usr/bin/ftp"
41 #define PATH_PFCTL		"/sbin/pfctl"
42 #define PATH_SPAMD_CONF		"/etc/mail/spamd.conf"
43 #define SPAMD_ARG_MAX		256 /* max # of args to an exec */
44 
45 struct cidr {
46 	u_int32_t addr;
47 	u_int8_t bits;
48 };
49 
50 struct bl {
51 	u_int32_t addr;
52 	int8_t b;
53 	int8_t w;
54 };
55 
56 struct blacklist {
57 	char *name;
58 	char *message;
59 	struct bl *bl;
60 	size_t blc, bls;
61 	u_int8_t black;
62 };
63 
64 u_int32_t	 imask(u_int8_t);
65 u_int8_t	 maxblock(u_int32_t, u_int8_t);
66 u_int8_t	 maxdiff(u_int32_t, u_int32_t);
67 struct cidr	*range2cidrlist(struct cidr *, u_int *, u_int *, u_int32_t,
68 		     u_int32_t);
69 void		 cidr2range(struct cidr, u_int32_t *, u_int32_t *);
70 char		*atop(u_int32_t);
71 int		 parse_netblock(char *, struct bl *, struct bl *, int);
72 int		 open_child(char *, char **);
73 int		 fileget(char *);
74 int		 open_file(char *, char *);
75 char		*fix_quoted_colons(char *);
76 void		 do_message(FILE *, char *);
77 struct bl	*add_blacklist(struct bl *, size_t *, size_t *, gzFile, int);
78 int		 cmpbl(const void *, const void *);
79 struct cidr	*collapse_blacklist(struct bl *, size_t, u_int *);
80 int		 configure_spamd(u_short, char *, char *, struct cidr *, u_int);
81 int		 configure_pf(struct cidr *);
82 int		 getlist(char **, char *, struct blacklist *, struct blacklist *);
83 __dead void	 usage(void);
84 
85 int		  debug;
86 int		  dryrun;
87 int		  greyonly = 1;
88 
89 extern char 	 *__progname;
90 
91 #define MAXIMUM(a,b) (((a)>(b))?(a):(b))
92 
93 u_int32_t
94 imask(u_int8_t b)
95 {
96 	if (b == 0)
97 		return (0);
98 	return (0xffffffffU << (32 - b));
99 }
100 
101 u_int8_t
102 maxblock(u_int32_t addr, u_int8_t bits)
103 {
104 	u_int32_t m;
105 
106 	while (bits > 0) {
107 		m = imask(bits - 1);
108 
109 		if ((addr & m) != addr)
110 			return (bits);
111 		bits--;
112 	}
113 	return (bits);
114 }
115 
116 u_int8_t
117 maxdiff(u_int32_t a, u_int32_t b)
118 {
119 	u_int8_t bits = 0;
120 	u_int32_t m;
121 
122 	b++;
123 	while (bits < 32) {
124 		m = imask(bits);
125 
126 		if ((a & m) != (b & m))
127 			return (bits);
128 		bits++;
129 	}
130 	return (bits);
131 }
132 
133 struct cidr *
134 range2cidrlist(struct cidr *list, u_int *cli, u_int *cls, u_int32_t start,
135     u_int32_t end)
136 {
137 	u_int8_t maxsize, diff;
138 	struct cidr *tmp;
139 
140 	while (end >= start) {
141 		maxsize = maxblock(start, 32);
142 		diff = maxdiff(start, end);
143 
144 		maxsize = MAXIMUM(maxsize, diff);
145 		if (*cls <= *cli + 1) {		/* one extra for terminator */
146 			tmp = reallocarray(list, *cls + 32,
147 			    sizeof(struct cidr));
148 			if (tmp == NULL)
149 				err(1, NULL);
150 			list = tmp;
151 			*cls += 32;
152 		}
153 		list[*cli].addr = start;
154 		list[*cli].bits = maxsize;
155 		(*cli)++;
156 		start = start + (1 << (32 - maxsize));
157 	}
158 	return (list);
159 }
160 
161 void
162 cidr2range(struct cidr cidr, u_int32_t *start, u_int32_t *end)
163 {
164 	*start = cidr.addr;
165 	*end = cidr.addr + (1 << (32 - cidr.bits)) - 1;
166 }
167 
168 char *
169 atop(u_int32_t addr)
170 {
171 	struct in_addr in;
172 
173 	memset(&in, 0, sizeof(in));
174 	in.s_addr = htonl(addr);
175 	return (inet_ntoa(in));
176 }
177 
178 int
179 parse_netblock(char *buf, struct bl *start, struct bl *end, int white)
180 {
181 	char astring[16], astring2[16];
182 	unsigned maskbits;
183 	struct cidr c;
184 
185 	/* skip leading spaces */
186 	while (*buf == ' ')
187 		buf++;
188 	/* bail if it's a comment */
189 	if (*buf == '#')
190 		return (0);
191 	/* otherwise, look for a netblock of some sort */
192 	if (sscanf(buf, "%15[^/]/%u", astring, &maskbits) == 2) {
193 		/* looks like a cidr */
194 		memset(&c.addr, 0, sizeof(c.addr));
195 		if (inet_net_pton(AF_INET, astring, &c.addr, sizeof(c.addr))
196 		    == -1)
197 			return (0);
198 		c.addr = ntohl(c.addr);
199 		if (maskbits > 32)
200 			return (0);
201 		c.bits = maskbits;
202 		cidr2range(c, &start->addr, &end->addr);
203 		end->addr += 1;
204 	} else if (sscanf(buf, "%15[0123456789.]%*[ -]%15[0123456789.]",
205 	    astring, astring2) == 2) {
206 		/* looks like start - end */
207 		memset(&start->addr, 0, sizeof(start->addr));
208 		memset(&end->addr, 0, sizeof(end->addr));
209 		if (inet_net_pton(AF_INET, astring, &start->addr,
210 		    sizeof(start->addr)) == -1)
211 			return (0);
212 		start->addr = ntohl(start->addr);
213 		if (inet_net_pton(AF_INET, astring2, &end->addr,
214 		    sizeof(end->addr)) == -1)
215 			return (0);
216 		end->addr = ntohl(end->addr) + 1;
217 		if (start > end)
218 			return (0);
219 	} else if (sscanf(buf, "%15[0123456789.]", astring) == 1) {
220 		/* just a single address */
221 		memset(&start->addr, 0, sizeof(start->addr));
222 		if (inet_net_pton(AF_INET, astring, &start->addr,
223 		    sizeof(start->addr)) == -1)
224 			return (0);
225 		start->addr = ntohl(start->addr);
226 		end->addr = start->addr + 1;
227 	} else
228 		return (0);
229 
230 	if (white) {
231 		start->b = 0;
232 		start->w = 1;
233 		end->b = 0;
234 		end->w = -1;
235 	} else {
236 		start->b = 1;
237 		start->w = 0;
238 		end->b = -1;
239 		end->w = 0;
240 	}
241 	return (1);
242 }
243 
244 int
245 open_child(char *file, char **argv)
246 {
247 	int pdes[2];
248 
249 	if (pipe(pdes) != 0)
250 		return (-1);
251 	switch (fork()) {
252 	case -1:
253 		close(pdes[0]);
254 		close(pdes[1]);
255 		return (-1);
256 	case 0:
257 		/* child */
258 		close(pdes[0]);
259 		if (pdes[1] != STDOUT_FILENO) {
260 			dup2(pdes[1], STDOUT_FILENO);
261 			close(pdes[1]);
262 		}
263 		execvp(file, argv);
264 		_exit(1);
265 	}
266 
267 	/* parent */
268 	close(pdes[1]);
269 	return (pdes[0]);
270 }
271 
272 int
273 fileget(char *url)
274 {
275 	char *argv[6];
276 
277 	argv[0] = "ftp";
278 	argv[1] = "-V";
279 	argv[2] = "-o";
280 	argv[3] = "-";
281 	argv[4] = url;
282 	argv[5] = NULL;
283 
284 	if (debug)
285 		fprintf(stderr, "Getting %s\n", url);
286 
287 	return (open_child(PATH_FTP, argv));
288 }
289 
290 int
291 open_file(char *method, char *file)
292 {
293 	char *url;
294 	char **ap, **argv;
295 	int len, i, oerrno;
296 
297 	if ((method == NULL) || (strcmp(method, "file") == 0))
298 		return (open(file, O_RDONLY));
299 	if ((strcmp(method, "http") == 0) ||
300 	    strcmp(method, "ftp") == 0) {
301 		if (asprintf(&url, "%s://%s", method, file) == -1)
302 			return (-1);
303 		i = fileget(url);
304 		free(url);
305 		return (i);
306 	} else if (strcmp(method, "exec") == 0) {
307 		len = strlen(file);
308 		argv = calloc(len, sizeof(char *));
309 		if (argv == NULL)
310 			return (-1);
311 		for (ap = argv; ap < &argv[len - 1] &&
312 		    (*ap = strsep(&file, " \t")) != NULL;) {
313 			if (**ap != '\0')
314 				ap++;
315 		}
316 		*ap = NULL;
317 		i = open_child(argv[0], argv);
318 		oerrno = errno;
319 		free(argv);
320 		errno = oerrno;
321 		return (i);
322 	}
323 	errx(1, "Unknown method %s", method);
324 	return (-1); /* NOTREACHED */
325 }
326 
327 /*
328  * fix_quoted_colons walks through a buffer returned by cgetent.  We
329  * look for quoted strings, to escape colons (:) in quoted strings for
330  * getcap by replacing them with \C so cgetstr() deals with it correctly
331  * without having to see the \C bletchery in a configuration file that
332  * needs to have urls in it. Frees the buffer passed to it, passes back
333  * another larger one, with can be used with cgetxxx(), like the original
334  * buffer, it must be freed by the caller.
335  * This should really be a temporary fix until there is a sanctioned
336  * way to make getcap(3) handle quoted strings like this in a nicer
337  * way.
338  */
339 char *
340 fix_quoted_colons(char *buf)
341 {
342 	int in = 0;
343 	size_t i, j = 0;
344 	char *newbuf, last;
345 
346 	/* Allocate enough space for a buf of all colons (impossible). */
347 	newbuf = malloc(2 * strlen(buf) + 1);
348 	if (newbuf == NULL)
349 		return (NULL);
350 	last = '\0';
351 	for (i = 0; i < strlen(buf); i++) {
352 		switch (buf[i]) {
353 		case ':':
354 			if (in) {
355 				newbuf[j++] = '\\';
356 				newbuf[j++] = 'C';
357 			} else
358 				newbuf[j++] = buf[i];
359 			break;
360 		case '"':
361 			if (last != '\\')
362 				in = !in;
363 			newbuf[j++] = buf[i];
364 			break;
365 		default:
366 			newbuf[j++] = buf[i];
367 		}
368 		last = buf[i];
369 	}
370 	free(buf);
371 	newbuf[j] = '\0';
372 	return (newbuf);
373 }
374 
375 void
376 do_message(FILE *sdc, char *msg)
377 {
378 	size_t i, bs = 0, bu = 0, len;
379 	ssize_t n;
380 	char *buf = NULL, last, *tmp;
381 	int fd;
382 
383 	len = strlen(msg);
384 	if (msg[0] == '"' && msg[len - 1] == '"') {
385 		/* quoted msg, escape newlines and send it out */
386 		msg[len - 1] = '\0';
387 		buf = msg + 1;
388 		bu = len - 2;
389 		goto sendit;
390 	} else {
391 		/*
392 		 * message isn't quoted - try to open a local
393 		 * file and read the message from it.
394 		 */
395 		fd = open(msg, O_RDONLY);
396 		if (fd == -1)
397 			err(1, "Can't open message from %s", msg);
398 		for (;;) {
399 			if (bu == bs) {
400 				tmp = realloc(buf, bs + 8192);
401 				if (tmp == NULL)
402 					err(1, NULL);
403 				bs += 8192;
404 				buf = tmp;
405 			}
406 
407 			n = read(fd, buf + bu, bs - bu);
408 			if (n == 0) {
409 				goto sendit;
410 			} else if (n == -1) {
411 				err(1, "Can't read from %s", msg);
412 			} else
413 				bu += n;
414 		}
415 		buf[bu]='\0';
416 	}
417  sendit:
418 	fprintf(sdc, ";\"");
419 	last = '\0';
420 	for (i = 0; i < bu; i++) {
421 		/* handle escaping the things spamd wants */
422 		switch (buf[i]) {
423 		case 'n':
424 			if (last == '\\')
425 				fprintf(sdc, "\\\\n");
426 			else
427 				fputc('n', sdc);
428 			last = '\0';
429 			break;
430 		case '\n':
431 			fprintf(sdc, "\\n");
432 			last = '\0';
433 			break;
434 		case '"':
435 			fputc('\\', sdc);
436 			/* FALLTHROUGH */
437 		default:
438 			fputc(buf[i], sdc);
439 			last = '\0';
440 		}
441 	}
442 	fputc('"', sdc);
443 	if (bs != 0)
444 		free(buf);
445 }
446 
447 /* retrieve a list from fd. add to blacklist bl */
448 struct bl *
449 add_blacklist(struct bl *bl, size_t *blc, size_t *bls, gzFile gzf, int white)
450 {
451 	int i, n, start, bu = 0, bs = 0, serrno = 0;
452 	char *buf = NULL, *tmp;
453 	struct bl *blt;
454 
455 	for (;;) {
456 		/* read in gzf, then parse */
457 		if (bu == bs) {
458 			tmp = realloc(buf, bs + (1024 * 1024) + 1);
459 			if (tmp == NULL) {
460 				serrno = errno;
461 				free(buf);
462 				buf = NULL;
463 				bs = 0;
464 				goto bldone;
465 			}
466 			bs += 1024 * 1024;
467 			buf = tmp;
468 		}
469 
470 		n = gzread(gzf, buf + bu, bs - bu);
471 		if (n == 0)
472 			goto parse;
473 		else if (n == -1) {
474 			serrno = errno;
475 			goto bldone;
476 		} else
477 			bu += n;
478 	}
479  parse:
480 	start = 0;
481 	/* we assume that there is an IP for every 14 bytes */
482 	if (*blc + bu / 7 >= *bls) {
483 		*bls += bu / 7;
484 		blt = reallocarray(bl, *bls, sizeof(struct bl));
485 		if (blt == NULL) {
486 			*bls -= bu / 7;
487 			serrno = errno;
488 			goto bldone;
489 		}
490 		bl = blt;
491 	}
492 	for (i = 0; i <= bu; i++) {
493 		if (*blc + 1 >= *bls) {
494 			*bls += 1024;
495 			blt = reallocarray(bl, *bls, sizeof(struct bl));
496 			if (blt == NULL) {
497 				*bls -= 1024;
498 				serrno = errno;
499 				goto bldone;
500 			}
501 			bl = blt;
502 		}
503 		if (i == bu || buf[i] == '\n') {
504 			buf[i] = '\0';
505 			if (parse_netblock(buf + start,
506 			    bl + *blc, bl + *blc + 1, white))
507 				*blc += 2;
508 			start = i + 1;
509 		}
510 	}
511 	if (bu == 0)
512 		errno = EIO;
513  bldone:
514 	free(buf);
515 	if (serrno)
516 		errno = serrno;
517 	return (bl);
518 }
519 
520 int
521 cmpbl(const void *a, const void *b)
522 {
523 	if (((struct bl *)a)->addr > ((struct bl *) b)->addr)
524 		return (1);
525 	if (((struct bl *)a)->addr < ((struct bl *) b)->addr)
526 		return (-1);
527 	return (0);
528 }
529 
530 /*
531  * collapse_blacklist takes blacklist/whitelist entries sorts, removes
532  * overlaps and whitelist portions, and returns netblocks to blacklist
533  * as lists of nonoverlapping cidr blocks suitable for feeding in
534  * printable form to pfctl or spamd.
535  */
536 struct cidr *
537 collapse_blacklist(struct bl *bl, size_t blc, u_int *clc)
538 {
539 	int bs = 0, ws = 0, state=0;
540 	u_int cli, cls, i;
541 	u_int32_t bstart = 0;
542 	struct cidr *cl;
543 	int laststate;
544 	u_int32_t addr;
545 
546 	if (blc == 0)
547 		return (NULL);
548 
549 	/*
550 	 * Overallocate by 10% to avoid excessive realloc due to white
551 	 * entries splitting up CIDR blocks.
552 	 */
553 	cli = 0;
554 	cls = (blc / 2) + (blc / 20) + 1;
555 	cl = reallocarray(NULL, cls, sizeof(struct cidr));
556 	if (cl == NULL)
557 		return (NULL);
558 	qsort(bl, blc, sizeof(struct bl), cmpbl);
559 	for (i = 0; i < blc;) {
560 		laststate = state;
561 		addr = bl[i].addr;
562 
563 		do {
564 			bs += bl[i].b;
565 			ws += bl[i].w;
566 			i++;
567 		} while (bl[i].addr == addr);
568 		if (state == 1 && bs == 0)
569 			state = 0;
570 		else if (state == 0 && bs > 0)
571 			state = 1;
572 		if (ws > 0)
573 			state = 0;
574 		if (laststate == 0 && state == 1) {
575 			/* start blacklist */
576 			bstart = addr;
577 		}
578 		if (laststate == 1 && state == 0) {
579 			/* end blacklist */
580 			cl = range2cidrlist(cl, &cli, &cls, bstart, addr - 1);
581 		}
582 		laststate = state;
583 	}
584 	cl[cli].addr = 0;
585 	*clc = cli;
586 	return (cl);
587 }
588 
589 int
590 configure_spamd(u_short dport, char *name, char *message,
591     struct cidr *blacklists, u_int count)
592 {
593 	int lport = IPPORT_RESERVED - 1, s;
594 	struct sockaddr_in sin;
595 	FILE* sdc;
596 
597 	s = rresvport(&lport);
598 	if (s == -1)
599 		return (-1);
600 	memset(&sin, 0, sizeof sin);
601 	sin.sin_len = sizeof(sin);
602 	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
603 	sin.sin_family = AF_INET;
604 	sin.sin_port = htons(dport);
605 	if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1)
606 		return (-1);
607 	sdc = fdopen(s, "w");
608 	if (sdc == NULL) {
609 		close(s);
610 		return (-1);
611 	}
612 	fputs(name, sdc);
613 	do_message(sdc, message);
614 	fprintf(sdc, ";inet;%u", count);
615 	while (blacklists->addr != 0) {
616 		fprintf(sdc, ";%s/%u", atop(blacklists->addr),
617 		    blacklists->bits);
618 		blacklists++;
619 	}
620 	fputc('\n', sdc);
621 	fclose(sdc);
622 	close(s);
623 	return (0);
624 }
625 
626 
627 int
628 configure_pf(struct cidr *blacklists)
629 {
630 	char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace",
631 	    "-f" "-", NULL};
632 	static FILE *pf = NULL;
633 	int pdes[2];
634 
635 	if (pf == NULL) {
636 		if (pipe(pdes) != 0)
637 			return (-1);
638 		switch (fork()) {
639 		case -1:
640 			close(pdes[0]);
641 			close(pdes[1]);
642 			return (-1);
643 		case 0:
644 			/* child */
645 			close(pdes[1]);
646 			if (pdes[0] != STDIN_FILENO) {
647 				dup2(pdes[0], STDIN_FILENO);
648 				close(pdes[0]);
649 			}
650 			execvp(PATH_PFCTL, argv);
651 			_exit(1);
652 		}
653 
654 		/* parent */
655 		close(pdes[0]);
656 		pf = fdopen(pdes[1], "w");
657 		if (pf == NULL) {
658 			close(pdes[1]);
659 			return (-1);
660 		}
661 	}
662 	while (blacklists->addr != 0) {
663 		fprintf(pf, "%s/%u\n", atop(blacklists->addr),
664 		    blacklists->bits);
665 		blacklists++;
666 	}
667 	return (0);
668 }
669 
670 int
671 getlist(char ** db_array, char *name, struct blacklist *blist,
672     struct blacklist *blistnew)
673 {
674 	char *buf, *method, *file, *message;
675 	int fd, black = 0, serror;
676 	size_t blc, bls;
677 	struct bl *bl = NULL;
678 	gzFile gzf;
679 
680 	if (cgetent(&buf, db_array, name) != 0)
681 		err(1, "Can't find \"%s\" in spamd config", name);
682 	buf = fix_quoted_colons(buf);
683 	if (cgetcap(buf, "black", ':') != NULL) {
684 		/* use new list */
685 		black = 1;
686 		blc = blistnew->blc;
687 		bls = blistnew->bls;
688 		bl = blistnew->bl;
689 	} else if (cgetcap(buf, "white", ':') != NULL) {
690 		/* apply to most recent blacklist */
691 		black = 0;
692 		blc = blist->blc;
693 		bls = blist->bls;
694 		bl = blist->bl;
695 	} else
696 		errx(1, "Must have \"black\" or \"white\" in %s", name);
697 
698 	switch (cgetstr(buf, "msg", &message)) {
699 	case -1:
700 		if (black)
701 			errx(1, "No msg for blacklist \"%s\"", name);
702 		break;
703 	case -2:
704 		err(1, NULL);
705 	}
706 
707 	switch (cgetstr(buf, "method", &method)) {
708 	case -1:
709 		method = NULL;
710 		break;
711 	case -2:
712 		err(1, NULL);
713 	}
714 
715 	switch (cgetstr(buf, "file", &file)) {
716 	case -1:
717 		errx(1, "No file given for %slist %s",
718 		    black ? "black" : "white", name);
719 	case -2:
720 		err(1, NULL);
721 	default:
722 		fd = open_file(method, file);
723 		if (fd == -1)
724 			err(1, "Can't open %s by %s method",
725 			    file, method ? method : "file");
726 		free(method);
727 		free(file);
728 		gzf = gzdopen(fd, "r");
729 		if (gzf == NULL)
730 			errx(1, "gzdopen");
731 	}
732 	free(buf);
733 	bl = add_blacklist(bl, &blc, &bls, gzf, !black);
734 	serror = errno;
735 	gzclose(gzf);
736 	if (bl == NULL) {
737 		errno = serror;
738 		warn("Could not add %slist %s", black ? "black" : "white",
739 		    name);
740 		return (0);
741 	}
742 	if (black) {
743 		if (debug)
744 			fprintf(stderr, "blacklist %s %zu entries\n",
745 			    name, blc / 2);
746 		blistnew->message = message;
747 		blistnew->name = name;
748 		blistnew->black = black;
749 		blistnew->bl = bl;
750 		blistnew->blc = blc;
751 		blistnew->bls = bls;
752 	} else {
753 		/* whitelist applied to last active blacklist */
754 		if (debug)
755 			fprintf(stderr, "whitelist %s %zu entries\n",
756 			    name, (blc - blist->blc) / 2);
757 		blist->bl = bl;
758 		blist->blc = blc;
759 		blist->bls = bls;
760 	}
761 	return (black);
762 }
763 
764 void
765 send_blacklist(struct blacklist *blist, in_port_t port)
766 {
767 	struct cidr *cidrs;
768 	u_int clc;
769 
770 	if (blist->blc > 0) {
771 		cidrs = collapse_blacklist(blist->bl, blist->blc, &clc);
772 		if (cidrs == NULL)
773 			err(1, NULL);
774 		if (!dryrun) {
775 			if (configure_spamd(port, blist->name,
776 			    blist->message, cidrs, clc) == -1)
777 				err(1, "Can't connect to spamd on port %d",
778 				    port);
779 			if (!greyonly && configure_pf(cidrs) == -1)
780 				err(1, "pfctl failed");
781 		}
782 		free(cidrs);
783 		free(blist->bl);
784 	}
785 }
786 
787 __dead void
788 usage(void)
789 {
790 
791 	fprintf(stderr, "usage: %s [-bDdn]\n", __progname);
792 	exit(1);
793 }
794 
795 int
796 main(int argc, char *argv[])
797 {
798 	size_t blc, bls, black, white;
799 	char *db_array[2], *buf, *name;
800 	struct blacklist *blists;
801 	struct servent *ent;
802 	int daemonize = 0, ch;
803 
804 	while ((ch = getopt(argc, argv, "bdDn")) != -1) {
805 		switch (ch) {
806 		case 'n':
807 			dryrun = 1;
808 			break;
809 		case 'd':
810 			debug = 1;
811 			break;
812 		case 'b':
813 			greyonly = 0;
814 			break;
815 		case 'D':
816 			daemonize = 1;
817 			break;
818 		default:
819 			usage();
820 			break;
821 		}
822 	}
823 	argc -= optind;
824 	argv += optind;
825 	if (argc != 0)
826 		usage();
827 
828 	if (pledge("stdio rpath inet proc exec", NULL) == -1)
829 		err(1, "pledge");
830 
831 	if (daemonize)
832 		daemon(0, 0);
833 
834 	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
835 		errx(1, "cannot find service \"spamd-cfg\" in /etc/services");
836 	ent->s_port = ntohs(ent->s_port);
837 
838 	db_array[0] = PATH_SPAMD_CONF;
839 	db_array[1] = NULL;
840 
841 	if (cgetent(&buf, db_array, "all") != 0)
842 		err(1, "Can't find \"all\" in spamd config");
843 	name = strsep(&buf, ": \t"); /* skip "all" at start */
844 	blists = NULL;
845 	blc = bls = 0;
846 	while ((name = strsep(&buf, ": \t")) != NULL) {
847 		if (*name) {
848 			/* extract config in order specified in "all" tag */
849 			if (blc == bls) {
850 				struct blacklist *tmp;
851 
852 				bls += 32;
853 				tmp = reallocarray(blists, bls,
854 				    sizeof(struct blacklist));
855 				if (tmp == NULL)
856 					err(1, NULL);
857 				blists = tmp;
858 			}
859 			if (blc == 0)
860 				black = white = 0;
861 			else {
862 				white = blc - 1;
863 				black = blc;
864 			}
865 			memset(&blists[black], 0, sizeof(struct blacklist));
866 			black = getlist(db_array, name, &blists[white],
867 			    &blists[black]);
868 			if (black && blc > 0) {
869 				/* collapse and free previous blacklist */
870 				send_blacklist(&blists[blc - 1], ent->s_port);
871 			}
872 			blc += black;
873 		}
874 	}
875 	/* collapse and free last blacklist */
876 	if (blc > 0)
877 		send_blacklist(&blists[blc - 1], ent->s_port);
878 	return (0);
879 }
880