Logo Search packages:      
Sourcecode: mailavenger version File versions  Download package

spf.C

/* $Id$ */

/*
 *
 * 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"

spfhosts_map smap;

#define SPF_VERS "v=spf1"

static int spftrace (getenv ("SPF_TRACE") ? atoi (getenv ("SPF_TRACE")) : 0);

static rxx spaceplus (" +");
static rxx domcidr ("^([\\w_\\-.]+)?(/(/)?(\\d{1,3}))?$");
static rxx ip4cidr ("^(\\d{1,3}(\\.\\d{1,3}){3})(/(\\d{1,3}))?$");

rxx &
spfhosts_map::linerx ()
{
  static rxx spfrx ("^\\s*([^\\x00-\\x20\\x7f:]+)\\s*:\\s*(|\\S.*)$");
  return spfrx;
}

bool
spfhosts_map::lookup (str *spfrecp, str domain)
{
  if (!load ())
    return false;

  domain = mytolower (domain);
  if (str *sp = table[domain]) {
    *spfrecp = *sp;
    return true;
  }
  const char *p = domain;
  while ((p = strchr (p + 1, '.'))) {
    if (str *sp = table[domain]) {
      *spfrecp = *sp;
      return true;
    }
  }
  return false;
}

spf_t::spf_t (in_addr a, str f, ptr<bhash<str> > lc, int rd)
  : addr (a), sender (f), fallback (NULL), tracedepth (0), recdepth (rd),
    loopcheck (lc ? lc : ptr<bhash<str> > (New refcounted<bhash<str> >)),
    ptr_cache_err (false), dnsrq (NULL), recurse (NULL)
{
}

spf_t::~spf_t ()
{
  if (dnsrq)
    dnsreq_cancel (dnsrq);
  if (recurse)
    delete recurse;
}

void
spf_t::getexp (cbv cb, ptr<txtlist> t, int err)
{
  dnsrq = NULL;
  if (t) {
    strbuf sb;
    for (u_int i = 0; i < t->t_ntxt; i++)
      sb << t->t_txts[i];
    explain = sb;
  }
  (*cb) ();
}

void
spf_t::finish (spf_result stat)
{
  if (stat != SPF_PASS && !explain && expdn && expdn.len ()) {
    if (!macro_subst (&expdn, expdn)) {
      getptr (wrap (this, &spf_t::finish, stat));
      return;
    }
    str n = expdn;
    expdn = NULL;
    if (dnsreq_t *d
      = dns_txtbyname (n, wrap (this, &spf_t::getexp,
                          wrap (this, &spf_t::finish, stat))))
      dnsrq = d;
    return;
  }
  if (stat != SPF_PASS && explain && explain.len ()) {
    if (!macro_subst (&explain, explain, true)) {
      getptr (wrap (this, &spf_t::finish, stat));
      return;
    }
  }

  if (spftrace > !!tracedepth) {
    warn ("SPF_TRACE: %*sRETURN ", tracedepth, "")
      << inet_ntoa (addr) << " / "
      << domain << " -> " << spf_print (stat) << "\n";
    if (explain)
      warn ("SPF_TRACE: %*sEXPLANATION ", tracedepth, "") << explain << "\n";
  }
  result = stat;
  cb_t c (cb);
  cb = NULL;
  domain = NULL;
  (*c) (this);
  if (cb)
    init ();
  else
    delete this;
}

bool
spf_t::suffix_check (const char *targ, str suffix)
{
  u_int n = strlen (targ);
  if (n < suffix.len ())
    return false;
  if (n == suffix.len ())
    return (!strcasecmp (targ, suffix));
  const char *p = targ + n - suffix.len ();
  if (p[-1] != '.')
    return false;
  return !strcasecmp (p, suffix);
}

void
spf_t::getptr (cbv cb)
{
  if (dnsreq_t *d = dns_hostbyaddr (addr, wrap (this, &spf_t::getptr_2, cb)))
    dnsrq = d;
}
void
spf_t::getptr_2 (cbv cb, ptr<hostent> h, int err)
{
  dnsrq = NULL;

  if (spftrace >= 4)
    printaddrs (sender, h, err);

  if (dns_tmperr (err)) {
    finish (SPF_ERROR);
    return;
  }
  ptr_cache = h;
  ptr_cache_err = err;
  (*cb) ();
}

void
spf_t::init ()
{
  redirect = NULL;
  curmech = NULL;

  if (!domain)
    if (!(domain = extract_domain (sender)))
      domain = sender;
  if (!validate_domain (domain, true))
    finish (SPF_UNKNOWN);
  else if (spfrec)
    parse_spf ();
  else if (!loopcheck->insert (mytolower (domain)) || recdepth > 20) {
    if (spftrace >= 2)
      warn ("SPF_TRACE: %*sLOOP ", tracedepth, "") << domain << "\n";
    finish (SPF_UNKNOWN);
  }
  else if (dnsreq_t *d = dns_txtbyname (domain, wrap (this, &spf_t::gettxt)))
    dnsrq = d;
}

void
spf_t::gettxt (ptr<txtlist> t, int err)
{
  dnsrq = NULL;

  if (spftrace >= 4)
    printtxtlist (sender, t, err);

  if (err) {
    if (dns_tmperr (err))
      finish (SPF_ERROR);
    else if (fallback && fallback->lookup (&spfrec, domain))
      parse_spf ();
    else
      finish (SPF_NONE);
    return;
  }

  for (u_int i = 0; i < t->t_ntxt; i++)
    if (!strncmp (t->t_txts[i], SPF_VERS, sizeof (SPF_VERS) - 1)
      && (t->t_txts[i][sizeof (SPF_VERS) - 1] == ' '
          || t->t_txts[i][sizeof (SPF_VERS) - 1] == '\0')) {
      if (spfrec) {
      finish (SPF_UNKNOWN);
      return;
      }
      else
      spfrec = t->t_txts[i] + sizeof (SPF_VERS) - 1;
    }

  if (spfrec)
    parse_spf ();
  else
    finish (SPF_NONE);
}

void
spf_t::parse_spf ()
{
  vec<str> dm;
  split (&dm, spaceplus, spfrec);
  if (spftrace > !!recdepth) {
    warn ("SPF_TRACE: %*sCHECK ", tracedepth, "")
      << inet_ntoa (addr) << " / " << domain << "\n";
    if (spftrace >= 2) {
      warn ("SPF_TRACE: %*sRULES", tracedepth, "");
      for (str *rp = dm.base (); rp < dm.lim (); rp++)
      warnx << " " << *rp;
      warnx << "\n";
    }
  }

  for (str *dmp = dm.base (); dmp < dm.lim (); dmp++) {
    if (!dmp->len ())
      continue;
    const char *eq = strchr (*dmp, '=');
    if (!strchr (*dmp, '='))
      mechv.push_back (*dmp);
    else {
      const char *co = strchr (*dmp, ':');
      if (co && co < eq)
      mechv.push_back (*dmp);
      else if (!strncasecmp (*dmp, "redirect=", 9))
      redirect = dmp->cstr () + 9;
      else if (!strncasecmp (*dmp, "exp=", 4)) {
      expdn = dmp->cstr () + 4;
      explain = NULL;
      }
    }
  }

  mech_start ();
}

/*
      reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
                    "$" | ","

      unreserved  = alphanum | mark

      mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"

      delims      = "<" | ">" | "#" | "%" | <">

      unwise      = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
*/

inline bool
needsescape (u_char c)
{
  if (isalnum (c))
    return false;
  if (c == '-' || c == '_' || c == '.' || c == '!' || c == '~'
      || c == '*' || c == '\'' || c == '(' || c == ')')
    return false;
  return true;
}
static str
urlescape (str in)
{
  strbuf sb;
  for (const char *p = in; *p; p++)
    if (needsescape (*p))
      sb.fmt ("%%%02x", (u_char) *p);
    else
      sb.fmt ("%c", *p);
  return sb;
}

bool
spf_t::macro_subst_inner (const strbuf &out, str in, bool exp)
{
  if (!in.len ())
    return true;

  str text;
  bool escape = isupper (in[0]);
  switch (tolower (in[0])) {
  case 'l':
    if (!(text = extract_local (sender)))
      text = "postmaster";
    break;
  case 's':
    if (extract_local (sender))
      text = sender;
    else
      text = strbuf ("postmaster@%s", sender.cstr ());
    break;
  case 'o':
    if (!(text = extract_domain (sender)))
      text = sender;
    break;
  case 'd':
    text = domain;
    break;
  case 'i':
    text = inet_ntoa (addr);
    break;
  case 'p':
    if (!ptrok ())
      return false;
    else if (ptr_cache)
      text = ptr_cache->h_name;
    else
      text = "unknown";
    break;
  case 'v':
    text = "in-addr";
    break;
  case 'h':
    text = helo ? helo : str ("unknown");
    break;
  case 'r':
    text = opt->hostname;
    break;
  case 'c':
    if (exp) {
      text = inet_ntoa (addr);
      break;
    }
    /* cascade */
  case 't':
    if (exp) {
      text = strbuf ("%lu", (u_long) time (NULL));
      break;
    }
    /* cascade */
  default:
    out << "%{" << in << "}";
    return true;
  }

  static rxx td ("^.(\\d+)?(r)?([\\.\\-\\+,\\/_=]+)?");
  if (!td.match (in)) {
    out << "%{" << in << "}";
    return true;
  }

  str spn = td[3];
  if (!spn)
    spn = ".";
  vec<str> parts;
  
  const char *p = text;
  while (*p) {
    int n = strcspn (p, spn);
    parts.push_back (str (p, n));
    p += n;
    if (*p)
      p++;
  }

  if (td[2]) {
    str *sp = parts.base (), *ep = parts.lim ();
    while (sp + 1 < ep) {
      str tmp = *--ep;
      *ep = *sp;
      *sp++ = tmp;
    }
  }

  if (str ns = td[1]) {
    u_int n = atoi (ns);
    if (n < parts.size ())
      parts.popn_front (parts.size () - n );
  }

  for (u_int i = 0; i < parts.size (); i++)
    if (i)
      out << "." << (escape ? urlescape (parts[i]) : parts[i]);
    else
      out << (escape ? urlescape (parts[i]) : parts[i]);

  return true;
}

bool
spf_t::macro_subst (str *outp, str in, bool exp)
{
  strbuf out;
  for (const char *p = in; *p; p++) {
    if (*p != '%') {
      out.fmt ("%c", *p);
      continue;
    }
    switch (*++p) {
    case '%':
      out.fmt ("%%");
      break;
    case '_':
      out.fmt (" ");
      break;
    case '-':
      out.fmt ("%%20");
      break;
    case '{':
      {
      const char *r = strchr (p, '}');
      if (!r) {
        out << "%" << p;
        if (spftrace)
          warn << "malformed macro " << in << "\n";
        *outp = out;
        return true;
      }
      if (!macro_subst_inner (out, str (p + 1, r - p - 1), exp))
        return false;
      p = r;
      break;
      }
    case '\0':
      out.fmt ("%%");
      if (spftrace >= 3)
      warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> ";
      *outp = out;
      if (spftrace >= 3)
      warnx << *outp << "\n";
      return true;
    default:
      out.fmt ("%%%c", *p);
      break;
    }
  }
  if (spftrace >= 3)
    warn ("SPF_TRACE: %*sMACRO ", tracedepth, "") << in << " -> ";
  *outp = out;
  if (spftrace >= 3)
    warnx << *outp << "\n";
  return true;
}

bool
spf_t::addr_check (int cidrlen, ref<hostent> h)
{
  if (spftrace >= 5)
    printaddrs ("addr_check", h);

  u_int32_t mask = ip4_mask (cidrlen);
  u_int32_t targ = addr.s_addr & mask;
  for (char **ap = h->h_addr_list; *ap; ap++)
    if (targ == (((in_addr *) *ap)->s_addr & mask))
      return true;
  return false;
}

void
spf_t::mech_start ()
{
  if (mechv.empty ()) {
    if (redirect) {
      curmech = (strbuf () << "redirect=" << redirect);
      if (!macro_subst (&redirect, redirect)) {
      getptr (wrap (this, &spf_t::mech_start));
      return;
      }
      spfrec = NULL;
      recdepth++;
      if (spftrace >= 2)
      warn ("SPF_TRACE: %*sREDIRECT ", tracedepth, "")
        << domain << " -> " << redirect << "\n";
      domain = redirect;
      init ();
    }
    else {
      curmech = NULL;
      finish (SPF_NEUTRAL);
    }
    return;
  }

  curmech = mechv.pop_front ();
  const char *mp = curmech;
  switch (*mp) {
  case '+':
  case '-':
  case '~':
  case '?':
    prefix = *mp++;
    break;
  default:
    prefix = '+';
    break;
  }

  const char *cp = strchr (mp, ':');
  if (!cp)
    cp = strchr (mp, '/');
  str arg;
  if (cp)
    arg = *cp == ':' ? cp + 1 : cp;
  str mech = mytolower (cp ? str (mp, cp - mp) : str (mp)); 

  if (mech == "all")
    mech_end (SPF_PASS);
  else if (mech == "include")
    mech_include (arg);
  else if (mech == "a")
    mech_a (arg);
  else if (mech == "mx")
    mech_mx (arg);
  else if (mech == "ptr")
    mech_ptr (arg);
  else if (mech == "ip4")
    mech_ip4 (arg);
  else if (mech == "ip6")
    mech_ip6 (arg);
  else if (mech == "exists")
    mech_exists (arg);
  else
    finish (SPF_UNKNOWN);
}

void
spf_t::mech_end (spf_result res)
{
  if (spftrace >= 2) {
    warn ("SPF_TRACE: %*sTEST ", tracedepth, "");
    if (curmech)
      warnx << curmech << " -> ";
    warnx << spf_print (res) << "\n";
  }
  switch (res) {
  case SPF_PASS:
    switch (prefix) {
    case '+':
      finish (SPF_PASS);
      break;
    case '-':
      finish (SPF_FAIL);
      break;
    case '~':
      finish (SPF_SOFTFAIL);
      break;
    case '?':
      finish (SPF_NEUTRAL);
      break;
    }
    break;
  case SPF_FAIL:
  case SPF_SOFTFAIL:
  case SPF_NEUTRAL:
    mech_start ();
    break;
  case SPF_NONE:
  case SPF_ERROR:
  case SPF_UNKNOWN:
    finish (res);
    break;
  default:
    panic ("unknown SPF state %d\n", res);
    break;
  }
}

void
spf_t::mech_include (str targ)
{
  if (!targ) {
    mech_end (SPF_UNKNOWN);
    return;
  }
  if (!macro_subst (&targ, targ)) {
    getptr (wrap (this, &spf_t::mech_include, targ));
    return;
  }
  if (spftrace >= 2)
    warn ("SPF_TRACE: %*sINCLUDE ", tracedepth, "") << targ << " ...\n";
  recurse = New spf_t (addr, sender, loopcheck, recdepth + 1);
  recurse->cb = wrap (this, &spf_t::mech_include_2);
  recurse->domain = targ;
  recurse->helo = helo;
  recurse->ptr_cache = ptr_cache;
  recurse->tracedepth = tracedepth + 1;
  recurse->init ();
}
void
spf_t::mech_include_2 (spf_t *)
{
  spf_result res = recurse->result;
  bool error = (res == SPF_NONE || res == SPF_ERROR || res == SPF_UNKNOWN);
  if (error || res == SPF_PASS || spftrace) {
    strbuf sb;
    sb << curmech << " (";
    if (recurse->curmech)
      sb << recurse->curmech << " -> ";
    sb << spf_print (res) << ")";
    curmech = sb;
  }
  recurse = NULL;
  if (res == SPF_NONE)
    res = SPF_UNKNOWN;
  mech_end (res);
}

void
spf_t::mech_a (str targ)
{
  int cidrlen = 32;
  if (!targ)
    targ = domain;
  else {
    if (!macro_subst (&targ, targ)) {
      getptr (wrap (this, &spf_t::mech_a, targ));
      return;
    }
    if (!domcidr.match (targ) || domcidr[3]) {
      mech_end (SPF_FAIL);
      return;
    }

    if (str cl = domcidr[4])
      cidrlen = atoi (domcidr[4]);
    if (str d = domcidr[1])
      targ = domcidr[1];
    else
      targ = domain;
  }

  if (dnsreq_t *d
      = dns_hostbyname (targ, wrap (this, &spf_t::mech_a_2, cidrlen),
                  false, false))
    dnsrq = d;
}
void
spf_t::mech_a_2 (int cidrlen, ptr<hostent> h, int err)
{
  dnsrq = NULL;

  if (err)
    mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL);
  else if (addr_check (cidrlen, h))
    mech_end (SPF_PASS);
  else
    mech_end (SPF_FAIL);
}

void
spf_t::mech_mx (str targ)
{
  int cidrlen = 32;
  if (!targ)
    targ = domain;
  else {
    if (!macro_subst (&targ, targ)) {
      getptr (wrap (this, &spf_t::mech_a, targ));
      return;
    }
    if (!domcidr.match (targ) || domcidr[3]) {
      mech_end (SPF_FAIL);
      return;
    }

    if (str cl = domcidr[4])
      cidrlen = atoi (domcidr[4]);
    if (str d = domcidr[1])
      targ = domcidr[1];
    else
      targ = domain;
  }

  if (dnsreq_t *d
      = dns_mxbyname (targ, wrap (this, &spf_t::mech_mx_2, cidrlen)))
    dnsrq = d;
}
void
spf_t::mech_mx_2 (int cidrlen, ptr<mxlist> mxl, int err)
{
  dnsrq = NULL;

  if (spftrace >= 5)
    printmxlist ("mech_mx_2", mxl, err);

  if (err)
    mech_end (dns_tmperr (err) ? SPF_ERROR : SPF_FAIL);
  else if (dnsreq_t *d
         = dns_hostbyname (mxl->m_mxes[0].name,
                       wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, 1),
                       false, false))
    dnsrq = d;
}
void
spf_t::mech_mx_3 (int cidrlen, ptr<mxlist> mxl, int n, ptr<hostent> h, int err)
{
  dnsrq = NULL;
  if (h && addr_check (cidrlen, h))
    mech_end (SPF_PASS);
  else if (err && dns_tmperr (err))
    mech_end (SPF_ERROR);
  else if (n >= mxl->m_nmx)
    mech_end (SPF_FAIL);
  else if (dnsreq_t *d
         = dns_hostbyname (mxl->m_mxes[n].name,
                       wrap (this, &spf_t::mech_mx_3, cidrlen, mxl, n+1),
                       false, false))
    dnsrq = d;
}

void
spf_t::mech_ptr (str targ)
{
  if (!ptrok ()) {
    getptr (wrap (this, &spf_t::mech_ptr, targ));
    return;
  }
  if (!targ)
    targ = domain;
  macro_subst (&targ, targ);

  if (!ptr_cache) {
    mech_end (dns_tmperr (ptr_cache_err) ? SPF_ERROR : SPF_FAIL);
    return;
  }

  if (suffix_check (ptr_cache->h_name, targ)) {
    mech_end (SPF_PASS);
    return;
  }
  for (char **np = ptr_cache->h_aliases; *np; np++)
    if (suffix_check (targ, *np)) {
      mech_end (SPF_PASS);
      return;
    }
  mech_end (SPF_FAIL);
}

void
spf_t::mech_ip4 (str targ)
{
  in_addr a;
  if (!targ || !ip4cidr.match (targ) || !inet_aton (ip4cidr[1], &a)) {
    mech_end (SPF_UNKNOWN);
    return;
  }

  u_int32_t mask;
  if (str cl = ip4cidr[4])
    mask = ip4_mask (atoi (cl));
  else
    mask = 0xffffffff;
  if ((addr.s_addr & mask) == (a.s_addr & mask))
    mech_end (SPF_PASS);
  else
    mech_end (SPF_FAIL);
}

void
spf_t::mech_ip6 (str targ)
{
  /* Never matches, no IPv6 support */
  mech_end (SPF_FAIL);
}

void
spf_t::mech_exists (str targ)
{
  if (!targ)
    targ = domain;
  else if (!macro_subst (&targ, targ)) {
    getptr (wrap (this, &spf_t::mech_exists, targ));
    return;
  }

  if (dnsreq_t *d = dns_hostbyname (targ,
                            wrap (this, &spf_t::mech_exists_2, targ),
                            false, false))
    dnsrq = d;
}
void
spf_t::mech_exists_2 (str targ, ptr<hostent> h, int err)
{
  dnsrq = NULL;
  if (spftrace >= 4)
    printaddrs (targ, h, err);
  if (h)
    mech_end (SPF_PASS);
  else if (dns_tmperr (err))
    mech_end (SPF_ERROR);
  else
    mech_end (SPF_FAIL);
}

static void
spf_check_3 (spfckcb_t cb, spf_result override, str omech, spf_t *spf)
{
  spf_result res (spf->result);

  if (override != SPF_INVALID) {
    if (res == SPF_NEUTRAL || res == SPF_NONE || res == SPF_UNKNOWN) {
      res = override;
      (*cb) (res, spf->explain, omech);
    }
    else
      (*cb) (res, spf->explain, spf->curmech);
    return;
  }

  switch (res) {
  case SPF_FAIL:
    if (opt->spf_fail) {
      spf->spfrec = opt->spf_fail;
      spf->cb = wrap (spf_check_3, cb, res, spf->curmech);
      spf->domain = NULL;
      spf->explain = opt->spf_exp;
      return;
    }
    break;
  case SPF_NONE:
    if (opt->spf_none) {
      spf->spfrec = opt->spf_none;
      spf->cb = wrap (spf_check_3, cb, res, spf->curmech);
      spf->domain = NULL;
      spf->explain = opt->spf_exp;
      return;
    }
    break;
  default:
    break;
  }
  (*cb) (res, spf->explain, spf->curmech);
}
static void
spf_check_2 (spfckcb_t cb, spf_t *spf)
{
  spf_result res (spf->result);

  switch (res) {
  case SPF_PASS:
  case SPF_FAIL:
  case SPF_SOFTFAIL:
  case SPF_ERROR:
    (*cb) (res, spf->explain, NULL);
    return;
  default:
    break;
  }

  spf->spfrec = NULL;
  spf->explain = opt->spf_exp;
  spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL));
}
void
spf_check (in_addr a, str from, spfckcb_t cb,
         str helo, ptr<const hostent> ptrc)
{
  spf_t *spf = New spf_t (a, from);
  spf->helo = helo;
  spf->ptr_cache = ptrc;
  spf->spfrec = opt->spf_local;
  spf->explain = opt->spf_exp;
  spf->fallback = &smap;
  if (opt->spf_local)
    spf->cb = wrap (spf_check_2, cb);
  else
    spf->cb = wrap (spf_check_3, cb, SPF_INVALID, str (NULL));
  spf->init ();
}

const char *
spf_print (spf_result res)
{
  switch (res) {
  case SPF_PASS:
    return "pass";
  case SPF_FAIL:
    return "fail";
  case SPF_SOFTFAIL:
    return "softfail";
  case SPF_NEUTRAL:
    return "neutral";
  case SPF_NONE:
    return "none";
  case SPF_ERROR:
    return "error";
  case SPF_UNKNOWN:
    return "unknown";
  default:
    return "bad SPF result code";
  }
}

const char *
spf1_print (spf_result res)
{
  switch (res) {
  case SPF_PASS:
    return "Pass";
  case SPF_FAIL:
    return "Fail";
  case SPF_SOFTFAIL:
    return "SoftFail";
  case SPF_NEUTRAL:
    return "Neutral";
  case SPF_NONE:
    return "None";
  case SPF_ERROR:
    return "TempError";
  case SPF_UNKNOWN:
    return "PermError";
  default:
    return "bad SPF result code";
  }
}

Generated by  Doxygen 1.6.0   Back to index