xref: /openbsd-src/libexec/spamd-setup/spamd-setup.c (revision 2b0358df1d88d06ef4139321dd05bd5e05d91eaf)
1 /*	$OpenBSD: spamd-setup.c,v 1.35 2008/10/03 18:58:52 jmc 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(u_int32_t, 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 	u_int32_t j = 0;
97 	int i;
98 
99 	for (i = 31; i > 31 - b; --i)
100 		j |= (1 << i);
101 	return (j);
102 }
103 
104 u_int8_t
105 maxblock(u_int32_t addr, u_int8_t bits)
106 {
107 	u_int32_t m;
108 
109 	while (bits > 0) {
110 		m = imask(bits - 1);
111 
112 		if ((addr & m) != addr)
113 			return (bits);
114 		bits--;
115 	}
116 	return (bits);
117 }
118 
119 u_int8_t
120 maxdiff(u_int32_t a, u_int32_t b)
121 {
122 	u_int8_t bits = 0;
123 	u_int32_t m;
124 
125 	b++;
126 	while (bits < 32) {
127 		m = imask(bits);
128 
129 		if ((a & m) != (b & m))
130 			return (bits);
131 		bits++;
132 	}
133 	return (bits);
134 }
135 
136 struct cidr *
137 range2cidrlist(u_int32_t start, u_int32_t end)
138 {
139 	struct cidr *list = NULL;
140 	size_t cs = 0, cu = 0;
141 	u_int8_t maxsize, diff;
142 	struct cidr *tmp;
143 
144 	while (end >= start) {
145 		maxsize = maxblock(start, 32);
146 		diff = maxdiff(start, end);
147 
148 		maxsize = MAX(maxsize, diff);
149 		if (cs <= cu + 1) {		/* one extra for terminator */
150 			tmp = realloc(list, (cs + 32) * sizeof(struct cidr));
151 			if (tmp == NULL)
152 				errx(1, "malloc failed");
153 			list = tmp;
154 			cs += 32;
155 		}
156 		list[cu].addr = start;
157 		list[cu].bits = maxsize;
158 		cu++;
159 		list[cu].addr = 0;
160 		list[cu].bits = 0;
161 		start = start + (1 << (32 - maxsize));
162 	}
163 	return (list);
164 }
165 
166 void
167 cidr2range(struct cidr cidr, u_int32_t *start, u_int32_t *end)
168 {
169 	*start = cidr.addr;
170 	*end = cidr.addr + (1 << (32 - cidr.bits)) - 1;
171 }
172 
173 char *
174 atop(u_int32_t addr)
175 {
176 	struct in_addr in;
177 
178 	memset(&in, 0, sizeof(in));
179 	in.s_addr = htonl(addr);
180 	return (inet_ntoa(in));
181 }
182 
183 int
184 parse_netblock(char *buf, struct bl *start, struct bl *end, int white)
185 {
186 	char astring[16], astring2[16];
187 	unsigned maskbits;
188 	struct cidr c;
189 
190 	/* skip leading spaces */
191 	while (*buf == ' ')
192 		buf++;
193 	/* bail if it's a comment */
194 	if (*buf == '#')
195 		return (0);
196 	/* otherwise, look for a netblock of some sort */
197 	if (sscanf(buf, "%15[^/]/%u", astring, &maskbits) == 2) {
198 		/* looks like a cidr */
199 		memset(&c.addr, 0, sizeof(c.addr));
200 		if (inet_net_pton(AF_INET, astring, &c.addr, sizeof(c.addr))
201 		    == -1)
202 			return (0);
203 		c.addr = ntohl(c.addr);
204 		if (maskbits > 32)
205 			return (0);
206 		c.bits = maskbits;
207 		cidr2range(c, &start->addr, &end->addr);
208 		end->addr += 1;
209 	} else if (sscanf(buf, "%15[0123456789.]%*[ -]%15[0123456789.]",
210 	    astring, astring2) == 2) {
211 		/* looks like start - end */
212 		memset(&start->addr, 0, sizeof(start->addr));
213 		memset(&end->addr, 0, sizeof(end->addr));
214 		if (inet_net_pton(AF_INET, astring, &start->addr,
215 		    sizeof(start->addr)) == -1)
216 			return (0);
217 		start->addr = ntohl(start->addr);
218 		if (inet_net_pton(AF_INET, astring2, &end->addr,
219 		    sizeof(end->addr)) == -1)
220 			return (0);
221 		end->addr = ntohl(end->addr) + 1;
222 		if (start > end)
223 			return (0);
224 	} else if (sscanf(buf, "%15[0123456789.]", astring) == 1) {
225 		/* just a single address */
226 		memset(&start->addr, 0, sizeof(start->addr));
227 		if (inet_net_pton(AF_INET, astring, &start->addr,
228 		    sizeof(start->addr)) == -1)
229 			return (0);
230 		start->addr = ntohl(start->addr);
231 		end->addr = start->addr + 1;
232 	} else
233 		return (0);
234 
235 	if (white) {
236 		start->b = 0;
237 		start->w = 1;
238 		end->b = 0;
239 		end->w = -1;
240 	} else {
241 		start->b = 1;
242 		start->w = 0;
243 		end->b = -1;
244 		end->w = 0;
245 	}
246 	return (1);
247 }
248 
249 int
250 open_child(char *file, char **argv)
251 {
252 	int pdes[2];
253 
254 	if (pipe(pdes) != 0)
255 		return (-1);
256 	switch (fork()) {
257 	case -1:
258 		close(pdes[0]);
259 		close(pdes[1]);
260 		return (-1);
261 	case 0:
262 		/* child */
263 		close(pdes[0]);
264 		if (pdes[1] != STDOUT_FILENO) {
265 			dup2(pdes[1], STDOUT_FILENO);
266 			close(pdes[1]);
267 		}
268 		execvp(file, argv);
269 		_exit(1);
270 	}
271 
272 	/* parent */
273 	close(pdes[1]);
274 	return (pdes[0]);
275 }
276 
277 int
278 fileget(char *url)
279 {
280 	char *argv[6];
281 
282 	argv[0] = "ftp";
283 	argv[1] = "-V";
284 	argv[2] = "-o";
285 	argv[3] = "-";
286 	argv[4] = url;
287 	argv[5] = NULL;
288 
289 	if (debug)
290 		fprintf(stderr, "Getting %s\n", url);
291 
292 	return (open_child(PATH_FTP, argv));
293 }
294 
295 int
296 open_file(char *method, char *file)
297 {
298 	char *url;
299 	char **ap, **argv;
300 	int len, i, oerrno;
301 
302 	if ((method == NULL) || (strcmp(method, "file") == 0))
303 		return (open(file, O_RDONLY));
304 	if ((strcmp(method, "http") == 0) ||
305 	    strcmp(method, "ftp") == 0) {
306 		asprintf(&url, "%s://%s", method, file);
307 		if (url == NULL)
308 			return (-1);
309 		i = fileget(url);
310 		free(url);
311 		return (i);
312 	} else if (strcmp(method, "exec") == 0) {
313 		len = strlen(file);
314 		argv = calloc(len, sizeof(char *));
315 		if (argv == NULL)
316 			errx(1, "malloc failed");
317 		for (ap = argv; ap < &argv[len - 1] &&
318 		    (*ap = strsep(&file, " \t")) != NULL;) {
319 			if (**ap != '\0')
320 				ap++;
321 		}
322 		*ap = NULL;
323 		i = open_child(argv[0], argv);
324 		oerrno = errno;
325 		free(argv);
326 		errno = oerrno;
327 		return (i);
328 	}
329 	errx(1, "Unknown method %s", method);
330 	return (-1); /* NOTREACHED */
331 }
332 
333 /*
334  * fix_quoted_colons walks through a buffer returned by cgetent.  We
335  * look for quoted strings, to escape colons (:) in quoted strings for
336  * getcap by replacing them with \C so cgetstr() deals with it correctly
337  * without having to see the \C bletchery in a configuration file that
338  * needs to have urls in it. Frees the buffer passed to it, passes back
339  * another larger one, with can be used with cgetxxx(), like the original
340  * buffer, it must be freed by the caller.
341  * This should really be a temporary fix until there is a sanctioned
342  * way to make getcap(3) handle quoted strings like this in a nicer
343  * way.
344  */
345 char *
346 fix_quoted_colons(char *buf)
347 {
348 	int in = 0;
349 	size_t i, j = 0;
350 	char *newbuf, last;
351 
352 	/* Allocate enough space for a buf of all colons (impossible). */
353 	newbuf = malloc(2 * strlen(buf) + 1);
354 	if (newbuf == NULL)
355 		return (NULL);
356 	last = '\0';
357 	for (i = 0; i < strlen(buf); i++) {
358 		switch (buf[i]) {
359 		case ':':
360 			if (in) {
361 				newbuf[j++] = '\\';
362 				newbuf[j++] = 'C';
363 			} else
364 				newbuf[j++] = buf[i];
365 			break;
366 		case '"':
367 			if (last != '\\')
368 				in = !in;
369 			newbuf[j++] = buf[i];
370 			break;
371 		default:
372 			newbuf[j++] = buf[i];
373 		}
374 		last = buf[i];
375 	}
376 	free(buf);
377 	newbuf[j] = '\0';
378 	return (newbuf);
379 }
380 
381 void
382 do_message(FILE *sdc, char *msg)
383 {
384 	size_t i, bs = 0, bu = 0, len;
385 	ssize_t n;
386 	char *buf = NULL, last, *tmp;
387 	int fd;
388 
389 	len = strlen(msg);
390 	if (msg[0] == '"' && msg[len - 1] == '"') {
391 		/* quoted msg, escape newlines and send it out */
392 		msg[len - 1] = '\0';
393 		buf = msg + 1;
394 		bu = len - 2;
395 		goto sendit;
396 	} else {
397 		/*
398 		 * message isn't quoted - try to open a local
399 		 * file and read the message from it.
400 		 */
401 		fd = open(msg, O_RDONLY);
402 		if (fd == -1)
403 			err(1, "Can't open message from %s", msg);
404 		for (;;) {
405 			if (bu == bs) {
406 				tmp = realloc(buf, bs + 8192);
407 				if (tmp == NULL)
408 					errx(1, "malloc failed");
409 				bs += 8192;
410 				buf = tmp;
411 			}
412 
413 			n = read(fd, buf + bu, bs - bu);
414 			if (n == 0) {
415 				goto sendit;
416 			} else if (n == -1) {
417 				err(1, "Can't read from %s", msg);
418 			} else
419 				bu += n;
420 		}
421 		buf[bu]='\0';
422 	}
423  sendit:
424 	fprintf(sdc, ";\"");
425 	last = '\0';
426 	for (i = 0; i < bu; i++) {
427 		/* handle escaping the things spamd wants */
428 		switch (buf[i]) {
429 		case 'n':
430 			if (last == '\\')
431 				fprintf(sdc, "\\\\n");
432 			else
433 				fputc('n', sdc);
434 			last = '\0';
435 			break;
436 		case '\n':
437 			fprintf(sdc, "\\n");
438 			last = '\0';
439 			break;
440 		case '"':
441 			fputc('\\', sdc);
442 			/* FALLTHROUGH */
443 		default:
444 			fputc(buf[i], sdc);
445 			last = '\0';
446 		}
447 	}
448 	fputc('"', sdc);
449 	if (bs != 0)
450 		free(buf);
451 }
452 
453 /* retrieve a list from fd. add to blacklist bl */
454 struct bl *
455 add_blacklist(struct bl *bl, size_t *blc, size_t *bls, gzFile gzf, int white)
456 {
457 	int i, n, start, bu = 0, bs = 0, serrno = 0;
458 	char *buf = NULL, *tmp;
459 	struct bl *blt;
460 
461 	for (;;) {
462 		/* read in gzf, then parse */
463 		if (bu == bs) {
464 			tmp = realloc(buf, bs + (1024 * 1024) + 1);
465 			if (tmp == NULL) {
466 				free(buf);
467 				buf = NULL;
468 				bs = 0;
469 				serrno = errno;
470 				goto bldone;
471 			}
472 			bs += 1024 * 1024;
473 			buf = tmp;
474 		}
475 
476 		n = gzread(gzf, buf + bu, bs - bu);
477 		if (n == 0)
478 			goto parse;
479 		else if (n == -1) {
480 			serrno = errno;
481 			goto bldone;
482 		} else
483 			bu += n;
484 	}
485  parse:
486 	start = 0;
487 	for (i = 0; i <= bu; i++) {
488 		if (*blc == *bls) {
489 			*bls += 1024;
490 			blt = realloc(bl, *bls * sizeof(struct bl));
491 			if (blt == NULL) {
492 				*bls -= 1024;
493 				serrno = errno;
494 				goto bldone;
495 			}
496 			bl = blt;
497 		}
498 		if (i == bu || buf[i] == '\n') {
499 			buf[i] = '\0';
500 			if (parse_netblock(buf + start,
501 			    bl + *blc, bl + *blc + 1, white))
502 				*blc += 2;
503 			start = i + 1;
504 		}
505 	}
506 	if (bu == 0)
507 		errno = EIO;
508  bldone:
509 	if (buf)
510 		free(buf);
511 	if (serrno)
512 		errno = serrno;
513 	return (bl);
514 }
515 
516 int
517 cmpbl(const void *a, const void *b)
518 {
519 	if (((struct bl *)a)->addr > ((struct bl *) b)->addr)
520 		return (1);
521 	if (((struct bl *)a)->addr < ((struct bl *) b)->addr)
522 		return (-1);
523 	return (0);
524 }
525 
526 /*
527  * collapse_blacklist takes blacklist/whitelist entries sorts, removes
528  * overlaps and whitelist portions, and returns netblocks to blacklist
529  * as lists of nonoverlapping cidr blocks suitable for feeding in
530  * printable form to pfctl or spamd.
531  */
532 struct cidr **
533 collapse_blacklist(struct bl *bl, size_t blc)
534 {
535 	int bs = 0, ws = 0, state=0, cli, i;
536 	u_int32_t bstart = 0;
537 	struct cidr **cl;
538 	int laststate;
539 	u_int32_t addr;
540 
541 	if (blc == 0)
542 		return (NULL);
543 	cl = calloc(((blc / 2) + 1), sizeof(struct cidr));
544 	if (cl == NULL) {
545 		return (NULL);
546 	}
547 	qsort(bl, blc, sizeof(struct bl), cmpbl);
548 	cli = 0;
549 	cl[cli] = NULL;
550 	for (i = 0; i < blc;) {
551 		laststate = state;
552 		addr = bl[i].addr;
553 
554 		do {
555 			bs += bl[i].b;
556 			ws += bl[i].w;
557 			i++;
558 		} while (bl[i].addr == addr);
559 		if (state == 1 && bs == 0)
560 			state = 0;
561 		else if (state == 0 && bs > 0)
562 			state = 1;
563 		if (ws > 0)
564 			state = 0;
565 		if (laststate == 0 && state == 1) {
566 			/* start blacklist */
567 			bstart = addr;
568 		}
569 		if (laststate == 1 && state == 0) {
570 			/* end blacklist */
571 			cl[cli++] = range2cidrlist(bstart, addr - 1);
572 			cl[cli] = NULL;
573 		}
574 		laststate = state;
575 	}
576 	return (cl);
577 }
578 
579 int
580 configure_spamd(u_short dport, char *name, char *message,
581     struct cidr **blacklists)
582 {
583 	int lport = IPPORT_RESERVED - 1, s;
584 	struct sockaddr_in sin;
585 	FILE* sdc;
586 
587 	s = rresvport(&lport);
588 	if (s == -1)
589 		return (-1);
590 	memset(&sin, 0, sizeof sin);
591 	sin.sin_len = sizeof(sin);
592 	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
593 	sin.sin_family = AF_INET;
594 	sin.sin_port = htons(dport);
595 	if (connect(s, (struct sockaddr *)&sin, sizeof sin) == -1)
596 		return (-1);
597 	sdc = fdopen(s, "w");
598 	if (sdc == NULL) {
599 		close(s);
600 		return (-1);
601 	}
602 	fprintf(sdc, "%s", name);
603 	do_message(sdc, message);
604 	while (*blacklists != NULL) {
605 		struct cidr *b = *blacklists;
606 		while (b->addr != 0) {
607 			fprintf(sdc, ";%s/%u", atop(b->addr), (b->bits));
608 			b++;
609 		}
610 		blacklists++;
611 	}
612 	fputc('\n', sdc);
613 	fclose(sdc);
614 	close(s);
615 	return (0);
616 }
617 
618 
619 int
620 configure_pf(struct cidr **blacklists)
621 {
622 	char *argv[9]= {"pfctl", "-q", "-t", "spamd", "-T", "replace",
623 	    "-f" "-", NULL};
624 	static FILE *pf = NULL;
625 	int pdes[2];
626 
627 	if (pf == NULL) {
628 		if (pipe(pdes) != 0)
629 			return (-1);
630 		switch (fork()) {
631 		case -1:
632 			close(pdes[0]);
633 			close(pdes[1]);
634 			return (-1);
635 		case 0:
636 			/* child */
637 			close(pdes[1]);
638 			if (pdes[0] != STDIN_FILENO) {
639 				dup2(pdes[0], STDIN_FILENO);
640 				close(pdes[0]);
641 			}
642 			execvp(PATH_PFCTL, argv);
643 			_exit(1);
644 		}
645 
646 		/* parent */
647 		close(pdes[0]);
648 		pf = fdopen(pdes[1], "w");
649 		if (pf == NULL) {
650 			close(pdes[1]);
651 			return (-1);
652 		}
653 	}
654 	while (*blacklists != NULL) {
655 		struct cidr *b = *blacklists;
656 
657 		while (b->addr != 0) {
658 			fprintf(pf, "%s/%u\n", atop(b->addr), (b->bits));
659 			b++;
660 		}
661 		blacklists++;
662 	}
663 	return (0);
664 }
665 
666 int
667 getlist(char ** db_array, char *name, struct blacklist *blist,
668     struct blacklist *blistnew)
669 {
670 	char *buf, *method, *file, *message;
671 	int fd, black = 0;
672 	size_t blc, bls;
673 	struct bl *bl = NULL;
674 	gzFile gzf;
675 
676 	if (cgetent(&buf, db_array, name) != 0)
677 		err(1, "Can't find \"%s\" in spamd config", name);
678 	buf = fix_quoted_colons(buf);
679 	if (cgetcap(buf, "black", ':') != NULL) {
680 		/* use new list */
681 		black = 1;
682 		blc = blistnew->blc;
683 		bls = blistnew->bls;
684 		bl = blistnew->bl;
685 	} else if (cgetcap(buf, "white", ':') != NULL) {
686 		/* apply to most recent blacklist */
687 		black = 0;
688 		blc = blist->blc;
689 		bls = blist->bls;
690 		bl = blist->bl;
691 	} else
692 		errx(1, "Must have \"black\" or \"white\" in %s", name);
693 
694 	switch (cgetstr(buf, "msg", &message)) {
695 	case -1:
696 		if (black)
697 			errx(1, "No msg for blacklist \"%s\"", name);
698 		break;
699 	case -2:
700 		errx(1, "malloc failed");
701 	}
702 
703 	switch (cgetstr(buf, "method", &method)) {
704 	case -1:
705 		method = NULL;
706 		break;
707 	case -2:
708 		errx(1, "malloc failed");
709 	}
710 
711 	switch (cgetstr(buf, "file", &file)) {
712 	case -1:
713 		errx(1, "No file given for %slist %s",
714 		    black ? "black" : "white", name);
715 	case -2:
716 		errx(1, "malloc failed");
717 	default:
718 		fd = open_file(method, file);
719 		if (fd == -1)
720 			err(1, "Can't open %s by %s method",
721 			    file, method ? method : "file");
722 		free(method);
723 		free(file);
724 		gzf = gzdopen(fd, "r");
725 		if (gzf == NULL)
726 			errx(1, "gzdopen");
727 	}
728 	free(buf);
729 	bl = add_blacklist(bl, &blc, &bls, gzf, !black);
730 	gzclose(gzf);
731 	if (bl == NULL) {
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, **tmp;
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 		for (tmp = cidrs; *tmp != NULL; tmp++)
773 			free(*tmp);
774 		free(cidrs);
775 		free(blist->bl);
776 	}
777 }
778 
779 __dead void
780 usage(void)
781 {
782 
783 	fprintf(stderr, "usage: %s [-bDdn]\n", __progname);
784 	exit(1);
785 }
786 
787 int
788 main(int argc, char *argv[])
789 {
790 	size_t dbs, dbc, blc, bls, black, white;
791 	char **db_array, *buf, *name;
792 	struct blacklist *blists;
793 	struct servent *ent;
794 	int daemonize = 0, i, ch;
795 
796 	while ((ch = getopt(argc, argv, "bdDn")) != -1) {
797 		switch (ch) {
798 		case 'n':
799 			dryrun = 1;
800 			break;
801 		case 'd':
802 			debug = 1;
803 			break;
804 		case 'b':
805 			greyonly = 0;
806 			break;
807 		case 'D':
808 			daemonize = 1;
809 			break;
810 		default:
811 			usage();
812 			break;
813 		}
814 	}
815 	argc -= optind;
816 	argv += optind;
817 	if (argc != 0)
818 		usage();
819 
820 	if (daemonize)
821 		daemon(0, 0);
822 
823 	if ((ent = getservbyname("spamd-cfg", "tcp")) == NULL)
824 		errx(1, "cannot find service \"spamd-cfg\" in /etc/services");
825 	ent->s_port = ntohs(ent->s_port);
826 
827 	dbs = argc + 2;
828 	dbc = 0;
829 	db_array = calloc(dbs, sizeof(char *));
830 	if (db_array == NULL)
831 		errx(1, "malloc failed");
832 
833 	db_array[dbc]= PATH_SPAMD_CONF;
834 	dbc++;
835 	for (i = 1; i < argc; i++)
836 		db_array[dbc++] = argv[i];
837 
838 	blists = NULL;
839 	blc = bls = 0;
840 	if (cgetent(&buf, db_array, "all") != 0)
841 		err(1, "Can't find \"all\" in spamd config");
842 	name = strsep(&buf, ": \t"); /* skip "all" at start */
843 	blc = 0;
844 	while ((name = strsep(&buf, ": \t")) != NULL) {
845 		if (*name) {
846 			/* extract config in order specified in "all" tag */
847 			if (blc == bls) {
848 				struct blacklist *tmp;
849 
850 				bls += 32;
851 				tmp = realloc(blists,
852 				    bls * sizeof(struct blacklist));
853 				if (tmp == NULL)
854 					errx(1, "malloc failed");
855 				blists = tmp;
856 			}
857 			if (blc == 0)
858 				black = white = 0;
859 			else {
860 				white = blc - 1;
861 				black = blc;
862 			}
863 			memset(&blists[black], 0, sizeof(struct blacklist));
864 			black = getlist(db_array, name, &blists[white],
865 			    &blists[black]);
866 			if (black && blc > 0) {
867 				/* collapse and free previous blacklist */
868 				send_blacklist(&blists[blc - 1], ent->s_port);
869 			}
870 			blc += black;
871 		}
872 	}
873 	/* collapse and free last blacklist */
874 	if (blc > 0)
875 		send_blacklist(&blists[blc - 1], ent->s_port);
876 	return (0);
877 }
878