xref: /openbsd-src/usr.sbin/spamdb/spamdb.c (revision 0033a982f8df7cf7eaf017b3f340212933280cc0)
1*0033a982Smestre /*	$OpenBSD: spamdb.c,v 1.36 2018/07/26 19:33:20 mestre Exp $	*/
284d82a5fSderaadt 
31f68c1d4Sbeck /*
41f68c1d4Sbeck  * Copyright (c) 2004 Bob Beck.  All rights reserved.
51f68c1d4Sbeck  *
61f68c1d4Sbeck  * Permission to use, copy, modify, and distribute this software for any
71f68c1d4Sbeck  * purpose with or without fee is hereby granted, provided that the above
81f68c1d4Sbeck  * copyright notice and this permission notice appear in all copies.
91f68c1d4Sbeck  *
101f68c1d4Sbeck  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
111f68c1d4Sbeck  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
121f68c1d4Sbeck  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
131f68c1d4Sbeck  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
141f68c1d4Sbeck  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
151f68c1d4Sbeck  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
161f68c1d4Sbeck  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
171f68c1d4Sbeck  */
181f68c1d4Sbeck 
191f68c1d4Sbeck #include <sys/types.h>
201f68c1d4Sbeck #include <sys/socket.h>
211f68c1d4Sbeck #include <netinet/in.h>
221f68c1d4Sbeck #include <arpa/inet.h>
231f68c1d4Sbeck #include <db.h>
241f68c1d4Sbeck #include <err.h>
251f68c1d4Sbeck #include <fcntl.h>
261f68c1d4Sbeck #include <stdio.h>
271f68c1d4Sbeck #include <stdlib.h>
281f68c1d4Sbeck #include <string.h>
291f68c1d4Sbeck #include <time.h>
3085714724Sitojun #include <netdb.h>
311e913b92Sbeck #include <ctype.h>
324020b879Sbeck #include <errno.h>
338929f197Smillert #include <unistd.h>
341f68c1d4Sbeck 
351f68c1d4Sbeck #include "grey.h"
361f68c1d4Sbeck 
371e913b92Sbeck /* things we may add/delete from the db */
381e913b92Sbeck #define WHITE 0
391e913b92Sbeck #define TRAPHIT 1
401e913b92Sbeck #define SPAMTRAP 2
4188a0bb7eSmillert #define GREY 3
421e913b92Sbeck 
43d0872fbcSbeck int	dblist(DB *);
44d0872fbcSbeck int	dbupdate(DB *, char *, int, int);
455ef50bb8Sdhill 
461f68c1d4Sbeck int
dbupdate(DB * db,char * ip,int add,int type)47d0872fbcSbeck dbupdate(DB *db, char *ip, int add, int type)
481f68c1d4Sbeck {
49a9073584Sbeck 	DBT		dbk, dbd;
501f68c1d4Sbeck 	struct gdata	gd;
511f68c1d4Sbeck 	time_t		now;
521f68c1d4Sbeck 	int		r;
532364f7c1Sitojun 	struct addrinfo hints, *res;
541f68c1d4Sbeck 
551f68c1d4Sbeck 	now = time(NULL);
560cf15a29Sitojun 	memset(&hints, 0, sizeof(hints));
5785714724Sitojun 	hints.ai_family = PF_UNSPEC;
5885714724Sitojun 	hints.ai_socktype = SOCK_DGRAM;	/*dummy*/
5985714724Sitojun 	hints.ai_flags = AI_NUMERICHOST;
60d0872fbcSbeck 	if (add && (type == TRAPHIT || type == WHITE)) {
612364f7c1Sitojun 		if (getaddrinfo(ip, NULL, &hints, &res) != 0) {
6244dd9caeSotto 			warnx("invalid ip address %s", ip);
631f68c1d4Sbeck 			goto bad;
641f68c1d4Sbeck 		}
652364f7c1Sitojun 		freeaddrinfo(res);
661e913b92Sbeck 	}
671f68c1d4Sbeck 	memset(&dbk, 0, sizeof(dbk));
681f68c1d4Sbeck 	dbk.size = strlen(ip);
691f68c1d4Sbeck 	dbk.data = ip;
701f68c1d4Sbeck 	memset(&dbd, 0, sizeof(dbd));
711f68c1d4Sbeck 	if (!add) {
721e913b92Sbeck 		/* remove entry */
7388a0bb7eSmillert 		if (type == GREY) {
7488a0bb7eSmillert 			for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r;
7588a0bb7eSmillert 			    r = db->seq(db, &dbk, &dbd, R_NEXT)) {
7688a0bb7eSmillert 				char *cp = memchr(dbk.data, '\n', dbk.size);
7788a0bb7eSmillert 				if (cp != NULL) {
7888a0bb7eSmillert 					size_t len = cp - (char *)dbk.data;
7988a0bb7eSmillert 					if (memcmp(ip, dbk.data, len) == 0 &&
8088a0bb7eSmillert 					    ip[len] == '\0')
8188a0bb7eSmillert 						break;
8288a0bb7eSmillert 				}
8388a0bb7eSmillert 			}
8488a0bb7eSmillert 		} else {
851f68c1d4Sbeck 			r = db->get(db, &dbk, &dbd, 0);
861f68c1d4Sbeck 			if (r == -1) {
871f68c1d4Sbeck 				warn("db->get failed");
881f68c1d4Sbeck 				goto bad;
891f68c1d4Sbeck 			}
9088a0bb7eSmillert 		}
911f68c1d4Sbeck 		if (r) {
921f68c1d4Sbeck 			warnx("no entry for %s", ip);
931f68c1d4Sbeck 			goto bad;
941f68c1d4Sbeck 		} else if (db->del(db, &dbk, 0)) {
951f68c1d4Sbeck 			warn("db->del failed");
961f68c1d4Sbeck 			goto bad;
971f68c1d4Sbeck 		}
981f68c1d4Sbeck 	} else {
991e913b92Sbeck 		/* add or update entry */
1001f68c1d4Sbeck 		r = db->get(db, &dbk, &dbd, 0);
1011f68c1d4Sbeck 		if (r == -1) {
1021f68c1d4Sbeck 			warn("db->get failed");
1031f68c1d4Sbeck 			goto bad;
1041f68c1d4Sbeck 		}
1051f68c1d4Sbeck 		if (r) {
1061e913b92Sbeck 			int i;
1071e913b92Sbeck 
1081f68c1d4Sbeck 			/* new entry */
1091f68c1d4Sbeck 			memset(&gd, 0, sizeof(gd));
1101f68c1d4Sbeck 			gd.first = now;
1111f68c1d4Sbeck 			gd.bcount = 1;
1121e913b92Sbeck 			switch (type) {
1131e913b92Sbeck 			case WHITE:
1141f68c1d4Sbeck 				gd.pass = now;
1151f68c1d4Sbeck 				gd.expire = now + WHITEEXP;
1161e913b92Sbeck 				break;
1171e913b92Sbeck 			case TRAPHIT:
1181e913b92Sbeck 				gd.expire = now + TRAPEXP;
1191e913b92Sbeck 				gd.pcount = -1;
1201e913b92Sbeck 				break;
1211e913b92Sbeck 			case SPAMTRAP:
1221e913b92Sbeck 				gd.expire = 0;
1231e913b92Sbeck 				gd.pcount = -2;
124a574b422Smillert 				/* ensure address is of the form user@host */
125a574b422Smillert 				if (strchr(ip, '@') == NULL)
126a574b422Smillert 					errx(-1, "not an email address: %s", ip);
1271e913b92Sbeck 				/* ensure address is lower case*/
1281e913b92Sbeck 				for (i = 0; ip[i] != '\0'; i++)
1292bfe0399Sderaadt 					if (isupper((unsigned char)ip[i]))
1302bfe0399Sderaadt 						ip[i] = tolower((unsigned char)ip[i]);
1311e913b92Sbeck 				break;
1321e913b92Sbeck 			default:
1331e913b92Sbeck 				errx(-1, "unknown type %d", type);
1341e913b92Sbeck 			}
1351f68c1d4Sbeck 			memset(&dbk, 0, sizeof(dbk));
1361f68c1d4Sbeck 			dbk.size = strlen(ip);
1371f68c1d4Sbeck 			dbk.data = ip;
1381f68c1d4Sbeck 			memset(&dbd, 0, sizeof(dbd));
1391f68c1d4Sbeck 			dbd.size = sizeof(gd);
1401f68c1d4Sbeck 			dbd.data = &gd;
1411f68c1d4Sbeck 			r = db->put(db, &dbk, &dbd, 0);
1421f68c1d4Sbeck 			if (r) {
1431f68c1d4Sbeck 				warn("db->put failed");
1441f68c1d4Sbeck 				goto bad;
1451f68c1d4Sbeck 			}
1461f68c1d4Sbeck 		} else {
14751210d5cSmillert 			if (gdcopyin(&dbd, &gd) == -1) {
1481f68c1d4Sbeck 				/* whatever this is, it doesn't belong */
1491f68c1d4Sbeck 				db->del(db, &dbk, 0);
1501f68c1d4Sbeck 				goto bad;
1511f68c1d4Sbeck 			}
1521f68c1d4Sbeck 			gd.pcount++;
1531e913b92Sbeck 			switch (type) {
1541e913b92Sbeck 			case WHITE:
1551e913b92Sbeck 				gd.pass = now;
1561f68c1d4Sbeck 				gd.expire = now + WHITEEXP;
1571e913b92Sbeck 				break;
1581e913b92Sbeck 			case TRAPHIT:
1591e913b92Sbeck 				gd.expire = now + TRAPEXP;
1601e913b92Sbeck 				gd.pcount = -1;
1611e913b92Sbeck 				break;
1621e913b92Sbeck 			case SPAMTRAP:
1631e913b92Sbeck 				gd.expire = 0; /* XXX */
1641e913b92Sbeck 				gd.pcount = -2;
1651e913b92Sbeck 				break;
1661e913b92Sbeck 			default:
1671e913b92Sbeck 				errx(-1, "unknown type %d", type);
1681e913b92Sbeck 			}
1691e913b92Sbeck 
1701f68c1d4Sbeck 			memset(&dbk, 0, sizeof(dbk));
1711f68c1d4Sbeck 			dbk.size = strlen(ip);
1721f68c1d4Sbeck 			dbk.data = ip;
1731f68c1d4Sbeck 			memset(&dbd, 0, sizeof(dbd));
1741f68c1d4Sbeck 			dbd.size = sizeof(gd);
1751f68c1d4Sbeck 			dbd.data = &gd;
1761f68c1d4Sbeck 			r = db->put(db, &dbk, &dbd, 0);
1771f68c1d4Sbeck 			if (r) {
1781f68c1d4Sbeck 				warn("db->put failed");
1791f68c1d4Sbeck 				goto bad;
1801f68c1d4Sbeck 			}
1811f68c1d4Sbeck 		}
1821f68c1d4Sbeck 	}
1831f68c1d4Sbeck 	return (0);
1841f68c1d4Sbeck  bad:
18544dd9caeSotto 	return (1);
1861f68c1d4Sbeck }
1871f68c1d4Sbeck 
1881f68c1d4Sbeck int
print_entry(DBT * dbk,DBT * dbd)189de9ad6b5Smillert print_entry(DBT *dbk, DBT *dbd)
1901f68c1d4Sbeck {
1911f68c1d4Sbeck 	struct gdata gd;
1921f68c1d4Sbeck 	char *a, *cp;
1931f68c1d4Sbeck 
194de9ad6b5Smillert 	if ((dbk->size < 1) || gdcopyin(dbd, &gd) == -1) {
195de9ad6b5Smillert 		warnx("bogus size db entry - bad db file?");
196de9ad6b5Smillert 		return (1);
1971f68c1d4Sbeck 	}
198de9ad6b5Smillert 	a = malloc(dbk->size + 1);
1991f68c1d4Sbeck 	if (a == NULL)
2001f68c1d4Sbeck 		err(1, "malloc");
201de9ad6b5Smillert 	memcpy(a, dbk->data, dbk->size);
202de9ad6b5Smillert 	a[dbk->size]='\0';
2031f68c1d4Sbeck 	cp = strchr(a, '\n');
2041e913b92Sbeck 	if (cp == NULL) {
2051e913b92Sbeck 		/* this is a non-greylist entry */
2061e913b92Sbeck 		switch (gd.pcount) {
2071e913b92Sbeck 		case -1: /* spamtrap hit, with expiry time */
2089d683440Sotto 			printf("TRAPPED|%s|%lld\n", a,
2099d683440Sotto 			    (long long)gd.expire);
2101e913b92Sbeck 			break;
2111e913b92Sbeck 		case -2: /* spamtrap address */
2121e913b92Sbeck 			printf("SPAMTRAP|%s\n", a);
2131e913b92Sbeck 			break;
2141e913b92Sbeck 		default: /* whitelist */
2159d683440Sotto 			printf("WHITE|%s|||%lld|%lld|%lld|%d|%d\n", a,
2169d683440Sotto 			    (long long)gd.first, (long long)gd.pass,
2179d683440Sotto 			    (long long)gd.expire, gd.bcount,
2181e913b92Sbeck 			    gd.pcount);
2191e913b92Sbeck 			break;
2201e913b92Sbeck 		}
2211e913b92Sbeck 	} else {
2225b0222eaSbeck 		char *helo, *from, *to;
2231f68c1d4Sbeck 
2241f68c1d4Sbeck 		/* greylist entry */
2251f68c1d4Sbeck 		*cp = '\0';
2265b0222eaSbeck 		helo = cp + 1;
2275b0222eaSbeck 		from = strchr(helo, '\n');
2285b0222eaSbeck 		if (from == NULL) {
2291f68c1d4Sbeck 			warnx("No from part in grey key %s", a);
230f08b6076Sbeck 			free(a);
231de9ad6b5Smillert 			return (1);
2321f68c1d4Sbeck 		}
2335b0222eaSbeck 		*from = '\0';
2345b0222eaSbeck 		from++;
2355b0222eaSbeck 		to = strchr(from, '\n');
2365b0222eaSbeck 		if (to == NULL) {
2375b0222eaSbeck 			/* probably old format - print it the
2385b0222eaSbeck 			 * with an empty HELO field instead
2395b0222eaSbeck 			 * of erroring out.
2405b0222eaSbeck 			 */
2419d683440Sotto 			printf("GREY|%s|%s|%s|%s|%lld|%lld|%lld|%d|%d\n",
2429d683440Sotto 			    a, "", helo, from, (long long)gd.first,
2439d683440Sotto 			    (long long)gd.pass, (long long)gd.expire,
2449d683440Sotto 			    gd.bcount, gd.pcount);
2455b0222eaSbeck 
2465b0222eaSbeck 		} else {
2471f68c1d4Sbeck 			*to = '\0';
2481f68c1d4Sbeck 			to++;
2499d683440Sotto 			printf("GREY|%s|%s|%s|%s|%lld|%lld|%lld|%d|%d\n",
2509d683440Sotto 			    a, helo, from, to, (long long)gd.first,
2519d683440Sotto 			    (long long)gd.pass, (long long)gd.expire,
2529d683440Sotto 			    gd.bcount, gd.pcount);
2535b0222eaSbeck 		}
2541f68c1d4Sbeck 	}
255f08b6076Sbeck 	free(a);
256de9ad6b5Smillert 
257de9ad6b5Smillert 	return (0);
258de9ad6b5Smillert }
259de9ad6b5Smillert 
260de9ad6b5Smillert int
dblist(DB * db)261de9ad6b5Smillert dblist(DB *db)
262de9ad6b5Smillert {
263de9ad6b5Smillert 	DBT		dbk, dbd;
264de9ad6b5Smillert 	int		r;
265de9ad6b5Smillert 
266de9ad6b5Smillert 	/* walk db, list in text format */
267de9ad6b5Smillert 	memset(&dbk, 0, sizeof(dbk));
268de9ad6b5Smillert 	memset(&dbd, 0, sizeof(dbd));
269de9ad6b5Smillert 	for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r;
270de9ad6b5Smillert 	    r = db->seq(db, &dbk, &dbd, R_NEXT)) {
271de9ad6b5Smillert 		if (print_entry(&dbk, &dbd) != 0) {
272de9ad6b5Smillert 			r = -1;
273de9ad6b5Smillert 			break;
274de9ad6b5Smillert 		}
2751f68c1d4Sbeck 	}
2761f68c1d4Sbeck 	db->close(db);
27784d82a5fSderaadt 	db = NULL;
278de9ad6b5Smillert 	return (r == -1);
279de9ad6b5Smillert }
280de9ad6b5Smillert 
281de9ad6b5Smillert int
dbshow(DB * db,char ** addrs)282de9ad6b5Smillert dbshow(DB *db, char **addrs)
283de9ad6b5Smillert {
284de9ad6b5Smillert 	DBT dbk, dbd;
285de9ad6b5Smillert 	int errors = 0;
286de9ad6b5Smillert 	char *a;
287de9ad6b5Smillert 
288de9ad6b5Smillert 	/* look up each addr */
289de9ad6b5Smillert 	while ((a = *addrs) != NULL) {
290de9ad6b5Smillert 		memset(&dbk, 0, sizeof(dbk));
291de9ad6b5Smillert 		dbk.size = strlen(a);
292de9ad6b5Smillert 		dbk.data = a;
293de9ad6b5Smillert 		memset(&dbd, 0, sizeof(dbd));
294de9ad6b5Smillert 		switch (db->get(db, &dbk, &dbd, 0)) {
295de9ad6b5Smillert 		case -1:
296de9ad6b5Smillert 			warn("db->get failed");
297de9ad6b5Smillert 			errors++;
298de9ad6b5Smillert 			goto done;
299de9ad6b5Smillert 		case 0:
300de9ad6b5Smillert 			if (print_entry(&dbk, &dbd) != 0) {
301de9ad6b5Smillert 				errors++;
302de9ad6b5Smillert 				goto done;
303de9ad6b5Smillert 			}
304de9ad6b5Smillert 			break;
305de9ad6b5Smillert 		case 1:
306de9ad6b5Smillert 		default:
307de9ad6b5Smillert 			/* not found */
308de9ad6b5Smillert 			errors++;
309de9ad6b5Smillert 			break;
310de9ad6b5Smillert 		}
311de9ad6b5Smillert 		addrs++;
312de9ad6b5Smillert 	}
313de9ad6b5Smillert  done:
3141f68c1d4Sbeck 	db->close(db);
31584d82a5fSderaadt 	db = NULL;
316de9ad6b5Smillert 	return (errors);
3171f68c1d4Sbeck }
3181f68c1d4Sbeck 
31944dd9caeSotto extern char *__progname;
32044dd9caeSotto 
3211f68c1d4Sbeck static int
usage(void)3221f68c1d4Sbeck usage(void)
3231f68c1d4Sbeck {
324de9ad6b5Smillert 	fprintf(stderr, "usage: %s [-adGTt] [keys ...]\n", __progname);
32584d82a5fSderaadt 	exit(1);
3261d7c8e7dSdhill 	/* NOTREACHED */
3271f68c1d4Sbeck }
3281f68c1d4Sbeck 
3291f68c1d4Sbeck int
main(int argc,char ** argv)3301f68c1d4Sbeck main(int argc, char **argv)
3311f68c1d4Sbeck {
332e1fc2954Scnst 	int i, ch, action = 0, type = WHITE, r = 0, c = 0;
3334020b879Sbeck 	HASHINFO	hashinfo;
334d0872fbcSbeck 	DB		*db;
3351f68c1d4Sbeck 
33688a0bb7eSmillert 	while ((ch = getopt(argc, argv, "adGtT")) != -1) {
3371f68c1d4Sbeck 		switch (ch) {
3381f68c1d4Sbeck 		case 'a':
3391f68c1d4Sbeck 			action = 1;
3401f68c1d4Sbeck 			break;
3411f68c1d4Sbeck 		case 'd':
3421f68c1d4Sbeck 			action = 2;
3431f68c1d4Sbeck 			break;
34488a0bb7eSmillert 		case 'G':
34588a0bb7eSmillert 			type = GREY;
34688a0bb7eSmillert 			break;
3471e913b92Sbeck 		case 't':
3481e913b92Sbeck 			type = TRAPHIT;
3491e913b92Sbeck 			break;
3501e913b92Sbeck 		case 'T':
3511e913b92Sbeck 			type = SPAMTRAP;
3521e913b92Sbeck 			break;
3531f68c1d4Sbeck 		default:
3541f68c1d4Sbeck 			usage();
3551f68c1d4Sbeck 			break;
3561f68c1d4Sbeck 		}
3571f68c1d4Sbeck 	}
358d0872fbcSbeck 	argc -= optind;
359d0872fbcSbeck 	argv += optind;
36003ac6a35Smillert 	if (action == 0 && type != WHITE)
36103ac6a35Smillert 		usage();
362d0872fbcSbeck 
3634020b879Sbeck 	memset(&hashinfo, 0, sizeof(hashinfo));
3646a594376Sotto 	db = dbopen(PATH_SPAMD_DB, O_EXLOCK | (action ? O_RDWR : O_RDONLY),
3656a594376Sotto 	    0600, DB_HASH, &hashinfo);
3664020b879Sbeck 	if (db == NULL) {
3676a594376Sotto 		err(1, "cannot open %s for %s", PATH_SPAMD_DB,
3686a594376Sotto 		    action ? "writing" : "reading");
3694020b879Sbeck 	}
3701f68c1d4Sbeck 
371aa29652cSmestre 	if (pledge("stdio", NULL) == -1)
3728a51f366Smestre 		err(1, "pledge");
3731a6938c5Sderaadt 
3741f68c1d4Sbeck 	switch (action) {
3751f68c1d4Sbeck 	case 0:
376de9ad6b5Smillert 		if (argc)
377de9ad6b5Smillert 			return dbshow(db, argv);
378de9ad6b5Smillert 		else
379d0872fbcSbeck 			return dblist(db);
3801f68c1d4Sbeck 	case 1:
38188a0bb7eSmillert 		if (type == GREY)
38288a0bb7eSmillert 			errx(2, "cannot add GREY entries");
383d0872fbcSbeck 		for (i=0; i<argc; i++)
384e1fc2954Scnst 			if (argv[i][0] != '\0') {
385e1fc2954Scnst 				c++;
386d0872fbcSbeck 				r += dbupdate(db, argv[i], 1, type);
387e1fc2954Scnst 			}
388e1fc2954Scnst 		if (c == 0)
389e1fc2954Scnst 			errx(2, "no addresses specified");
390d0872fbcSbeck 		break;
3911f68c1d4Sbeck 	case 2:
392d0872fbcSbeck 		for (i=0; i<argc; i++)
393e1fc2954Scnst 			if (argv[i][0] != '\0') {
394e1fc2954Scnst 				c++;
395d0872fbcSbeck 				r += dbupdate(db, argv[i], 0, type);
396e1fc2954Scnst 			}
397e1fc2954Scnst 		if (c == 0)
398e1fc2954Scnst 			errx(2, "no addresses specified");
399d0872fbcSbeck 		break;
4001f68c1d4Sbeck 	default:
4011f68c1d4Sbeck 		errx(-1, "bad action");
4021f68c1d4Sbeck 	}
403d0872fbcSbeck 	db->close(db);
404d0872fbcSbeck 	return (r);
4051f68c1d4Sbeck }
406