xref: /openbsd-src/libexec/spamd-setup/spamd-setup.c (revision 50b7afb2c2c0993b0894d4e34bf857cb13ed9c80)
1 /*	$OpenBSD: spamd-setup.c,v 1.38 2012/12/04 02:24:47 deraadt 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 <sys/socket.h>
28 #include <netinet/in.h>
29 #include <arpa/inet.h>
30 #include <errno.h>
31 #include <fcntl.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <unistd.h>
36 #include <err.h>
37 #include <netinet/ip_ipsp.h>
38 #include <netdb.h>
39 #include <zlib.h>
40 
41 #define PATH_FTP		"/usr/bin/ftp"
42 #define PATH_PFCTL		"/sbin/pfctl"
43 #define PATH_SPAMD_CONF		"/etc/mail/spamd.conf"
44 #define SPAMD_ARG_MAX		256 /* max # of args to an exec */
45 
46 struct cidr {
47 	u_int32_t addr;
48 	u_int8_t bits;
49 };
50 
51 struct bl {
52 	u_int32_t addr;
53 	int8_t b;
54 	int8_t w;
55 };
56 
57 struct blacklist {
58 	char *name;
59 	char *message;
60 	struct bl *bl;
61 	size_t blc, bls;
62 	u_int8_t black;
63 	int count;
64 };
65 
66 u_int32_t	 imask(u_int8_t);
67 u_int8_t	 maxblock(u_int32_t, u_int8_t);
68 u_int8_t	 maxdiff(u_int32_t, u_int32_t);
69 struct cidr	*range2cidrlist(struct cidr *, int *, int *, u_int32_t,
70 		     u_int32_t);
71 void		 cidr2range(struct cidr, u_int32_t *, u_int32_t *);
72 char		*atop(u_int32_t);
73 int		 parse_netblock(char *, struct bl *, struct bl *, int);
74 int		 open_child(char *, char **);
75 int		 fileget(char *);
76 int		 open_file(char *, char *);
77 char		*fix_quoted_colons(char *);
78 void		 do_message(FILE *, char *);
79 struct bl	*add_blacklist(struct bl *, size_t *, size_t *, gzFile, int);
80 int		 cmpbl(const void *, const void *);
81 struct cidr	*collapse_blacklist(struct bl *, size_t);
82 int		 configure_spamd(u_short, char *, char *, struct cidr *);
83 int		 configure_pf(struct cidr *);
84 int		 getlist(char **, char *, struct blacklist *, struct blacklist *);
85 __dead void	 usage(void);
86 
87 int		  debug;
88 int		  dryrun;
89 int		  greyonly = 1;
90 
91 extern char 	 *__progname;
92 
93 u_int32_t
94 imask(u_int8_t b)
95 {
96 	if (b == 0)
97 		return (0);
98 	return (0xffffffff << (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, int *cli, 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 = MAX(maxsize, diff);
145 		if (*cls <= *cli + 1) {		/* one extra for terminator */
146 			tmp = realloc(list, (*cls + 32) * sizeof(struct cidr));
147 			if (tmp == NULL)
148 				errx(1, "malloc failed");
149 			list = tmp;
150 			*cls += 32;
151 		}
152 		list[*cli].addr = start;
153 		list[*cli].bits = maxsize;
154 		(*cli)++;
155 		start = start + (1 << (32 - maxsize));
156 	}
157 	return (list);
158 }
159 
160 void
161 cidr2range(struct cidr cidr, u_int32_t *start, u_int32_t *end)
162 {
163 	*start = cidr.addr;
164 	*end = cidr.addr + (1 << (32 - cidr.bits)) - 1;
165 }
166 
167 char *
168 atop(u_int32_t addr)
169 {
170 	struct in_addr in;
171 
172 	memset(&in, 0, sizeof(in));
173 	in.s_addr = htonl(addr);
174 	return (inet_ntoa(in));
175 }
176 
177 int
178 parse_netblock(char *buf, struct bl *start, struct bl *end, int white)
179 {
180 	char astring[16], astring2[16];
181 	unsigned maskbits;
182 	struct cidr c;
183 
184 	/* skip leading spaces */
185 	while (*buf == ' ')
186 		buf++;
187 	/* bail if it's a comment */
188 	if (*buf == '#')
189 		return (0);
190 	/* otherwise, look for a netblock of some sort */
191 	if (sscanf(buf, "%15[^/]/%u", astring, &maskbits) == 2) {
192 		/* looks like a cidr */
193 		memset(&c.addr, 0, sizeof(c.addr));
194 		if (inet_net_pton(AF_INET, astring, &c.addr, sizeof(c.addr))
195 		    == -1)
196 			return (0);
197 		c.addr = ntohl(c.addr);
198 		if (maskbits > 32)
199 			return (0);
200 		c.bits = maskbits;
201 		cidr2range(c, &start->addr, &end->addr);
202 		end->addr += 1;
203 	} else if (sscanf(buf, "%15[0123456789.]%*[ -]%15[0123456789.]",
204 	    astring, astring2) == 2) {
205 		/* looks like start - end */
206 		memset(&start->addr, 0, sizeof(start->addr));
207 		memset(&end->addr, 0, sizeof(end->addr));
208 		if (inet_net_pton(AF_INET, astring, &start->addr,
209 		    sizeof(start->addr)) == -1)
210 			return (0);
211 		start->addr = ntohl(start->addr);
212 		if (inet_net_pton(AF_INET, astring2, &end->addr,
213 		    sizeof(end->addr)) == -1)
214 			return (0);
215 		end->addr = ntohl(end->addr) + 1;
216 		if (start > end)
217 			return (0);
218 	} else if (sscanf(buf, "%15[0123456789.]", astring) == 1) {
219 		/* just a single address */
220 		memset(&start->addr, 0, sizeof(start->addr));
221 		if (inet_net_pton(AF_INET, astring, &start->addr,
222 		    sizeof(start->addr)) == -1)
223 			return (0);
224 		start->addr = ntohl(start->addr);
225 		end->addr = start->addr + 1;
226 	} else
227 		return (0);
228 
229 	if (white) {
230 		start->b = 0;
231 		start->w = 1;
232 		end->b = 0;
233 		end->w = -1;
234 	} else {
235 		start->b = 1;
236 		start->w = 0;
237 		end->b = -1;
238 		end->w = 0;
239 	}
240 	return (1);
241 }
242 
243 int
244 open_child(char *file, char **argv)
245 {
246 	int pdes[2];
247 
248 	if (pipe(pdes) != 0)
249 		return (-1);
250 	switch (fork()) {
251 	case -1:
252 		close(pdes[0]);
253 		close(pdes[1]);
254 		return (-1);
255 	case 0:
256 		/* child */
257 		close(pdes[0]);
258 		if (pdes[1] != STDOUT_FILENO) {
259 			dup2(pdes[1], STDOUT_FILENO);
260 			close(pdes[1]);
261 		}
262 		execvp(file, argv);
263 		_exit(1);
264 	}
265 
266 	/* parent */
267 	close(pdes[1]);
268 	return (pdes[0]);
269 }
270 
271 int
272 fileget(char *url)
273 {
274 	char *argv[6];
275 
276 	argv[0] = "ftp";
277 	argv[1] = "-V";
278 	argv[2] = "-o";
279 	argv[3] = "-";
280 	argv[4] = url;
281 	argv[5] = NULL;
282 
283 	if (debug)
284 		fprintf(stderr, "Getting %s\n", url);
285 
286 	return (open_child(PATH_FTP, argv));
287 }
288 
289 int
290 open_file(char *method, char *file)
291 {
292 	char *url;
293 	char **ap, **argv;
294 	int len, i, oerrno;
295 
296 	if ((method == NULL) || (strcmp(method, "file") == 0))
297 		return (open(file, O_RDONLY));
298 	if ((strcmp(method, "http") == 0) ||
299 	    strcmp(method, "ftp") == 0) {
300 		asprintf(&url, "%s://%s", method, file);
301 		if (url == NULL)
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 			errx(1, "malloc failed");
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 					errx(1, "malloc failed");
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 16 bytes */
482 	if (*blc + bu / 8 >= *bls) {
483 		*bls += bu / 8;
484 		blt = realloc(bl, *bls * sizeof(struct bl));
485 		if (blt == NULL) {
486 			*bls -= bu / 8;
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 = realloc(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 	if (buf)
515 		free(buf);
516 	if (serrno)
517 		errno = serrno;
518 	return (bl);
519 }
520 
521 int
522 cmpbl(const void *a, const void *b)
523 {
524 	if (((struct bl *)a)->addr > ((struct bl *) b)->addr)
525 		return (1);
526 	if (((struct bl *)a)->addr < ((struct bl *) b)->addr)
527 		return (-1);
528 	return (0);
529 }
530 
531 /*
532  * collapse_blacklist takes blacklist/whitelist entries sorts, removes
533  * overlaps and whitelist portions, and returns netblocks to blacklist
534  * as lists of nonoverlapping cidr blocks suitable for feeding in
535  * printable form to pfctl or spamd.
536  */
537 struct cidr *
538 collapse_blacklist(struct bl *bl, size_t blc)
539 {
540 	int bs = 0, ws = 0, state=0, 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 	cli = 0;
549 	cls = (blc / 2) + 1;
550 	cl = calloc(cls, sizeof(struct cidr));
551 	if (cl == NULL) {
552 		return (NULL);
553 	}
554 	qsort(bl, blc, sizeof(struct bl), cmpbl);
555 	for (i = 0; i < blc;) {
556 		laststate = state;
557 		addr = bl[i].addr;
558 
559 		do {
560 			bs += bl[i].b;
561 			ws += bl[i].w;
562 			i++;
563 		} while (bl[i].addr == addr);
564 		if (state == 1 && bs == 0)
565 			state = 0;
566 		else if (state == 0 && bs > 0)
567 			state = 1;
568 		if (ws > 0)
569 			state = 0;
570 		if (laststate == 0 && state == 1) {
571 			/* start blacklist */
572 			bstart = addr;
573 		}
574 		if (laststate == 1 && state == 0) {
575 			/* end blacklist */
576 			cl = range2cidrlist(cl, &cli, &cls, bstart, addr - 1);
577 		}
578 		laststate = state;
579 	}
580 	cl[cli].addr = 0;
581 	return (cl);
582 }
583 
584 int
585 configure_spamd(u_short dport, char *name, char *message,
586     struct cidr *blacklists)
587 {
588 	int lport = IPPORT_RESERVED - 1, s;
589 	struct sockaddr_in sin;
590 	FILE* sdc;
591 
592 	s = rresvport(&lport);
593 	if (s == -1)
594 		return (-1);
595 	memset(&sin, 0, sizeof sin);
596 	sin.sin_len = sizeof(sin);
597 	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
598 	sin.sin_family = AF_INET;
599 	sin.sin_port = htons(dport);
600 	if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1)
601 		return (-1);
602 	sdc = fdopen(s, "w");
603 	if (sdc == NULL) {
604 		close(s);
605 		return (-1);
606 	}
607 	fprintf(sdc, "%s", name);
608 	do_message(sdc, message);
609 	while (blacklists->addr != 0) {
610 		fprintf(sdc, ";%s/%u", atop(blacklists->addr),
611 		    blacklists->bits);
612 		blacklists++;
613 	}
614 	fputc('\n', sdc);
615 	fclose(sdc);
616 	close(s);
617 	return (0);
618 }
619 
620 
621 int
622 configure_pf(struct cidr *blacklists)
623 {
624 	char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace",
625 	    "-f" "-", NULL};
626 	static FILE *pf = NULL;
627 	int pdes[2];
628 
629 	if (pf == NULL) {
630 		if (pipe(pdes) != 0)
631 			return (-1);
632 		switch (fork()) {
633 		case -1:
634 			close(pdes[0]);
635 			close(pdes[1]);
636 			return (-1);
637 		case 0:
638 			/* child */
639 			close(pdes[1]);
640 			if (pdes[0] != STDIN_FILENO) {
641 				dup2(pdes[0], STDIN_FILENO);
642 				close(pdes[0]);
643 			}
644 			execvp(PATH_PFCTL, argv);
645 			_exit(1);
646 		}
647 
648 		/* parent */
649 		close(pdes[0]);
650 		pf = fdopen(pdes[1], "w");
651 		if (pf == NULL) {
652 			close(pdes[1]);
653 			return (-1);
654 		}
655 	}
656 	while (blacklists->addr != 0) {
657 		fprintf(pf, "%s/%u\n", atop(blacklists->addr),
658 		    blacklists->bits);
659 		blacklists++;
660 	}
661 	return (0);
662 }
663 
664 int
665 getlist(char ** db_array, char *name, struct blacklist *blist,
666     struct blacklist *blistnew)
667 {
668 	char *buf, *method, *file, *message;
669 	int fd, black = 0, serror;
670 	size_t blc, bls;
671 	struct bl *bl = NULL;
672 	gzFile gzf;
673 
674 	if (cgetent(&buf, db_array, name) != 0)
675 		err(1, "Can't find \"%s\" in spamd config", name);
676 	buf = fix_quoted_colons(buf);
677 	if (cgetcap(buf, "black", ':') != NULL) {
678 		/* use new list */
679 		black = 1;
680 		blc = blistnew->blc;
681 		bls = blistnew->bls;
682 		bl = blistnew->bl;
683 	} else if (cgetcap(buf, "white", ':') != NULL) {
684 		/* apply to most recent blacklist */
685 		black = 0;
686 		blc = blist->blc;
687 		bls = blist->bls;
688 		bl = blist->bl;
689 	} else
690 		errx(1, "Must have \"black\" or \"white\" in %s", name);
691 
692 	switch (cgetstr(buf, "msg", &message)) {
693 	case -1:
694 		if (black)
695 			errx(1, "No msg for blacklist \"%s\"", name);
696 		break;
697 	case -2:
698 		errx(1, "malloc failed");
699 	}
700 
701 	switch (cgetstr(buf, "method", &method)) {
702 	case -1:
703 		method = NULL;
704 		break;
705 	case -2:
706 		errx(1, "malloc failed");
707 	}
708 
709 	switch (cgetstr(buf, "file", &file)) {
710 	case -1:
711 		errx(1, "No file given for %slist %s",
712 		    black ? "black" : "white", name);
713 	case -2:
714 		errx(1, "malloc failed");
715 	default:
716 		fd = open_file(method, file);
717 		if (fd == -1)
718 			err(1, "Can't open %s by %s method",
719 			    file, method ? method : "file");
720 		free(method);
721 		free(file);
722 		gzf = gzdopen(fd, "r");
723 		if (gzf == NULL)
724 			errx(1, "gzdopen");
725 	}
726 	free(buf);
727 	bl = add_blacklist(bl, &blc, &bls, gzf, !black);
728 	serror = errno;
729 	gzclose(gzf);
730 	if (bl == NULL) {
731 		errno = serror;
732 		warn("Could not add %slist %s", black ? "black" : "white",
733 		    name);
734 		return (0);
735 	}
736 	if (black) {
737 		blistnew->message = message;
738 		blistnew->name = name;
739 		blistnew->black = black;
740 		blistnew->bl = bl;
741 		blistnew->blc = blc;
742 		blistnew->bls = bls;
743 	} else {
744 		/* whitelist applied to last active blacklist */
745 		blist->bl = bl;
746 		blist->blc = blc;
747 		blist->bls = bls;
748 	}
749 	if (debug)
750 		fprintf(stderr, "%slist %s %zu entries\n",
751 		    black ? "black" : "white", name, blc / 2);
752 	return (black);
753 }
754 
755 void
756 send_blacklist(struct blacklist *blist, in_port_t port)
757 {
758 	struct cidr *cidrs;
759 
760 	if (blist->blc > 0) {
761 		cidrs = collapse_blacklist(blist->bl, blist->blc);
762 		if (cidrs == NULL)
763 			errx(1, "malloc failed");
764 		if (!dryrun) {
765 			if (configure_spamd(port, blist->name,
766 			    blist->message, cidrs) == -1)
767 				err(1, "Can't connect to spamd on port %d",
768 				    port);
769 			if (!greyonly && configure_pf(cidrs) == -1)
770 				err(1, "pfctl failed");
771 		}
772 		free(cidrs);
773 		free(blist->bl);
774 	}
775 }
776 
777 __dead void
778 usage(void)
779 {
780 
781 	fprintf(stderr, "usage: %s [-bDdn]\n", __progname);
782 	exit(1);
783 }
784 
785 int
786 main(int argc, char *argv[])
787 {
788 	size_t blc, bls, black, white;
789 	char *db_array[2], *buf, *name;
790 	struct blacklist *blists;
791 	struct servent *ent;
792 	int daemonize = 0, ch;
793 
794 	while ((ch = getopt(argc, argv, "bdDn")) != -1) {
795 		switch (ch) {
796 		case 'n':
797 			dryrun = 1;
798 			break;
799 		case 'd':
800 			debug = 1;
801 			break;
802 		case 'b':
803 			greyonly = 0;
804 			break;
805 		case 'D':
806 			daemonize = 1;
807 			break;
808 		default:
809 			usage();
810 			break;
811 		}
812 	}
813 	argc -= optind;
814 	argv += optind;
815 	if (argc != 0)
816 		usage();
817 
818 	if (daemonize)
819 		daemon(0, 0);
820 
821 	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
822 		errx(1, "cannot find service \"spamd-cfg\" in /etc/services");
823 	ent->s_port = ntohs(ent->s_port);
824 
825 	db_array[0] = PATH_SPAMD_CONF;
826 	db_array[1] = NULL;
827 
828 	if (cgetent(&buf, db_array, "all") != 0)
829 		err(1, "Can't find \"all\" in spamd config");
830 	name = strsep(&buf, ": \t"); /* skip "all" at start */
831 	blists = NULL;
832 	blc = bls = 0;
833 	while ((name = strsep(&buf, ": \t")) != NULL) {
834 		if (*name) {
835 			/* extract config in order specified in "all" tag */
836 			if (blc == bls) {
837 				struct blacklist *tmp;
838 
839 				bls += 32;
840 				tmp = realloc(blists,
841 				    bls * sizeof(struct blacklist));
842 				if (tmp == NULL)
843 					errx(1, "malloc failed");
844 				blists = tmp;
845 			}
846 			if (blc == 0)
847 				black = white = 0;
848 			else {
849 				white = blc - 1;
850 				black = blc;
851 			}
852 			memset(&blists[black], 0, sizeof(struct blacklist));
853 			black = getlist(db_array, name, &blists[white],
854 			    &blists[black]);
855 			if (black && blc > 0) {
856 				/* collapse and free previous blacklist */
857 				send_blacklist(&blists[blc - 1], ent->s_port);
858 			}
859 			blc += black;
860 		}
861 	}
862 	/* collapse and free last blacklist */
863 	if (blc > 0)
864 		send_blacklist(&blists[blc - 1], ent->s_port);
865 	return (0);
866 }
867