1/*	$OpenBSD: spamdb.c,v 1.36 2018/07/26 19:33:20 mestre Exp $	*/
2
3/*
4 * Copyright (c) 2004 Bob Beck.  All rights reserved.
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 */
18
19#include <sys/types.h>
20#include <sys/socket.h>
21#include <netinet/in.h>
22#include <arpa/inet.h>
23#include <db.h>
24#include <err.h>
25#include <fcntl.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <time.h>
30#include <netdb.h>
31#include <ctype.h>
32#include <errno.h>
33#include <unistd.h>
34
35#include "grey.h"
36
37/* things we may add/delete from the db */
38#define WHITE 0
39#define TRAPHIT 1
40#define SPAMTRAP 2
41#define GREY 3
42
43int	dblist(DB *);
44int	dbupdate(DB *, char *, int, int);
45
46int
47dbupdate(DB *db, char *ip, int add, int type)
48{
49	DBT		dbk, dbd;
50	struct gdata	gd;
51	time_t		now;
52	int		r;
53	struct addrinfo hints, *res;
54
55	now = time(NULL);
56	memset(&hints, 0, sizeof(hints));
57	hints.ai_family = PF_UNSPEC;
58	hints.ai_socktype = SOCK_DGRAM;	/*dummy*/
59	hints.ai_flags = AI_NUMERICHOST;
60	if (add && (type == TRAPHIT || type == WHITE)) {
61		if (getaddrinfo(ip, NULL, &hints, &res) != 0) {
62			warnx("invalid ip address %s", ip);
63			goto bad;
64		}
65		freeaddrinfo(res);
66	}
67	memset(&dbk, 0, sizeof(dbk));
68	dbk.size = strlen(ip);
69	dbk.data = ip;
70	memset(&dbd, 0, sizeof(dbd));
71	if (!add) {
72		/* remove entry */
73		if (type == GREY) {
74			for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r;
75			    r = db->seq(db, &dbk, &dbd, R_NEXT)) {
76				char *cp = memchr(dbk.data, '\n', dbk.size);
77				if (cp != NULL) {
78					size_t len = cp - (char *)dbk.data;
79					if (memcmp(ip, dbk.data, len) == 0 &&
80					    ip[len] == '\0')
81						break;
82				}
83			}
84		} else {
85			r = db->get(db, &dbk, &dbd, 0);
86			if (r == -1) {
87				warn("db->get failed");
88				goto bad;
89			}
90		}
91		if (r) {
92			warnx("no entry for %s", ip);
93			goto bad;
94		} else if (db->del(db, &dbk, 0)) {
95			warn("db->del failed");
96			goto bad;
97		}
98	} else {
99		/* add or update entry */
100		r = db->get(db, &dbk, &dbd, 0);
101		if (r == -1) {
102			warn("db->get failed");
103			goto bad;
104		}
105		if (r) {
106			int i;
107
108			/* new entry */
109			memset(&gd, 0, sizeof(gd));
110			gd.first = now;
111			gd.bcount = 1;
112			switch (type) {
113			case WHITE:
114				gd.pass = now;
115				gd.expire = now + WHITEEXP;
116				break;
117			case TRAPHIT:
118				gd.expire = now + TRAPEXP;
119				gd.pcount = -1;
120				break;
121			case SPAMTRAP:
122				gd.expire = 0;
123				gd.pcount = -2;
124				/* ensure address is of the form user@host */
125				if (strchr(ip, '@') == NULL)
126					errx(-1, "not an email address: %s", ip);
127				/* ensure address is lower case*/
128				for (i = 0; ip[i] != '\0'; i++)
129					if (isupper((unsigned char)ip[i]))
130						ip[i] = tolower((unsigned char)ip[i]);
131				break;
132			default:
133				errx(-1, "unknown type %d", type);
134			}
135			memset(&dbk, 0, sizeof(dbk));
136			dbk.size = strlen(ip);
137			dbk.data = ip;
138			memset(&dbd, 0, sizeof(dbd));
139			dbd.size = sizeof(gd);
140			dbd.data = &gd;
141			r = db->put(db, &dbk, &dbd, 0);
142			if (r) {
143				warn("db->put failed");
144				goto bad;
145			}
146		} else {
147			if (gdcopyin(&dbd, &gd) == -1) {
148				/* whatever this is, it doesn't belong */
149				db->del(db, &dbk, 0);
150				goto bad;
151			}
152			gd.pcount++;
153			switch (type) {
154			case WHITE:
155				gd.pass = now;
156				gd.expire = now + WHITEEXP;
157				break;
158			case TRAPHIT:
159				gd.expire = now + TRAPEXP;
160				gd.pcount = -1;
161				break;
162			case SPAMTRAP:
163				gd.expire = 0; /* XXX */
164				gd.pcount = -2;
165				break;
166			default:
167				errx(-1, "unknown type %d", type);
168			}
169
170			memset(&dbk, 0, sizeof(dbk));
171			dbk.size = strlen(ip);
172			dbk.data = ip;
173			memset(&dbd, 0, sizeof(dbd));
174			dbd.size = sizeof(gd);
175			dbd.data = &gd;
176			r = db->put(db, &dbk, &dbd, 0);
177			if (r) {
178				warn("db->put failed");
179				goto bad;
180			}
181		}
182	}
183	return (0);
184 bad:
185	return (1);
186}
187
188int
189print_entry(DBT *dbk, DBT *dbd)
190{
191	struct gdata gd;
192	char *a, *cp;
193
194	if ((dbk->size < 1) || gdcopyin(dbd, &gd) == -1) {
195		warnx("bogus size db entry - bad db file?");
196		return (1);
197	}
198	a = malloc(dbk->size + 1);
199	if (a == NULL)
200		err(1, "malloc");
201	memcpy(a, dbk->data, dbk->size);
202	a[dbk->size]='\0';
203	cp = strchr(a, '\n');
204	if (cp == NULL) {
205		/* this is a non-greylist entry */
206		switch (gd.pcount) {
207		case -1: /* spamtrap hit, with expiry time */
208			printf("TRAPPED|%s|%lld\n", a,
209			    (long long)gd.expire);
210			break;
211		case -2: /* spamtrap address */
212			printf("SPAMTRAP|%s\n", a);
213			break;
214		default: /* whitelist */
215			printf("WHITE|%s|||%lld|%lld|%lld|%d|%d\n", a,
216			    (long long)gd.first, (long long)gd.pass,
217			    (long long)gd.expire, gd.bcount,
218			    gd.pcount);
219			break;
220		}
221	} else {
222		char *helo, *from, *to;
223
224		/* greylist entry */
225		*cp = '\0';
226		helo = cp + 1;
227		from = strchr(helo, '\n');
228		if (from == NULL) {
229			warnx("No from part in grey key %s", a);
230			free(a);
231			return (1);
232		}
233		*from = '\0';
234		from++;
235		to = strchr(from, '\n');
236		if (to == NULL) {
237			/* probably old format - print it the
238			 * with an empty HELO field instead
239			 * of erroring out.
240			 */
241			printf("GREY|%s|%s|%s|%s|%lld|%lld|%lld|%d|%d\n",
242			    a, "", helo, from, (long long)gd.first,
243			    (long long)gd.pass, (long long)gd.expire,
244			    gd.bcount, gd.pcount);
245
246		} else {
247			*to = '\0';
248			to++;
249			printf("GREY|%s|%s|%s|%s|%lld|%lld|%lld|%d|%d\n",
250			    a, helo, from, to, (long long)gd.first,
251			    (long long)gd.pass, (long long)gd.expire,
252			    gd.bcount, gd.pcount);
253		}
254	}
255	free(a);
256
257	return (0);
258}
259
260int
261dblist(DB *db)
262{
263	DBT		dbk, dbd;
264	int		r;
265
266	/* walk db, list in text format */
267	memset(&dbk, 0, sizeof(dbk));
268	memset(&dbd, 0, sizeof(dbd));
269	for (r = db->seq(db, &dbk, &dbd, R_FIRST); !r;
270	    r = db->seq(db, &dbk, &dbd, R_NEXT)) {
271		if (print_entry(&dbk, &dbd) != 0) {
272			r = -1;
273			break;
274		}
275	}
276	db->close(db);
277	db = NULL;
278	return (r == -1);
279}
280
281int
282dbshow(DB *db, char **addrs)
283{
284	DBT dbk, dbd;
285	int errors = 0;
286	char *a;
287
288	/* look up each addr */
289	while ((a = *addrs) != NULL) {
290		memset(&dbk, 0, sizeof(dbk));
291		dbk.size = strlen(a);
292		dbk.data = a;
293		memset(&dbd, 0, sizeof(dbd));
294		switch (db->get(db, &dbk, &dbd, 0)) {
295		case -1:
296			warn("db->get failed");
297			errors++;
298			goto done;
299		case 0:
300			if (print_entry(&dbk, &dbd) != 0) {
301				errors++;
302				goto done;
303			}
304			break;
305		case 1:
306		default:
307			/* not found */
308			errors++;
309			break;
310		}
311		addrs++;
312	}
313 done:
314	db->close(db);
315	db = NULL;
316	return (errors);
317}
318
319extern char *__progname;
320
321static int
322usage(void)
323{
324	fprintf(stderr, "usage: %s [-adGTt] [keys ...]\n", __progname);
325	exit(1);
326	/* NOTREACHED */
327}
328
329int
330main(int argc, char **argv)
331{
332	int i, ch, action = 0, type = WHITE, r = 0, c = 0;
333	HASHINFO	hashinfo;
334	DB		*db;
335
336	while ((ch = getopt(argc, argv, "adGtT")) != -1) {
337		switch (ch) {
338		case 'a':
339			action = 1;
340			break;
341		case 'd':
342			action = 2;
343			break;
344		case 'G':
345			type = GREY;
346			break;
347		case 't':
348			type = TRAPHIT;
349			break;
350		case 'T':
351			type = SPAMTRAP;
352			break;
353		default:
354			usage();
355			break;
356		}
357	}
358	argc -= optind;
359	argv += optind;
360	if (action == 0 && type != WHITE)
361		usage();
362
363	memset(&hashinfo, 0, sizeof(hashinfo));
364	db = dbopen(PATH_SPAMD_DB, O_EXLOCK | (action ? O_RDWR : O_RDONLY),
365	    0600, DB_HASH, &hashinfo);
366	if (db == NULL) {
367		err(1, "cannot open %s for %s", PATH_SPAMD_DB,
368		    action ? "writing" : "reading");
369	}
370
371	if (pledge("stdio", NULL) == -1)
372		err(1, "pledge");
373
374	switch (action) {
375	case 0:
376		if (argc)
377			return dbshow(db, argv);
378		else
379			return dblist(db);
380	case 1:
381		if (type == GREY)
382			errx(2, "cannot add GREY entries");
383		for (i=0; i<argc; i++)
384			if (argv[i][0] != '\0') {
385				c++;
386				r += dbupdate(db, argv[i], 1, type);
387			}
388		if (c == 0)
389			errx(2, "no addresses specified");
390		break;
391	case 2:
392		for (i=0; i<argc; i++)
393			if (argv[i][0] != '\0') {
394				c++;
395				r += dbupdate(db, argv[i], 0, type);
396			}
397		if (c == 0)
398			errx(2, "no addresses specified");
399		break;
400	default:
401		errx(-1, "bad action");
402	}
403	db->close(db);
404	return (r);
405}
406