/* $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<cbs> waitq;
ihash_entry<addrcache_entry> hashlink;
tailq_entry<addrcache_entry> agelink;
addrcache_entry (const str &a);
~addrcache_entry ();
void setstatus (res_t r, str msg);
};
static ihash<const str, addrcache_entry,
&addrcache_entry::addr, &addrcache_entry::hashlink> addrcache;
static tailq<addrcache_entry, &addrcache_entry::agelink> 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<cbs> 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<void, str, int>::ref chatcb_t;
private:
ptr<aios> aio;
bool lineconsistent (strbuf sb, str line);
void concb (str line, int err);
void helocb (bool ehlo, str line, int err);
void chatcb (ref<vec<str> > 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<mailserv> hlink;
tailq_entry<mailserv> llink;
mailserv (str n, int fd);
virtual ~mailserv ();
void helo ();
void chat (const vec<str> &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<const str, mailserv, &mailserv::name, &mailserv::hlink> mstab;
static tailq<mailserv, &mailserv::llink> 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<str> &cmds, mailserv::chatcb_t cb)
{
assert (!busy);
busy = true;
aio->readcancel ();
touch ();
ref<vec<str> > cv = New refcounted<vec<str> > (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<vec<str> > 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<void, mailserv *, int, ptr<mxlist> >::ref cb_t;
str name;
cb_t cb;
int n;
ptr<mxlist> 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<mxlist> 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<unsigned> (++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<hostent> 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<mxlist> mxl)
{
addrcache_report (addr, msg);
(*cb) (msg, mxl);
}
static void
vrfy_4 (str relay, str addr, vrfycb_t cb, ptr<mxlist> 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<mxlist> 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<str> 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));
}
syntax highlighted by Code2HTML, v. 0.9.1