/* This file is part of Mailfromd.
   Copyright (C) 2007-2025 Sergey Poznyakoff

   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 3, 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, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <string.h>
#include "mailfromd.h"
#include "spf.h"

typedef struct spf_term spf_term;

#define SPF_MOD_EXP      0
#define SPF_MOD_REDIRECT 1
#define MAX_SPF_MOD      2
typedef struct spf_data {
	/* Internal data */
	mu_opool_t tmpool;       /* Opool for keeping temporary values */
	char *buffer;            /* Expansion and transformation buffer */
	size_t bufsize;          /* Size of buffer */
	spf_term *mod[MAX_SPF_MOD];
	size_t loopno;
	/* Input data */
	spf_query_t q;
	struct mu_cidr addr;     /* Originator IP as CIDR */
	/* Output data */
	spf_result result;       /* SPF result if a term throws an exception */
	spf_answer_t *answer;
} spf_data;

static char *spf_result_str[] = {
	"None",
	"Neutral",
	"Pass",
	"Fail",
	"SoftFail",
	"TempError",
	"PermError",
};

static spf_result spf_check_host_internal(spf_query_t *q, spf_answer_t *answer,
					  size_t loopno);

int
spf_data_init(struct spf_data *dat, spf_query_t *q, spf_answer_t *answer,
	      size_t loopno)
{
	struct addrinfo *res, hints;
	int rc;

	memset(&hints, 0, sizeof(hints));
	hints.ai_flags = AI_NUMERICHOST;
	rc = getaddrinfo(q->ipstr, NULL, &hints, &res);
	if (rc) {
		mu_error(_("spf_data_init: %s: %s"),
			 q->ipstr, gai_strerror(rc));
		return 1;
	}

	memset(dat, 0, sizeof dat[0]);
	mu_opool_create(&dat->tmpool, MU_OPOOL_ENOMEMABRT);
	dat->q = *q;

	rc = mu_cidr_from_sockaddr(&dat->addr, res->ai_addr);
	freeaddrinfo(res);
	if (rc) {
		mu_error(_("spf_data_init: can't convert IP address %s: %s"),
			 q->ipstr, mu_strerror(rc));
		return 1;
	}

	dat->answer = answer;
	dat->loopno = loopno;
	return 0;
}

void
spf_data_alloc(struct spf_data *dat, size_t s)
{
	if (dat->bufsize < s) {
		dat->bufsize = s;
		dat->buffer = mu_realloc(dat->buffer, dat->bufsize);
	}
}

void
spf_data_free(struct spf_data *dat)
{
	free(dat->buffer);
	mu_opool_destroy(&dat->tmpool);
}

char *
spf_data_ptr(struct spf_data *dat)
{
	return mu_opool_finish(dat->tmpool, NULL);
}

/* Swap LEN bytes between A and B */
void
swapi(char *a, char *b, size_t len)
{
	int t;
	while (len--) {
		t = *b;
		*b++ = *a;
		*a++ = t;
	}
}

/* Swap LEN bytes between A and B in backward direction */
void
swapd(char *a, char *b, size_t len)
{
	int t;
	while (len--) {
		t = *b;
		*b-- = *a;
		*a-- = t;
	}
}

static char *
scanforw(char *s, char const *delim)
{
	for (; *s; s++)
		if (strchr(delim, *s))
			return s;
	return NULL;
}

static char *
scanback(char *s, char const *delim, size_t n)
{
	s += n;
	while (n > 0) {
		n--;
		s--;
		if (strchr(delim, *s))
			return s;
	}
	return NULL;
}

/* Reverse the order of parts of BUF, delimited by DELIM */
void
spf_reverse(char *buf, size_t len, char const *delim)
{
	char *lp, *rp;
	size_t llen, rlen;
	char *tmp = NULL;
	size_t tmpsize = 0;
	size_t delta;
	size_t bufsize = strlen(buf);

	do {
		lp = scanforw(buf, delim);
		if (!lp)
			break;
		llen = lp - buf;
		rp = scanback(buf, delim, bufsize) + 1;
		rlen = bufsize - (rp - buf);

		if (llen == rlen)
			swapi(buf, rp, llen);
		else if (llen < rlen) {
			swapi(buf, rp, llen);
			delta = rlen - llen;
			if (tmpsize < delta) {
				tmpsize = delta;
				tmp = mu_realloc(tmp, tmpsize);
			}
			memcpy(tmp, rp + llen, delta);
			memmove(lp + delta, lp, bufsize - (lp - buf) - delta);
			memcpy(buf + llen, tmp, delta);
			rp += delta;
			lp += delta;
		} else /* if (rlen < llen) */ {
			swapd(buf + llen - 1, rp + rlen - 1, rlen);
			delta = llen - rlen;
			if (tmpsize < delta) {
				tmpsize = delta;
				tmp = mu_realloc(tmp, tmpsize);
			}
			memcpy(tmp, buf, delta);
			memmove(buf, buf + delta, rp - buf - 1);
			rp -= delta;
			memcpy(rp, tmp, delta);
			lp -= delta;
		}
		buf = lp + 1;
		bufsize = rp - buf - 1;
	} while (rp > lp + 1);
	free(tmp);
}

/* BUFFER contains several DELIM-separated parts. Retain at most
   NPARTS right-hand parts of them. If NPARTS is greater than the
   actual number of parts, then do nothing */
void
spf_truncate(char *buffer, char *delim, unsigned nparts)
{
	char *p;
	size_t len;

	len = strlen(buffer);
	while (nparts--) {
		if (len == 0)
			return;
		p = scanback(buffer, delim, len);
		if (!p)
			return;
		if (p == buffer)
			return;
		len = p - buffer;
	}
	p++;
	len = strlen(p);
	memmove(buffer, p, len + 1);
}

/* Replace with dots all occurrences of DELIM in first LEN bytes of BUF */
void
spf_repl(char *buf, size_t len, char const *delim)
{
	while (len--) {
		if (strchr(delim, *buf))
			*buf = '.';
		buf++;
	}
}

#define DELIMITERS ".-+,/_="

/* Perform an RFC 4408 transformation.
   DAT - current spf_data
   X   - the substituted value
   L   - its length
   PPTR points to the current position in the macro string.
   Return 0 if successful and move PPTR past the closing curly brace.
   If there is no brace, return 1 (failure) */
int
spf_transform(struct spf_data *dat, const char *x, size_t l, const char **pptr)
{
	const char *p = *pptr;
	int nparts = 0;
	int reverse = 0;
	char delim[sizeof(DELIMITERS)] = ".";
	int dlen;

	while (mu_isdigit(*p))
		nparts = nparts * 10 + *p++ - '0';

	if (*p == 'r') {
		p++;
		reverse = 1;
	}

	if (*p && (dlen = strspn(p, ".-+,/_=")) > 0) {
		if (dlen > sizeof(delim)-1)
			dlen = sizeof(delim)-1;
		memcpy(delim, p, dlen);
		delim[dlen] = 0;
		p += dlen;
	}
	if (*p != '}')
		return 1;
	*pptr = p + 1;

	if (l == 0)
		l = strlen(x);

	spf_data_alloc(dat, l + 1);
	memcpy(dat->buffer, x, l);
	dat->buffer[l] = 0;

	if (reverse)
		spf_reverse(dat->buffer, l, delim);
	if (nparts) {
		spf_truncate(dat->buffer, delim, nparts);
		l = strlen(dat->buffer);
	}
	spf_repl(dat->buffer, l, delim);

	mu_opool_append(dat->tmpool, dat->buffer, l);

	return 0;
}

/* Return 1 if NAME ends in DOMAIN */
static int
domain_match(const char *name, const char *domain)
{
	const char *np = name + strlen(name) - 1;
	const char *dp = domain + strlen(domain) - 1;

	while (1) {
		if (tolower(*dp) != tolower(*np))
			return 0;
		if (np == name)
			return dp == domain;
		np--;
		if (dp == domain)
			return *np == '.';
		dp--;
	}
}

static int
to_xdig(unsigned char b)
{
	if (b >= 0xa)
		return 'a' + b - 0xa;
	else
		return '0' + b;
}

/*
 * Max. buffer capacity for dotted-quad IPv4 is 4*4 = 16 bytes (four
 * three-digit segments, each terminated by a dot, or \0 for the last
 * one).
 * Max. buffer capacity for IPv6 addresses is 16*3 = 48 bytes (sixteen
 * 2-hexdigit segments, each terminated by a dot, or \0 for the last
 * one).
 * IP buffer must be able to accomodate the larger of the two:
 */
#define IPBUF_DOTTED_SIZE (16*3)

/* Expand a single macro as per RFC 4408, chapter 8.
   PPTR points to the macro symbol, right past the opening curly brace.
   ALLOW_EXP is 1 if the expansion of macros c, r and t is allowed (see
   RFC 4408, page 27).
   Return 0 on success, 1 on failure.
   Before returning advance PPTR past the last character parsed (a closing
   '}' in case of success). */
int
spf_expand_do(struct spf_data *dat, int allow_exp, const char **pptr)
{
	char *q;

	switch (*(*pptr)++) {
	case 'd':
	case 'D':
		return spf_transform(dat, dat->q.domain, 0, pptr);

	case 'h':
	case 'H':
		return spf_transform(dat, dat->q.helo_domain, 0, pptr);

	case 'i':
	case 'I':
		if (dat->addr.family == AF_INET6) {
			char ipbuf[IPBUF_DOTTED_SIZE], *p;
			int i;

			/* For IPv6 addresses, the "i" macro expands to a
			   dot-format address. */
			p = ipbuf;
			for (i = 0; i < dat->addr.len; i++) {
				if (i > 0)
					*p++ = '.';
				*p++ = to_xdig(dat->addr.address[i] >> 4);
				*p++ = to_xdig(dat->addr.address[i] & 0xf);
			}
			return spf_transform(dat, ipbuf, 0, pptr);
		} else
			return spf_transform(dat, dat->q.ipstr, 0, pptr);

	case 'l':
	case 'L':
		q = strchr(dat->q.sender, '@');
		if (!q)
			return 1;
		return spf_transform(dat, dat->q.sender, q - dat->q.sender,
				     pptr);

	case 'o':
	case 'O':
		q = strchr(dat->q.sender, '@');
		if (!q)
			return 1;
		return spf_transform(dat, q + 1, 0, pptr);

	case 'p':
	case 'P':
	{
		int rc;
		size_t i;
		char *name = NULL;
		struct dns_reply reply;
		dns_status status = ptr_validate(dat->q.ipstr, &reply);
		if (status != dns_success)
			return spf_transform(dat, "unknown", 0, pptr);

		for (i = 0; i < reply.count; i++)
			if (strcasecmp(reply.data.str[i], dat->q.domain) == 0) {
				name = reply.data.str[i];
				break;
			}

		if (!name) {
			for (i = 0; i < reply.count; i++)
				if (domain_match(reply.data.str[i],
						 dat->q.domain)) {
					name = reply.data.str[i];
					break;
				}

			if (!name)
				name = reply.data.str[0];
		}

		rc = spf_transform(dat, name, 0, pptr);
		dns_reply_free(&reply);
		return rc;
	}

	case 's':
	case 'S':
		return spf_transform(dat, dat->q.sender, 0, pptr);

	case 'v':
	case 'V':
		/* FIXME: add IPv6 support */
		return spf_transform(dat, "in-addr", 0, pptr);

	case 'c':
	case 'C':
		if (!allow_exp)
			return 1;
		else {
			/*
			 * [For IPv6 addresses, ] [t]he "c" macro can expand to
			 * any of the hexadecimal colon-format addresses
			 * specified in Section 2.2 of [RFC4291].  It is
			 * intended for humans to read.
			 */
			char buf[MU_CIDR_MAXBUFSIZE];
			mu_cidr_to_string(&dat->addr, MU_CIDR_FMT_ADDRONLY,
					  buf, sizeof(buf), NULL);
			return spf_transform(dat, buf, 0, pptr);
		}

	case 'r':
	case 'R':
		if (!allow_exp)
			return 1;
		else
			return spf_transform(dat, dat->q.my_domain, 0, pptr);

	case 't':
	case 'T':
		if (!allow_exp)
			return 1;
		else {
			char buf[NUMERIC_BUFSIZE_BOUND];
			snprintf(buf, sizeof buf, "%lu",
				 (unsigned long) time(NULL));
			mu_opool_appendz(dat->tmpool, buf);
		}
		break;

	default:
		return 1;
	}
	return 0;
}

/* Expand the macro string INPUT as per RFC 4408, chapter 8.
   ALLOW_EXP is 1 if the expansion of macros c, r and t is allowed (see
   RFC 4408, page 27).
   Return 0 on success, 1 on failure.
   To obtain the expanded string, run spf_data_ptr(dat); */
int
_spf_macro_expand(const char *input, struct spf_data *dat, int allow_exp)
{
	const char *p;

	while (p = strchr(input, '%')) {
		size_t len = p - input;
		if (len > 0)
			mu_opool_append(dat->tmpool, input, len);
		switch (p[1]) {
		case '{':
			p += 2;
			if (spf_expand_do(dat, allow_exp, &p))
				return 1;
			break;

		case '%':
			mu_opool_append_char(dat->tmpool, '%');
			p += 2;
			break;

		case '_':
			mu_opool_append_char(dat->tmpool, ' ');
			p += 2;
			break;

		case '-':
			mu_opool_appendz(dat->tmpool, "%20");
			p += 2;
			break;

		default:
			return 1;
		}
		input = p;
	}
	if (input)
		mu_opool_appendz(dat->tmpool, input);
	mu_opool_append_char(dat->tmpool, 0);
	return 0;
}

/* The interface function for _spf_macro_expand above.
   Takes care about unfinished opool memory in case of failure. */
int
spf_macro_expand(const char *input, struct spf_data *dat, int allow_exp)
{
	int rc = _spf_macro_expand(input, dat, allow_exp);
	if (rc)
		mu_opool_free(dat->tmpool, NULL);
	return rc;
}


/* ******************* */
/*  SPF term evaluator */

typedef enum {
	spf_term_mechanism,
	spf_term_modifier
} spf_term_type;

typedef enum {
	spf_arg_none,
	spf_arg_domain_spec,
	spf_arg_ipv4,
	spf_arg_ipv6
} spf_arg_type;

typedef struct {
	spf_arg_type type;
	union {
		char *domain_spec;
		struct mu_cidr ip;
	} v;
} spf_term_arg;

typedef enum {
	spf_term_match,
	spf_term_nomatch,
	spf_term_exception,
} spf_term_result;

typedef spf_term_result (*spf_term_handler)(struct spf_data *dat,
					    spf_term_arg *arg,
					    unsigned long masklen);

struct spf_term {
	spf_term_type type;           /* Term type */
	const char *expr;             /* Original expression (for debugging)*/
	spf_term_handler handler;     /* Term handler */
	int has_arg;                  /* Is an explicit argument given */
	spf_term_arg arg;             /* Argument if has_arg==1 */
	spf_result qualifier;         /* only for type == spf_term_directive */
	unsigned long masklen;        /* Netmask length */
};

struct spf_term_syntax {
	char *tag;
	spf_term_type type;
	spf_arg_type argtype;
	unsigned long default_masklen; /* 0 if not allowed */
	spf_term_handler handler;
	int mod_index; /* Modifier index */
};

#define DNS_CATCH(expr) \
	switch (expr) {                                           \
	case dns_success:                                         \
		break;                                            \
	case dns_not_found:                                       \
	case dns_failure:                                         \
	case dns_too_many:					  \
		return spf_term_nomatch;                          \
	case dns_temp_failure:                                    \
		dat->result = spf_temp_error;                     \
		return spf_term_exception;                        \
	}


/* Handlers for particular terms */

/* RFC 4408, 5.1.
     all              = "all"
*/
static spf_term_result
mech_all(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	dat->mod[SPF_MOD_REDIRECT] = NULL;
	return spf_term_match;
}

/* 5.2.
	 include          = "include"  ":" domain-spec
*/
static spf_term_result
mech_include(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	spf_result res;
	spf_term_result tres;
	spf_query_t query;
	size_t mechn;

	if (!arg) {
		mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
			 ("include used without argument"));
		dat->result = spf_perm_error;
		return spf_term_exception;
	}
	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("include %s", arg->v.domain_spec));

	query = dat->q;
	query.domain = arg->v.domain_spec;
	mechn = dat->answer->mechn;
	res = spf_check_host_internal(&query, dat->answer, dat->loopno);
	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("check_host returned %s", spf_result_str[res]));
	switch (res) {
	case spf_pass:
		tres = spf_term_match;
		break;

	case spf_fail:
	case spf_soft_fail:
	case spf_neutral:
		/* Remove any mechanisms that may have been saved during
		   interior check_host */
		while (dat->answer->mechn > mechn)
			free(dat->answer->mechv[--dat->answer->mechn]);
		tres = spf_term_nomatch;
		break;

	case spf_temp_error:
		dat->result = spf_temp_error;
		tres = spf_term_exception;
		break;

	case spf_perm_error:
	case spf_none:
		dat->result = spf_perm_error;
		tres = spf_term_exception;
		break;
	}

	return tres;
}

static char *
cidr_as_string(struct mu_cidr *cidr, char *buf, size_t bufsize)
{
	if (mu_cidr_to_string(cidr, MU_CIDR_FMT_SIMPLIFY, buf, bufsize, NULL)) {
		if (bufsize >= 2) {
			buf[0] = '?';
			buf[1] = 0;
		} else
			buf[0] = 0;
	}
	return buf;
}

static void
fill_cidr_netmask(struct mu_cidr *cidr, unsigned long masklen)
{
	int i, cnt;

	cnt = masklen / 8;
	for (i = 0; i < cnt; i++)
		cidr->netmask[i] = 0xff;
	if (i < MU_INADDR_BYTES) {
		cnt = 8 - masklen % 8;
		cidr->netmask[i++] = (0xff >> cnt) << cnt;
		for (; i < MU_INADDR_BYTES; i++)
			cidr->netmask[i] = 0;
	}
	for (i = 0; i < MU_INADDR_BYTES; i++)
		cidr->address[i] &= cidr->netmask[i];
}

static int
ip_matches(int family, void *ip, unsigned long masklen, struct mu_cidr const *addr)
{
	struct mu_cidr t;

	t.family = family;
	t.len = _mu_inaddr_to_bytes(t.family, ip, t.address);
	fill_cidr_netmask(&t, masklen);
	return mu_cidr_match(&t, (struct mu_cidr*)addr) == 0;
}

/* 5.3.
      A                = "a"      [ ":" domain-spec ] [ dual-cidr-length ]
*/
static spf_term_result
mech_a(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	const char *domain_spec;
	struct dns_reply r;
	size_t i;
	spf_term_result res;

	if (arg)
		domain_spec = arg->v.domain_spec;
	else
		domain_spec = dat->q.domain;

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("A domain_spec=%s, masklen=%lu", domain_spec, masklen));

	DNS_CATCH((dat->addr.family == AF_INET6 ? aaaa_lookup : a_lookup)(domain_spec, &r));

	res = spf_term_nomatch;
	for (i = 0; i < r.count; i++) {
		if (ip_matches(dat->addr.family,
			       dat->addr.family == AF_INET6
			        ? (void*)&r.data.ip6[i]
			        : (void*)&r.data.ip[i], masklen, &dat->addr)) {
			res = spf_term_match;
			break;
		}
	}
	dns_reply_free(&r);
	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 (res == spf_term_match ? "A matches" : "A does not match"));
	return res;
}

/* 5.4.
      MX               = "mx"     [ ":" domain-spec ] [ dual-cidr-length ]
*/
static spf_term_result
mech_mx(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	struct dns_reply reply;
	size_t i;
	spf_term_result result = spf_term_nomatch;
	const char *domain_spec;

	if (arg)
		domain_spec = arg->v.domain_spec;
	else
		domain_spec = dat->q.domain;

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("MX domain_spec=%s, masklen=%lu",
		  domain_spec, masklen));

	DNS_CATCH(mx_lookup(domain_spec, resolve_none, &reply));

	for (i = 0; i < reply.count; i++) {
		spf_term_arg targ;
		spf_term_result res;

		targ.type = spf_arg_domain_spec;
		targ.v.domain_spec = reply.data.str[i];
		res = mech_a(dat, &targ, masklen);
		if (res == spf_term_match) {
			result = res;
			break;
		}
	}

	dns_reply_free(&reply);
	return result;
}

/* 5.5.
      PTR              = "ptr"    [ ":" domain-spec ]
*/
static spf_term_result
mech_ptr(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	struct dns_reply reply;
	size_t i;
	const char *domain_spec;
	spf_term_result result = spf_term_nomatch;

	if (arg)
		domain_spec = arg->v.domain_spec;
	else
		domain_spec = dat->q.domain;

	DNS_CATCH(ptr_validate(dat->q.ipstr, &reply));

	for (i = 0; i < reply.count; i++)
		if (domain_match(reply.data.str[i], domain_spec)) {
			result = spf_term_match;
			break;
		}
	dns_reply_free(&reply);
	return result;
}

/* 5.6.
      IP4              = "ip4"      ":" ip4-network   [ ip4-cidr-length ]
      IP6              = "ip6"      ":" ip6-network   [ ip6-cidr-length ]
*/
static spf_term_result
mech_ip(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	char ipstr[MU_CIDR_MAXBUFSIZE];
	struct mu_cidr cidr;

	if (!arg) {
		mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
			 ("ip4/ip6 used without argument"));
		dat->result = spf_perm_error;
		return spf_term_exception;
	}
	cidr = arg->v.ip;
	fill_cidr_netmask(&cidr, masklen);
	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("IP%d: %s",
		  cidr.family == AF_INET6 ? 6 : 4,
		  cidr_as_string(&cidr, ipstr, sizeof(ipstr))));

	if (mu_cidr_match(&cidr, &dat->addr) == 0)
		return spf_term_match;
	return spf_term_nomatch;
}

/* 5.7.
      exists           = "exists"   ":" domain-spec
*/
static spf_term_result
mech_exists(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	struct dns_reply r;
	if (!arg) {
		mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
			 ("exists used without argument"));
		dat->result = spf_perm_error;
		return spf_term_exception;
	}
	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("EXISTS domain_spec=%s", arg->v.domain_spec));

	/*
	 * The resulting domain name is used for a DNS A RR lookup
	 * (even when the connection type is IPv6).
	 */
	DNS_CATCH(a_lookup(arg->v.domain_spec, &r));
	dns_reply_free(&r);

	return spf_term_match;
}

/* 6.1.
      redirect         = "redirect" "=" domain-spec
*/
static spf_term_result
mod_redirect(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	spf_query_t query;

	query = dat->q;
	query.domain = arg->v.domain_spec;
	dat->result = spf_check_host_internal(&query, dat->answer, dat->loopno);
	if (dat->result == spf_none)
		dat->result = spf_perm_error;
	return spf_term_exception;
}

/* 6.2.
      explanation      = "exp" "=" domain-spec
*/
static spf_term_result
mod_exp(spf_data *dat, spf_term_arg *arg, unsigned long masklen)
{
	struct dns_reply r;

	if (arg->v.domain_spec
	    && txt_lookup(arg->v.domain_spec, &r) == dns_success) {
		int i;
		char *text;

		for (i = 0; i < r.count; i++) {
			mu_opool_appendz(dat->tmpool, r.data.str[i]);
		}
		dns_reply_free(&r);
		mu_opool_append_char(dat->tmpool, 0);
		text = spf_data_ptr(dat);

		if (dat->q.exp_prefix)
			mu_opool_appendz(dat->tmpool, dat->q.exp_prefix);
		if (spf_macro_expand(text, dat, 1)) {
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
				 ("error expanding explanation text %s",
				  text));
//FIXME			mu_opool_append_char(dat->tmpool, 0);
		} else
			dat->answer->exp_text = mu_strdup(spf_data_ptr(dat));
	}
	return spf_term_nomatch;
}


/* Syntax driver table */
static struct spf_term_syntax term_syntax[] = {
	{ "all", spf_term_mechanism, spf_arg_none, 0, mech_all },
	{ "include", spf_term_mechanism, spf_arg_domain_spec, 0,
	  mech_include },
	{ "a", spf_term_mechanism, spf_arg_domain_spec, 32, mech_a },
	{ "mx", spf_term_mechanism, spf_arg_domain_spec, 32, mech_mx },
	{ "ptr", spf_term_mechanism, spf_arg_domain_spec, 0, mech_ptr },
	{ "ip4", spf_term_mechanism, spf_arg_ipv4, 32, mech_ip },
	{ "ip6", spf_term_mechanism, spf_arg_ipv6, 128, mech_ip },
	{ "exists", spf_term_mechanism, spf_arg_domain_spec, 0, mech_exists },
	{ "redirect", spf_term_modifier, spf_arg_domain_spec, 0,
	  mod_redirect, SPF_MOD_REDIRECT },
	{ "exp", spf_term_modifier, spf_arg_domain_spec, 0, mod_exp,
	  SPF_MOD_EXP },
	{ NULL }
};

/* Return a syntax driver for the given TAG */
static const struct spf_term_syntax *
find_syntax(const char *tag)
{
	const struct spf_term_syntax *p;
	for (p = term_syntax; p->tag; p++)
		if (strcasecmp(p->tag, tag) == 0)
			return p;
	return NULL;
}

#define ISSPACE(c) ((c) == ' ' || (c) == '\t')
#define skip_word(p) while (*(p) && !ISSPACE(*(p))) ++(p)
#define skip_space(p) while (*(p) && ISSPACE(*(p))) ++(p)

/* Parse the SPFv1 record REC.
   On success, return 0 and store the compiled array of terms in PTERMV,
   the number of terms in PTERMC, and the number of modifiers in PMODC.
   Make sure the compiled modifiers follow the mechanisms.
*/
static int
parse_record(char *rec, struct spf_data *dat, int *ptermc, spf_term **ptermv)
{
	int termc = 0;
	char *p;
	int i;
	spf_term term;
	char *expr_space;
	spf_term modv[MAX_SPF_MOD];

	memset(modv, 0, sizeof modv);

	/* Skip the initial version declaration */
	skip_word(rec);

	/* Allocate enough memory for debugging strings */
	mu_opool_alloc(dat->tmpool, strlen(rec) + 1);
	expr_space = mu_opool_finish(dat->tmpool, NULL);

	while (*rec) {
		char *arg;
		char *maskp = NULL;
		spf_term_type type = spf_term_mechanism;
		const struct spf_term_syntax *synt;

		skip_space(rec);
		p = rec;
		skip_word(rec);
		if (*rec)
			*rec++ = 0;

		term.expr = expr_space;
		strcpy(expr_space, p);
		expr_space += rec - p;

		switch (*p) {
		case '+':
			term.qualifier = spf_pass;
			p++;
			break;

		case '-':
			term.qualifier = spf_fail;
			p++;
			break;

		case '~':
			term.qualifier = spf_soft_fail;
			p++;
			break;

		case '?':
			term.qualifier = spf_neutral;
			p++;
			break;

		default:
			term.qualifier = spf_pass;
		}


		for (arg = p; *arg; arg++)
			if (*arg == ':') {
				*arg++ = 0;
				type = spf_term_mechanism;
				break;
			} else if (*arg == '/') {
				maskp = arg + 1;
				*arg = 0;
				type = spf_term_mechanism;
				break;
			} else if (*arg == '=') {
				*arg++ = 0;
				type = spf_term_modifier;
				break;
			}

		synt = find_syntax(p);
		if (!synt) {
			if (type == spf_term_modifier) {
				mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					 ("ignoring unknown modifier %s", p));
				continue;
			} else {
				mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					 ("unknown mechanism %s", p));
				return 1;
			}
		}

		if (synt->type != type) {
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
				 ("invalid use of %s", p));
			return 1;
		}

		term.type = synt->type;
		term.handler = synt->handler;
		term.masklen = synt->default_masklen;

		if (!maskp) {
			if (arg) {
				maskp = strchr(arg, '/');
				if (maskp)
					*maskp++ = 0;
			}
		}

		if (maskp) {
			if (synt->default_masklen) {
				char *endp;
				term.masklen = strtoul(maskp, &endp, 0);
				if (*endp == '/') {
					mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					      ("ignoring unsupported IPv6 mask %s",
					       endp));
				} else if (*endp) {
					mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					      ("invalid netmask %s "
					       "(stopped near %s)",
					       maskp, endp));
					return 1;
				}
			} else {
				mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
				      ("error: netmask used with %s", p));
				return 1;
			}
		}

		if (!*arg)
			term.has_arg = 0;
		else if (synt->argtype == spf_arg_none) {
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
			      ("%s used with an argument", p));
			return 1;
		} else {
			term.has_arg = 1;
			term.arg.type = synt->argtype;
			switch (synt->argtype) {
			default:
				abort();

			case spf_arg_domain_spec:
				term.arg.v.domain_spec = arg;
				break;

			case spf_arg_ipv4:
				if (mu_cidr_from_string(&term.arg.v.ip, arg)
				    || term.arg.v.ip.family != AF_INET) {
					mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
						 ("invalid IPv4: %s", arg));
					return 1;
				}
				break;

			case spf_arg_ipv6:
				if (mu_cidr_from_string(&term.arg.v.ip, arg)
				    || term.arg.v.ip.family != AF_INET6) {
					mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
						 ("invalid IPv6: %s", arg));
					return 1;
				}
				break;
			}
		}

		if (term.type == spf_term_modifier) {
			if (modv[synt->mod_index].handler) {
				mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					 ("duplicate %s modifier", p));
				return 1;
			}
			modv[synt->mod_index] = term;
		} else {
			mu_opool_append(dat->tmpool, &term, sizeof term);
			termc++;
		}
	}

	*ptermc = termc;
	*ptermv = (spf_term*) spf_data_ptr(dat);

	for (i = 0; i < NELEMS(modv); i++)
		dat->mod[i] = mu_opool_dup(dat->tmpool, &modv[i],
					   sizeof modv[0]);
	return 0;
}

static int
expand_term_arg(struct spf_data *dat, spf_term *term, spf_term_arg **parg)
{
	if (term->has_arg) {
		spf_term_arg *arg = &term->arg;
		if (arg->type == spf_arg_domain_spec) {
			if (spf_macro_expand(arg->v.domain_spec, dat, 0)) {
				mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
					 ("error expanding %s",
					  arg->v.domain_spec));
				return 1;
			}
			arg->v.domain_spec = spf_data_ptr(dat);
		}
		*parg = arg;
	} else
		*parg = NULL;
	return 0;
}

/* Parse and evaluate SPFv1 record REC. */
spf_result
spf_eval_record(char *rec, struct spf_data *dat)
{
	int i;
	int termc;
	spf_term *termv, *tp;
	spf_result result = spf_neutral;
	const char *match = NULL;
	spf_term_arg *parg;

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,
		 ("Parsing SPF record: %s", rec));
	if (parse_record(rec, dat, &termc, &termv))
	    return spf_perm_error;

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
		 ("Evaluating SPF record"));
	for (i = 0; i < termc; i++) {
		tp = &termv[i];

		if (expand_term_arg(dat, tp, &parg)) {
			result = spf_perm_error;
			break;
		}

		mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
			 ("SPF TERM: %s", tp->expr));
		switch (tp->handler(dat, parg, tp->masklen)) {
		case spf_term_match:
			match = tp->expr;
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
				 ("term matched"));
			result = tp->qualifier;
			break;

		case spf_term_nomatch:
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
				 ("term did not match"));
			continue;

		case spf_term_exception:
			mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE1,
				 ("term threw exception: %s",
			       spf_result_str[dat->result]));
			result = dat->result;
			break;
		}
		break;
	}

	if (match)
		spf_answer_add_mech(dat->answer, match);

	if (!match && (tp = dat->mod[SPF_MOD_REDIRECT])->handler) {
		if (expand_term_arg(dat, tp, &parg))
			result = spf_perm_error;
		else {
			spf_answer_add_mech(dat->answer, tp->expr);
			if (tp->handler(dat, parg, tp->masklen)
			    == spf_term_exception)
				result = dat->result;
			else if (dat->answer)
				free(dat->answer->mechv[--dat->answer->mechn]);
		}
	} else if (result == spf_fail
		   && (tp = dat->mod[SPF_MOD_EXP])->handler) {
		if (expand_term_arg(dat, tp, &parg))
			result = spf_perm_error;
		else
			tp->handler(dat, parg, tp->masklen);
	}

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,
		 ("result = %s", spf_result_str[result]));
	return result;
}

#define POSTMASTER_PFX "postmaster@"

static spf_result
spf_exec_query(char *rec, struct spf_data *dat)
{
	if (!strchr(dat->q.sender, '@')) {
		mu_opool_append(dat->tmpool, POSTMASTER_PFX,
				sizeof(POSTMASTER_PFX) - 1);
		dat->q.sender = mu_opool_dup(dat->tmpool, dat->q.sender,
					     strlen(dat->q.sender) + 1);
	}

	return spf_eval_record(rec, dat);
}

spf_result
spf_test_record(const char *rec, spf_query_t *q, spf_answer_t *a)
{
	struct spf_data dat;
	spf_result result;

	mu_debug(MF_SOURCE_SPF,
		 MU_DEBUG_TRACE0,
		 ("SPF record: %s, ip=%s, domain=%s, sender=%s",
		  rec, q->ipstr, q->domain, q->sender));

	if (a)
		memset(a, 0, sizeof *a);
	if (spf_data_init(&dat, q, a, 0))
		return spf_perm_error;

	result = spf_exec_query(mu_opool_dup(dat.tmpool, rec, strlen(rec) + 1),
				&dat);

	spf_data_free(&dat);

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,
		 ("result = %s", spf_result_str[result]));

	return result;
}

char *
spf_test_macro_expand(const char *input, spf_query_t *q, int allow_exp)
{
	struct spf_data dat;
	char *res;

	if (spf_data_init(&dat, q, NULL, 0))
		return NULL;
	if (spf_macro_expand(input, &dat, allow_exp)) {
		spf_data_free(&dat);
		return NULL;
	}
	res = mu_strdup(spf_data_ptr(&dat));
	spf_data_free(&dat);
	return res;
}

/* Implementation of check_host() function. RFC 4408, 4. */
spf_result
spf_check_host_internal(spf_query_t *q, spf_answer_t *a, size_t loopno)
{
	char *spf_rec = NULL;
	struct spf_data dat;
	spf_result result;
#define SPF_RETURN(res, text) {						\
		mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,		\
		      ("check_host(%s, %s, %s) = %s; %s",		\
		       q->ipstr, q->domain, q->sender,			\
		       spf_result_str[res],				\
		       text));						\
		return res; }

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,
		 ("check_host(%s, %s, %s)",
		  q->ipstr, q->domain, q->sender));

	if (loopno > SPF_MAX_RECURSION) {
		mu_debug(MF_SOURCE_SPF, MU_DEBUG_ERROR,
			 ("SPF recursion limit reached"));
		return spf_perm_error;
	}

	if (a)
		memset(a, 0, sizeof *a);

	switch (spf_lookup(q->domain, &spf_rec)) {
	case dns_success:
		break;

	case dns_too_many:
		SPF_RETURN(spf_perm_error,
			   "too many SPF records published");

	case dns_not_found:
	case dns_failure:
		SPF_RETURN(spf_none,
			   "invalid domain or no SPF records published");

	case dns_temp_failure:
		SPF_RETURN(spf_temp_error, "DNS temporary failure");
	}

	mu_debug(MF_SOURCE_SPF, MU_DEBUG_TRACE0,
		 ("SPF record: %s", spf_rec));

	if (spf_data_init(&dat, q, a, loopno + 1))
		SPF_RETURN(spf_perm_error, "spf_data_init failed");

	result = spf_exec_query(spf_rec, &dat);

	spf_data_free(&dat);
	free(spf_rec);

	SPF_RETURN(result, "");
}

spf_result
spf_check_host(spf_query_t *q, spf_answer_t *a)
{
	return spf_check_host_internal(q, a, 0);
}

void
spf_answer_free(spf_answer_t *ans)
{
	size_t i;

	free(ans->exp_text);
	for (i = 0; i < ans->mechn; i++)
		free(ans->mechv[i]);
	free(ans->mechv);
}

void
spf_answer_add_mech(spf_answer_t *ans, char const *mech)
{
	if (!ans)
		return;
	if (ans->mechn == ans->mechmax) {
		if (ans->mechmax == 0)
			ans->mechmax = SPF_MAX_RECURSION;
		ans->mechv = mu_2nrealloc(ans->mechv,
					  &ans->mechmax,
					  sizeof(ans->mechv[0]));
	}
	ans->mechv[ans->mechn++] = mu_strdup(mech);
}
