/* $Id: vrfy.C,v 1.21 2006/03/23 07:30:38 dm Exp $ */ /* * * Copyright (C) 2004 David Mazieres (dm@uun.org) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA * */ #include "asmtpd.h" struct addrcache_entry { enum res_t { LOCKED, VALID, INVALID }; const str addr; res_t status; str msg; time_t last; vec waitq; ihash_entry hashlink; tailq_entry agelink; addrcache_entry (const str &a); ~addrcache_entry (); void setstatus (res_t r, str msg); }; static ihash addrcache; static tailq addrcache_expire; addrcache_entry::addrcache_entry (const str &a) : addr (a), status (INVALID), last (0) { addrcache.insert (this); addrcache_expire.insert_tail (this); for (addrcache_entry *ac = addrcache_expire.first, *nac; ac != this && ac->status != LOCKED; ac = nac) { nac = ac->agelink.next; if (ac->last + opt->vrfy_cachetime <= timenow +0U) delete ac; } } addrcache_entry::~addrcache_entry () { assert (status != LOCKED); assert (waitq.empty ()); addrcache.remove (this); addrcache_expire.remove (this); } void addrcache_entry::setstatus (res_t r, str m) { last = timenow; addrcache_expire.remove (this); addrcache_expire.insert_tail (this); if (r != INVALID) msg = NULL; else if (m && m.len () && m[0] == '4' || m[0] == '5') msg = m; else msg = "451 SMTP callback failure\r\n"; status = r; vec v; v.swap (waitq); while (!v.empty ()) (*v.pop_front ()) (msg); } static void addrcache_report (str addr, str msg) { addrcache_entry *ac = addrcache[addr]; if (!ac) ac = New addrcache_entry (addr); if (!msg || (msg.len () && msg[0] == '2')) ac->setstatus (addrcache_entry::VALID, NULL); else ac->setstatus (addrcache_entry::INVALID, msg); } static void addrcache_query (str addr, cbs cb) { addrcache_entry *ac = addrcache[addr]; if (!ac) ac = New addrcache_entry (addr); if (ac->status == addrcache_entry::LOCKED) { ac->waitq.push_back (cb); return; } if (ac->last + opt->vrfy_cachetime <= timenow + 0U) ac->setstatus (addrcache_entry::LOCKED, NULL); (*cb) (ac->msg); } class mailserv { public: typedef callback::ref chatcb_t; private: ptr aio; bool lineconsistent (strbuf sb, str line); void concb (str line, int err); void helocb (bool ehlo, str line, int err); void chatcb (ref > cv, chatcb_t cb, strbuf sb, str line, int err); void unsolicited (str line, int err); void touch (); static void cleanup (); protected: bool busy; bool noop; void idle (bool clean = false); virtual void fail (bool temp, str error); virtual void ready () { busy = false; } public: static u_int nmailserv; static str heloname () { return opt->hostname; } const str name; ihash_entry hlink; tailq_entry llink; mailserv (str n, int fd); virtual ~mailserv (); void helo (); void chat (const vec &cmds, chatcb_t cb); static mailserv *lookup (str name); }; struct mailserv_readycb : mailserv { timecb_t *tmo; cbb::ptr cb; mailserv_readycb (str n, int fd) : mailserv (n, fd), tmo (NULL) {} ~mailserv_readycb () { timecb_remove (tmo); if (cb) (*cb) (false); } void ready () { idle (); busy = true; tmo = delaycb (opt->vrfy_delay, wrap (this, &mailserv_readycb::ready_2)); } void ready_2 () { tmo = NULL; busy = false; cbb c (cb); cb = NULL; (*c) (true); } }; static ihash mstab; static tailq msq; u_int mailserv::nmailserv; void mailserv::cleanup () { // XXX - this will, of course, fail to close mailservs behind slow clients while (nmailserv > opt->max_revclients && msq.first && !msq.first->busy) delete msq.first; } mailserv::mailserv (str n, int fd) : aio (aios::alloc (fd)), busy (true), name (mytolower (n)) { nmailserv++; mstab.insert (this); msq.insert_tail (this); if (opt->debug_smtpc) aio->setdebug (strbuf ("%s (R%d)", name.cstr (), fd)); aio->settimeout (opt->client_timeout); cleanup (); } mailserv::~mailserv () { nmailserv--; mstab.remove (this); msq.remove (this); } void mailserv::helo () { aio->readline (wrap (this, &mailserv::concb)); } void mailserv::fail (bool temp, str error) { if (opt->debug_smtpc) warn << name << ": " << error << "\n"; delete this; } void mailserv::concb (str line, int err) { if (!line || line.len () < 4) fail (true, err ? strerror (err) : "eof"); else if (line[3] == '-') aio->readline (wrap (this, &mailserv::concb)); else if (line[0] != '2') fail (line[0] != '5', substr (line, 4)); else { aio << "EHLO " << heloname () << "\r\n"; aio->readline (wrap (this, &mailserv::helocb, true)); } } void mailserv::helocb (bool ehlo, str line, int err) { if (!line || line.len () < 4) fail (true, err ? strerror (err) : "eof"); else if (line[3] == '-') aio->readline (wrap (this, &mailserv::helocb, ehlo)); else if (line[0] == '5' && ehlo) { aio << "HELO " << heloname () << "\r\n"; aio->readline (wrap (this, &mailserv::helocb, false)); } else if (line[0] != '2') fail (line[0] != '5', substr (line, 4)); else { busy = false; ready (); idle (); } } void mailserv::chat (const vec &cmds, mailserv::chatcb_t cb) { assert (!busy); busy = true; aio->readcancel (); touch (); ref > cv = New refcounted > (cmds); if (cv->empty ()) { busy = false; (*cb) (NULL, 0); idle (true); } else { noop = !strncasecmp (cv->front (), "noop", 4); aio << cv->pop_front () << "\r\n"; aio->readline (wrap (this, &mailserv::chatcb, cv, cb, strbuf ())); } } bool mailserv::lineconsistent (strbuf sb, str line) { static rxx resprx ("^(\\d\\d\\d)(-| )(.*)$"); if (!line || !resprx.match (line)) return false; if (sb.tosuio ()->resid () > 3) { char buf[3]; sb.tosuio ()->copyout (buf, 3); if (memcmp (buf, line.cstr (), 3)) return false; } else if (!sb.tosuio ()->resid ()) sb << resprx[1] << "-server " << name << " reports:\r\n"; sb << line << "\r\n"; return true; } void mailserv::chatcb (ref > cv, chatcb_t cb, strbuf sb, str line, int err) { if (!lineconsistent (sb, line)) { #if 1 if (line) { warn ("inconsistent line -------------------------------------------\n"); warn << sb << line << "\n"; warn ("-------------------------------------------------------------\n"); } #endif fail (true, err ? strerror (err) : "eof"); (*cb) (NULL, err); } else if (line[3] == '-') aio->readline (wrap (this, &mailserv::chatcb, cv, cb, sb)); else if ((!noop && line[0] != '2') || cv->empty ()) { busy = false; (*cb) (sb, 0); idle (true); } else { sb.tosuio ()->clear (); noop = !strncasecmp (cv->front (), "noop", 4); aio << cv->pop_front () << "\r\n"; aio->readline (wrap (this, &mailserv::chatcb, cv, cb, sb)); } } void mailserv::idle (bool clean) { if (!busy) { aio->readcancel (); aio->readline (wrap (this, &mailserv::unsolicited)); } if (clean) cleanup (); } void mailserv::unsolicited (str line, int err) { fail (true, "eof"); } void mailserv::touch () { msq.remove (this); msq.insert_tail (this); } mailserv * mailserv::lookup (str name) { name = mytolower (name); for (mailserv *msp = mstab[name]; msp; msp = mstab.nextkeq (msp)) if (!msp->busy) return msp; return NULL; } class mxconnect { typedef callback >::ref cb_t; str name; cb_t cb; int n; ptr mxl; int fallthrough_error; in_addr bindaddr; mxconnect (str nn, cb_t c, in_addr ba) : name (nn), cb (c), n (-1), fallthrough_error (ARERR_NXREC), bindaddr (ba) {} void start () { dns_mxbyname (name, wrap (this, &mxconnect::mxcb)); } void mxcb (ptr m, int dnserr) { mxl = m; //printmxlist (name, mxl, dnserr); if (mxl) trymx (); else if (dnserr == ARERR_NXREC) trya (name); else { (*cb) (NULL, dnserr, mxl); delete this; } } void trymx () { if (!mxl || implicit_cast (++n) >= mxl->m_nmx) { (*cb) (NULL, fallthrough_error, mxl); delete this; } else if (mailserv *msp = mailserv::lookup (mxl->m_mxes[n].name)) { (*cb) (msp, 0, mxl); delete this; } else trya (mxl->m_mxes[n].name); } void trya (str a) { /* XXX - probably want a smaller timeout */ dns_hostbyname (a, wrap (this, &mxconnect::tryip, a)); } void tryip (str a, ptr h, int err) { if (!h) { if (fallthrough_error) { if (dns_tmperr (err)) fallthrough_error = err; else if (!dns_tmperr (fallthrough_error)) fallthrough_error = err; } errno = dns_tmperr (err) ? EAGAIN : ENOENT; tcpcb (a, -1); return; } fallthrough_error = 0; u_int32_t ip = ntohl (((in_addr *) h->h_addr)->s_addr); if (ip >> 24 == 0 || ip >> 24 == 127 || ip == 0xffffffff) { errno = EACCES; tcpcb (a, -1); return; } /* XXX - this is kind of gross, and also only applies to * connections that are not already cached. */ in_addr save = inet_bindaddr; inet_bindaddr = bindaddr; tcpconnect (*(in_addr *) h->h_addr, 25, wrap (this, &mxconnect::tcpcb, a)); inet_bindaddr = save; } void tcpcb (str a, int fd) { if (fd < 0) { if (opt->debug_smtpc) warn << "mxconnect: " << a << " for " << name << ": " << strerror (errno) << "\n";; trymx (); } else { if (opt->debug_smtpc) warn << "mxconnect: " << a << " for " << name << ": success (fd " << fd << ")\n"; mailserv_readycb *msp = New mailserv_readycb (a, fd); msp->cb = wrap (this, &mxconnect::ready, msp); msp->helo (); } } void ready (mailserv *msp, bool ok) { if (ok) { (*cb) (msp, 0, mxl); delete this; } else trymx (); } public: static void alloc (str n, cb_t c, in_addr bindaddr) { (New mxconnect (n, c, bindaddr))->start (); } }; inline void vrfy_mkcb (str addr, vrfycb_t cb, str msg, ptr mxl) { addrcache_report (addr, msg); (*cb) (msg, mxl); } static void vrfy_4 (str relay, str addr, vrfycb_t cb, ptr mxl, str line, int err) { if (line && line[0] == '2') vrfy_mkcb (addr, cb, NULL, mxl); else if (line && (line[0] == '4' || line[0] == '5')) { if (line.len () > 4) vrfy_mkcb (addr, cb, line, mxl); else if (line[0] == '5') vrfy_mkcb (addr, cb, "550 bad user \r\n", mxl); else vrfy_mkcb (addr, cb, "451 bad user \r\n", mxl); } else { if (err) { str msg (strbuf ("451 %s: %s\r\n", relay.cstr (), strerror (err))); vrfy_mkcb (addr, cb, msg, mxl); } else vrfy_mkcb (addr, cb, strbuf ("451 %s: SMTP protocol failure\r\n", relay.cstr ()), mxl); } } static void vrfy_3 (str relay, str addr, in_addr cli, vrfycb_t cb, mailserv *msp, int dnserr, ptr mxl) { if (dnserr) vrfy_mkcb (addr, cb, strbuf ("%03d %s\r\n", dns_tmperr (dnserr) ? 451 : 553, dns_strerror (dnserr)), NULL); else if (!msp) vrfy_mkcb (addr, cb, strbuf () << "451 could not connect to server for " << relay << "\r\n", NULL); else { vec cmds; cmds.push_back ("RSET"); cmds.push_back (strbuf ("NOOP %s is sending mail from <%s>; if forged, " "consider an SPF record (http://www.openspf.org/)", inet_ntoa (cli), addr.cstr ())); cmds.push_back ("MAIL FROM:<>"); cmds.push_back (strbuf () << "RCPT TO:<" << addr << ">"); msp->chat (cmds, wrap (vrfy_4, relay, addr, cb, mxl)); } } static void vrfy_2 (in_addr bindaddr, str relay, str addr, in_addr cli, vrfycb_t cb, str msg) { if (msg) (*cb) (msg, NULL); else mxconnect::alloc (relay, wrap (vrfy_3, relay, addr, cli, cb), bindaddr); } void vrfy (in_addr bindaddr, str addr, in_addr cli, vrfycb_t cb) { str relay = extract_relay (addr); if (!relay) (*cb) ("501 syntax error\r\n", NULL); addr = domain_tolower (addr); addrcache_query (addr, wrap (vrfy_2, bindaddr, relay, addr, cli, cb)); }