qmail-smtpd: Validation of recipient mailbox names.
authorMark Wooding <mdw@distorted.org.uk>
Tue, 9 Aug 2005 12:55:05 +0000 (12:55 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 14 Feb 2006 03:08:01 +0000 (03:08 +0000)
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.

12 files changed:
Makefile
TARGETS
addrcheck.c [new file with mode: 0644]
addrcheck.h [new file with mode: 0644]
debian/changelog
debian/control
hier.c
install-big.c
qmail-smtpd.8
qmail-smtpd.c
qmail-valid-addresses [new file with mode: 0644]
qmail-valid-addresses.8 [new file with mode: 0644]

index 6bf4a6c..f3d8ebd 100644 (file)
--- 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 (file)
--- 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 (file)
index 0000000..2ff9d13
--- /dev/null
@@ -0,0 +1,181 @@
+#include "cdb.h"
+#include "stralloc.h"
+#include "byte.h"
+#include "str.h"
+#include "addrcheck.h"
+#include <errno.h>
+
+/* #define DEBUG */
+#ifdef DEBUG
+#  define D(x) x
+#  include <stdio.h>
+#  include <sys/types.h>
+#  include <unistd.h>
+#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 <sys/types.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <errno.h>
+#include <unistd.h>
+
+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 (file)
index 0000000..7c51909
--- /dev/null
@@ -0,0 +1,6 @@
+#ifndef ADDRCHECK_H
+#define ADDRCHECK_H
+
+extern int addrcheck(int, const char *, int *);
+
+#endif
index 66fb5fd..c85d4b9 100644 (file)
@@ -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 <mdw@nsict.org>  Mon,  2 May 2005 14:44:12 +0100
 
index 4ea5783..9bd4434 100644 (file)
@@ -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 (file)
--- 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);
 }
index d5594f9..a390e03 100644 (file)
@@ -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);
index e53e263..d00f339 100644 (file)
@@ -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
index 6f453ce..82ac46a 100644 (file)
@@ -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 (file)
index 0000000..b77be52
--- /dev/null
@@ -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 (file)
index 0000000..46dae31
--- /dev/null
@@ -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).