xref: /openbsd/libexec/spamd-setup/spamd-setup.c (revision 404b540a)
1 /*	$OpenBSD: spamd-setup.c,v 1.37 2009/09/09 16:05:55 claudio 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/param.h>
28 #include <sys/socket.h>
29 #include <netinet/in.h>
30 #include <arpa/inet.h>
31 #include <errno.h>
32 #include <fcntl.h>
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <unistd.h>
37 #include <err.h>
38 #include <netinet/ip_ipsp.h>
39 #include <netdb.h>
40 #include <zlib.h>
41 
42 #define PATH_FTP		"/usr/bin/ftp"
43 #define PATH_PFCTL		"/sbin/pfctl"
44 #define PATH_SPAMD_CONF		"/etc/mail/spamd.conf"
45 #define SPAMD_ARG_MAX		256 /* max # of args to an exec */
46 
47 struct cidr {
48 	u_int32_t addr;
49 	u_int8_t bits;
50 };
51 
52 struct bl {
53 	u_int32_t addr;
54 	int8_t b;
55 	int8_t w;
56 };
57 
58 struct blacklist {
59 	char *name;
60 	char *message;
61 	struct bl *bl;
62 	size_t blc, bls;
63 	u_int8_t black;
64 	int count;
65 };
66 
67 u_int32_t	 imask(u_int8_t);
68 u_int8_t	 maxblock(u_int32_t, u_int8_t);
69 u_int8_t	 maxdiff(u_int32_t, u_int32_t);
70 struct cidr	*range2cidrlist(struct cidr *, int *, int *, u_int32_t,
71 		     u_int32_t);
72 void		 cidr2range(struct cidr, u_int32_t *, u_int32_t *);
73 char		*atop(u_int32_t);
74 int		 parse_netblock(char *, struct bl *, struct bl *, int);
75 int		 open_child(char *, char **);
76 int		 fileget(char *);
77 int		 open_file(char *, char *);
78 char		*fix_quoted_colons(char *);
79 void		 do_message(FILE *, char *);
80 struct bl	*add_blacklist(struct bl *, size_t *, size_t *, gzFile, int);
81 int		 cmpbl(const void *, const void *);
82 struct cidr	*collapse_blacklist(struct bl *, size_t);
83 int		 configure_spamd(u_short, char *, char *, struct cidr *);
84 int		 configure_pf(struct cidr *);
85 int		 getlist(char **, char *, struct blacklist *, struct blacklist *);
86 __dead void	 usage(void);
87 
88 int		  debug;
89 int		  dryrun;
90 int		  greyonly = 1;
91 
92 extern char 	 *__progname;
93 
94 u_int32_t
95 imask(u_int8_t b)
96 {
97 	if (b == 0)
98 		return (0);
99 	return (0xffffffff << (32 - b));
100 }
101 
102 u_int8_t
103 maxblock(u_int32_t addr, u_int8_t bits)
104 {
105 	u_int32_t m;
106 
107 	while (bits > 0) {
108 		m = imask(bits - 1);
109 
110 		if ((addr & m) != addr)
111 			return (bits);
112 		bits--;
113 	}
114 	return (bits);
115 }
116 
117 u_int8_t
118 maxdiff(u_int32_t a, u_int32_t b)
119 {
120 	u_int8_t bits = 0;
121 	u_int32_t m;
122 
123 	b++;
124 	while (bits < 32) {
125 		m = imask(bits);
126 
127 		if ((a & m) != (b & m))
128 			return (bits);
129 		bits++;
130 	}
131 	return (bits);
132 }
133 
134 struct cidr *
135 range2cidrlist(struct cidr *list, int *cli, int *cls, u_int32_t start,
136     u_int32_t end)
137 {
138 	u_int8_t maxsize, diff;
139 	struct cidr *tmp;
140 
141 	while (end >= start) {
142 		maxsize = maxblock(start, 32);
143 		diff = maxdiff(start, end);
144 
145 		maxsize = MAX(maxsize, diff);
146 		if (*cls <= *cli + 1) {		/* one extra for terminator */
147 			tmp = realloc(list, (*cls + 32) * sizeof(struct cidr));
148 			if (tmp == NULL)
149 				errx(1, "malloc failed");
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 		asprintf(&url, "%s://%s", method, file);
302 		if (url == NULL)
303 			return (-1);
304 		i = fileget(url);
305 		free(url);
306 		return (i);
307 	} else if (strcmp(method, "exec") == 0) {
308 		len = strlen(file);
309 		argv = calloc(len, sizeof(char *));
310 		if (argv == NULL)
311 			errx(1, "malloc failed");
312 		for (ap = argv; ap < &argv[len - 1] &&
313 		    (*ap = strsep(&file, " \t")) != NULL;) {
314 			if (**ap != '\0')
315 				ap++;
316 		}
317 		*ap = NULL;
318 		i = open_child(argv[0], argv);
319 		oerrno = errno;
320 		free(argv);
321 		errno = oerrno;
322 		return (i);
323 	}
324 	errx(1, "Unknown method %s", method);
325 	return (-1); /* NOTREACHED */
326 }
327 
328 /*
329  * fix_quoted_colons walks through a buffer returned by cgetent.  We
330  * look for quoted strings, to escape colons (:) in quoted strings for
331  * getcap by replacing them with \C so cgetstr() deals with it correctly
332  * without having to see the \C bletchery in a configuration file that
333  * needs to have urls in it. Frees the buffer passed to it, passes back
334  * another larger one, with can be used with cgetxxx(), like the original
335  * buffer, it must be freed by the caller.
336  * This should really be a temporary fix until there is a sanctioned
337  * way to make getcap(3) handle quoted strings like this in a nicer
338  * way.
339  */
340 char *
341 fix_quoted_colons(char *buf)
342 {
343 	int in = 0;
344 	size_t i, j = 0;
345 	char *newbuf, last;
346 
347 	/* Allocate enough space for a buf of all colons (impossible). */
348 	newbuf = malloc(2 * strlen(buf) + 1);
349 	if (newbuf == NULL)
350 		return (NULL);
351 	last = '\0';
352 	for (i = 0; i < strlen(buf); i++) {
353 		switch (buf[i]) {
354 		case ':':
355 			if (in) {
356 				newbuf[j++] = '\\';
357 				newbuf[j++] = 'C';
358 			} else
359 				newbuf[j++] = buf[i];
360 			break;
361 		case '"':
362 			if (last != '\\')
363 				in = !in;
364 			newbuf[j++] = buf[i];
365 			break;
366 		default:
367 			newbuf[j++] = buf[i];
368 		}
369 		last = buf[i];
370 	}
371 	free(buf);
372 	newbuf[j] = '\0';
373 	return (newbuf);
374 }
375 
376 void
377 do_message(FILE *sdc, char *msg)
378 {
379 	size_t i, bs = 0, bu = 0, len;
380 	ssize_t n;
381 	char *buf = NULL, last, *tmp;
382 	int fd;
383 
384 	len = strlen(msg);
385 	if (msg[0] == '"' && msg[len - 1] == '"') {
386 		/* quoted msg, escape newlines and send it out */
387 		msg[len - 1] = '\0';
388 		buf = msg + 1;
389 		bu = len - 2;
390 		goto sendit;
391 	} else {
392 		/*
393 		 * message isn't quoted - try to open a local
394 		 * file and read the message from it.
395 		 */
396 		fd = open(msg, O_RDONLY);
397 		if (fd == -1)
398 			err(1, "Can't open message from %s", msg);
399 		for (;;) {
400 			if (bu == bs) {
401 				tmp = realloc(buf, bs + 8192);
402 				if (tmp == NULL)
403 					errx(1, "malloc failed");
404 				bs += 8192;
405 				buf = tmp;
406 			}
407 
408 			n = read(fd, buf + bu, bs - bu);
409 			if (n == 0) {
410 				goto sendit;
411 			} else if (n == -1) {
412 				err(1, "Can't read from %s", msg);
413 			} else
414 				bu += n;
415 		}
416 		buf[bu]='\0';
417 	}
418  sendit:
419 	fprintf(sdc, ";\"");
420 	last = '\0';
421 	for (i = 0; i < bu; i++) {
422 		/* handle escaping the things spamd wants */
423 		switch (buf[i]) {
424 		case 'n':
425 			if (last == '\\')
426 				fprintf(sdc, "\\\\n");
427 			else
428 				fputc('n', sdc);
429 			last = '\0';
430 			break;
431 		case '\n':
432 			fprintf(sdc, "\\n");
433 			last = '\0';
434 			break;
435 		case '"':
436 			fputc('\\', sdc);
437 			/* FALLTHROUGH */
438 		default:
439 			fputc(buf[i], sdc);
440 			last = '\0';
441 		}
442 	}
443 	fputc('"', sdc);
444 	if (bs != 0)
445 		free(buf);
446 }
447 
448 /* retrieve a list from fd. add to blacklist bl */
449 struct bl *
450 add_blacklist(struct bl *bl, size_t *blc, size_t *bls, gzFile gzf, int white)
451 {
452 	int i, n, start, bu = 0, bs = 0, serrno = 0;
453 	char *buf = NULL, *tmp;
454 	struct bl *blt;
455 
456 	for (;;) {
457 		/* read in gzf, then parse */
458 		if (bu == bs) {
459 			tmp = realloc(buf, bs + (1024 * 1024) + 1);
460 			if (tmp == NULL) {
461 				serrno = errno;
462 				free(buf);
463 				buf = NULL;
464 				bs = 0;
465 				goto bldone;
466 			}
467 			bs += 1024 * 1024;
468 			buf = tmp;
469 		}
470 
471 		n = gzread(gzf, buf + bu, bs - bu);
472 		if (n == 0)
473 			goto parse;
474 		else if (n == -1) {
475 			serrno = errno;
476 			goto bldone;
477 		} else
478 			bu += n;
479 	}
480  parse:
481 	start = 0;
482 	/* we assume that there is an IP for every 16 bytes */
483 	if (*blc + bu / 8 >= *bls) {
484 		*bls += bu / 8;
485 		blt = realloc(bl, *bls * sizeof(struct bl));
486 		if (blt == NULL) {
487 			*bls -= bu / 8;
488 			serrno = errno;
489 			goto bldone;
490 		}
491 		bl = blt;
492 	}
493 	for (i = 0; i <= bu; i++) {
494 		if (*blc + 1 >= *bls) {
495 			*bls += 1024;
496 			blt = realloc(bl, *bls * sizeof(struct bl));
497 			if (blt == NULL) {
498 				*bls -= 1024;
499 				serrno = errno;
500 				goto bldone;
501 			}
502 			bl = blt;
503 		}
504 		if (i == bu || buf[i] == '\n') {
505 			buf[i] = '\0';
506 			if (parse_netblock(buf + start,
507 			    bl + *blc, bl + *blc + 1, white))
508 				*blc += 2;
509 			start = i + 1;
510 		}
511 	}
512 	if (bu == 0)
513 		errno = EIO;
514  bldone:
515 	if (buf)
516 		free(buf);
517 	if (serrno)
518 		errno = serrno;
519 	return (bl);
520 }
521 
522 int
523 cmpbl(const void *a, const void *b)
524 {
525 	if (((struct bl *)a)->addr > ((struct bl *) b)->addr)
526 		return (1);
527 	if (((struct bl *)a)->addr < ((struct bl *) b)->addr)
528 		return (-1);
529 	return (0);
530 }
531 
532 /*
533  * collapse_blacklist takes blacklist/whitelist entries sorts, removes
534  * overlaps and whitelist portions, and returns netblocks to blacklist
535  * as lists of nonoverlapping cidr blocks suitable for feeding in
536  * printable form to pfctl or spamd.
537  */
538 struct cidr *
539 collapse_blacklist(struct bl *bl, size_t blc)
540 {
541 	int bs = 0, ws = 0, state=0, cli, cls, i;
542 	u_int32_t bstart = 0;
543 	struct cidr *cl;
544 	int laststate;
545 	u_int32_t addr;
546 
547 	if (blc == 0)
548 		return (NULL);
549 	cli = 0;
550 	cls = (blc / 2) + 1;
551 	cl = calloc(cls, sizeof(struct cidr));
552 	if (cl == NULL) {
553 		return (NULL);
554 	}
555 	qsort(bl, blc, sizeof(struct bl), cmpbl);
556 	for (i = 0; i < blc;) {
557 		laststate = state;
558 		addr = bl[i].addr;
559 
560 		do {
561 			bs += bl[i].b;
562 			ws += bl[i].w;
563 			i++;
564 		} while (bl[i].addr == addr);
565 		if (state == 1 && bs == 0)
566 			state = 0;
567 		else if (state == 0 && bs > 0)
568 			state = 1;
569 		if (ws > 0)
570 			state = 0;
571 		if (laststate == 0 && state == 1) {
572 			/* start blacklist */
573 			bstart = addr;
574 		}
575 		if (laststate == 1 && state == 0) {
576 			/* end blacklist */
577 			cl = range2cidrlist(cl, &cli, &cls, bstart, addr - 1);
578 		}
579 		laststate = state;
580 	}
581 	cl[cli].addr = 0;
582 	return (cl);
583 }
584 
585 int
586 configure_spamd(u_short dport, char *name, char *message,
587     struct cidr *blacklists)
588 {
589 	int lport = IPPORT_RESERVED - 1, s;
590 	struct sockaddr_in sin;
591 	FILE* sdc;
592 
593 	s = rresvport(&lport);
594 	if (s == -1)
595 		return (-1);
596 	memset(&sin, 0, sizeof sin);
597 	sin.sin_len = sizeof(sin);
598 	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
599 	sin.sin_family = AF_INET;
600 	sin.sin_port = htons(dport);
601 	if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1)
602 		return (-1);
603 	sdc = fdopen(s, "w");
604 	if (sdc == NULL) {
605 		close(s);
606 		return (-1);
607 	}
608 	fprintf(sdc, "%s", name);
609 	do_message(sdc, message);
610 	while (blacklists->addr != 0) {
611 		fprintf(sdc, ";%s/%u", atop(blacklists->addr),
612 		    blacklists->bits);
613 		blacklists++;
614 	}
615 	fputc('\n', sdc);
616 	fclose(sdc);
617 	close(s);
618 	return (0);
619 }
620 
621 
622 int
623 configure_pf(struct cidr *blacklists)
624 {
625 	char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace",
626 	    "-f" "-", NULL};
627 	static FILE *pf = NULL;
628 	int pdes[2];
629 
630 	if (pf == NULL) {
631 		if (pipe(pdes) != 0)
632 			return (-1);
633 		switch (fork()) {
634 		case -1:
635 			close(pdes[0]);
636 			close(pdes[1]);
637 			return (-1);
638 		case 0:
639 			/* child */
640 			close(pdes[1]);
641 			if (pdes[0] != STDIN_FILENO) {
642 				dup2(pdes[0], STDIN_FILENO);
643 				close(pdes[0]);
644 			}
645 			execvp(PATH_PFCTL, argv);
646 			_exit(1);
647 		}
648 
649 		/* parent */
650 		close(pdes[0]);
651 		pf = fdopen(pdes[1], "w");
652 		if (pf == NULL) {
653 			close(pdes[1]);
654 			return (-1);
655 		}
656 	}
657 	while (blacklists->addr != 0) {
658 		fprintf(pf, "%s/%u\n", atop(blacklists->addr),
659 		    blacklists->bits);
660 		blacklists++;
661 	}
662 	return (0);
663 }
664 
665 int
666 getlist(char ** db_array, char *name, struct blacklist *blist,
667     struct blacklist *blistnew)
668 {
669 	char *buf, *method, *file, *message;
670 	int fd, black = 0, serror;
671 	size_t blc, bls;
672 	struct bl *bl = NULL;
673 	gzFile gzf;
674 
675 	if (cgetent(&buf, db_array, name) != 0)
676 		err(1, "Can't find \"%s\" in spamd config", name);
677 	buf = fix_quoted_colons(buf);
678 	if (cgetcap(buf, "black", ':') != NULL) {
679 		/* use new list */
680 		black = 1;
681 		blc = blistnew->blc;
682 		bls = blistnew->bls;
683 		bl = blistnew->bl;
684 	} else if (cgetcap(buf, "white", ':') != NULL) {
685 		/* apply to most recent blacklist */
686 		black = 0;
687 		blc = blist->blc;
688 		bls = blist->bls;
689 		bl = blist->bl;
690 	} else
691 		errx(1, "Must have \"black\" or \"white\" in %s", name);
692 
693 	switch (cgetstr(buf, "msg", &message)) {
694 	case -1:
695 		if (black)
696 			errx(1, "No msg for blacklist \"%s\"", name);
697 		break;
698 	case -2:
699 		errx(1, "malloc failed");
700 	}
701 
702 	switch (cgetstr(buf, "method", &method)) {
703 	case -1:
704 		method = NULL;
705 		break;
706 	case -2:
707 		errx(1, "malloc failed");
708 	}
709 
710 	switch (cgetstr(buf, "file", &file)) {
711 	case -1:
712 		errx(1, "No file given for %slist %s",
713 		    black ? "black" : "white", name);
714 	case -2:
715 		errx(1, "malloc failed");
716 	default:
717 		fd = open_file(method, file);
718 		if (fd == -1)
719 			err(1, "Can't open %s by %s method",
720 			    file, method ? method : "file");
721 		free(method);
722 		free(file);
723 		gzf = gzdopen(fd, "r");
724 		if (gzf == NULL)
725 			errx(1, "gzdopen");
726 	}
727 	free(buf);
728 	bl = add_blacklist(bl, &blc, &bls, gzf, !black);
729 	serror = errno;
730 	gzclose(gzf);
731 	if (bl == NULL) {
732 		errno = serror;
733 		warn("Could not add %slist %s", black ? "black" : "white",
734 		    name);
735 		return (0);
736 	}
737 	if (black) {
738 		blistnew->message = message;
739 		blistnew->name = name;
740 		blistnew->black = black;
741 		blistnew->bl = bl;
742 		blistnew->blc = blc;
743 		blistnew->bls = bls;
744 	} else {
745 		/* whitelist applied to last active blacklist */
746 		blist->bl = bl;
747 		blist->blc = blc;
748 		blist->bls = bls;
749 	}
750 	if (debug)
751 		fprintf(stderr, "%slist %s %zu entries\n",
752 		    black ? "black" : "white", name, blc / 2);
753 	return (black);
754 }
755 
756 void
757 send_blacklist(struct blacklist *blist, in_port_t port)
758 {
759 	struct cidr *cidrs;
760 
761 	if (blist->blc > 0) {
762 		cidrs = collapse_blacklist(blist->bl, blist->blc);
763 		if (cidrs == NULL)
764 			errx(1, "malloc failed");
765 		if (!dryrun) {
766 			if (configure_spamd(port, blist->name,
767 			    blist->message, cidrs) == -1)
768 				err(1, "Can't connect to spamd on port %d",
769 				    port);
770 			if (!greyonly && configure_pf(cidrs) == -1)
771 				err(1, "pfctl failed");
772 		}
773 		free(cidrs);
774 		free(blist->bl);
775 	}
776 }
777 
778 __dead void
779 usage(void)
780 {
781 
782 	fprintf(stderr, "usage: %s [-bDdn]\n", __progname);
783 	exit(1);
784 }
785 
786 int
787 main(int argc, char *argv[])
788 {
789 	size_t blc, bls, black, white;
790 	char *db_array[2], *buf, *name;
791 	struct blacklist *blists;
792 	struct servent *ent;
793 	int daemonize = 0, ch;
794 
795 	while ((ch = getopt(argc, argv, "bdDn")) != -1) {
796 		switch (ch) {
797 		case 'n':
798 			dryrun = 1;
799 			break;
800 		case 'd':
801 			debug = 1;
802 			break;
803 		case 'b':
804 			greyonly = 0;
805 			break;
806 		case 'D':
807 			daemonize = 1;
808 			break;
809 		default:
810 			usage();
811 			break;
812 		}
813 	}
814 	argc -= optind;
815 	argv += optind;
816 	if (argc != 0)
817 		usage();
818 
819 	if (daemonize)
820 		daemon(0, 0);
821 
822 	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
823 		errx(1, "cannot find service \"spamd-cfg\" in /etc/services");
824 	ent->s_port = ntohs(ent->s_port);
825 
826 	db_array[0] = PATH_SPAMD_CONF;
827 	db_array[1] = NULL;
828 
829 	if (cgetent(&buf, db_array, "all") != 0)
830 		err(1, "Can't find \"all\" in spamd config");
831 	name = strsep(&buf, ": \t"); /* skip "all" at start */
832 	blists = NULL;
833 	blc = bls = 0;
834 	while ((name = strsep(&buf, ": \t")) != NULL) {
835 		if (*name) {
836 			/* extract config in order specified in "all" tag */
837 			if (blc == bls) {
838 				struct blacklist *tmp;
839 
840 				bls += 32;
841 				tmp = realloc(blists,
842 				    bls * sizeof(struct blacklist));
843 				if (tmp == NULL)
844 					errx(1, "malloc failed");
845 				blists = tmp;
846 			}
847 			if (blc == 0)
848 				black = white = 0;
849 			else {
850 				white = blc - 1;
851 				black = blc;
852 			}
853 			memset(&blists[black], 0, sizeof(struct blacklist));
854 			black = getlist(db_array, name, &blists[white],
855 			    &blists[black]);
856 			if (black && blc > 0) {
857 				/* collapse and free previous blacklist */
858 				send_blacklist(&blists[blc - 1], ent->s_port);
859 			}
860 			blc += black;
861 		}
862 	}
863 	/* collapse and free last blacklist */
864 	if (blc > 0)
865 		send_blacklist(&blists[blc - 1], ent->s_port);
866 	return (0);
867 }
868