From ca51b51d75ecbf78f6c5786b6d3379d296e89d85 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Tue, 9 Aug 2005 12:55:05 +0000 Subject: [PATCH] qmail-smtpd: Validation of recipient mailbox names. Lots of spam arrives for non-existent mailboxes. If the SMTP server accepts it, we have to put up with the bounces. We introduce a new CDB which describes all the valid mailboxes on the system. --- Makefile | 16 +++-- TARGETS | 1 + addrcheck.c | 181 ++++++++++++++++++++++++++++++++++++++++++++++++ addrcheck.h | 6 ++ debian/changelog | 1 + debian/control | 3 +- hier.c | 3 + install-big.c | 1 + qmail-smtpd.8 | 34 +++++++++ qmail-smtpd.c | 31 +++++++++ qmail-valid-addresses | 81 ++++++++++++++++++++++ qmail-valid-addresses.8 | 20 ++++++ 12 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 addrcheck.c create mode 100644 addrcheck.h create mode 100644 qmail-valid-addresses create mode 100644 qmail-valid-addresses.8 diff --git a/Makefile b/Makefile index 6bf4a6c..f3d8ebd 100644 --- a/Makefile +++ b/Makefile @@ -935,7 +935,7 @@ preline.0 condredirect.0 bouncesaying.0 except.0 maildirmake.0 \ maildir2mbox.0 maildirwatch.0 qmail.0 qmail-limits.0 qmail-log.0 \ qmail-control.0 qmail-header.0 qmail-users.0 dot-qmail.0 \ qmail-command.0 tcp-environ.0 maildir.0 mbox.0 addresses.0 \ -envelopes.0 forgeries.0 +envelopes.0 forgeries.0 qmail-valid-addresses.0 mbox.0: \ mbox.5 @@ -1532,12 +1532,12 @@ auto_split.h ./compile qmail-showctl.c qmail-smtpd: \ -load qmail-smtpd.o rcpthosts.o commands.o timeoutread.o \ +load qmail-smtpd.o addrcheck.o rcpthosts.o commands.o timeoutread.o \ timeoutwrite.o ip.o ipme.o ipalloc.o control.o constmap.o received.o \ date822fmt.o now.o qmail.o cdb.a fd.a wait.a datetime.a getln.a \ open.a sig.a case.a env.a stralloc.a alloc.a substdio.a error.a str.a \ fs.a auto_qmail.o socket.lib - ./load qmail-smtpd rcpthosts.o commands.o timeoutread.o \ + ./load qmail-smtpd addrcheck.o rcpthosts.o commands.o timeoutread.o \ timeoutwrite.o ip.o ipme.o ipalloc.o control.o constmap.o \ received.o date822fmt.o now.o qmail.o cdb.a fd.a wait.a \ datetime.a getln.a open.a sig.a case.a env.a stralloc.a \ @@ -1553,9 +1553,13 @@ compile qmail-smtpd.c sig.h readwrite.h stralloc.h gen_alloc.h \ substdio.h alloc.h auto_qmail.h control.h received.h constmap.h \ error.h ipme.h ip.h ipalloc.h ip.h gen_alloc.h ip.h qmail.h \ substdio.h str.h fmt.h scan.h byte.h case.h env.h now.h datetime.h \ -exit.h rcpthosts.h timeoutread.h timeoutwrite.h commands.h +exit.h rcpthosts.h timeoutread.h timeoutwrite.h commands.h addrcheck.h ./compile qmail-smtpd.c +addrcheck.o: \ +compile addrcheck.c cdb.h stralloc.h byte.h str.h + ./compile addrcheck.c + qmail-start: \ load qmail-start.o prot.o fd.a auto_uids.o ./load qmail-start prot.o fd.a auto_uids.o @@ -1627,6 +1631,10 @@ qmail-users.9 conf-break conf-spawn | sed s}SPAWN}"`head -1 conf-spawn`"}g \ > qmail-users.5 +qmail-valid-addresses.0: \ +qmail-valid-addresses.8 + nroff -man qmail-valid-addresses.8 > qmail-valid-addresses.0 + qmail.0: \ qmail.7 nroff -man qmail.7 > qmail.0 diff --git a/TARGETS b/TARGETS index facdad7..175aab7 100644 --- a/TARGETS +++ b/TARGETS @@ -249,6 +249,7 @@ received.o qmail-qmqpd qmail-qmtpd.o rcpthosts.o +addrcheck.o qmail-qmtpd qmail-smtpd.o qmail-smtpd diff --git a/addrcheck.c b/addrcheck.c new file mode 100644 index 0000000..2ff9d13 --- /dev/null +++ b/addrcheck.c @@ -0,0 +1,181 @@ +#include "cdb.h" +#include "stralloc.h" +#include "byte.h" +#include "str.h" +#include "addrcheck.h" +#include + +/* #define DEBUG */ +#ifdef DEBUG +# define D(x) x +# include +# include +# include +#else +# define D(x) +#endif + +#define STRALLOC_INIT { 0 } + +static int probe(int cdb, int prefix, const char *key, int len, + const char *suffix, uint32 *dlen) +{ + static stralloc k = STRALLOC_INIT; + char ch = prefix; + int rc; + + k.len = 0; + if (!stralloc_append(&k, &ch) || + !stralloc_catb(&k, key, len) || + (suffix && !stralloc_cats(&k, suffix))) + return (-1); + D( fprintf(stderr, "*** `%.*s' -> ", k.len, k.s); ) + rc = cdb_seek(cdb, k.s, k.len, dlen); + D( if (rc == -1) + fprintf(stderr, "error: %s\n", strerror(errno)); + else if (rc == 0) + fprintf(stderr, "not found\n"); + else if (!*dlen) + fprintf(stderr, "empty\n"); + else { + int n = *dlen; + int nn; + char buf[256]; + off_t pos = lseek(cdb, 0, SEEK_CUR); + fprintf(stderr, "`"); + while (n) { + nn = sizeof(buf); if (nn > n) nn = n; + read(cdb, buf, nn); + fwrite(buf, 1, nn, stderr); + n -= nn; + } + fprintf(stderr, "'\n"); + lseek(cdb, pos, SEEK_SET); + } ) + return (rc); +} + +static int localprobe(int cdb, const char *key, int len, + const char *suffix, int *rc) +{ + int err; + uint32 dlen; + char ch; + + if ((err = probe(cdb, 'L', key, len, suffix, &dlen)) < 0) + return (-1); + if (!err) { *rc = 0; return (0); } + if (dlen != 1) { errno = EINVAL; return (-1); } + if (read(cdb, &ch, 1) != 1) { errno = EIO; return (-1); } + *rc = ch; + return (1); +} + +static int local(int cdb, const char *l, int len, int *rc) +{ + int code; + int err = 0; + int dash; + + if ((err = localprobe(cdb, l, len, 0, &code)) != 0) goto done; + + for (;;) { + dash = byte_rchr(l, len, '-'); + if (dash == len) break; + if ((err = localprobe(cdb, l, dash, "-default", &code)) != 0) goto done; + len = dash; + } + *rc = 0; + return (0); + +done: + if (err >= 0) { + switch (code) { + case '+': *rc = 1; break; + case '-': *rc = 0; break; + default: errno = EINVAL; err = -1; break; + } + } + return (err); +} + +static int virt(int cdb, const char *u, int ulen, + const char *addr, int alen, int *rc) +{ + static stralloc l = STRALLOC_INIT; + uint32 dlen; + int err; + + if ((err = probe(cdb, 'V', addr, alen, 0, &dlen)) <= 0) + return (err); + if (!stralloc_ready(&l, dlen + 1)) return (-1); + if (read(cdb, l.s, dlen) != dlen) { errno = EIO; return (-1); } + l.s[dlen] = '-'; + l.len = dlen + 1; + if (!stralloc_catb(&l, u, ulen)) return (-1); + D( printf("*** virtual map -> `%.*s'\n", l.len, l.s); ) + if (local(cdb, l.s, l.len, rc) < 0) return (-1); + return (1); +} + +int addrcheck(int cdb, const char *addr, int *rc) +{ + int at, len, dot; + int err = 0; + uint32 dlen; + + len = str_len(addr); + at = str_chr(addr, '@'); + if (!addr[at]) + return (local(cdb, addr, len, rc)); + + if ((err = virt(cdb, addr, at, addr, len, rc)) != 0) + return (err); + dot = at + 1; + while (addr[dot]) { + if ((err = virt(cdb, addr, at, addr + dot, len - dot, rc)) != 0) + return (err); + dot += byte_chr(addr + dot + 1, len - dot - 1, '.') + 1; + } + + if ((err = probe(cdb, '@', addr + at + 1, len - at - 1, 0, &dlen)) < 0) + return (-1); + if (!err) { *rc = 1; return (0); } + if (dlen != 0) { errno = EINVAL; return (-1); } + + return (local(cdb, addr, at, rc)); +} + +#ifdef TEST +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + int fd; + int rc; + int i; + + if (argc < 3) { + fprintf(stderr, "usage: addrcheck CDB ADDR...\n"); + return (1); + } + if ((fd = open(argv[1], O_RDONLY)) < 0) { + perror(argv[1]); + return (1); + } + for (i = 2; i < argc; i++) { + if (addrcheck(fd, argv[i], &rc) < 0) { + perror("checking"); + return (1); + } + printf("%s: %s\n", argv[i], rc ? "ok" : "bad"); + } + return (0); +} + +#endif diff --git a/addrcheck.h b/addrcheck.h new file mode 100644 index 0000000..7c51909 --- /dev/null +++ b/addrcheck.h @@ -0,0 +1,6 @@ +#ifndef ADDRCHECK_H +#define ADDRCHECK_H + +extern int addrcheck(int, const char *, int *); + +#endif diff --git a/debian/changelog b/debian/changelog index 66fb5fd..c85d4b9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,6 +2,7 @@ qmail (1.03-5) unstable; urgency=low * make it build again. * add mini-qmail package. + * support checking of recipient mailboxes in qmail-smtpd. -- Mark Wooding Mon, 2 May 2005 14:44:12 +0100 diff --git a/debian/control b/debian/control index 4ea5783..9bd4434 100644 --- a/debian/control +++ b/debian/control @@ -8,7 +8,8 @@ Package: qmail Architecture: any Section: mail Priority: extra -Depends: ${shlibs:Depends}, netbase, procmail +Depends: ${shlibs:Depends}, netbase, procmail, + python (>= 2.3.5), python-cdb, nsict-cdb Provides: mail-transport-agent Conflicts: mail-transport-agent Suggests: pine | mail-reader diff --git a/hier.c b/hier.c index 9d6441b..0c692f7 100644 --- a/hier.c +++ b/hier.c @@ -132,6 +132,7 @@ char *home; c(home,"bin","qmail-qmqpd",auto_uido,auto_gidq,0755); c(home,"bin","qmail-qmtpd",auto_uido,auto_gidq,0755); c(home,"bin","qmail-smtpd",auto_uido,auto_gidq,0755); + c(home,"bin","qmail-valid-addresses",auto_uido,auto_gidq,0755); c(home,"bin","sendmail",auto_uido,auto_gidq,0755); c(home,"bin","tcp-env",auto_uido,auto_gidq,0755); c(home,"bin","qreceipt",auto_uido,auto_gidq,0755); @@ -254,4 +255,6 @@ char *home; c(home,"man/cat8","qmail-smtpd.0",auto_uido,auto_gidq,0644); c(home,"man/man8","qmail-command.8",auto_uido,auto_gidq,0644); c(home,"man/cat8","qmail-command.0",auto_uido,auto_gidq,0644); + c(home,"man/man8","qmail-valid-addresses.8",auto_uido,auto_gidq,0644); + c(home,"man/cat8","qmail-valid-addresses.0",auto_uido,auto_gidq,0644); } diff --git a/install-big.c b/install-big.c index d5594f9..a390e03 100644 --- a/install-big.c +++ b/install-big.c @@ -132,6 +132,7 @@ char *home; c(home,"bin","qmail-qmqpd",auto_uido,auto_gidq,0755); c(home,"bin","qmail-qmtpd",auto_uido,auto_gidq,0755); c(home,"bin","qmail-smtpd",auto_uido,auto_gidq,0755); + c(home,"bin","qmail-valid-addresses",auto_uido,auto_gidq,0755); c(home,"bin","sendmail",auto_uido,auto_gidq,0755); c(home,"bin","tcp-env",auto_uido,auto_gidq,0755); c(home,"bin","qreceipt",auto_uido,auto_gidq,0755); diff --git a/qmail-smtpd.8 b/qmail-smtpd.8 index e53e263..d00f339 100644 --- a/qmail-smtpd.8 +++ b/qmail-smtpd.8 @@ -37,6 +37,40 @@ accepts messages that contain long lines or non-ASCII characters, even though such messages violate the SMTP protocol. .SH "CONTROL FILES" .TP 5 +.I addrcheck.cdb +A database of acceptable mailboxes. If present, this is used to report +erroneous RCPT TO commands, which can reduce the amount of junk mail +accepted. It contains an encoding of the virtual domains map +.RB ( \c +.BI V domain +maps to +.IR prefix ), +the local domains +.RB ( \c +.BI @ domain +maps to an empty string), and the available local parts +.RB ( \c +.BI L mailbox +maps to +.B + +if the address is valid or +.B \- +if not). It's best made using +.BR qmail-valid-addresses (8). +.TP 5 +.I addrcheck-delay +Delay in seconds before reporting bad mailbox names after the +.I addrcheck-slow +limit is reached. The default is 2. +.TP 5 +.I addrcheck-limit +Number of bad mailbox names to tolerate before dropping the connection. +Zero means an infinite number. The default is 50. +.TP 5 +.I addrcheck-slow +Number of bad mailbox names to tolerate before imposing delays. The +default is 5. +.TP 5 .I badmailfrom Unacceptable envelope sender addresses. .B qmail-smtpd diff --git a/qmail-smtpd.c b/qmail-smtpd.c index 6f453ce..82ac46a 100644 --- a/qmail-smtpd.c +++ b/qmail-smtpd.c @@ -20,6 +20,7 @@ #include "now.h" #include "exit.h" #include "rcpthosts.h" +#include "addrcheck.h" #include "timeoutread.h" #include "timeoutwrite.h" #include "commands.h" @@ -47,10 +48,12 @@ void die_alarm() { out("451 timeout (#4.4.2)\r\n"); flush(); _exit(1); } void die_nomem() { out("421 out of memory (#4.3.0)\r\n"); flush(); _exit(1); } void die_control() { out("421 unable to read controls (#4.3.0)\r\n"); flush(); _exit(1); } void die_ipme() { out("421 unable to figure out my IP addresses (#4.3.0)\r\n"); flush(); _exit(1); } +void die_badaddr() { out("553 too many bad recipients: sulking (#5.5.1)\r\n"); flush(); _exit(1); } void straynewline() { out("451 See http://pobox.com/~djb/docs/smtplf.html.\r\n"); flush(); _exit(1); } void err_bmf() { out("553 sorry, your envelope sender is in my badmailfrom list (#5.7.1)\r\n"); } void err_nogateway() { out("553 sorry, that domain isn't in my list of allowed rcpthosts (#5.7.1)\r\n"); } +void err_badaddr() { out("553 unknown mailbox (#5.1.1)\r\n"); } void err_unimpl() { out("502 unimplemented (#5.5.1)\r\n"); } void err_syntax() { out("555 syntax error (#5.5.4)\r\n"); } void err_wantmail() { out("503 MAIL first (#5.5.1)\r\n"); } @@ -99,6 +102,11 @@ struct constmap maprelayhosts; int bmfok = 0; stralloc bmf = {0}; struct constmap mapbmf; +static int ac_slow = 5; +static int ac_limit = 50; +static int ac_delay = 2; +static int ac_count = 0; +static int ac_fd = -1; void setup() { @@ -129,6 +137,13 @@ void setup() die_nomem(); } + if (control_readint(&ac_slow, "control/addrcheck-slow") == -1 || + control_readint(&ac_slow, "control/addrcheck-limit") == -1 || + control_readint(&ac_slow, "control/addrcheck-delay") == -1) + die_control(); + + if ((ac_fd = open_read("control/addrcheck.cdb")) < 0 && errno != error_noent) + die_control(); if (control_readint(&databytes,"control/databytes") == -1) die_control(); x = env_get("DATABYTES"); @@ -283,6 +298,22 @@ void smtp_rcpt(arg) char *arg; { } else if (!addrallowed()) { err_nogateway(); return; } + if (ac_fd != -1) { + int rc; + if (addrcheck(ac_fd, addr.s, &rc) < 0) { + if (errno == error_nomem) + die_nomem(); + else + die_control(); + } + if (!rc) { + ac_count++; + if (ac_limit && ac_count >= ac_limit) die_badaddr(); + if (ac_delay && ac_count >= ac_slow) sleep(ac_delay); + err_badaddr(); + return; + } + } if (!stralloc_cats(&rcptto,"T")) die_nomem(); if (!stralloc_cats(&rcptto,addr.s)) die_nomem(); if (!stralloc_0(&rcptto)) die_nomem(); diff --git a/qmail-valid-addresses b/qmail-valid-addresses new file mode 100644 index 0000000..b77be52 --- /dev/null +++ b/qmail-valid-addresses @@ -0,0 +1,81 @@ +#! /usr/bin/python + +import os +import cdb + +def sort(l): + l = [] + l + l.sort() + return l +class struct (object): + def __init__(me, **kw): + me.__dict__.update(kw) + def __repr__(me): + return (type(me).__name__ + + '(' + + ', '.join(['%s = %r' % (k, me.__dict__[k]) + for k in me.__dict__ + if k[0] != '_']) + + ')') +class userentry (struct): + pass + +os.chdir('/var/qmail') + +umap = {} +udb = cdb.init('users/cdb') +for k in udb.keys(): + if len(k) == 0 or k[0] != '!': + continue + v = udb[k].split('\0') + u = userentry(user = v[0], uid = int(v[1]), gid = int(v[2]), home = v[3], + dash = v[4], pre = v[5]) + if k[-1] == '\0': + u.name = k[1:-1] + u.wild = 0 + else: + u.name = k[1:] + u.wild = 1 + umap[u.name] = u +del udb + +map = {} +def addlocal(p, l, forcep = False): + l = 'L' + l + if not os.path.exists(p): + if forcep: + map[l] = '+' + return + f = open(p) + top = f.readline() + f.close() + if len(top) > 0 and top[0] == '!': + map[l] = '-' + else: + map[l] = '+' +for k in sort(umap.keys()): + u = umap[k] + qm = '.qmail' + u.dash + u.pre + qmlen = len(qm) + if u.wild: + for p in os.listdir(u.home): + if not p.startswith(qm): + continue + ext = p[qmlen:] + addlocal(os.path.join(u.home, p), u.name + ext) + else: + addlocal(os.path.join(u.home, qm), u.name, u.dash == '') + +for dom in open('control/locals'): + if len(dom) and dom[-1] == '\n': + dom = dom[:-1] + map['@' + dom] = '' + +for v in open('control/virtualdomains'): + if len(v) and v[-1] == '\n': + v = v[:-1] + (addr, pre) = v.split(':', 2) + map['V' + addr] = pre + +for l in sort(map.keys()): + print '%s:%s' % (l, map[l]) diff --git a/qmail-valid-addresses.8 b/qmail-valid-addresses.8 new file mode 100644 index 0000000..46dae31 --- /dev/null +++ b/qmail-valid-addresses.8 @@ -0,0 +1,20 @@ +.TH qmail-valid-addresses 8 +.SH NAME +qmail-valid-addresses \- prepare addresses for use with qmail-smtpd +.SH SYNOPSIS +.B qmail-valid-addresses +.SH DESCRIPTION +.B qmail-valid-addresses +scans the +.B users/cdb +file, and the +.B local +and +.B virtualdomain +control files, and emits a textual representation of the required +.I addrcheck +database in a form acceptable to +.BR cdb-map (8). +.SH "SEE ALSO" +qmail-smtpd(8), +cdb-map(1). -- 2.11.0