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