<commit_before>/******************************************************************************/
/*          pfixtools: a collection of postfix related tools                  */
/*          ~~~~~~~~~                                                         */
/*  ________________________________________________________________________  */
/*                                                                            */
/*  Redistribution and use in source and binary forms, with or without        */
/*  modification, are permitted provided that the following conditions        */
/*  are met:                                                                  */
/*                                                                            */
/*  1. Redistributions of source code must retain the above copyright         */
/*     notice, this list of conditions and the following disclaimer.          */
/*  2. Redistributions in binary form must reproduce the above copyright      */
/*     notice, this list of conditions and the following disclaimer in the    */
/*     documentation and/or other materials provided with the distribution.   */
/*  3. The names of its contributors may not be used to endorse or promote    */
/*     products derived from this software without specific prior written     */
/*     permission.                                                            */
/*                                                                            */
/*  THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS   */
/*  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED         */
/*  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE    */
/*  DISCLAIMED.  IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY         */
/*  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL        */
/*  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS   */
/*  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)     */
/*  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       */
/*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN  */
/*  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE           */
/*  POSSIBILITY OF SUCH DAMAGE.                                               */
/*                                                                            */
/*   Copyright (c) 2006-2010 the Authors                                      */
/*   see AUTHORS and source files for details                                 */
/******************************************************************************/

/*
 * Copyright © 2009 <NAME>
 */

#include <ctype.h>

#include "spf.h"
#include "spf_tokens.h"
#include "buffer.h"
#include "utils.h"

#define SPF_MAX_RECUSION 15

typedef struct spf_rule_t {
    spf_code_t qualifier;
    spf_ruleid_t rule;
    buffer_t content;
    ip_t ip;
    cidrlen_t cidr4;
    cidrlen_t cidr6;
} spf_rule_t;
ARRAY(spf_rule_t);
#define SPF_RULE_INIT { 0, 0, ARRAY_INIT, { .v6 = { 0 } }, 0, 0 }

/* TODO: cleanup this structure.
 */
struct spf_t {
    unsigned txt_received : 1;
    unsigned txt_inerror  : 1;
    unsigned txt_toomany  : 1;
    unsigned spf_received : 1;
    unsigned spf_inerror  : 1;
    unsigned spf_nolookup : 1;
    unsigned canceled     : 1;
    unsigned is_ip6       : 1;
    unsigned use_domain   : 1;
    unsigned in_macro     : 1;
    unsigned a_dnserror   : 1;
    unsigned use_exp      : 1;
    unsigned in_exp       : 1;

    ip4_t ip4;
    ip6_t ip6;
    buffer_t ip;
    buffer_t domain;
    buffer_t sender;
    buffer_t helo;
    buffer_t validated;
    buffer_t explanation;

    buffer_t record;
    A(spf_rule_t) rules;
    uint8_t current_rule;
    int8_t redirect_pos;
    int8_t explanation_pos;

    buffer_t domainspec;

    int8_t recursions;
    struct spf_t* subquery;

    uint8_t a_resolutions;
    uint8_t mech_withdns;

    uint8_t queries;
    spf_result_t exit;
    void* data;
};

#define current_rule(spf) array_elt((spf)->rules, (spf)->current_rule)

static PA(spf_t) spf_pool = ARRAY_INIT;
static A(spf_rule_t) spf_rule_pool = ARRAY_INIT;

static char domainname[128];
static uint8_t domainname_len;
static buffer_t query_buffer = ARRAY_INIT;
static buffer_t dns_buffer = ARRAY_INIT;

static int created = 0;
static int acquired = 0;
static int deleted = 0;
static int released = 0;

static spf_t* spf_new(void)
{
    ++created;
    return p_new(spf_t, 1);
}

static void spf_rule_wipe(spf_rule_t* rule)
{
    array_wipe(rule->content);
}

static void spf_wipe(spf_t* spf)
{
    array_append(spf_rule_pool, array_start(spf->rules), array_len(spf->rules));
    array_wipe(spf->rules);
    array_wipe(spf->domain);
    array_wipe(spf->ip);
    array_wipe(spf->sender);
    array_wipe(spf->helo);
    array_wipe(spf->validated);
    array_wipe(spf->record);
    array_wipe(spf->domainspec);
    array_wipe(spf->explanation);
    p_clear(spf, 1);
}

static void spf_delete(spf_t **spf)
{
    if (*spf) {
        ++deleted;
        spf_wipe(*spf);
        p_delete(spf);
    }
}

static spf_t* spf_acquire(void)
{
    ++acquired;
    spf_t* spf = NULL;
    if (array_len(spf_pool)) {
        spf = array_pop_last(spf_pool);
    } else {
        spf = spf_new();
    }
    debug("spf pool: acquiring %p - pool length: %d (created %d)", spf, array_len(spf_pool), created);
    return spf;
}

static int spf_module_init(void)
{
    if (gethostname(domainname, 128) < 0) {
        return -1;
    }

    const char* pos = strchr(domainname, '.');
    if (pos == NULL) {
        return -1;
    }
    if (strchr(pos + 1, '.') != NULL) {
        memmove(domainname, pos + 1, 127 - (pos - domainname));
    }
    domainname_len = m_strlen(domainname);
    return 0;
}
module_init(spf_module_init);

static void spf_module_exit(void)
{
    array_deep_wipe(spf_pool, spf_delete);
    array_deep_wipe(spf_rule_pool, spf_rule_wipe);
    buffer_wipe(&query_buffer);
    buffer_wipe(&dns_buffer);
    debug("spf: Created: %d, Deleted: %d, Acquired: %d, Release: %d", created, deleted, acquired, released);
}
module_exit(spf_module_exit);

static bool spf_release(spf_t* spf, bool decrement)
{
    if (decrement) {
        --spf->queries;
    }
    if (spf->canceled && spf->queries == 0) {
        ++released;
        debug("spf pool: releasing %p - pool length: %d (created: %d)", spf, array_len(spf_pool) + 1, created);
        array_append(spf_rule_pool, array_start(spf->rules), array_len(spf->rules));
        array_len(spf->rules) = 0;
        buffer_reset(&spf->domain);
        buffer_reset(&spf->ip);
        buffer_reset(&spf->sender);
        buffer_reset(&spf->record);
        buffer_reset(&spf->helo);
        buffer_reset(&spf->validated);
        buffer_reset(&spf->domainspec);
        buffer_reset(&spf->explanation);
        spf_t bak = *spf;
        p_clear(spf, 1);
        spf->rules = bak.rules;
        spf->domain = bak.domain;
        spf->ip = bak.ip;
        spf->sender = bak.sender;
        spf->record = bak.record;
        spf->helo = bak.helo;
        spf->validated = bak.validated;
        spf->domainspec = bak.domainspec;
        spf->explanation = bak.explanation;
        array_add(spf_pool, spf);
        return true;
    }
    return false;
}

static bool spf_validate_domain(const char* restrict domain) {
    int label_count = 0;
    int label_length = 0;
    const char* restrict pos = domain;
    while (*pos != '\0') {
        if (*pos == '.') {
            if (label_length == 0) {
                debug("spf: invalid domain name \"%s\": contains a non-terminal zero-length label", domain);
                return false;
            }
            label_length = 0;
        } else if (!isalnum(*pos) && *pos != '-' && *pos != '_') {
            debug("spf: invalid domain name \"%s\": contains illegal character '%c'", domain, *pos);
            return false;
        } else {
            if (label_length == 0) {
                ++label_count;
            }
            ++label_length;
            if (label_length > 63) {
                debug("spf: invalid domain name \"%s\": contains a too long label", domain);
                return false;
            }
        }
        ++pos;
    }
    return label_count > 1;
}

static bool spf_query(spf_t* spf, const char* query, dns_rrtype_t rtype, ub_callback_t cb)
{
    buffer_reset(&query_buffer);
    buffer_addstr(&query_buffer, query);
    if (array_last(query_buffer) != '.') {
        buffer_addch(&query_buffer, '.');
    }
    debug("spf (depth=%d): performing query of type %d for %s", spf->recursions, rtype, query);
    if (dns_resolve(array_start(query_buffer), rtype, cb, spf)) {
        if (rtype == DNS_RRT_A || rtype == DNS_RRT_AAAA) {
            ++spf->a_resolutions;
        }
        ++spf->queries;
        return true;
    }
    return false;
}

static void spf_next(spf_t* spf, bool start);

static void spf_exit(spf_t* spf, spf_code_t code)
{
    if (spf->in_macro) {
        spf_next(spf, false);
        return;
    }
    if (log_level >= LOG_NOTICE) {
        const char* str = NULL;
        switch (code) {
          case SPF_NONE: str = "NONE"; break;
          case SPF_NEUTRAL: str = "NEUTRAL"; break;
          case SPF_PASS: str = "PASS"; break;
          case SPF_FAIL: str = "FAIL"; break;
          case SPF_SOFTFAIL: str = "SOFTFAIL"; break;
          case SPF_TEMPERROR: str = "TEMPERROR"; break;
          case SPF_PERMERROR: str = "PERMERROR"; break;
        }
        info("spf (depth=%d): result for query is %s", spf->recursions, str);
    }
    if (spf->exit) {
        if (array_len(spf->explanation) == 0) {
            spf->exit(code, NULL, spf->data);
        } else {
            spf->exit(code, array_start(spf->explanation), spf->data);
        }
    }
    spf_cancel(spf);
}

static void spf_write_macro(buffer_t* restrict  buffer, static_str_t str, bool escape)
{
    if (!escape) {
        buffer_add(buffer, str.str, str.len);
    } else {
        for (int i = 0 ; i < str.len ; ++i) {
            const char c = str.str[i];
            /* ";" / "?" / ":"       |
             *    |                |  / "@" / "&" / "=" / "+" / "$" / "," / "/" */
            if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~'
                || c == ';' || c == '?' || c == ':' || c == '@' || c == '&' || c == '='
                || c == '+' || c == '$' || c == ',' || c == '/') {
                buffer_addch(buffer, c);
            } else {
                buffer_addf(buffer, "%%%02x", c);
            }
        }
    }
}


/* Rule processing
 */
static void spf_include_exit(spf_code_t result, const char* exp, void* arg);
static void spf_redirect_exit(spf_code_t result, const char* exp, void* arg);
static void spf_a_receive(void* arg, int err, struct ub_result* result);
static void spf_mx_receive(void* arg, int err, struct ub_result* result);
static void spf_exists_receive(void* arg, int err, struct ub_result* result);
static void spf_exp_receive(void* arg, int err, struct ub_result* result);
static bool spf_run_ptr_resolution(spf_t* spf, const char* domainspec, bool in_macro);

static bool spf_dns_in_error(int err, struct ub_result* result)
{
    return (err != 0 || (result->rcode != DNS_RCODE_NOERROR && result->rcode != DNS_RCODE_NXDOMAIN));
}

typedef enum {
    SPFEXP_VALID,
    SPFEXP_INVALID,
    SPFEXP_SYNTAX,
    SPFEXP_DNS
} spf_expansion_t;

static spf_expansion_t spf_expand_pattern(spf_t* spf, buffer_t* restrict buffer, char identifier, int parts,
                                          bool reverse, bool escape, const char* restrict delimiters,
                                          int delimiters_count) {
    static_str_t sections[256] = { { .str = 0, .len = 0}, };
    static_str_t* pos = sections;
    switch (identifier) {
      case 's':
        pos->str = array_start(spf->sender);
        pos->len = array_len(spf->sender);
        break;
      case 'l':
        pos->str = array_start(spf->sender);
        pos->len = strchr(pos->str, '@') - pos->str;
        break;
      case 'o':
        pos->str = strchr(array_start(spf->sender), '@') + 1;
        pos->len = m_strlen(pos->str);
        break;
      case 'd':
        pos->str = array_start(spf->domain);
        pos->len = array_len(spf->domain);
        break;
      case 'i': {
        buffer_reset(&dns_buffer);
        if (!spf->is_ip6) {
            ip_print_4(&dns_buffer, spf->ip4, false, false);
        } else {
            ip_print_6(&dns_buffer, spf->ip6, false, false);
        }
        pos->str = array_start(dns_buffer);
        pos->len = array_len(dns_buffer);
      } break;
      case 'p':
        if (array_len(spf->validated) > 0) {
            pos->str = array_start(spf->validated);
            pos->len = array_len(spf->validated);
        } else if (spf_run_ptr_resolution(spf, NULL, true)) {
            return SPFEXP_DNS;
        } else {
            pos->str = "unknown";
            pos->len = m_strlen(pos->str);
        }
        break;
      case 'v':
        pos->str = spf->is_ip6 ? "ip6" : "in-addr";
        pos->len = m_strlen(pos->str);
        break;
      case 'h':
        pos->str = array_start(spf->helo);
        pos->len = array_len(spf->helo);
        break;

      /* explanation specific macros
       */
      case 'c':
        pos->str = array_start(spf->ip);
        pos->len = array_len(spf->ip);
        break;
      case 'r':
        pos->str = domainname;
        pos->len = domainname_len;
        break;
      case 't': {
        buffer_reset(&dns_buffer);
        buffer_addf(&dns_buffer, "%lu", (long int)time(0));
        pos->str = array_start(dns_buffer);
        pos->len = array_len(dns_buffer);
      } break;
      default:
        return SPFEXP_SYNTAX;
    }
    const char* restrict c = pos->str;
    const char* restrict end = pos->str + pos->len;
    const char* restrict delim_end = delimiters + delimiters_count;
    pos->len = 0;
    while (c < end) {
        const char* delim = delimiters;
        bool is_delim = false;
        while (delim < delim_end) {
            if (*c == *delim) {
                ++pos;
                is_delim = true;
                pos->str = c + 1;
                pos->len = 0;
                break;
            }
            ++delim;
        }
        if (!is_delim) {
            ++(pos->len);
        }
        ++c;
    }
    if (parts > pos - sections + 1) {
        parts = pos - sections + 1;
    }
    int i = 0;
    for (i = 0 ; i < parts ; ++i) {
        if (i != 0) {
            buffer_addch(buffer, '.');
        }
        if (reverse) {
            spf_write_macro(buffer, sections[parts - 1 - i], escape);
        } else {
            spf_write_macro(buffer, pos[i - parts + 1], escape);
        }
    }
    return SPFEXP_VALID;
}

static spf_expansion_t spf_expand(spf_t* spf, const char* restrict macrostring)
{
    buffer_reset(&spf->domainspec);
    if (m_strlen(macrostring) == 0) {
        spf->use_domain = true;
        return SPFEXP_VALID;
    }
    const char* const macrostart = macrostring;
    while (*macrostring != '\0') {
        const char* next_format = m_strchrnul(macrostring, '%');
        buffer_add(&spf->domainspec, macrostring, next_format - macrostring);
        macrostring = next_format;
        if (*macrostring != '\0') {
            ++macrostring;
            switch (*macrostring) {
              case '%':
                buffer_addch(&spf->domainspec, '%');
                break;
              case '_':
                buffer_addch(&spf->domainspec, ' ');
                break;
              case '-':
                buffer_addstr(&spf->domainspec, "%20");
                break;
              case '{': {
                ++macrostring;
                next_format = strchr(macrostring, '}');
                if (next_format == NULL) {
                    debug("spf (depth=%d): unmatched %%{ in macro \"%s\"", spf->recursions, macrostring);
                    return SPFEXP_SYNTAX;
                }
                char* end;
                char entity = ascii_tolower(*macrostring);
                bool escape = isupper(*macrostring);
                int parts = 256;
                bool reverse = false;
                const char* delimiters = ".";
                int delimiters_count = 1;
                ++macrostring;
                if (isdigit(*macrostring)) {
                    parts = strtol(macrostring, &end, 10);
                    if (parts < 0) {
                        debug("spf (depth=%d): invalid number of parts (%d) in macro  \"%s\"", spf->recursions, parts, macrostring);
                        return SPFEXP_SYNTAX;
                    }
                    macrostring = end;
                }
                if (*macrostring == 'r') {
                    reverse = true;
                    ++macrostring;
                }
                if (macrostring < next_format) {
                    delimiters = macrostring;
                    delimiters_count = next_format - delimiters;
                }
                switch (spf_expand_pattern(spf, &spf->domainspec, entity, parts, reverse, escape,
                                           delimiters, delimiters_count)) {
                  case SPFEXP_SYNTAX:
                    debug("spf (depth=%d): invalid macro %c in \"%s\"", spf->recursions, entity, macrostring);
                    return SPFEXP_SYNTAX;
                  case SPFEXP_DNS:
                    return SPFEXP_DNS;
                  default: break;
                }
                macrostring = next_format;
              } break;
            }
            ++macrostring;
        }
    }
    debug("spf (depth=%d): macro \"%s\" parsed: \"%s\"", spf->recursions, macrostart, array_start(spf->domainspec));
    if (!spf_validate_domain(array_start(spf->domainspec))) {
        return SPFEXP_INVALID;
    } else {
        return SPFEXP_VALID;
    }
}

static bool spf_subquery(spf_t* restrict spf, const char* restrict domain, spf_result_t cb, bool no_explanation)
{
    if (spf->recursions >= SPF_MAX_RECUSION) {
        return false;
    } else {
        spf_code_t code;
        spf->subquery = spf_check(array_start(spf->ip), domain,
                                  array_start(spf->sender), array_start(spf->helo),
                                  cb, spf->spf_nolookup, no_explanation | !spf->use_exp, spf, &code);
        if (spf->subquery == NULL) {
            return false;
        }
        spf->subquery->mech_withdns = spf->mech_withdns;
        spf->subquery->recursions = spf->recursions + 1;
        return true;
    }
}

static void spf_explanation_expand(spf_t* spf)
{
    if (array_len(spf->explanation) == 0) {
        spf_exit(spf, SPF_FAIL);
        return;
    }
    switch (spf_expand(spf, array_start(spf->explanation))) {
      case SPFEXP_DNS:
        return;
      case SPFEXP_INVALID: /* Invalid is valid here, since we do not require the result to be a domain name */
      case SPFEXP_VALID: {
        buffer_t temp = spf->domainspec;
        spf->domainspec = spf->explanation;
        spf->explanation = temp;
        spf_exit(spf, SPF_FAIL);
      } return;
      default:
        buffer_reset(&spf->explanation);
        spf_exit(spf, SPF_FAIL);
        return;
    }
}

static void spf_explanation(spf_t* spf)
{
    if (array_len(spf->explanation) > 0) {
        spf_explanation_expand(spf);
        return;
    }
    spf->in_exp = true;
    spf_rule_t* rule = array_ptr(spf->rules, spf->explanation_pos);
    switch (spf_expand(spf, array_start(rule->content))) {
      case SPFEXP_SYNTAX:
        spf_exit(spf, SPF_FAIL);
        return;
      case SPFEXP_DNS:
        return;
      default:
        break;
    }
    spf_query(spf, array_start(spf->domainspec), DNS_RRT_TXT, spf_exp_receive);
}

static void spf_match(spf_t* spf)
{
    if (array_elt(spf->rules, spf->current_rule).qualifier == SPF_FAIL
        && spf->use_exp && spf->explanation_pos >= 0) {
        /* here, we should fetch the explanation */
        spf_explanation(spf);
    } else {
        spf_exit(spf, array_elt(spf->rules, spf->current_rule).qualifier);
    }
}

static bool spf_limit_dns_mechanism(spf_t* spf, bool increment)
{
    if (spf->mech_withdns >= 10) {
        spf_exit(spf, SPF_PERMERROR);
        return true;
    }
    if (increment) {
        ++spf->mech_withdns;
    }
    return false;
}

static inline const char* spf_domainspec(const spf_t* spf)
{
    return spf->use_domain ? array_start(spf->domain) : array_start(spf->domainspec);
}

static void spf_next(spf_t* spf, bool start)
{
    while (true) {
        if (!start && !spf->in_macro) {
            ++spf->current_rule;
        }
        bool from_macro = spf->in_macro;
        start = false;
        spf->a_dnserror = false;
        spf->use_domain = false;
        spf->in_macro   = false;
        if (spf->in_exp) {
            spf_explanation(spf);
            return;
        }
        if (spf->current_rule >= array_len(spf->rules)) {
            if (spf->redirect_pos >= 0) {
                if (spf_limit_dns_mechanism(spf, !from_macro)) {
                    return;
                }
                debug("spf (debug=%d): reached the end of spf record, running redirect", spf->recursions);
                spf_rule_t* rule = array_ptr(spf->rules, spf->redirect_pos);
                switch (spf_expand(spf, array_start(rule->content))) {
                  case SPFEXP_SYNTAX:
                    spf_exit(spf, SPF_PERMERROR);
                    return;
                  default:
                    break;
                }
                if (!spf_subquery(spf, array_start(spf->domainspec), spf_redirect_exit, false)) {
                    warn("spf: maximum recursion depth exceeded, error");
                    spf_exit(spf, SPF_PERMERROR);
                }
                return;
            } else {
                debug("spf (depth=%d): reached the end of spf record", spf->recursions);
                spf_exit(spf, SPF_NEUTRAL);
                return;
            }
        }
        spf_rule_t* rule = array_ptr(spf->rules, spf->current_rule);
        info("spf (depth=%d): processing rule %s: %s", spf->recursions,
              rule->rule == SPF_RULE_UNKNOWN ? "unknown" : spftokens[rule->rule],
              array_len(rule->content) == 0 ? "(empty)" : array_start(rule->content));
        switch (rule->rule) {
          case SPF_RULE_ALL:
            spf_match(spf);
            return;

          case SPF_RULE_INCLUDE: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                if (!spf_subquery(spf, spf_domainspec(spf), spf_include_exit, true)) {
                    warn("spf: maximum recursion depth exceeded, error");
                    spf_exit(spf, SPF_PERMERROR);
                }
                return;
              default: break;
            }
          } break;

          case SPF_RULE_MX:
          case SPF_RULE_A: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                if (rule->rule == SPF_RULE_MX) {
                    if (!spf_query(spf, spf_domainspec(spf), DNS_RRT_MX, spf_mx_receive)) {
                        spf_exit(spf, SPF_TEMPERROR);
                    }
                } else {
                    if (!spf_query(spf, spf_domainspec(spf), spf->is_ip6 ? DNS_RRT_AAAA : DNS_RRT_A,
                                                             spf_a_receive)) {
                        spf_exit(spf, SPF_TEMPERROR);
                    }
                }
                return;
              default: break;
            }
          } break;

          case SPF_RULE_IP4: {
            if (!spf->is_ip6 && ip_compare_4(spf->ip4, rule->ip.v4, rule->cidr4)) {
                spf_match(spf);
                return;
            }
          } break;

          case SPF_RULE_IP6: {
            if (spf->is_ip6 && ip_compare_6(spf->ip6, rule->ip.v6, rule->cidr6)) {
                spf_match(spf);
                return;
            }
          } break;

          case SPF_RULE_EXISTS: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                spf_query(spf, array_start(spf->domainspec), DNS_RRT_A, spf_exists_receive);
                return;
              default:
                break;
            }
          } break;

          case SPF_RULE_PTR: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            if (spf_run_ptr_resolution(spf, array_start(rule->content), false)) {
                return;
            }
          } break;

          /* Modifiers and unknown items are skipped
           */
          default:
            break;
        }
    }
}

/* A Mechanism
 */
static void spf_a_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    int i;
    --spf->a_resolutions;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): A received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on A query for %s", spf->recursions, result->qname);
        spf->a_dnserror = true;
        if (spf->a_resolutions == 0) {
            spf_exit(spf, SPF_TEMPERROR);
        }
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): A answer received for %s", spf->recursions, result->qname);
    for (i = 0 ; result->data[i] != NULL ; ++i) {
        bool match = false;
        if (result->qtype == DNS_RRT_AAAA) {
            match = spf->is_ip6 && ip_compare_6(spf->ip6, (uint8_t*)result->data[i], current_rule(spf).cidr6);
        } else {
            ip4_t ip = ip_read_4((uint8_t*)result->data[i]);
            match = !spf->is_ip6 && ip_compare_4(spf->ip4, ip, current_rule(spf).cidr4);
        }
        if (match) {
            if (spf->in_macro || current_rule(spf).rule == SPF_RULE_PTR) {
                info("spf (depth=%d): PTR validated by domain %s", spf->recursions, result->qname);
                if (spf->use_domain) {
                    buffer_reset(&spf->validated);
                    buffer_addstr(&spf->validated, result->qname);
                    if (array_last(spf->validated) == '.') {
                        array_last(spf->validated) = '\0';
                        --array_len(spf->validated);
                    }
                }
            }
            spf_match(spf);
            ub_resolve_free(result);
            return;
        }
    }
    if (spf->a_resolutions == 0) {
        if (spf->a_dnserror) {
            spf_exit(spf, SPF_TEMPERROR);
        } else {
            spf_next(spf, false);
        }
    }
    ub_resolve_free(result);
}

/* MX Mechanism
 */
static void spf_mx_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    int i;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): MX received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on query for MX entry for %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): MX entry received for %s", spf->recursions, result->qname);
    for (i = 0 ; result->data[i] != NULL ; ++i) {
        const char* pos = result->data[i] + 2;
        buffer_reset(&dns_buffer);
        if (i >= 10) {
            info("spf (depth=%d): too many MX entries for %s", spf->recursions, result->qname);
            break;
        }
        while (*pos != '\0') {
            uint8_t count = *pos;
            ++pos;
            buffer_add(&dns_buffer, pos, count);
            buffer_addch(&dns_buffer, '.');
            pos += count;
        }
        spf_query(spf, array_start(dns_buffer), spf->is_ip6 ? DNS_RRT_AAAA : DNS_RRT_A,
                                                spf_a_receive);
    }
    if (spf->a_resolutions == 0) {
        info("spf (depth=%d): no MX entry for %s", spf->recursions, result->qname);
        spf_next(spf, false);
    }
    ub_resolve_free(result);
}

/* EXISTS Mechanism
 */
static void spf_exists_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): A received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on A query for existence for %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): existence query received for %s", spf->recursions, result->qname);
    if (result->rcode == DNS_RCODE_NOERROR) {
        spf_match(spf);
    } else {
        spf_next(spf, false);
    }
    ub_resolve_free(result);
}

/* PTR Mechanism
 */
static void spf_ptr_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): PTR received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error for PTR query on %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    for (int i = 0 ; result->data[i] != NULL ; ++i) {
        const char* pos = result->data[i];
        buffer_reset(&dns_buffer);
        if (spf->a_resolutions >= 10) {
            info("spf (depth=%d): too many PTR entries for %s", spf->recursions, result->qname);
            break;
        }
        while (*pos != '\0') {
            uint8_t count = *pos;
            ++pos;
            buffer_add(&dns_buffer, pos, count);
            buffer_addch(&dns_buffer, '.');
            pos += count;
        }

        debug("spf (depth=%d): found %s, to be compared to %s", spf->recursions,
              array_start(dns_buffer), array_start(spf->domainspec));
        ssize_t diff = array_len(dns_buffer) - array_len(spf->domainspec);
        bool match = false;
        if (diff == 0) {
            if (strcasecmp(array_start(spf->domainspec), array_start(dns_buffer)) == 0) {
                debug("spf (depth=%d): PTR potential entry found for domain %s", spf->recursions, array_start(dns_buffer));
                match = true;
            }<commit_msg>Fix possible crash in spf lookup caused by an invalid implicit cast.<commit_after>/******************************************************************************/
/*          pfixtools: a collection of postfix related tools                  */
/*          ~~~~~~~~~                                                         */
/*  ________________________________________________________________________  */
/*                                                                            */
/*  Redistribution and use in source and binary forms, with or without        */
/*  modification, are permitted provided that the following conditions        */
/*  are met:                                                                  */
/*                                                                            */
/*  1. Redistributions of source code must retain the above copyright         */
/*     notice, this list of conditions and the following disclaimer.          */
/*  2. Redistributions in binary form must reproduce the above copyright      */
/*     notice, this list of conditions and the following disclaimer in the    */
/*     documentation and/or other materials provided with the distribution.   */
/*  3. The names of its contributors may not be used to endorse or promote    */
/*     products derived from this software without specific prior written     */
/*     permission.                                                            */
/*                                                                            */
/*  THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS   */
/*  OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED         */
/*  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE    */
/*  DISCLAIMED.  IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY         */
/*  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL        */
/*  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS   */
/*  OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)     */
/*  HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       */
/*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN  */
/*  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE           */
/*  POSSIBILITY OF SUCH DAMAGE.                                               */
/*                                                                            */
/*   Copyright (c) 2006-2010 the Authors                                      */
/*   see AUTHORS and source files for details                                 */
/******************************************************************************/

/*
 * Copyright © 2009 <NAME>
 */

#include <ctype.h>

#include "spf.h"
#include "spf_tokens.h"
#include "buffer.h"
#include "utils.h"

#define SPF_MAX_RECUSION 15

typedef struct spf_rule_t {
    spf_code_t qualifier;
    spf_ruleid_t rule;
    buffer_t content;
    ip_t ip;
    cidrlen_t cidr4;
    cidrlen_t cidr6;
} spf_rule_t;
ARRAY(spf_rule_t);
#define SPF_RULE_INIT { 0, 0, ARRAY_INIT, { .v6 = { 0 } }, 0, 0 }

/* TODO: cleanup this structure.
 */
struct spf_t {
    unsigned txt_received : 1;
    unsigned txt_inerror  : 1;
    unsigned txt_toomany  : 1;
    unsigned spf_received : 1;
    unsigned spf_inerror  : 1;
    unsigned spf_nolookup : 1;
    unsigned canceled     : 1;
    unsigned is_ip6       : 1;
    unsigned use_domain   : 1;
    unsigned in_macro     : 1;
    unsigned a_dnserror   : 1;
    unsigned use_exp      : 1;
    unsigned in_exp       : 1;

    ip4_t ip4;
    ip6_t ip6;
    buffer_t ip;
    buffer_t domain;
    buffer_t sender;
    buffer_t helo;
    buffer_t validated;
    buffer_t explanation;

    buffer_t record;
    A(spf_rule_t) rules;
    uint8_t current_rule;
    int8_t redirect_pos;
    int8_t explanation_pos;

    buffer_t domainspec;

    int8_t recursions;
    struct spf_t* subquery;

    uint8_t a_resolutions;
    uint8_t mech_withdns;

    uint8_t queries;
    spf_result_t exit;
    void* data;
};

#define current_rule(spf) array_elt((spf)->rules, (spf)->current_rule)

static PA(spf_t) spf_pool = ARRAY_INIT;
static A(spf_rule_t) spf_rule_pool = ARRAY_INIT;

static char domainname[128];
static uint8_t domainname_len;
static buffer_t query_buffer = ARRAY_INIT;
static buffer_t dns_buffer = ARRAY_INIT;

static int created = 0;
static int acquired = 0;
static int deleted = 0;
static int released = 0;

static spf_t* spf_new(void)
{
    ++created;
    return p_new(spf_t, 1);
}

static void spf_rule_wipe(spf_rule_t* rule)
{
    array_wipe(rule->content);
}

static void spf_wipe(spf_t* spf)
{
    array_append(spf_rule_pool, array_start(spf->rules), array_len(spf->rules));
    array_wipe(spf->rules);
    array_wipe(spf->domain);
    array_wipe(spf->ip);
    array_wipe(spf->sender);
    array_wipe(spf->helo);
    array_wipe(spf->validated);
    array_wipe(spf->record);
    array_wipe(spf->domainspec);
    array_wipe(spf->explanation);
    p_clear(spf, 1);
}

static void spf_delete(spf_t **spf)
{
    if (*spf) {
        ++deleted;
        spf_wipe(*spf);
        p_delete(spf);
    }
}

static spf_t* spf_acquire(void)
{
    ++acquired;
    spf_t* spf = NULL;
    if (array_len(spf_pool)) {
        spf = array_pop_last(spf_pool);
    } else {
        spf = spf_new();
    }
    debug("spf pool: acquiring %p - pool length: %d (created %d)", spf, array_len(spf_pool), created);
    return spf;
}

static int spf_module_init(void)
{
    if (gethostname(domainname, 128) < 0) {
        return -1;
    }

    const char* pos = strchr(domainname, '.');
    if (pos == NULL) {
        return -1;
    }
    if (strchr(pos + 1, '.') != NULL) {
        memmove(domainname, pos + 1, 127 - (pos - domainname));
    }
    domainname_len = m_strlen(domainname);
    return 0;
}
module_init(spf_module_init);

static void spf_module_exit(void)
{
    array_deep_wipe(spf_pool, spf_delete);
    array_deep_wipe(spf_rule_pool, spf_rule_wipe);
    buffer_wipe(&query_buffer);
    buffer_wipe(&dns_buffer);
    debug("spf: Created: %d, Deleted: %d, Acquired: %d, Release: %d", created, deleted, acquired, released);
}
module_exit(spf_module_exit);

static bool spf_release(spf_t* spf, bool decrement)
{
    if (decrement) {
        --spf->queries;
    }
    if (spf->canceled && spf->queries == 0) {
        ++released;
        debug("spf pool: releasing %p - pool length: %d (created: %d)", spf, array_len(spf_pool) + 1, created);
        array_append(spf_rule_pool, array_start(spf->rules), array_len(spf->rules));
        array_len(spf->rules) = 0;
        buffer_reset(&spf->domain);
        buffer_reset(&spf->ip);
        buffer_reset(&spf->sender);
        buffer_reset(&spf->record);
        buffer_reset(&spf->helo);
        buffer_reset(&spf->validated);
        buffer_reset(&spf->domainspec);
        buffer_reset(&spf->explanation);
        spf_t bak = *spf;
        p_clear(spf, 1);
        spf->rules = bak.rules;
        spf->domain = bak.domain;
        spf->ip = bak.ip;
        spf->sender = bak.sender;
        spf->record = bak.record;
        spf->helo = bak.helo;
        spf->validated = bak.validated;
        spf->domainspec = bak.domainspec;
        spf->explanation = bak.explanation;
        array_add(spf_pool, spf);
        return true;
    }
    return false;
}

static bool spf_validate_domain(const char* restrict domain) {
    int label_count = 0;
    int label_length = 0;
    const char* restrict pos = domain;
    while (*pos != '\0') {
        if (*pos == '.') {
            if (label_length == 0) {
                debug("spf: invalid domain name \"%s\": contains a non-terminal zero-length label", domain);
                return false;
            }
            label_length = 0;
        } else if (!isalnum(*pos) && *pos != '-' && *pos != '_') {
            debug("spf: invalid domain name \"%s\": contains illegal character '%c'", domain, *pos);
            return false;
        } else {
            if (label_length == 0) {
                ++label_count;
            }
            ++label_length;
            if (label_length > 63) {
                debug("spf: invalid domain name \"%s\": contains a too long label", domain);
                return false;
            }
        }
        ++pos;
    }
    return label_count > 1;
}

static bool spf_query(spf_t* spf, const char* query, dns_rrtype_t rtype, ub_callback_t cb)
{
    buffer_reset(&query_buffer);
    buffer_addstr(&query_buffer, query);
    if (array_last(query_buffer) != '.') {
        buffer_addch(&query_buffer, '.');
    }
    debug("spf (depth=%d): performing query of type %d for %s", spf->recursions, rtype, query);
    if (dns_resolve(array_start(query_buffer), rtype, cb, spf)) {
        if (rtype == DNS_RRT_A || rtype == DNS_RRT_AAAA) {
            ++spf->a_resolutions;
        }
        ++spf->queries;
        return true;
    }
    return false;
}

static void spf_next(spf_t* spf, bool start);

static void spf_exit(spf_t* spf, spf_code_t code)
{
    if (spf->in_macro) {
        spf_next(spf, false);
        return;
    }
    if (log_level >= LOG_NOTICE) {
        const char* str = NULL;
        switch (code) {
          case SPF_NONE: str = "NONE"; break;
          case SPF_NEUTRAL: str = "NEUTRAL"; break;
          case SPF_PASS: str = "PASS"; break;
          case SPF_FAIL: str = "FAIL"; break;
          case SPF_SOFTFAIL: str = "SOFTFAIL"; break;
          case SPF_TEMPERROR: str = "TEMPERROR"; break;
          case SPF_PERMERROR: str = "PERMERROR"; break;
        }
        info("spf (depth=%d): result for query is %s", spf->recursions, str);
    }
    if (spf->exit) {
        if (array_len(spf->explanation) == 0) {
            spf->exit(code, NULL, spf->data);
        } else {
            spf->exit(code, array_start(spf->explanation), spf->data);
        }
    }
    spf_cancel(spf);
}

static void spf_write_macro(buffer_t* restrict  buffer, static_str_t str, bool escape)
{
    if (!escape) {
        buffer_add(buffer, str.str, str.len);
    } else {
        for (int i = 0 ; i < str.len ; ++i) {
            const char c = str.str[i];
            /* ";" / "?" / ":"       |
             *    |                |  / "@" / "&" / "=" / "+" / "$" / "," / "/" */
            if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~'
                || c == ';' || c == '?' || c == ':' || c == '@' || c == '&' || c == '='
                || c == '+' || c == '$' || c == ',' || c == '/') {
                buffer_addch(buffer, c);
            } else {
                buffer_addf(buffer, "%%%02x", c);
            }
        }
    }
}


/* Rule processing
 */
static void spf_include_exit(spf_code_t result, const char* exp, void* arg);
static void spf_redirect_exit(spf_code_t result, const char* exp, void* arg);
static void spf_a_receive(void* arg, int err, struct ub_result* result);
static void spf_mx_receive(void* arg, int err, struct ub_result* result);
static void spf_exists_receive(void* arg, int err, struct ub_result* result);
static void spf_exp_receive(void* arg, int err, struct ub_result* result);
static bool spf_run_ptr_resolution(spf_t* spf, const char* domainspec, bool in_macro);

static bool spf_dns_in_error(int err, struct ub_result* result)
{
    return (err != 0 || (result->rcode != DNS_RCODE_NOERROR && result->rcode != DNS_RCODE_NXDOMAIN));
}

typedef enum {
    SPFEXP_VALID,
    SPFEXP_INVALID,
    SPFEXP_SYNTAX,
    SPFEXP_DNS
} spf_expansion_t;

static spf_expansion_t spf_expand_pattern(spf_t* spf, buffer_t* restrict buffer, char identifier, int parts,
                                          bool reverse, bool escape, const char* restrict delimiters,
                                          int delimiters_count) {
    static_str_t sections[256] = { { .str = 0, .len = 0}, };
    static_str_t* pos = sections;
    switch (identifier) {
      case 's':
        pos->str = array_start(spf->sender);
        pos->len = array_len(spf->sender);
        break;
      case 'l':
        pos->str = array_start(spf->sender);
        pos->len = strchr(pos->str, '@') - pos->str;
        break;
      case 'o':
        pos->str = strchr(array_start(spf->sender), '@') + 1;
        pos->len = m_strlen(pos->str);
        break;
      case 'd':
        pos->str = array_start(spf->domain);
        pos->len = array_len(spf->domain);
        break;
      case 'i': {
        buffer_reset(&dns_buffer);
        if (!spf->is_ip6) {
            ip_print_4(&dns_buffer, spf->ip4, false, false);
        } else {
            ip_print_6(&dns_buffer, spf->ip6, false, false);
        }
        pos->str = array_start(dns_buffer);
        pos->len = array_len(dns_buffer);
      } break;
      case 'p':
        if (array_len(spf->validated) > 0) {
            pos->str = array_start(spf->validated);
            pos->len = array_len(spf->validated);
        } else if (spf_run_ptr_resolution(spf, NULL, true)) {
            return SPFEXP_DNS;
        } else {
            pos->str = "unknown";
            pos->len = m_strlen(pos->str);
        }
        break;
      case 'v':
        pos->str = spf->is_ip6 ? "ip6" : "in-addr";
        pos->len = m_strlen(pos->str);
        break;
      case 'h':
        pos->str = array_start(spf->helo);
        pos->len = array_len(spf->helo);
        break;

      /* explanation specific macros
       */
      case 'c':
        pos->str = array_start(spf->ip);
        pos->len = array_len(spf->ip);
        break;
      case 'r':
        pos->str = domainname;
        pos->len = domainname_len;
        break;
      case 't': {
        buffer_reset(&dns_buffer);
        buffer_addf(&dns_buffer, "%lu", (long int)time(0));
        pos->str = array_start(dns_buffer);
        pos->len = array_len(dns_buffer);
      } break;
      default:
        return SPFEXP_SYNTAX;
    }
    const char* restrict c = pos->str;
    const char* restrict end = pos->str + pos->len;
    const char* restrict delim_end = delimiters + delimiters_count;
    pos->len = 0;
    while (c < end) {
        const char* delim = delimiters;
        bool is_delim = false;
        while (delim < delim_end) {
            if (*c == *delim) {
                ++pos;
                is_delim = true;
                pos->str = c + 1;
                pos->len = 0;
                break;
            }
            ++delim;
        }
        if (!is_delim) {
            ++(pos->len);
        }
        ++c;
    }
    if (parts > pos - sections + 1) {
        parts = pos - sections + 1;
    }
    int i = 0;
    for (i = 0 ; i < parts ; ++i) {
        if (i != 0) {
            buffer_addch(buffer, '.');
        }
        if (reverse) {
            spf_write_macro(buffer, sections[parts - 1 - i], escape);
        } else {
            spf_write_macro(buffer, pos[i - parts + 1], escape);
        }
    }
    return SPFEXP_VALID;
}

static spf_expansion_t spf_expand(spf_t* spf, const char* restrict macrostring)
{
    buffer_reset(&spf->domainspec);
    if (m_strlen(macrostring) == 0) {
        spf->use_domain = true;
        return SPFEXP_VALID;
    }
    const char* const macrostart = macrostring;
    while (*macrostring != '\0') {
        const char* next_format = m_strchrnul(macrostring, '%');
        buffer_add(&spf->domainspec, macrostring, next_format - macrostring);
        macrostring = next_format;
        if (*macrostring != '\0') {
            ++macrostring;
            switch (*macrostring) {
              case '%':
                buffer_addch(&spf->domainspec, '%');
                break;
              case '_':
                buffer_addch(&spf->domainspec, ' ');
                break;
              case '-':
                buffer_addstr(&spf->domainspec, "%20");
                break;
              case '{': {
                ++macrostring;
                next_format = strchr(macrostring, '}');
                if (next_format == NULL) {
                    debug("spf (depth=%d): unmatched %%{ in macro \"%s\"", spf->recursions, macrostring);
                    return SPFEXP_SYNTAX;
                }
                char* end;
                char entity = ascii_tolower(*macrostring);
                bool escape = isupper(*macrostring);
                int parts = 256;
                bool reverse = false;
                const char* delimiters = ".";
                int delimiters_count = 1;
                ++macrostring;
                if (isdigit(*macrostring)) {
                    parts = strtol(macrostring, &end, 10);
                    if (parts < 0) {
                        debug("spf (depth=%d): invalid number of parts (%d) in macro  \"%s\"", spf->recursions, parts, macrostring);
                        return SPFEXP_SYNTAX;
                    }
                    macrostring = end;
                }
                if (*macrostring == 'r') {
                    reverse = true;
                    ++macrostring;
                }
                if (macrostring < next_format) {
                    delimiters = macrostring;
                    delimiters_count = next_format - delimiters;
                }
                switch (spf_expand_pattern(spf, &spf->domainspec, entity, parts, reverse, escape,
                                           delimiters, delimiters_count)) {
                  case SPFEXP_SYNTAX:
                    debug("spf (depth=%d): invalid macro %c in \"%s\"", spf->recursions, entity, macrostring);
                    return SPFEXP_SYNTAX;
                  case SPFEXP_DNS:
                    return SPFEXP_DNS;
                  default: break;
                }
                macrostring = next_format;
              } break;
            }
            ++macrostring;
        }
    }
    debug("spf (depth=%d): macro \"%s\" parsed: \"%s\"", spf->recursions, macrostart, array_start(spf->domainspec));
    if (!spf_validate_domain(array_start(spf->domainspec))) {
        return SPFEXP_INVALID;
    } else {
        return SPFEXP_VALID;
    }
}

static bool spf_subquery(spf_t* restrict spf, const char* restrict domain, spf_result_t cb, bool no_explanation)
{
    if (spf->recursions >= SPF_MAX_RECUSION) {
        return false;
    } else {
        spf_code_t code;
        spf->subquery = spf_check(array_start(spf->ip), domain,
                                  array_start(spf->sender), array_start(spf->helo),
                                  cb, spf->spf_nolookup, no_explanation | !spf->use_exp, spf, &code);
        if (spf->subquery == NULL) {
            return false;
        }
        spf->subquery->mech_withdns = spf->mech_withdns;
        spf->subquery->recursions = spf->recursions + 1;
        return true;
    }
}

static void spf_explanation_expand(spf_t* spf)
{
    if (array_len(spf->explanation) == 0) {
        spf_exit(spf, SPF_FAIL);
        return;
    }
    switch (spf_expand(spf, array_start(spf->explanation))) {
      case SPFEXP_DNS:
        return;
      case SPFEXP_INVALID: /* Invalid is valid here, since we do not require the result to be a domain name */
      case SPFEXP_VALID: {
        buffer_t temp = spf->domainspec;
        spf->domainspec = spf->explanation;
        spf->explanation = temp;
        spf_exit(spf, SPF_FAIL);
      } return;
      default:
        buffer_reset(&spf->explanation);
        spf_exit(spf, SPF_FAIL);
        return;
    }
}

static void spf_explanation(spf_t* spf)
{
    if (array_len(spf->explanation) > 0) {
        spf_explanation_expand(spf);
        return;
    }
    spf->in_exp = true;
    spf_rule_t* rule = array_ptr(spf->rules, spf->explanation_pos);
    switch (spf_expand(spf, array_start(rule->content))) {
      case SPFEXP_SYNTAX:
        spf_exit(spf, SPF_FAIL);
        return;
      case SPFEXP_DNS:
        return;
      default:
        break;
    }
    spf_query(spf, array_start(spf->domainspec), DNS_RRT_TXT, spf_exp_receive);
}

static void spf_match(spf_t* spf)
{
    if (array_elt(spf->rules, spf->current_rule).qualifier == SPF_FAIL
        && spf->use_exp && spf->explanation_pos >= 0) {
        /* here, we should fetch the explanation */
        spf_explanation(spf);
    } else {
        spf_exit(spf, array_elt(spf->rules, spf->current_rule).qualifier);
    }
}

static bool spf_limit_dns_mechanism(spf_t* spf, bool increment)
{
    if (spf->mech_withdns >= 10) {
        spf_exit(spf, SPF_PERMERROR);
        return true;
    }
    if (increment) {
        ++spf->mech_withdns;
    }
    return false;
}

static inline const char* spf_domainspec(const spf_t* spf)
{
    return spf->use_domain ? array_start(spf->domain) : array_start(spf->domainspec);
}

static void spf_next(spf_t* spf, bool start)
{
    while (true) {
        if (!start && !spf->in_macro) {
            ++spf->current_rule;
        }
        bool from_macro = spf->in_macro;
        start = false;
        spf->a_dnserror = false;
        spf->use_domain = false;
        spf->in_macro   = false;
        if (spf->in_exp) {
            spf_explanation(spf);
            return;
        }
        if (spf->current_rule >= array_len(spf->rules)) {
            if (spf->redirect_pos >= 0) {
                if (spf_limit_dns_mechanism(spf, !from_macro)) {
                    return;
                }
                debug("spf (debug=%d): reached the end of spf record, running redirect", spf->recursions);
                spf_rule_t* rule = array_ptr(spf->rules, spf->redirect_pos);
                switch (spf_expand(spf, array_start(rule->content))) {
                  case SPFEXP_SYNTAX:
                    spf_exit(spf, SPF_PERMERROR);
                    return;
                  default:
                    break;
                }
                if (!spf_subquery(spf, array_start(spf->domainspec), spf_redirect_exit, false)) {
                    warn("spf: maximum recursion depth exceeded, error");
                    spf_exit(spf, SPF_PERMERROR);
                }
                return;
            } else {
                debug("spf (depth=%d): reached the end of spf record", spf->recursions);
                spf_exit(spf, SPF_NEUTRAL);
                return;
            }
        }
        spf_rule_t* rule = array_ptr(spf->rules, spf->current_rule);
        info("spf (depth=%d): processing rule %s: %s", spf->recursions,
              rule->rule == SPF_RULE_UNKNOWN ? "unknown" : spftokens[rule->rule],
              array_len(rule->content) == 0 ? "(empty)" : array_start(rule->content));
        switch (rule->rule) {
          case SPF_RULE_ALL:
            spf_match(spf);
            return;

          case SPF_RULE_INCLUDE: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                if (!spf_subquery(spf, spf_domainspec(spf), spf_include_exit, true)) {
                    warn("spf: maximum recursion depth exceeded, error");
                    spf_exit(spf, SPF_PERMERROR);
                }
                return;
              default: break;
            }
          } break;

          case SPF_RULE_MX:
          case SPF_RULE_A: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                if (rule->rule == SPF_RULE_MX) {
                    if (!spf_query(spf, spf_domainspec(spf), DNS_RRT_MX, spf_mx_receive)) {
                        spf_exit(spf, SPF_TEMPERROR);
                    }
                } else {
                    if (!spf_query(spf, spf_domainspec(spf), spf->is_ip6 ? DNS_RRT_AAAA : DNS_RRT_A,
                                                             spf_a_receive)) {
                        spf_exit(spf, SPF_TEMPERROR);
                    }
                }
                return;
              default: break;
            }
          } break;

          case SPF_RULE_IP4: {
            if (!spf->is_ip6 && ip_compare_4(spf->ip4, rule->ip.v4, rule->cidr4)) {
                spf_match(spf);
                return;
            }
          } break;

          case SPF_RULE_IP6: {
            if (spf->is_ip6 && ip_compare_6(spf->ip6, rule->ip.v6, rule->cidr6)) {
                spf_match(spf);
                return;
            }
          } break;

          case SPF_RULE_EXISTS: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            switch (spf_expand(spf, array_start(rule->content))) {
              case SPFEXP_SYNTAX:
                spf_exit(spf, SPF_PERMERROR);
                return;
              case SPFEXP_DNS:
                return;
              case SPFEXP_VALID:
                spf_query(spf, array_start(spf->domainspec), DNS_RRT_A, spf_exists_receive);
                return;
              default:
                break;
            }
          } break;

          case SPF_RULE_PTR: {
            if (spf_limit_dns_mechanism(spf, !from_macro)) {
                return;
            }
            if (spf_run_ptr_resolution(spf, array_start(rule->content), false)) {
                return;
            }
          } break;

          /* Modifiers and unknown items are skipped
           */
          default:
            break;
        }
    }
}

/* A Mechanism
 */
static void spf_a_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    int i;
    --spf->a_resolutions;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): A received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on A query for %s", spf->recursions, result->qname);
        spf->a_dnserror = true;
        if (spf->a_resolutions == 0) {
            spf_exit(spf, SPF_TEMPERROR);
        }
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): A answer received for %s", spf->recursions, result->qname);
    for (i = 0 ; result->data[i] != NULL ; ++i) {
        bool match = false;
        if (result->qtype == DNS_RRT_AAAA) {
            match = spf->is_ip6 && ip_compare_6(spf->ip6, (uint8_t*)result->data[i], current_rule(spf).cidr6);
        } else {
            ip4_t ip = ip_read_4((uint8_t*)result->data[i]);
            match = !spf->is_ip6 && ip_compare_4(spf->ip4, ip, current_rule(spf).cidr4);
        }
        if (match) {
            if (spf->in_macro || current_rule(spf).rule == SPF_RULE_PTR) {
                info("spf (depth=%d): PTR validated by domain %s", spf->recursions, result->qname);
                if (spf->use_domain) {
                    buffer_reset(&spf->validated);
                    buffer_addstr(&spf->validated, result->qname);
                    if (array_last(spf->validated) == '.') {
                        array_last(spf->validated) = '\0';
                        --array_len(spf->validated);
                    }
                }
            }
            spf_match(spf);
            ub_resolve_free(result);
            return;
        }
    }
    if (spf->a_resolutions == 0) {
        if (spf->a_dnserror) {
            spf_exit(spf, SPF_TEMPERROR);
        } else {
            spf_next(spf, false);
        }
    }
    ub_resolve_free(result);
}

/* MX Mechanism
 */
static void spf_mx_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    int i;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): MX received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on query for MX entry for %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): MX entry received for %s", spf->recursions, result->qname);
    for (i = 0 ; result->data[i] != NULL ; ++i) {
        const char* pos = result->data[i] + 2;
        buffer_reset(&dns_buffer);
        if (i >= 10) {
            info("spf (depth=%d): too many MX entries for %s", spf->recursions, result->qname);
            break;
        }
        while (*pos != '\0') {
            uint8_t count = *pos;
            ++pos;
            buffer_add(&dns_buffer, pos, count);
            buffer_addch(&dns_buffer, '.');
            pos += count;
        }
        spf_query(spf, array_start(dns_buffer), spf->is_ip6 ? DNS_RRT_AAAA : DNS_RRT_A,
                                                spf_a_receive);
    }
    if (spf->a_resolutions == 0) {
        info("spf (depth=%d): no MX entry for %s", spf->recursions, result->qname);
        spf_next(spf, false);
    }
    ub_resolve_free(result);
}

/* EXISTS Mechanism
 */
static void spf_exists_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): A received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error on A query for existence for %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    debug("spf (depth=%d): existence query received for %s", spf->recursions, result->qname);
    if (result->rcode == DNS_RCODE_NOERROR) {
        spf_match(spf);
    } else {
        spf_next(spf, false);
    }
    ub_resolve_free(result);
}

/* PTR Mechanism
 */
static void spf_ptr_receive(void* arg, int err, struct ub_result* result)
{
    spf_t* spf = arg;
    if (spf_release(spf, true)) {
        debug("spf (depth=%d): PTR received but processing already finished", spf->recursions);
        ub_resolve_free(result);
        return;
    }
    if (spf_dns_in_error(err, result)) {
        debug("spf (depth=%d): DNS error for PTR query on %s", spf->recursions, result->qname);
        spf_exit(spf, SPF_TEMPERROR);
        ub_resolve_free(result);
        return;
    }
    for (int i = 0 ; result->data[i] != NULL ; ++i) {
        const char* pos = result->data[i];
        buffer_reset(&dns_buffer);
        if (spf->a_resolutions >= 10) {
            info("spf (depth=%d): too many PTR entries for %s", spf->recursions, result->qname);
            break;
        }
        while (*pos != '\0') {
            uint8_t count = *pos;
            ++pos;
            buffer_add(&dns_buffer, pos, count);
            buffer_addch(&dns_buffer, '.');
            pos += count;
        }

        debug("spf (depth=%d): found %s, to be compared to %s", spf->recursions,
              array_start(dns_buffer), array_start(spf->domainspec));
        ssize_t diff = ((ssize_t)array_len(dns_buffer)) - ((ssize_t)array_len(spf->domainspec));
        bool match = false;
        if (diff == 0) {
            if (strcasecmp(array_start(spf->domainspec), array_start(dns_buffer)) == 0) {
                debug("spf (depth=%d): PTR potential entry found for domain %s", spf->recursions, array_start(dns_buffer));
                match = true;
            }
{'fraction_non_alphanumeric': 0.12994761580704892, 'fraction_numerical': 0.0056657223796034, 'mean_word_length': 1.807731615196623, 'pattern_counts': {'":': 8, '<': 21, '<?xml version=': 0, '>': 541, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 68, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2150842', 'n_tokens_mistral': 21227, 'n_tokens_neox': 20273, 'n_words': 4459}