/*
 * $Id: cf41221b5d7b833181094002083e221c9ad870d9 $
 *
 * 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.
 *
 */

#include <stdio.h>
#include <regex.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>

#include <ldap.h>

#include "ntl.h"
#include "log.h"
#include "dns.h"
#include "check.h"
#include "config.h"

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

/*
 * return codes:
 * -1 - unable to check atm
 * 0  - everything believed to be fine
 * 1  - not recognized as a valid mail address
 * 2  - appears to be local but does not seem to exist
 * 3  - is non-local and has no valid MX or A record, so we couldn't send mail to it
 * 4  - is non-local and needs to be relayed, but we have to deny this
 *
 * flag == 1 for relay, flag == 2 for short check
 */
int
check_mailaddress(const char *address, int flag)
{
	LDAPMessage *res, *res2;
	LDAP *ld;
	DNS_ENTRY **dns_a, **dns_mx;
	regex_t cregex;
	char buf[1024], buf2[1024];
	char *domain;
	const char *p;
	int e;

	DEBUG(("check_mailaddress() => address [%s]", address));

	/*
	 * XXX/GCC: GCC3.4 Bug under Gentoo Hardened
	 * warning: 'e' might be used uninitialized in this function
	 */
	e = 0;

	/*
	 * (0) Sanity Check
	 */
	if (strlen(address) > 255)
		return 1;
	p = address;
	while (*p != '\0') {
		if (*p < 32 || *p > 126)
			return 1;
		++p;
	}

	/*
	 * (1) Basic Regex Check
	 */
	if (regcomp(&cregex, nsmc_mail_regex, REG_EXTENDED) != 0) {
		xfatal("fatal: internal error: check_mailaddress() => mail regex \"%s\" does not compile", nsmc_mail_regex);
		return -1;
	}
	if (regexec(&cregex, address, 0, NULL, 0) != 0) {
		regfree(&cregex);
		return 1;
	}
	regfree(&cregex);

	/*
	 * (2) Check Against LDAP
	 */
	if (nsmc_ldap_cfgs == NULL || nsmc_ldap_cfgs[0] == NULL || nsmc_ldap_cfgs[1] == NULL) {
		xfatal("fatal: internal error: check_mailaddress() => no suitable LDAP configs loaded, unable to check");
		return -1;
	}

	if ((ld = ntl_connect(nsmc_ldap_cfgs[0])) == NULL
	    && (ld = ntl_connect(nsmc_ldap_cfgs[1])) == NULL) {
		xwarn("warn: check_mailaddress() => unable to connect to any LDAP server");
		return -1;
	}

	/* NOTE: we assure '@' exists above in regex check */
	domain = strchr(address, '@') + 1;

	/*
	 * (2.1) check if we're responsible at all
	 */
	(void)snprintf(buf, sizeof(buf), "rcpthost=%s", domain);
	(void)snprintf(buf2, sizeof(buf2), "cn=%s,ou=config,%s", nsmc_name, nsmc_mail_basedn);
	if ((res = ntl_search(ld, buf2, LDAP_SCOPE_BASE, buf, NULL)) == NULL
	    || ldap_first_entry(ld, res) == NULL) {
		DEBUG(("check_mailaddress() => not responsible for <%s>", buf));
		if (res != NULL)
			(void)ldap_msgfree(res);
		if (flag != 1) {
			e = 4;
			goto out;
		}
		goto remote;
	}
	(void)ldap_msgfree(res);

	DEBUG(("check_mailaddress() => we're responsible for <%s>", buf));

	e = 0;
	/*
	 * (2.2) check if this sucker is local
	 */
	(void)snprintf(buf, sizeof(buf), "ou=%s,%s", domain, nsmc_mail_basedn); /* see above */
	if ((res = ntl_search(ld, buf, LDAP_SCOPE_BASE, NULL, NULL)) != NULL
	    && ldap_first_entry(ld, res) != NULL) {
		/*
		 * Local
		 *
		 * (2.2)(a) check if mail address exists
		 */
		DEBUG(("check_mailaddress() => domain <%s> is local", domain));
		(void)snprintf(buf, sizeof(buf), "(|(mail=%s)(mailAlternateAddress=%s))", address, address);
		if ((res2 = ntl_search(ld, nsmc_mail_basedn, LDAP_SCOPE_SUBTREE, buf, NULL)) == NULL
		    || ldap_first_entry(ld, res2) == NULL) {
			DEBUG(("check_mailaddress() => mail address <%s> not found in LDAP", address));
			if (res != NULL)
				(void)ldap_msgfree(res);
			if (res2 != NULL)
				(void)ldap_msgfree(res2);
			e = 2;
			goto out;
		}
		if (res != NULL)
			(void)ldap_msgfree(res);
		if (res2 != NULL)
			(void)ldap_msgfree(res2);
	} else {
		/*
		 * Remote
		 */
		if (res != NULL)
			(void)ldap_msgfree(res);

remote:
		DEBUG(("check_mailaddress() => domain <%s> is remote", domain));

		if (flag == 2)
			goto out; /* short check requested */

		/*
		 * (2.2)(b) check if domain has A or MX record
		 */
		e = -1;
		dns_a = NULL;
		if ((dns_mx = get_mx_for_domain(domain)) == NULL) {
			xinfo("info: check_mailaddress() => DNS soft error while getting MX for <%s>", domain);
			e = 0;
		} else {
			if (*dns_mx == NULL) {
				if ((dns_a = get_a_for_domain(domain)) == NULL) {
					xinfo("info: check_mailaddress() => DNS soft error while getting A for <%s>", domain);
					e = 0;
				} else if (*dns_a == NULL) {
					DEBUG(("check_mailaddress() => neither A or MX record for domain [%s] found", domain));
					e = 3;
				} else
					e = 0;
			} else
				e = 0;
		}
		if (dns_mx != NULL)
			dns_entry_free(dns_mx);
		if (dns_a != NULL)
			dns_entry_free(dns_a);
	}

out:
	(void)ldap_unbind(ld);

	return e;
}

/*
 * return codes:
 * -1 - unable to determine atm
 * 0  - attribute is FALSE or empty
 * 1  - attribute is TRUE
 * 2  - address not found in LDAP
 *
 * if ret:  read all attributes and return them in d_attrs (domain config) and u_attrs (user config)
 */
int
want_generic(const char *suffix, const char *address, const char **attrs, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	int e;
	char buf[1024], domain[64];
	char *p;
	LDAP *ld;
	LDAPMessage *res, *entry;
	NTL_ATTR *a;

	assert(attrs != NULL);
	assert(*attrs != '\0');

	DEBUG(("want_generic() => called for %s address <%s> and attribute <%s>", suffix, address, attrs[0]));

	/*
	 * (0) Isolate Domain from Address
	 *
	 * NOTE: Users of want_generic() have to make sure mail address is somewhat sane.
	 */
	(void)snprintf(buf, sizeof(buf), "%s", address);
	if ((p = strchr(buf, '@')) == NULL) {
		xfatal("fatal: internal error: want_generic() => localpart/domain delimiter '@' not found in address <%s>", address);
		return -1;
	}
	(void)snprintf(domain, sizeof(domain), "%s", p + 1);

	/*
	 * (1) Connect to LDAP if no cached results
	 */
	ld = NULL;
	if (*d_attrs == NULL || *u_attrs == NULL) {
		if (nsmc_ldap_cfgs == NULL || nsmc_ldap_cfgs[0] == NULL || nsmc_ldap_cfgs[1] == NULL) {
			xfatal("fatal: internal error: want_generic() => no suitable LDAP configs loaded, unable to determine anything");
			return -1;
		}

		if ((ld = ntl_connect(nsmc_ldap_cfgs[0])) == NULL
		    && (ld = ntl_connect(nsmc_ldap_cfgs[1])) == NULL) {
			xwarn("warn: want_%s() => unable to connect to any LDAP server", suffix);
			return -1;
		}
	}

	/* default */
	e = 0;

	/*
	 * (2) Read Domain Config
	 */
	if (*d_attrs != NULL) {
		if (ntl_lookup_val(*d_attrs, attrs[0], "TRUE"))
			e = 1;
	} else {
		(void)snprintf(buf, sizeof(buf), "ou=%s,%s", domain, nsmc_mail_basedn);
		if ((res = ntl_search(ld, buf, LDAP_SCOPE_BASE, NULL, ret ? NULL : attrs)) != NULL
		    && (entry = ldap_first_entry(ld, res)) != NULL) {
			if (ret) {
				*d_attrs = ntl_entry(ld, entry);
				if (ntl_lookup_val(*d_attrs, attrs[0], "TRUE"))
					e = 1;
			} else {
				a = ntl_entry(ld, entry);
				if (ntl_lookup_val(a, attrs[0], "TRUE"))
					e = 1;
				ntl_entry_free(a);
			}
		}
		(void)ldap_msgfree(res);
	}

	/*
	 * (3) Read User Config
	 *
	 * NOTE:  User config overrides domain config, so we explicitly check for FALSE here.
	 */
	if (*u_attrs != NULL) {
		if (ntl_lookup_val(*u_attrs, attrs[0], "TRUE"))
			e = 1;
		else if (ntl_lookup_val(*u_attrs, attrs[0], "FALSE"))
			e = 0;
	} else {
		(void)snprintf(buf, sizeof(buf), "(|(mail=%s)(mailAlternateAddress=%s))", address, address);
		if ((res = ntl_search(ld, nsmc_mail_basedn, LDAP_SCOPE_SUBTREE, buf, ret ? NULL : attrs)) != NULL
		    && (entry = ldap_first_entry(ld, res)) != NULL) {
			if (ret) {
				*u_attrs = ntl_entry(ld, entry);
				if (ntl_lookup_val(*u_attrs, attrs[0], "TRUE"))
					e = 1;
				else if (ntl_lookup_val(*u_attrs, attrs[0], "FALSE"))
					e = 0;
			} else {
				a = ntl_entry(ld, entry);
				if (ntl_lookup_val(a, attrs[0], "TRUE"))
					e = 1;
				else if (ntl_lookup_val(a, attrs[0], "FALSE"))
					e = 0;
				ntl_entry_free(a);
			}
		} else {
			xinfo("info: want_%s() => mail address <%s> not found in LDAP", suffix, address);
			e = 2;
		}
		(void)ldap_msgfree(res);
	}

	if (ld != NULL)
		(void)ldap_unbind(ld);

	DEBUG(("want_generic() => result for %s address <%s> is (%d)", suffix, address, e));

	return e;
}


int
is_sane_ip(const char *ip)
{
	const char *ip4;

	if (ip == NULL || *ip == '\0')
		return 0;

	/*
	 * XXX/NOTE:  We don't support IPv6 addresses.
	 * We do however support IPv4 in IPv6 prefixes here.
	 */
	ip4 = ip;
	if (strncasecmp(ip, IPV6_V4PREFIX, strlen(IPV6_V4PREFIX)) == 0)
		ip4 = ip + strlen(IPV6_V4PREFIX);

	if (strlen(ip4) < 7 || strlen(ip4) > 15)
		return 0;

	while (*ip4 != '\0') {
		if (!isdigit((int)*ip4) && *ip4 != '.')
			return 0;
		ip4++;
	}

	return 1;
}

int
is_mx_us(const char *mx, int port)
{
	int flag, i;

	flag = 0;
	for (i = 0; !flag && nsmc_ports != NULL && nsmc_ports[i] != NULL; i++)
		if (port == atoi(nsmc_ports[i]))
			flag = 1;
	for (i = 0; !flag && nsmc_ports_ssl != NULL && nsmc_ports_ssl[i] != NULL; i++)
		if (port == atoi(nsmc_ports_ssl[i]))
			flag = 1;

	if (!flag)
		return 0;

	/* What we do here is to check if at least one of these is true:
	 *   (1) string(nsmc_name)            =? string(bestMX)
	 *   (2) string(nsmc_alias)           =? string(bestMX)
	 *   (3) string(nsmc_ip)==ip(nsmc_name) =? ip(bestMX)
	 */
	return strcasecmp(mx, nsmc_name) == 0 || strcasecmp(mx, nsmc_alias) == 0 || strcasecmp(resolve_to_a(mx), nsmc_ip) == 0;
}
