/*
 * $Id: 99c0b951ac84fda3fd8461371e3e791d0eec02ea $
 *
 * 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 <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <resolv.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>

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

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

static DNS_ENTRY *get_dns_data(const unsigned char **, const unsigned char *, const unsigned char *);
static int cmpmx(const void *, const void *);

in_addr_t
resolve(const char *h)
{
	struct hostent *hp;
	in_addr_t ip;

	if ((ip = inet_addr(h)) != INADDR_NONE)
		return ip;

	if ((hp = gethostbyname(h)) == NULL)
		return INADDR_NONE;
	else {
		(void)memcpy(&ip, hp->h_addr, (size_t)hp->h_length);
		return ip;
	}
}

char *
ip_to_a(in_addr_t ip)
{
	struct in_addr addr;
	char *p;

	addr.s_addr = ip;
	p = inet_ntoa(addr);

	return p;
}

char *
resolve_to_a(const char *h)
{
	char *p;

	p = ip_to_a(resolve(h));

	return p;
}

struct clinfo *
identify(const struct sockaddr *addr, socklen_t salen)
{
	struct clinfo *clinfo;

	clinfo = nsm_calloc(1, sizeof(struct clinfo));

	if (getnameinfo(addr, salen, clinfo->name, sizeof(clinfo->name), NULL, 0, NI_NAMEREQD))
		*clinfo->name = '\0';
	/* append nsmc_domain if getnameinfo() does not return FQDN as expected to, since wie did not supply NI_NOFQDN */
	if (*clinfo->name != '\0' && strchr(clinfo->name, '.') == NULL)
		(void)snprintf(clinfo->name, sizeof(clinfo->name), "%s.%s", clinfo->name, nsmc_domain);
	if (*clinfo->name == '.')
		*clinfo->name = '\0';
	DEBUG(("identify() => getnameinfo() for Reverse Name returned <%s>", clinfo->name));

	if (getnameinfo(addr, salen, clinfo->ip, sizeof(clinfo->ip), NULL, 0, NI_NUMERICHOST))
		*clinfo->ip = '\0';
	DEBUG(("identify() => getnameinfo() for IP Address returned [%s]", clinfo->ip));

	return clinfo;
}

static DNS_ENTRY *
get_dns_data(const unsigned char **pos, const unsigned char *dnsbuf, const unsigned char *dnsbuf_end)
{
	DNS_ENTRY *dnsentry;

	dnsentry = nsm_calloc(1, sizeof(DNS_ENTRY));
	NS_GET16(dnsentry->type, *pos);
	NS_GET16(dnsentry->class,*pos);
	NS_GET32(dnsentry->ttl, *pos);
	NS_GET16(dnsentry->length, *pos);

	DEBUG(("get_dns_data() => type=>%u class=>%u ttl=>%u length=>%u", dnsentry->type, dnsentry->class, dnsentry->ttl, dnsentry->length));

	switch (dnsentry->type) {
	case T_MX:
		NS_GET16(dnsentry->pref, *pos);
		(void)dn_expand(dnsbuf, dnsbuf_end, *pos, dnsentry->print, sizeof(dnsentry->print));
		*pos += dnsentry->length;
		DEBUG(("get_dns_data() => pref=>%u, value=>%s", dnsentry->pref, dnsentry->print));
		break;
	case T_A:
		if (dnsentry->length != 4) {
			xwarn("warn: get_dns_data() => bad address length (%d != %d)", dnsentry->length, 4);
			goto error_out;
		}

		(void)memcpy(dnsentry->data, *pos, (size_t)dnsentry->length);
		*pos += dnsentry->length + 2;
		dnsentry->print[0] = '\0';
		if (inet_ntop(AF_INET, dnsentry->data, dnsentry->print, sizeof(dnsentry->print)) == NULL) {
			xwarn("warn: get_dns_data() => unable to get presentation format of IP address [%s]", strerror(errno));
			goto error_out;
		}
		DEBUG(("get_dns_data() => IP address [%s]", dnsentry->print));
		break;
	case T_CNAME:
		(void)dn_expand(dnsbuf, dnsbuf_end, *pos, dnsentry->print, sizeof(dnsentry->print));
		*pos += dnsentry->length + 2;
		DEBUG(("get_dns_data() => CNAME value=>%s", dnsentry->print));
		break;
	case T_TXT:
		(void)memcpy(dnsentry->print, *pos+1, (size_t)**pos);
		dnsentry->print[**pos] = '\0';
		DEBUG(("get_dns_data() => TXT value=>%s", dnsentry->print));
		*pos += dnsentry->length + 2;
		break;
	case T_NS:
		(void)dn_expand(dnsbuf, dnsbuf_end, *pos, dnsentry->print, sizeof(dnsentry->print));
		*pos += dnsentry->length + 2;
		DEBUG(("get_dns_data() => NS value=>%s", dnsentry->print));
		break;
	default:
		DEBUG(("get_dns_data() => unexpected result type (%d)", dnsentry->type));
		goto error_out;
	}

	return dnsentry;

error_out:
	nsm_free(dnsentry);
	return NULL;
}

static int
cmpmx(const void *p1, const void *p2)
{
	DNS_ENTRY *a, *b;

	a = *(DNS_ENTRY * const *)p1;
	b = *(DNS_ENTRY * const *)p2;

	if (a->pref == b->pref)
		return 0;
	else if (a->pref > b->pref)
		return 1;
	return -1;
}

#define MAX_MX	32
DNS_ENTRY **
get_mx_for_domain(const char *domain)
{
	DNS_ENTRY **dnsentry;
	unsigned char dnsbuf[4096];
	char dnsbuf2[4096];
	const unsigned char *pos;
	const unsigned char *dnsbuf_end;
	int i, k, len, status;

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

	dnsentry = nsm_calloc(MAX_MX + 1, sizeof(DNS_ENTRY *));

	len = res_query(domain, C_IN, T_MX, dnsbuf, sizeof(dnsbuf));
	if (len < 0 && (h_errno == NO_DATA || h_errno == NO_RECOVERY)) {
		DEBUG(("get_mx_for_domain() => MX for <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0 && h_errno == HOST_NOT_FOUND) {
		DEBUG(("get_mx_for_domain() => host <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0) {
		DEBUG(("get_mx_for_domain() => soft error getting MX for <%s>", domain));
		nsm_free(dnsentry);
		return NULL;
	}

	pos = dnsbuf;
	dnsbuf_end = dnsbuf + len;

	/*
	 * skip over header and question section
	 */
	pos += NS_HFIXEDSZ;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status + 4;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status;

	k = 0;
	for (i = 0; i < MAX_MX-1 && pos < dnsbuf_end; i++) {
		if ((dnsentry[k] = get_dns_data(&pos, dnsbuf, dnsbuf_end)) == NULL)
			break;
		/* ignore spurious non-MX results */
		if (dnsentry[k]->type == T_MX)
			k++;
		else {
			nsm_free(dnsentry[k]);
			dnsentry[k] = NULL;
		}
	}
	dnsentry[k+1] = NULL;

	/*
	 * sort MX entries, lower priority first
	 */
	qsort(dnsentry, (size_t)k, sizeof(DNS_ENTRY *), cmpmx);

	return dnsentry;
}

#define MAX_A	32
DNS_ENTRY **
get_a_for_domain(const char *domain)
{
	DNS_ENTRY **dnsentry;
	unsigned char dnsbuf[4096];
	char dnsbuf2[4096];
	const unsigned char *pos;
	const unsigned char *dnsbuf_end;
	int i, len, status;

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

	dnsentry = nsm_calloc(MAX_A + 1, sizeof(DNS_ENTRY *));

	len = res_query(domain, C_IN, T_A, dnsbuf, sizeof(dnsbuf));
	if (len < 0 && (h_errno == NO_DATA || h_errno == NO_RECOVERY)) {
		DEBUG(("get_a_for_domain() => A for <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0 && h_errno == HOST_NOT_FOUND) {
		DEBUG(("get_a_for_domain() => host <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0) {
		DEBUG(("get_a_for_domain() => soft error getting A for <%s>", domain));
		nsm_free(dnsentry);
		return NULL;
	}

	pos = dnsbuf;
	dnsbuf_end = dnsbuf + len;

	/*
	 * skip over header and question section
	 */
	pos += NS_HFIXEDSZ;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status + 4;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status;

	for (i = 0; i < MAX_A-1 && pos < dnsbuf_end; i++) {
		dnsentry[i] = get_dns_data(&pos, dnsbuf, dnsbuf_end);
		if (dnsentry[i] == NULL)
			break;
	}
	dnsentry[i+1] = NULL;

	return dnsentry;
}

#define MAX_TXT	8
DNS_ENTRY **
get_txt_for_domain(const char *domain)
{
	DNS_ENTRY **dnsentry;
	unsigned char dnsbuf[4096];
	char dnsbuf2[4096];
	const unsigned char *pos;
	const unsigned char *dnsbuf_end;
	int i, k, len, status;

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

	dnsentry = nsm_calloc(MAX_TXT + 1, sizeof(DNS_ENTRY *));

	len = res_query(domain, C_IN, T_TXT, dnsbuf, sizeof(dnsbuf));
	if (len < 0 && (h_errno == NO_DATA || h_errno == NO_RECOVERY)) {
		DEBUG(("get_txt_for_domain() => TXT for <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0 && h_errno == HOST_NOT_FOUND) {
		DEBUG(("get_txt_for_domain() => host <%s> not found", domain));
		dnsentry[0] = NULL;
		return dnsentry;
	} else if (len < 0) {
		DEBUG(("get_txt_for_domain() => soft error getting TXT for <%s>", domain));
		nsm_free(dnsentry);
		return NULL;
	}

	pos = dnsbuf;
	dnsbuf_end = dnsbuf + len;

	/*
	 * skip over header and question section
	 */
	pos += NS_HFIXEDSZ;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status + 4;
	status = dn_expand(dnsbuf, dnsbuf + len, pos, dnsbuf2, sizeof(dnsbuf2));
	pos += status;

	k = 0;
	for (i = 0; i < MAX_TXT-1 && pos < dnsbuf_end; i++) {
		if ((dnsentry[k] = get_dns_data(&pos, dnsbuf, dnsbuf_end)) == NULL)
			break;
		/* ignore spurious non-TXT results */
		if (dnsentry[k]->type == T_TXT)
			k++;
		else {
			nsm_free(dnsentry[k]);
			dnsentry[k] = NULL;
		}
	}
	dnsentry[k+1] = NULL;

	return dnsentry;
}

const char *
get_next_mx(const char *rcpt)
{
	static DNS_ENTRY **dns_a = NULL, **dns_mx = NULL, **dns_use = NULL;
	static char lastrcpt[512] = "";
	char *domain;
	static int n = -2;

	if (rcpt == NULL) {
		if (dns_use != NULL) {
			dns_entry_free(dns_use);
			dns_use = NULL;
		}
		n = -2;
		return NULL;
	}

	domain = strchr(rcpt, '@') + 1;
	DEBUG(("get_next_mx() => rcpt <%s>, domain <%s>", rcpt, domain));

	if (n == -2 || strcasecmp(rcpt, lastrcpt) != 0) {
		if (n != -2 && dns_use != NULL) {
			dns_entry_free(dns_use);
			dns_use = NULL;
		}
		(void)snprintf(lastrcpt, sizeof(lastrcpt), "%s", rcpt);
		/*
		 * resolve
		 */
		DEBUG(("get_next_mx() => resolving mail exchanger for domain <%s>", domain));

		if ((dns_mx = get_mx_for_domain(domain)) == NULL) {
			DEBUG(("get_next_mx() => soft error on MX record, \"try again later\""));
			return NULL;
		} else if (*dns_mx == NULL) {
			DEBUG(("get_next_mx() => MX record not found, trying A"));
			nsm_free(dns_mx);
			if ((dns_a = get_a_for_domain(domain)) == NULL) {
				DEBUG(("get_next_mx() => soft error on A record, \"try again later\""));
				return NULL;
			} else if (*dns_a == NULL) {
				nsm_free(dns_a);
				DEBUG(("get_next_mx() => A record not found.  Out of luck."));
				return NULL;
			}
			dns_use = dns_a;
		} else
			dns_use = dns_mx;
		n = -1;
	}

	/* get next result */
	n++;
	if (dns_use != NULL && dns_use[n] != NULL)
		return dns_use[n]->print;
	return NULL;
}

void
dns_entry_free(DNS_ENTRY **e)
{
	int i;

	if (e == NULL)
		return;

	for (i = 0; e[i] != NULL; i++)
		nsm_free(e[i]);
	nsm_free(e);
}
