Hi there, everyone;

        Due to the increasing SPAM problem, the subject of recipient validation
during SMTP has been brought up a few times. Some people want it, some
people are against it. I, personally, was neutral.
        However, in recent times, a couple of domains I manage (with almost 2
million valid mailboxes between them) have been repeatedly hit by
mass-mailers, opportunists, or simply braindamaged idiots who flood my
servers with mail which:

a) is sent to a non-existent address
b) is sent using a return-path address whose MX is located at a poor
line or a poor machine (or both).

        The result has been a steady growth of average queue size from 20k
mails to over 100k mails (and rising). Queue size isn't usually
important, but when you reboot a machine or have a network outage, it
takes forever to resume normal operation. Since I have a pretty high
mail address burn-rate, there are LOTS of e-mail addresses out there (in
spammer lists, mostly) which were valid once, but aren't valid anymore.
        So... I patched qmail-smtpd to validate "RCPT TO" in the LDAP server
before accepting the mail into the queue. If there's no mail or
mailAlternateAddress matching the given address, a 550 error is
returned; for example:

Connected to mx.example.com.
Escape character is '^]'.
220 mx.example.com ESMTP
HELO test
250 mx.example.com
MAIL FROM: <[EMAIL PROTECTED]>
250 ok
RCPT TO: <[EMAIL PROTECTED]>
250 ok
RCPT TO: <[EMAIL PROTECTED]>
550 recipient unavailable (#5.7.1)
RCPT TO: <[EMAIL PROTECTED]>
250 ok

In the above example, the message will be queued for [EMAIL PROTECTED]
and [EMAIL PROTECTED], but it won't be for [EMAIL PROTECTED]

This is what happens:

1 - qmail-smtpd checks at startup if the environment var CHECKRCPT
exists. If it doesn't, no validation will be performed. This is useful
when used with tcpserver. (1.2.3.:allow,RELAYCLIENT="",CHECKRCPT=1)

2 - if CHECKRCPT is true, control/checkdomains will be read.

3 - If the recipient domain is listed in checkdomains, control/checkskip
will be read.

4 - If the recipient local part is *not* listed in checkskip,
qmail-smtpd searches the LDAP server for
[EMAIL PROTECTED]


Thoughts:

Why CHECKRCPT? - Because it's an easy way to completely disable the
checks.
Why checkdomains? - Because I only want to check a few of my own
domains. I could use locals, but that would be unconfortable if I wanted
a whole domain to sit in ~alias.
Why checkskip? - Because some addresses aren't at the LDAP server, like
root, postmaster, and mailer-daemon. They sit in ~alias, and would be
rejected if searched.


The patch uses a compile-time define at Makefile (-DENVELOPE_SMTPCHECK),
which can be commented out. It applies cleany to qmail-ldap-20030101

Comments, ideas, and fixes are welcome;

Thanks;
-- 
Ricardo Cerqueira
"ASCII stupid question, get a stupid ANSI"
diff -u qmail-1.03/Makefile qmail-1.03-envelopecheck/Makefile
--- qmail-1.03/Makefile	Tue Jan 14 17:25:29 2003
+++ qmail-1.03-envelopecheck/Makefile	Tue Jan 14 17:44:27 2003
@@ -10,8 +10,11 @@
 # systems)
 # -DEXTERNAL_TODO to use the external high-performance todo processing (this
 # avoids the silly qmail syndrome with high injection rates)
+# -DENVELOPE_SMTPCHECK to validate the envelope recipient in the LDAP
+# tree during the SMTP conversation (requires SMTPCHECK_OBJS and FLAGS to
+# compile)
 #LDAPFLAGS=-DQLDAP_CLUSTER -DEXTERNAL_TODO
-LDAPFLAGS=-DQLDAP_CLUSTER -DEXTERNAL_TODO -DDASH_EXT
+LDAPFLAGS=-DQLDAP_CLUSTER -DEXTERNAL_TODO -DDASH_EXT -DENVELOPE_SMTPCHECK
 
 # Perhaps you have different ldap libraries, change them here
 LDAPLIBS=-L/usr/local/lib -lldap -llber
@@ -64,6 +67,11 @@
 #INCTAI=../libtai-0.60
 #LIBTAI=../libtai-0.60
 
+# uncomment the following if you want qmail-smtpd to validate the
+# envelope recipient
+SMTPCHECK_OBJS=qldap-ldaplib.o qldap-errno.o str_cpy.o auto_break.o fmt_strn.o byte_repl.o error_str.o $(LDAPLIBS)
+SMTPCHECK_FLAGS=$(LDAPFLAGS) $(LDAPINCLUDES) $(DEBUG)
+
 # Just for me, make from time to time a backup
 BACKUPPATH=/backup/qmail-backup/qmail-ldap.`date "+%Y%m%d-%H%M"`.tar
 # STOP editing HERE !!!
@@ -1840,7 +1848,7 @@
 	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 fs.a auto_qmail.o dns.o str.a \
-	`cat dns.lib` `cat socket.lib` ${TLSLIBS}
+	`cat dns.lib` `cat socket.lib` ${TLSLIBS} ${SMTPCHECK_OBJS}
 
 qmail-smtpd.0: \
 qmail-smtpd.8
@@ -1852,7 +1860,7 @@
 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 rbl.h
-	./compile ${TLSON} ${TLSINCLUDES} qmail-smtpd.c
+	./compile ${TLSON} ${TLSINCLUDES} ${SMTPCHECK_FLAGS} qmail-smtpd.c
 
 qmail-start: \
 load qmail-start.o prot.o fd.a auto_uids.o
diff -u qmail-1.03/qmail-smtpd.c qmail-1.03-envelopecheck/qmail-smtpd.c
--- qmail-1.03/qmail-smtpd.c	Tue Jan 14 17:25:29 2003
+++ qmail-1.03-envelopecheck/qmail-smtpd.c	Tue Jan 14 17:47:06 2003
@@ -33,11 +33,30 @@
 SSL *ssl = NULL;
 stralloc clientcert = {0};
 #endif
+#ifdef ENVELOPE_SMTPCHECK
+#include "qmail-ldap.h"
+#include "qldap-errno.h"
+#include "qldap-ldaplib.h"
+#include "qlx.h"
+#include "auto_break.h"
+#endif
 
 #define MAXHOPS 100
 unsigned int databytes = 0;
 int timeout = 1200;
 
+
+#ifdef ENVELOPE_SMTPCHECK
+/* initialize the string arrays for domains to check + exceptions */
+
+stralloc checkdoms = {0};
+struct constmap mapcheckdoms;
+stralloc checkskip = {0};
+struct constmap mapcheckskip;
+
+/* init done */
+#endif
+
 #ifdef TLS
 int flagtimedout = 0;
 void sigalrm()
@@ -141,6 +160,9 @@
 void err_bmfunknown() { out("553 sorry, your mail from a host without RR DNS was administratvely denied. (#5.7.1)\r\n"); }
 void err_maxrcpt() { out("553 sorry, too many recipients (#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"); }
+#ifdef ENVELOPE_SMTPCHECK
+void err_nosuchuser() { out("550 recipient unavailable (#5.7.1)\r\n"); }
+#endif
 void err_badbounce() { out("550 sorry, I don't accept bounce messages with more than one recipient. Go read RFC2821. (#5.7.1)\r\n"); }
 #ifdef TLS
 void err_nogwcert() { out("553 no valid cert for gatewaying (#5.7.1)\r\n"); }
@@ -226,6 +248,9 @@
 int tarpitcount = 0;
 int tarpitdelay = 5;
 int maxrcptcount = 0;
+#ifdef ENVELOPE_SMTPCHECK
+int checkrcpt = 0;
+#endif
 
 void setup()
 {
@@ -281,6 +306,31 @@
   if (brtok)
     if (!constmap_init(&mapbadrcptto,brt.s,brt.len,0)) die_nomem();
 
+#ifdef ENVELOPE_SMTPCHECK
+
+  x = env_get("CHECKRCPT");
+  if (x) { scan_ulong(x,&u); checkrcpt = u; };
+  if (checkrcpt) {
+   switch(control_readfile(&checkdoms,"control/checkdomains",0)) {
+    case -1:
+      die_control();
+    case 0:
+      if (!constmap_init(&mapcheckdoms,"",0,0)) die_nomem(); break;
+    case 1:
+      if (!constmap_init(&mapcheckdoms,checkdoms.s,checkdoms.len,0)) die_nomem(); break;
+   }
+   switch(control_readfile(&checkskip,"control/checkskip",0)) {
+    case -1:
+      die_control();
+    case 0:
+      if (!constmap_init(&mapcheckskip,"",0,0)) die_nomem(); break;
+    case 1:
+      if (!constmap_init(&mapcheckskip,checkskip.s,checkskip.len,0)) die_nomem(); break;
+   }
+  }
+
+#endif
+
   rblok = rblinit();
   if (rblok == -1) die_control();
 
@@ -739,6 +789,19 @@
   }
   logpid(3); logstring(3,"rcpt to ="); logstring(3,addr.s); logflush(3);
 
+#ifdef ENVELOPE_SMTPCHECK
+
+  init_ldap( 0, 0, 0, 0, 0, 0, 0 );
+
+  if (checkrcpt && checklocalpart() && checkdomain() && qldap_get(&addr)) 
+  {
+      logpid(1); logstring(1,"bad rcpt for"); logstring(1,addr.s); logflush(1);
+      err_nosuchuser();
+      if (errdisconnect) err_quit();
+      return;
+  }
+#endif
+
   /* blockrelay stupid sendwhale bug relay probing */
   if (blockrelayprobe) /* don't enable this if you use percenthack */
   {
@@ -1123,3 +1186,152 @@
   if (commands(&ssin,&smtpcommands) == 0) die_read();
   die_nomem();
 }
+
+#ifdef ENVELOPE_SMTPCHECK
+/* This was taken from lspawn and trimmed down */
+
+int qldap_get(stralloc *mail)
+{
+   userinfo   info;
+   extrainfo  extra[1];
+   searchinfo search;
+   char *attrs[] = { 0 };
+   int  ret;
+   int  at;
+   int  i;
+   int  len;
+   int  force_forward;
+   char *s;
+   stralloc filter = {0};
+   unsigned long tid;
+
+   /* check the mailaddress for illegal characters       *
+    * escape '*', ,'\', '(' and ')' with a preceding '\' */
+   if (!escape_forldap(mail) ) _exit(QLX_NOMEM);
+
+   at = 0;
+   i = 0;
+   s = mail->s;
+   len = mail->len;
+
+     logline(16, "starting search");
+   ret = qldap_open();
+   if ( ret != 0 ) goto ldap_fail;
+
+   for (at = len - 1; s[at] != '@' && at >= 0 ; at--) ; 
+   /* handels also mail with 2 @ */
+   /* at = index to last @ sign in mail address */
+   /* s = mailaddress, len = lenght of address */
+   /* i = position of current '-' */
+   i = at;
+
+   do {
+     /* this handles the "catch all" and "-default" extension */
+	 /* but also the normal eMail address */
+
+     /* build the search string for the email address */
+     if (!stralloc_copys(&filter,"(" ) ) _exit(QLX_NOMEM);
+
+     /* mail address */
+     if (!stralloc_cats(&filter,"|(")) _exit(QLX_NOMEM);
+     if (!stralloc_cats(&filter,LDAP_MAIL)) _exit(QLX_NOMEM);
+     if (!stralloc_cats(&filter,"=")) _exit(QLX_NOMEM);
+     /* username till current '-' */
+     if (!stralloc_catb(&filter,s, i)) _exit(QLX_NOMEM);
+     if ( i != at ) { /* do not append catchall in the first round */
+       /* catchall or default */
+       if ( i != 0 ) /* add '-' */
+         if (!stralloc_cats(&filter,auto_break)) _exit(QLX_NOMEM);
+       if (!stralloc_cats(&filter,LDAP_CATCH_ALL)) _exit(QLX_NOMEM);
+     }
+     /* @damin.com */
+     if (!stralloc_catb(&filter,s+at, len-at)) _exit(QLX_NOMEM);
+
+     /* mailalternate address */
+     if (!stralloc_cats(&filter,")(")) _exit(QLX_NOMEM);
+     if (!stralloc_cats(&filter,LDAP_MAILALTERNATE)) _exit(QLX_NOMEM);
+     if (!stralloc_cats(&filter,"=")) _exit(QLX_NOMEM);
+     /* username till current '-' */
+     if (!stralloc_catb(&filter,s, i)) _exit(QLX_NOMEM);
+     if ( i != at ) { /* do not append catchall in the first round */
+       /* catchall or default */
+       if ( i != 0 ) /* add '-' */
+         if (!stralloc_cats(&filter,auto_break)) _exit(QLX_NOMEM);
+       if (!stralloc_cats(&filter,LDAP_CATCH_ALL)) _exit(QLX_NOMEM);
+     }
+     /* @domain.com */
+     if (!stralloc_catb(&filter,s+at, len-at)) _exit(QLX_NOMEM);
+     if (!stralloc_cats(&filter,"))")) _exit(QLX_NOMEM);
+
+     if (!stralloc_0(&filter)) _exit(QLX_NOMEM);
+     
+     search.filter = filter.s;
+     search.bindpw = 0;    /* rebind off */
+  
+     /* initalize the different objects */
+     extra[0].what = 0;
+  
+     /* do the search for the email address */
+     ret = qldap_lookup(&search, attrs, &info, extra);
+
+     if ( ret == 0 || i == 0 ) break; /* something found or nothing found */
+#ifdef DASH_EXT
+     /* XXX if mail starts with a - it will probably not work as expected */
+     while ( i != 0 ) {
+       i--;
+       if ( s[i] == *auto_break ) break;
+     }
+#else
+     /* normal qmail-ldap behavior test for [EMAIL PROTECTED] and
+        [EMAIL PROTECTED] */
+     i = 0;
+#endif
+
+   } while ( ret != 0 && qldap_errno == LDAP_NOSUCH );
+
+   alloc_free(filter.s); filter.s = 0;
+
+ldap_fail:
+   qldap_close(); /* now close the ldap (TCP) connection */
+   if ( ret != 0 ) {
+      switch(qldap_errno) { /* Return OK unless entry isn't explicitly there */
+        case LDAP_NOSUCH:
+          return 1;
+          break;
+        default:
+          return 0;
+          break;
+      }
+      return 0; /* just in case... */
+   }
+
+
+   return 0;
+}
+/* end -- LDAP server query routines */
+
+int checkdomain()
+{
+  int j;
+  j = byte_rchr(addr.s,addr.len,'@');
+  j++;
+  if (j < addr.len)
+    if (constmap(&mapcheckdoms, addr.s + j, addr.len - j - 1))
+      return 1;
+  return 0;
+}
+
+int checklocalpart()
+{
+  int j;
+  j = byte_rchr(addr.s,addr.len,'@');
+  j++;
+  if (j < addr.len) 
+  {
+    if (constmap(&mapcheckskip, addr.s, j - 1))
+      return 0;
+  }
+  return 1;
+}
+
+#endif

Reply via email to