/*
 * $Id: f78fd2f63531c85ba33f13d98d4f03d5539a2183 $
 *
 * This file is part of nsmtp.
 *
 * Copyright (c) 2007, 2008 Niklas Olmes <niklas@noxa.de>
 *                          http://www.nevm.de/nsmtp/
 *                          NEVM-RIPE
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License Version 2
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

/*
 * RfC4408
 * http://www.openspf.org/SPF_Record_Syntax
 */

/*
 * NOTE:  We explicitly don't support every possible shit
 *        you can do with SPF, like qualified "include",
 *        or macros.  Or "exists", which does only make
 *        sense with macros.
 */

#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <ctype.h>

#include "alloc.h"
#include "dns.h"
#include "log.h"
#include "cidr.h"
#include "smtp.h"
#include "spf.h"
#include "config.h"

#define MAX_SEEN	20

static const char rcsid[] = "$Id: f78fd2f63531c85ba33f13d98d4f03d5539a2183 $";

static char *next_token(const char *);

static char *
next_token(const char *txt)
{
	char *p;

	if ((p = strchr(txt, ' ')) == NULL)
		return NULL;

	while (*p == ' ' || *p == '\t')
		p++;

	if (*p == '\0')
		return NULL;

	return p;
}

SPF_INFO *
init_spf(void)
{
	SPF_INFO *spf;

	spf = nsm_calloc(1, sizeof(SPF_INFO));
	spf->version = 0;
	spf->net = NULL;
	spf->qual = NULL;
	spf->def = '\0';
	spf->seen = NULL;
	spf->seencount = 0;

	return spf;
}

void
destroy_spf(SPF_INFO *spf)
{
	int i;

	if (spf == NULL)
		return;

	if (spf->net != NULL) {
		i = 0;
		while (spf->net[i] != NULL) {
			nsm_free(spf->net[i]);
			i++;
		}
		nsm_free(spf->net);
	}
	if (spf->qual != NULL)
		nsm_free(spf->qual);
	if (spf->seen != NULL) {
		i = 0;
		while (spf->seen[i] != NULL) {
			nsm_free(spf->seen[i]);
			i++;
		}
		nsm_free(spf->seen);
	}
	nsm_free(spf);
}

/*
 * NOTE:  RfC4408 4.6: "If there are any syntax errors, check_host() returns immediately with
 *                      the result "PermError"."
 *
 * Return Codes:
 * 1 = parse error
 * 0 = okay
 */
int
parse_spf(SPF_INFO **ospf, const char *txt, const char *domain)
{
	SPF_INFO *spf;
	struct hostent *hp;
	struct in_addr addr;
	char buf[128], tmp[128];
	char *cur_domain, cur_qual;
	const char *mx;
	char *p;
	int i, j, n, pos;
	int cur_prefix;

	if ((p = strcasestr(txt, "v=spf1")) == NULL)
		return 1;

	DEBUG(("parse_spf() => to parse:  <%s>", p));

	spf = *ospf;

	if (++(spf->seencount) >= MAX_SEEN) {
		xwarn("warn: parse_spf() => seencount >= (%d), putting an end to this.  circular references?", MAX_SEEN);
		return 1;
	}

	if (spf->version == 0)
		spf->version = 1;
	if (spf->qual == NULL) {
		spf->qual = nsm_calloc(1, sizeof(char));
		spf->qual[0] = '\0';
	}
	if (spf->net == NULL) {
		spf->net = nsm_calloc(1, sizeof(char *));
		spf->net[0] = NULL;
	}
	if (spf->def == '\0')
		spf->def = '?';
	if (spf->seen == NULL) {
		/* NOTE: See spf.h why we absolutely need this shit. */
		spf->seen = nsm_calloc(2, sizeof(char *));
		spf->seen[0] = nsm_strdup(domain);
		spf->seen[1] = NULL;
	} else {
		n = 0;
		while (spf->seen[n] != NULL && strcasecmp(spf->seen[n], domain) != 0)
			n++;
		if (spf->seen[n] == NULL) {
			spf->seen = nsm_realloc(spf->seen, (n+2) * sizeof(char *));
			spf->seen[n] = nsm_strdup(domain);
			spf->seen[n+1] = NULL;
			n++;
		} else {
			DEBUG(("parse_spf() => already seen domain <%s>, return", domain));
			return 0;
		}
	}

	n = 0;
	while (spf->net[n] != NULL)
		n++;
	DEBUG(("parse_spf() => n == (%d)", n));

	#define ALLOC_MORE()	spf->qual = nsm_realloc(spf->qual, (n+2) * sizeof(char)); \
				spf->net = nsm_realloc(spf->net, (n+2) * sizeof(char *)); \
				spf->qual[n+1] = '\0'; \
				spf->net[n+1] = NULL;
	#define GET_TOKEN(odl)	(void)snprintf(buf, sizeof(buf), "%s", p); \
				pos = 0; \
				while (*p != '\0' && *p != ' ' && *p != '\t' && *p != odl) { \
				++p; \
				++pos; \
				} \
				buf[pos] = '\0';
	#define GOOD_QUAL()	if (!(*p == '+' || *p == '-' || *p == '~' || *p == '?')) \
					return 1;
	#define GOOD_PREFIX()	if (atoi(p) < 0 || atoi(p) > 32) { \
					DEBUG(("parse_spf() => prefix (%d) to a out of range 0 <= x <= 32", atoi(p))); \
					if (cur_domain != NULL) \
						nsm_free(cur_domain); \
					return 1; \
				}

	while ((p = next_token(p)) != NULL) {
		cur_domain = NULL;

		if (strncasecmp(p, "all", 3) == 0 || strncasecmp(p+1, "all", 3) == 0) {
			if (*p == 'a' || *p == 'A')
				spf->def = '+';
			else {
				GOOD_QUAL();
				spf->def = *p;
				++p;
			}
			p += 3;
			DEBUG(("parse_spf() => got \"all\" token:  qual=(%c)", spf->def));

		} else if (strncasecmp(p, "ip4:", 4) == 0 || strncasecmp(p+1, "ip4:", 4) == 0) {
			cur_qual = '+';
			if (*p != 'i' && *p != 'I') {
				GOOD_QUAL();
				cur_qual = *p;
				++p;
			}

			p += 4;
			GET_TOKEN(' ');

			ALLOC_MORE();
			spf->net[n] = nsm_strdup(buf);
			spf->qual[n] = cur_qual;
			DEBUG(("parse_spf() => got \"ip4:\" token:  net=<%s> qual=(%c) (ADD)", spf->net[n], spf->qual[n]));
			n++;

		} else if (strncasecmp(p, "ip6:", 4) == 0 || strncasecmp(p+1, "ip6:", 4) == 0) {
			DEBUG(("parse_spf() => got \"ip6:\" token which we don't support, ignore"));

		} else if (*p == 'a' || *p == 'A' || *(p+1) == 'a' || *(p+1) == 'A') {
			cur_qual = '+';
			if (*p != 'a' && *p != 'A') {
				GOOD_QUAL();
				cur_qual = *p;
				++p;
			}

			cur_domain = nsm_strdup(domain);
			cur_prefix = 32;

			++p;
			if (*p == '/' && isdigit((int)*(p+1))) {
				++p;
				GOOD_PREFIX();
				cur_prefix = atoi(p);
			} else if (*p == ':') {
				++p;
				GET_TOKEN('/');
				nsm_free(cur_domain);
				cur_domain = nsm_strdup(buf);
				if (*p == '/') {
					++p;
					GOOD_PREFIX();
					cur_prefix = atoi(p);
				}
			} else if (*p == '\0' || *p == ' ' || *p == '\t') {
				/* standalone "a" */
				;
			} else {
				xinfo("info: parse_spf() => unexpected suffix to \"a\" token, domain <%s>", domain);
				nsm_free(cur_domain);
				return 1;
			}

			DEBUG(("parse_spf() => got \"a\" token:  domain=<%s> qual=(%c) prefix=(%d)", cur_domain, cur_qual, cur_prefix));

			if ((hp = gethostbyname(cur_domain)) == NULL) {
				DEBUG(("parse_spf() => unable to resolve <%s>", cur_domain));
				nsm_free(cur_domain);
				continue;
			}

			for (i = 0; hp->h_addr_list[i] != NULL; i++) {
				ALLOC_MORE();
				spf->qual[n] = cur_qual;
				(void)memcpy(&addr.s_addr, hp->h_addr_list[i], (size_t)hp->h_length);
				(void)snprintf(buf, sizeof(buf), "%s/%d", inet_ntoa(addr), cur_prefix);
				spf->net[n] = nsm_strdup(buf);
				DEBUG(("parse_spf() => <%s> #(%d) address <%s> (ADD)", cur_domain, i, spf->net[n]));
				++n;
			}

			nsm_free(cur_domain);

		} else if (strncasecmp(p, "mx", 2) == 0 || strncasecmp(p+1, "mx", 2) == 0) {
			cur_qual = '+';
			if (*p != 'm' && *p != 'M') {
				GOOD_QUAL();
				cur_qual = *p;
				++p;
			}

			cur_domain = nsm_strdup(domain);
			cur_prefix = 32;

			p += 2;
			if (*p == '/' && isdigit((int)*(p+1))) {
				++p;
				GOOD_PREFIX();
				cur_prefix = atoi(p);
			} else if (*p == ':') {
				++p;
				GET_TOKEN('/');
				nsm_free(cur_domain);
				cur_domain = nsm_strdup(buf);
				if (*p == '/') {
					++p;
					GOOD_PREFIX();
					cur_prefix = atoi(p);
				}
			} else if (*p == '\0' || *p == ' ' || *p == '\t') {
				/* standalone "mx" */
				;
			} else {
				xinfo("info: parse_spf() => unexpected suffix to \"mx\" token, domain <%s>", domain);
				nsm_free(cur_domain);
				return 1;
			}

			DEBUG(("parse_spf() => got \"mx\" token:  domain=<%s> qual=(%c) prefix=(%d)", cur_domain, cur_qual, cur_prefix));

			i = 0;
			(void)snprintf(tmp, sizeof(tmp), "foo@%s", cur_domain);
			(void)get_next_mx(NULL);
			while ((mx = get_next_mx(tmp)) != NULL) {
				DEBUG(("parse_spf() => <%s> #(%d) mx <%s>", cur_domain, i, mx));

				if ((hp = gethostbyname(mx)) == NULL) {
					DEBUG(("parse_spf() => unable to resolve <%s>", mx));
					continue;
				}

				for (j = 0; hp->h_addr_list[j] != NULL; j++) {
					ALLOC_MORE();
					spf->qual[n] = cur_qual;
					(void)memcpy(&addr.s_addr, hp->h_addr_list[j], (size_t)hp->h_length);
					(void)snprintf(buf, sizeof(buf), "%s/%d", inet_ntoa(addr), cur_prefix);
					spf->net[n] = nsm_strdup(buf);
					DEBUG(("parse_spf() => addresses mx <%s> #(%d) <%s> (ADD)", mx, j, spf->net[n]));
					++n;
				}
				++i;
			}
			(void)get_next_mx(NULL);

			nsm_free(cur_domain);

		} else if (strncasecmp(p, "ptr", 3) == 0 || strncasecmp(p+1, "ptr", 3) == 0) {
			cur_qual = '+';
			if (*p != 'p' && *p != 'P') {
				GOOD_QUAL();
				cur_qual = *p;
				++p;
			}

			cur_domain = nsm_strdup(domain);
			cur_prefix = 32;

			p += 3;
			if (*p == ':') {
				++p;
				GET_TOKEN(' ');
				nsm_free(cur_domain);
				cur_domain = nsm_strdup(buf);
			}

			DEBUG(("parse_spf() => got \"ptr\" token:  domain=<%s> (ADD)", cur_domain));

			ALLOC_MORE();
			spf->qual[n] = cur_qual;
			(void)snprintf(buf, sizeof(buf), "!%s", cur_domain);
			spf->net[n] = nsm_strdup(buf);
			n++;

			nsm_free(cur_domain);

		} else if (strncasecmp(p, "include:", 8) == 0) {
			p += 8;
			GET_TOKEN(' ');

			DEBUG(("parse_spf() => got \"include\" token:  domain=<%s>", buf));

			if (get_spf(ospf, buf) != 0) {
				xinfo("info: parse_spf() => get_spf() for include returned failure, returning failure, too.  domain=<%s> include=<%s>", domain, buf);
				return 1;
			}

			spf = *ospf;
			n = 0;
			while (spf->net[n] != NULL)
				n++;
			DEBUG(("parse_spf() => new n == (%d) after include", n));

		} else if (strncasecmp(p, "redirect=", 9) == 0) {
			p += 9;
			GET_TOKEN(' ');

			DEBUG(("parse_spf() => got \"redirect\" token, throwing everything away we parsed yet and going to:  domain=<%s>", buf));

			destroy_spf(spf);
			*ospf = init_spf();
			return get_spf(ospf, buf);

		} else {
			xinfo("info: parse_spf() => unhandled token.  domain=<%s>", domain);
			return 1;
		}
	}

	return 0;
}

/*
 * Return Codes:
 * -1 - could not get record
 * 0  - okay
 * 1  - parse error
 */
int
get_spf(SPF_INFO **spf, const char *domain)
{
	DNS_ENTRY **dnsentry;
	int i, flag;

	if ((dnsentry = get_txt_for_domain(domain)) == NULL || dnsentry[0] == NULL) {
		DEBUG(("get_spf() => unable to get TXT record for <%s>", domain));
		dns_entry_free(dnsentry);
		return -1;
	}

	flag = 0;
	for (i = 0; dnsentry[i] != NULL; i++) {
		DEBUG(("get_spf() => got TXT record #(%d) <%s>", i, dnsentry[i]->print));
		if (strcasestr(dnsentry[i]->print, "v=spf1") != NULL) {
			DEBUG(("get_spf() => looks like SPF v1 record, invoking parse_spf()"));
			if (parse_spf(spf, dnsentry[i]->print, domain) != 0) {
				xinfo("info: get_spf() => parse_spf() returned failure, returning failure, too.  domain=<%s>", domain);
				dns_entry_free(dnsentry);
				return 1;
			}
			flag = 1;
		}
	}

	dns_entry_free(dnsentry);

	if (flag)
		return 0;

	return 1;
}

/*
 * NOTE:  RfC4408 4.6.2: "Each mechanism is considered in turn from left to right. [..]
 *                        When a mechanism is evaluated, one of three things can happen: it can
 *                        match, not match, or throw an exception.
 *                        If it matches, processing ends and the qualifier value is returned as
 *                        the result of that record."
 * Return Codes:
 * '+'  - pass
 * '-'  - fail
 * '~'  - softfail
 * '?'  - neutral
 */
int
check_spf(SPF_INFO *spf, const char *ip, const char *reverse)
{
	struct hostent *hp;
	struct in_addr addr;
	char a[128], b[128];
	int i, j;

	if (spf == NULL)
		return -1;

	DEBUG(("check_spf() => ip=[%s]", ip));

	for (i = 0; spf->net != NULL && spf->net[i] != NULL && spf->qual != NULL && spf->qual[i] != '\0'; i++) {
		if (*spf->net[i] == '!') {
			/*
			 * PTR
			 */
			DEBUG(("check_spf() => ptr=<%s> qual=(%c)", spf->net[i] + 1, spf->qual[i]));

			if (reverse == NULL || *reverse == '\0')
				continue;

			(void)snprintf(a, sizeof(a), ".%s", reverse);
			(void)snprintf(b, sizeof(b), ".%s", spf->net[i] + 1);

			DEBUG(("check_spf() => a=<%s> b=<%s>", a, b));

			if (strcasestr(a, b) != NULL) {
				if ((hp = gethostbyname(reverse)) == NULL) {
					DEBUG(("check_spf() => unable to resolve <%s>", reverse));
					continue;
				}
				for (j = 0; hp->h_addr_list[j] != NULL; j++) {
					(void)memcpy(&addr.s_addr, hp->h_addr_list[j], (size_t)hp->h_length);
					DEBUG(("check_spf() => <%s> resolves to #(%d) ip [%s]", reverse, j, inet_ntoa(addr)));
					if (strcmp(inet_ntoa(addr), ip) == 0)
						return spf->qual[j];
				}
			}
		} else {
			/*
			 * A, MX, IP4, ...
			 */
			DEBUG(("check_spf() => net=<%s> qual=(%c)", spf->net[i], spf->qual[i]));

			if (cidr_ip_in_net(ip, spf->net[i]) == 1)
				return spf->qual[i];
		}
	}

	return spf->def;
}

/*
 * Return codes:
 * 0  - allow
 * 1  - softfail
 * 1  - deny
 */
int
check_spf_mail_from(SMTP_CTX *ctx, const char *from)
{
	SPF_INFO *spf;
	char buf[256];
	char *domain;
	int r;

	if (ctx == NULL || (domain = strchr(from, '@')) == NULL)
		return 1;
	++domain;

	DEBUG(("check_spf_mail_from() => from=<%s> domain=<%s>", from, domain));

	(void)snprintf(buf, sizeof(buf), "\treceiver=%s; client-ip=%s; identity=mailfrom;\n"
					 "\tenvelope-from=<%s>; helo=%s;",
					 nsmc_name, ctx->client_ip,
					 from, ctx->helo == NULL ? "none" : ctx->helo);

	spf = init_spf();
	switch (get_spf(&spf, domain)) {
	case -1:
		nsm_asprintf(&ctx->spfinfo, "None (nsmtp on %s: domain %s does not seem to publish SPF v1 TXT records, no check performed)\n%s",
					    nsmc_name, domain, buf);
		destroy_spf(spf);
		return 0;
	case 1:
		nsm_asprintf(&ctx->spfinfo, "TempError (nsmtp on %s was unable to parse and/or get all supplementary records for the SPF TXT records of domain %s)\n%s",
					    nsmc_name, domain, buf);
		destroy_spf(spf);
		return 0;
	case 0:
	default:
		/* okay */
		;
	}

	switch (check_spf(spf, ctx->client_ip, ctx->client_rn)) {
	case '+':
		nsm_asprintf(&ctx->spfinfo, "Pass (nsmtp on %s: domain %s designates %s [%s] as permitted sender)\n%s",
					    nsmc_name, domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip, buf);
		r = 0;
		break;
	case '?':
		nsm_asprintf(&ctx->spfinfo, "Neutral (nsmtp on %s: domain %s is neutral to %s [%s] as sender)\n%s",
					    nsmc_name, domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip, buf);
		r = 0;
		break;
	case '~':
		nsm_asprintf(&ctx->spfinfo, "SoftFail (nsmtp on %s: domain %s does not explicitly designate %s [%s] as permitted sender but has a SoftFail policy)\n%s",
					    nsmc_name, domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip, buf);
		nsm_asprintf(&ctx->spfsmtp, "451-4.3.0 Greylisted: SPF mail from policy check returned: SoftFail\r\n"
					    "451-4.3.0 Domain %s does not explicitly designate you (%s [%s]) as permitted sender but has a SoftFail policy.\r\n"
					    "451-4.3.0 receiver=%s; client-ip=%s; identity=mailfrom; envelope-from=<%s>; helo=%s;\r\n"
					    "451-4.3.0 Please note that this is not a policy decision of this MTA, but that of domain <%s>.  Contact them.\r\n"
					    "451 4.3.0 See http://www.openspf.org/Introduction and RfC 4408 for details about SPF.\r\n",
					    domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip,
					    nsmc_name, ctx->client_ip, from, ctx->helo == NULL ? "none" : ctx->helo,
					    domain);
		r = 1;
		break;
	case '-':
		nsm_asprintf(&ctx->spfinfo, "Fail (nsmtp on %s: domain %s does not designate %s [%s] as permitted sender)\n%s",
					    nsmc_name, domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip, buf);
		nsm_asprintf(&ctx->spfsmtp, "550-5.7.1 SPF mail from policy check returned: Fail\r\n"
					    "550-5.7.1 Domain %s does not designate you (%s [%s]) as permitted sender.\r\n"
					    "550-5.7.1 receiver=%s; client-ip=%s; identity=mailfrom; envelope-from=<%s>; helo=%s;\r\n"
					    "550-5.7.1 Please note that this is not a policy decision of this MTA, but that of domain <%s>.  Contact them.\r\n"
					    "550 5.7.1 See http://www.openspf.org/Introduction and RfC 4408 for details about SPF.\r\n",
					    domain, *ctx->client_rn == '\0' ? "unknown" : ctx->client_rn, ctx->client_ip,
					    nsmc_name, ctx->client_ip, from, ctx->helo == NULL ? "none" : ctx->helo,
					    domain);
		r = 2;
		break;
	default:
		xwarn("warn: check_spf_mail_from() => internal error: unhandled result from check_spf()");
		r = 0;
		break;
	}

	destroy_spf(spf);

	return r;
}
