/* This file is part of Mailfromd.
   Copyright (C) 2005-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 <sys/types.h>
#include <stdlib.h>
#include <errno.h>
#include <ctype.h>
#include <adns.h>
#include <mailutils/alloc.h>
#include <mailutils/argcv.h>
#include <mailutils/io.h>
#include <mailutils/stream.h>
#include <mailutils/cstr.h>
#include <mailutils/wordsplit.h>
#include <mailutils/assoc.h>

#include <mflib/exceptions.h>
#include "libmf.h"
#include "dns.h"

#define DEFAULT_QFLAGS \
	(adns_qf_cname_loose | \
	 adns_qf_quoteok_query | \
	 adns_qf_quoteok_cname | \
	 adns_qf_quoteok_anshost)

static mu_debug_handle_t debug_handle;
static adns_state state;

static void
dns_log_cb(adns_state ads, void *logfndata, const char *fmt, va_list al)
{
/* FIXME: Could have used just:
     mu_diag_vprintf(MU_DIAG_DEBUG, fmt, al);
   but it will emit \e<N> directives in the middle of the string, which
   upsets the mailutils' logstream implementation.

   A possible workaround would be to use logfndata to select between
   mu_diag_vprintf,mu_diag_cont_vprintf or appropriate mu_debug_log_
   call.

   For the time being, a simplified approach is used: */
	mu_stream_vprintf(mu_strerr, fmt, al);
}

static void
dnsbase_finish(void)
{
	if (state) {
		adns_finish(state);
		state = NULL;
	}
}

void
dnsbase_real_init(char *configtext)
{
	int rc;
	int flags;
	mu_debug_level_t lev;
	static int cleanup_installed;

	/* Dispose of a previosly allocated state, if any */
	dnsbase_finish();

	flags = adns_if_nosigpipe;
	if (mu_debug_get_category_level(debug_handle, &lev) == 0
	    && (lev & MU_DEBUG_LEVEL_MASK(MU_DEBUG_TRACE9)))
		flags |= adns_if_debug;
	rc = adns_init_logfn(&state, flags, configtext, dns_log_cb, NULL);
	if (rc) {
		mu_diag_funcall(MU_DIAG_ERROR, "adns_init", NULL, rc);
		exit(1);
	}
	if (!cleanup_installed) {
		atexit(dnsbase_finish);
		cleanup_installed = 1;
	}
}

void
dnsbase_file_init(char const *filename)
{
	if (!filename)
		dnsbase_real_init(NULL);
	else {
		mu_stream_t str;
		mu_off_t sz;
		int rc;
		char *cfg;

		rc = mu_file_stream_create(&str, filename, MU_STREAM_READ);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_file_stream_create",
					filename, rc);
			return;
		}
		rc = mu_stream_size(str, &sz);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_size",
					filename, rc);
			mu_stream_destroy(&str);
			return;
		}

		if (sz > ((size_t)~0)) {
			mu_error(_("%s too big"), filename);
			mu_stream_destroy(&str);
			return;
		}

		cfg = mu_alloc(sz + 1);

		rc = mu_stream_read(str, cfg, sz, NULL);
		mu_stream_destroy(&str);
		if (rc) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_stream_read",
					filename, rc);
			return;
		}
		cfg[sz] = 0;
		dnsbase_real_init(cfg);
		free(cfg);
	}
}

static adns_state
get_state(void)
{
	if (!state)
		dnsbase_real_init(NULL);
	return state;
}

size_t
dns_reply_elsize(struct dns_reply *reply)
{
	switch (reply->type) {
	case dns_reply_ip:
		return sizeof(reply->data.ip[0]);
	case dns_reply_ip6:
		return sizeof(reply->data.ip6[0]);
	case dns_reply_str:
		return sizeof(reply->data.str[0]);
	}
	abort();
}

void
dns_reply_init(struct dns_reply *reply, dns_reply_type type, size_t count)
{
	reply->type = type;
	reply->count = count;
	reply->maxcount = count;
	if (count)
		reply->data.ptr = mu_calloc(count, dns_reply_elsize(reply));
	else
		reply->data.ptr = NULL;
}

void
dns_reply_ip_push(struct dns_reply *reply, void *item)
{
	if (reply->count == reply->maxcount)
		reply->data.ip = mu_2nrealloc(reply->data.ip,
					      &reply->maxcount,
					      sizeof(reply->data.ip[0]));
	reply->data.ip[reply->count++] = *(struct in_addr*)item;
}

void
dns_reply_ip6_push(struct dns_reply *reply, void *item)
{
	if (reply->count == reply->maxcount)
		reply->data.ip6 = mu_2nrealloc(reply->data.ip6,
					       &reply->maxcount,
					       sizeof(reply->data.ip6[0]));
	reply->data.ip6[reply->count++] = *(struct in6_addr *)item;
}

void
dns_reply_str_push(struct dns_reply *reply, void *item)
{
	if (reply->count == reply->maxcount)
		reply->data.str = mu_2nrealloc(reply->data.str,
					       &reply->maxcount,
					       sizeof(reply->data.str[0]));
	reply->data.str[reply->count++] = item;
}

void
dns_reply_push(struct dns_reply *reply, void *item)
{
	switch (reply->type) {
	case dns_reply_ip:
		dns_reply_ip_push(reply, item);
		break;
	case dns_reply_ip6:
		dns_reply_ip6_push(reply, item);
		break;
	case dns_reply_str:
		dns_reply_str_push(reply, item);
		break;
	default:
		abort();
	}
}

void
dns_reply_free(struct dns_reply *reply)
{
	int i;

	switch (reply->type) {
	case dns_reply_str:
		for (i = 0; i < reply->count; i++)
			free(reply->data.str[i]);
		free(reply->data.str);
		break;
	case dns_reply_ip:
		free(reply->data.ip);
		break;
	case dns_reply_ip6:
		free(reply->data.ip6);
		break;
	}
}

int
dns_str_is_ipv4(const char *addr)
{
	int dot_count;
	int digit_count;

	dot_count = 0;
	digit_count = 0;
	while (*addr != 0) {
		if (*addr == '.') {
			if (++dot_count > 4)
				return 0;
			digit_count = 0;
		} else if (!(isdigit(*addr) && ++digit_count <= 3)) {
			return 0;
		}
		addr++;
	}

	return dot_count == 3;
}

int
dns_str_is_ipv6(const char *addr)
{
	int col_count = 0; /* Number of colons */
	int dcol = 0;      /* Did we encounter a double-colon? */
	int dig_count = 0; /* Number of digits in the last group */

	for (; *addr; addr++) {
		if (!isascii(*addr))
			return 0;
		else if (isxdigit(*addr)) {
			if (++dig_count > 4)
				return 0;
		} else if (*addr == ':') {
			if (col_count && dig_count == 0 && ++dcol > 1)
				return 0;
			if (++col_count > 7)
				return 0;
			dig_count = 0;
		} else
			return 0;
	}
	return col_count == 7 || dcol;
}

static int
errno_to_dns_status(int e)
{
	switch (e) {
	case 0:
		return dns_success;
	case EAGAIN:
#ifdef EINPROGRESS
	case EINPROGRESS:
#endif
#ifdef ETIMEDOUT
	case ETIMEDOUT:
#endif
		return dns_temp_failure;
	default:
		return dns_failure;
	}
}

/* Table of correspondence between ADNS status codes and dns status.
   Values are increased by 1 to be able to tell whether the entry is
   initialized or not. */
int adns_to_dns_tab[] = {
#define STAT(s) ((s)+1)
	[adns_s_ok]                  = STAT(dns_success),

	[adns_s_nomemory]            = STAT(dns_failure),
	[adns_s_unknownrrtype]       = STAT(dns_failure),
	[adns_s_systemfail]          = STAT(dns_failure),

	/* remotely induced errors, detected locally */
	[adns_s_timeout]             = STAT(dns_temp_failure),
	[adns_s_allservfail]         = STAT(dns_temp_failure),
	[adns_s_norecurse]           = STAT(dns_temp_failure),
	[adns_s_invalidresponse]     = STAT(dns_failure),
	[adns_s_unknownformat]       = STAT(dns_failure),

	/* remotely induced errors), reported by remote server to us */
	[adns_s_rcodeservfail]       = STAT(dns_not_found),
	[adns_s_rcodeformaterror]    = STAT(dns_not_found),
	[adns_s_rcodenotimplemented] = STAT(dns_not_found),
	[adns_s_rcoderefused]        = STAT(dns_not_found),
	[adns_s_rcodeunknown]        = STAT(dns_not_found),

	/* remote configuration errors */
	[adns_s_inconsistent]        = STAT(dns_not_found),
	[adns_s_prohibitedcname]     = STAT(dns_not_found),
	[adns_s_answerdomaininvalid] = STAT(dns_not_found),
	[adns_s_answerdomaintoolong] = STAT(dns_not_found),
	[adns_s_invaliddata]         = STAT(dns_not_found),

	/* permanent problems with the query */
	[adns_s_querydomainwrong]    = STAT(dns_failure),
	[adns_s_querydomaininvalid]  = STAT(dns_failure),
	[adns_s_querydomaintoolong]  = STAT(dns_failure),

	/* permanent errors */
	[adns_s_nxdomain]            = STAT(dns_not_found),
	[adns_s_nodata]              = STAT(dns_not_found),
#undef STAT
};

/* Convert ADNS status code E to DNS status. */
static int
adns_to_dns_status(int e)
{
	int r;

	/* If it is negative, fail right away */
	if (e < 0)
		return dns_failure;
	/* If it is not in table, it still can be a valid, but unhandled
	   value */
	if (e >= MU_ARRAY_SIZE(adns_to_dns_tab))
		return e < adns_s_max_permfail ? dns_not_found : dns_failure;
	/* Now, look up in the table */
	if ((r = adns_to_dns_tab[e]) > 0)
		return r - 1;
	/* If not found in table, use adns_s_max_ constants to decide the
	   error class.
	*/
	if (e < adns_s_max_localfail)
		return dns_failure;
	if (e < adns_s_max_remotefail)
		return dns_not_found;
	if (e < adns_s_max_tempfail)
		return dns_temp_failure;
	if (e < adns_s_max_misconfig)
		return dns_not_found;
	if (e < adns_s_max_misquery)
		return dns_not_found;
	return dns_not_found;
}

/*
 * dns_query and friends - a query wrapper.
 *
 * Adns library expressly disallows CNAMEs pointing to another
 * CNAMEs, and a good thing it does (for the reference, see RFC 1034,
 * section 3.6.2).  However, reportedly such CNAME chains are being
 * used quite often, e.g. for pointing to TXT records.  Thus, a practical
 * need for supporting CNAME chains to some extent does exist.
 *
 * The dns_query function below is a wrapper over adns_synchronous that
 * follows CNAME with limited length, with CNAME loop detection.  The
 * maximum length of a chain is given by the dns_max_cname_chain variable.
 * Values 0 and 1 disable CNAME chain support.  If a the length limit is
 * hit or a CNAME loop is detected, adns_s_prohibitedcname status is
 * returned.
 */
size_t dns_max_cname_chain = 2;

/*
 * To track encountered CNAMES, a singly linked list is used.  This
 * means, among others, that dns_max_cname_chain better be sufficiently
 * small.
 */
struct cname_record {
	struct cname_record *next; /* Pointer to next record */
	char name[1];              /* Actual name follows the structure */
};

/* A list of recorded CNAMEs */
struct cname_record_list {
	size_t count;              /* Number of elements in the list. */
	struct cname_record *head, *tail;
};

#define CNAME_RECORD_LIST_INITIALIZER { 0, NULL, NULL }

/*
 * Free the list entries from RECLIST.  The structure RECLIST points to
 * should be allocated on stack, and therefore not freed.
 */
static void
cname_record_list_free(struct cname_record_list *reclist)
{
	struct cname_record *rec = reclist->head;
	while (rec) {
		struct cname_record *next = rec->next;
		free(rec);
		rec = next;
	}
}

/*
 * Install NAME into RECLIST.  Return pointer to the allocated copy
 * of the name.  Return NULL if NAME is already stored in the list.
 */
static char const *
cname_install(struct cname_record_list *reclist, char const *name)
{
	struct cname_record *rec;

	for (rec = reclist->head; rec; rec = rec->next) {
		if (mu_c_strcasecmp(rec->name, name) == 0)
			return NULL;
	}
	rec = mu_alloc(sizeof(*rec) + strlen(name));
	strcpy(rec->name, name);
	rec->next = NULL;
	if (!reclist->head)
		reclist->head = rec;
	else
		reclist->tail->next = rec;
	reclist->tail = rec;
	reclist->count++;
	return rec->name;
}

/*
 * dns_query - look up a label NAME of RR type TYPE in the DNS.  Follow
 * CNAME chains of up to dns_max_cname_chain elements.  In other respects
 * the behavior is the same as that of adns_synchronous.
 *
 * FIXME: in the presence of a CNAME chain, this function does two
 * extra lookups, compared with the hypothetical libresolv implementation.
 * This is due to the specifics of libadns.
 */
int
dns_query(const char *name, adns_rrtype type, adns_answer **ans_ret)
{
	adns_state state = get_state();
	adns_answer *ans = NULL, *cnans = NULL;
	int rc;

	/*
	 * First, look up the requested RR type.  If the actual record is
	 * a CNAME pointing to the requested RR, this will be handled by
	 * adns due to adns_qf_cname_loose flag in DEFAULT_QFLAGS.
	 *
	 * If it is a CNAME pointing to a CNAME, this will result in the
	 * first extra lookup (see FIXME above).
	 */
	rc = adns_synchronous(state, name, type, DEFAULT_QFLAGS, &ans);
	if (rc == 0 && ans->status == adns_s_prohibitedcname
	    && dns_max_cname_chain > 1) {
		struct cname_record_list cname_rec = CNAME_RECORD_LIST_INITIALIZER;

		/* Record the queried name, first. */
		cname_install(&cname_rec, name);

		/* Follow the CNAME chain. */
		while (cname_rec.count - 1 <= dns_max_cname_chain) {
			if ((rc = adns_synchronous(state, name, adns_r_cname,
						   DEFAULT_QFLAGS, &cnans)))
				break;
			if (cnans->status == adns_s_ok) {
				/*
				 * CNAME found. Record it and continue.
				 */
				name = cname_install(&cname_rec, cnans->rrs.str[0]);
				free(cnans);
				if (!name)
					/*
					 * Loop detected.  Returned ans
					 * retains the adns_s_prohibitedcname
					 * status.
					 */
					break;
			} else if (cnans->status == adns_s_nodata) {
				/*
				 * RR found, but has a different type.
				 * Look up the requested type using the last
				 * recorded name.  This accounts for second
				 * extra lookup.
				 */
				free(cnans);
				rc = adns_synchronous(state, name, type, DEFAULT_QFLAGS, &ans);
				break;
			} else {
				/*
				 * Another error.  Replace original answer with
				 * the last one.
				 */
				free(ans);
				ans = cnans;
				break;
			}
		}
		cname_record_list_free(&cname_rec);
	}

	if (rc == 0)
		*ans_ret = ans;
	else
		free(ans);
	return rc;
}

typedef dns_status (*dns_lookup_fn)(const char *, struct dns_reply *);

static dns_lookup_fn
lookup_function(int resolve_family)
{
	switch (resolve_family) {
	case resolve_ip4:
		return a_lookup;
		break;
	case resolve_ip6:
		return aaaa_lookup;
		break;
	default:
		break;
	}
	return NULL;
}

dns_status
soa_check(const char *name, int resolve_family, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = dns_query(name, adns_r_soa_raw, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		if (resolve_family != resolve_none) {
			dns_lookup_fn lookup = lookup_function(resolve_family);
			if (lookup == NULL)
				return dns_failure;
			status = lookup(ans->rrs.soa->mname, reply);
		} else {
			dns_reply_init(reply, dns_reply_str, 1);
			reply->data.str[0] = mu_strdup (ans->rrs.soa->mname);
		}
		free(ans);
	}
	return status;
}

static dns_status
dns_reply_resolve(struct dns_reply *reply, int family)
{
	size_t i;
	struct dns_reply res;
	dns_lookup_fn lookup;

	if (family == resolve_none)
		return dns_success;

	lookup = lookup_function(family);
	if (lookup == NULL)
		return dns_failure;

	dns_reply_init(&res, (family == resolve_ip4) ? dns_reply_ip : dns_reply_ip6, 0);
	for (i = 0; i < reply->count; i++) {
		struct dns_reply r;
		dns_status stat = lookup(reply->data.str[i], &r);
		if (stat == dns_success) {
			size_t n;
			for (n = 0; n < r.count; n++) {
				dns_reply_push(&res,
					       (family == resolve_ip4)
					       ? (void*) &r.data.ip[n]
					       : (void*) &r.data.ip6[n]);
			}
			dns_reply_free(&r);
		}
	}
	dns_reply_free(reply);
	*reply = res;
	if (res.count == 0)
		return dns_not_found;
	return dns_success;
}

/* Return MX records for the given HOST. */
dns_status
mx_lookup(const char *host, int resolve_family, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	int i;

	rc = dns_query(host, adns_r_mx, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status != dns_success)
		return status;

	dns_reply_init(reply, dns_reply_str, ans->nrrs);
	for (i = 0; i < ans->nrrs; i++)
		reply->data.str[i] = mu_strdup(ans->rrs.inthostaddr[i].ha.host);
	free(ans);

	status = dns_reply_resolve(reply, resolve_family);

	return status;
}

static int
cidr_reverse_ip4(char *buf, size_t bufsize, struct mu_cidr const *cidr)
{
	int i, n;
	char *start = buf;

	for (i = cidr->len - 1; i >= 0; i--) {
		unsigned c = cidr->address[i];
		n = snprintf(buf, bufsize, "%u", c);
		if (n < 0 || n >= bufsize)
			return -1;
		buf += n;
		bufsize -= n;
		*buf++ = '.';
		bufsize--;
	}
	return buf - start;
}

static int
cidr_reverse_ip6(char *buf, size_t bufsize, struct mu_cidr const *cidr)
{
	int i;
	char *start = buf;
	static char xdig[] = "0123456789abcdef";

	if (bufsize < IPV6_DOTTED_BUFSIZE)
		return -1;
	for (i = cidr->len - 1; i >= 0; i--) {
		unsigned c = cidr->address[i];
		*buf++ = xdig[c & 0xf];
		*buf++ = '.';
		*buf++ = xdig[(c >> 4) & 0xf];
		*buf++ = '.';
	}
	return buf - start;
}

static int
cidr_reverse(struct mu_cidr const *cidr, char *buf, size_t bufsize)
{
	size_t n;
	int (*rev)(char *, size_t, struct mu_cidr const *);

	switch (cidr->family) {
	case AF_INET:
		n = IPV4_DOTTED_BUFSIZE;
		rev = cidr_reverse_ip4;
		break;

	case AF_INET6:
		n = IPV6_DOTTED_BUFSIZE;
		rev = cidr_reverse_ip6;
		break;

	default:
		return -1;
	}

	if (n > bufsize)
		return -1;

	return rev(buf, bufsize, cidr);
}

int
cidr_to_arpa(struct mu_cidr const *cidr, char *buf, size_t bufsize)
{
	size_t n;
	char const *domain;

	switch (cidr->family) {
	case AF_INET:
		domain = IPV4_INADDR_DOMAIN;
		break;

	case AF_INET6:
		domain = IPV6_INADDR_DOMAIN;
		break;

	default:
		return -1;
	}

	n = cidr_reverse(cidr, buf, bufsize);
	buf += n;
	bufsize -= n;

	if (strlen(domain) >= bufsize)
		return -1;
	strcpy(buf, domain);
	return 0;
}

int
dns_reverse_ipstr(const char *ipstr, char *buf, size_t bufsize)
{
	struct mu_cidr cidr;
	if (mu_cidr_from_string(&cidr, ipstr))
		return -1;
	return cidr_reverse(&cidr, buf, bufsize);
}

dns_status
dns_reverse_name(const char *ipstr, char *buf, size_t bufsize)
{
	struct mu_cidr cidr;
	if (mu_cidr_from_string(&cidr, ipstr))
		return dns_failure;
	if (cidr_to_arpa(&cidr, buf, bufsize))
		return dns_failure;
	return dns_success;
}

dns_status
dns_resolve_ipstr(const char *ipstr, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	struct mu_cidr cidr;
	char buf[IPMAX_INADDR_BUFSIZE];
	size_t buflen = sizeof(buf);

	if (mu_cidr_from_string(&cidr, ipstr))
		return dns_failure;

	if (cidr_to_arpa(&cidr, buf, buflen))
		return dns_failure;

	rc = dns_query(buf, adns_r_ptr_raw, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);

	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_str, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++) {
			reply->data.str[i] = mu_strdup(ans->rrs.str[i]);
		}
	}
	free(ans);
	return status;
}

dns_status
dns_resolve_hostname(const char *host, int family, char **ipbuf)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	adns_rrtype type;

	switch (family) {
	case resolve_ip4:
		type = adns_r_a;
		break;

	case resolve_ip6:
		type = adns_r_aaaa;
		break;

	default:
		return dns_failure;
	}

	rc = dns_query(host, type, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		char ipstr[IPMAX_DOTTED_BUFSIZE];
		switch (type) {
		case adns_r_a:
			inet_ntop(AF_INET, &ans->rrs.inaddr[0], ipstr,
				  sizeof(ipstr));
			break;
		case adns_r_aaaa:
			inet_ntop(AF_INET6, &ans->rrs.in6addr[0], ipstr,
				  sizeof(ipstr));
			break;
		default:
			status = dns_failure;
		}
		if (status == dns_success)
			*ipbuf = mu_strdup(ipstr);
	}
	free(ans);
	return status;
}


dns_status
a_lookup(const char *host, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = dns_query(host, adns_r_a, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_ip, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++)
			reply->data.ip[i] = ans->rrs.inaddr[i];
	}
	free(ans);
	return status;
}

dns_status
aaaa_lookup(const char *host, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = dns_query(host, adns_r_aaaa, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_ip6, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++)
			reply->data.ip6[i] = ans->rrs.in6addr[i];
	}
	return status;
}

dns_status
ptr_lookup(const char *host, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = dns_query(host, adns_r_ptr_raw, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_str, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++)
			reply->data.str[i] = mu_strdup(ans->rrs.str[i]);
	}
	free(ans);
	return status;
}

dns_status
txt_lookup(const char *name, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;

	rc = dns_query(name, adns_r_txt, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status == dns_success) {
		int i;
		dns_reply_init(reply, dns_reply_str, ans->nrrs);
		for (i = 0; i < ans->nrrs; i++) {
			size_t l = 0;
			int j;
			for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++)
				l += ans->rrs.manyistr[i][j].i;
			reply->data.str[i] = mu_alloc(l + 1);
			reply->data.str[i][0] = 0;
			l = 0;
			for (j = 0; ans->rrs.manyistr[i][j].i > 0; j++) {
				memcpy(reply->data.str[i] + l,
				       ans->rrs.manyistr[i][j].str,
				       ans->rrs.manyistr[i][j].i);
				l += ans->rrs.manyistr[i][j].i;
			}
			reply->data.str[i][l] = 0;
		}
	}
	free(ans);
	return status;
}

#define VSPF1_STR "v=spf1"
#define VSPF1_LEN (sizeof(VSPF1_STR)-1)

dns_status
spf_lookup(const char *domain, char **rec)
{
	dns_status status;
	struct dns_reply reply;

	status = txt_lookup(domain, &reply);
	if (status == dns_success) {
		int i;
		int found = -1;

		status = dns_not_found;

		for (i = 0; i < reply.count; i++) {
			if (mu_c_strncasecmp(reply.data.str[i],
					     VSPF1_STR, VSPF1_LEN) == 0
			    && (reply.data.str[i][VSPF1_LEN] == 0
				|| mu_isspace(reply.data.str[i][VSPF1_LEN]))) {
				if (found == -1) {
					found = i;
					status = dns_success;
				} else {
					found = -1;
					status = dns_too_many;
					break;
				}
			}
		}

		if (status == dns_success)
			*rec = mu_strdup(reply.data.str[found]);

		dns_reply_free(&reply);
	}
	return status;
}

dns_status
dkim_lookup(const char *domain, const char *sel, char ***retval)
{
	dns_status status;
	struct dns_reply reply;
	char *dk;

	if (mu_asprintf(&dk, "%s._domainkey.%s", sel, domain))
		mu_alloc_die();
	status = txt_lookup(dk, &reply);
	free(dk);
	if (status == dns_success) {
		int i;
		char **rv = mu_calloc(reply.count + 1, sizeof(*rv));

		for (i = 0; i < reply.count; i++) {
			rv[i] = mu_strdup(reply.data.str[i]);
		}
		*retval = rv;
		dns_reply_free(&reply);
	}
	return status;
}

static int
ipaddr_eq(struct dns_reply *reply, int i, void *addr)
{
	void *ip;

	if (reply->type == dns_reply_ip) {
		ip = &reply->data.ip[i];
	} else {
		ip = &reply->data.ip6[i];
	}
	return memcmp(ip, addr, dns_reply_elsize(reply));
}
/* rfc4408, chapter 5.5 */
dns_status
ptr_validate(const char *ipstr, struct dns_reply *reply)
{
	size_t i;
	union {
		struct in_addr ip4;
		struct in6_addr ip6;
	} ip;

	dns_status status;
	struct dns_reply ptr_reply;
	dns_status result = dns_not_found;
	dns_status (*lookup)(const char *, struct dns_reply *);
	int family;

	status = dns_resolve_ipstr(ipstr, &ptr_reply);
	if (status != dns_success)
		return status;

	/*
	 * Since dns_resolve_ipstr succeeded, it is either IPv4 or
	 * IPv6.
	 */
	if (dns_str_is_ipv4(ipstr)) {
		lookup = a_lookup;
		family = AF_INET;
	} else {
		lookup = aaaa_lookup;
		family = AF_INET6;
	}

	if (inet_pton(family, ipstr, &ip) != 1)
		return dns_failure;

	if (reply)
		dns_reply_init(reply, dns_reply_str, 0);
	for (i = 0; i < ptr_reply.count; i++) {
		struct dns_reply r;
		status = lookup(ptr_reply.data.str[i], &r);
		if (status == dns_success) {
			size_t k;

			for (k = 0; k < r.count; k++) {
				if (ipaddr_eq(&r, k, &ip) == 0) {
					result = dns_success;
					if (reply)
						dns_reply_push(reply,
							       mu_strdup(ptr_reply.data.str[i]));
					break;
				}
			}
			dns_reply_free(&r);
		}
	}
	dns_reply_free(&ptr_reply);

	return result;
}

mf_status
dns_to_mf_status(dns_status stat)
{
	static mf_status trans_tab[] = {
		[dns_success] = mfe_success,
		[dns_not_found] = mfe_not_found,
		[dns_failure] = mfe_failure,
		[dns_temp_failure] = mfe_temp_failure,
		[dns_too_many] = mfe_too_many
	};

	if (stat >= NELEMS(trans_tab))
		return mfe_failure;
	return trans_tab[stat];
}

dns_status
mf_to_dns_status(mf_status stat)
{
	return (dns_status) stat;
}

mf_status
resolve_ipstr(const char *ipstr, char **phbuf)
{
	dns_status dstat;
	struct dns_reply reply;

	mu_debug(debug_handle, MU_DEBUG_TRACE8,
		 ("Getting canonical name for %s", ipstr));

	dstat = dns_resolve_ipstr(ipstr, &reply);

	switch (dstat) {
	case dns_success:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s resolved to %s", ipstr, reply.data.str[0]));
		*phbuf = mu_strdup(reply.data.str[0]);
		dns_reply_free(&reply);
		break;

	default:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s not resolved", ipstr));
	}
	return dns_to_mf_status(dstat);
}

mf_status
resolve_hostname(const char *host, int resolve_family, char **pipbuf)
{
	char *ipbuf;
	dns_status dstat;

	mu_debug(debug_handle, MU_DEBUG_TRACE8,
		 ("Getting IP address for %s", host));

	dstat = dns_resolve_hostname(host, resolve_family, &ipbuf);
	switch (dstat) {
	case dns_success:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s resolved to %s", host, ipbuf));
		*pipbuf = ipbuf;
		break;

	default:
		mu_debug(debug_handle, MU_DEBUG_TRACE8,
			 ("%s not resolved", host));
	}
	return dns_to_mf_status(dstat);
}

/* Return NS records for the given DOMAIN. */
dns_status
ns_lookup(const char *domain, int resolve_family, struct dns_reply *reply)
{
	dns_status status = dns_failure;
	int rc;
	adns_answer *ans;
	int i;

	rc = dns_query(domain, adns_r_ns_raw, &ans);
	if (rc)
		return errno_to_dns_status(rc);
	status = adns_to_dns_status(ans->status);
	if (status != dns_success)
		return status;

	dns_reply_init(reply, dns_reply_str, ans->nrrs);
	for (i = 0; i < ans->nrrs; i++)
		reply->data.str[i] = mu_strdup(ans->rrs.str[i]);
	free(ans);

	status = dns_reply_resolve(reply, resolve_family);

	return status;
}

int
cb_resolv_conf(void *data, mu_config_value_t *arg)
{
	if (mu_cfg_assert_value_type(arg, MU_CFG_STRING))
		return 1;
	dnsbase_file_init(arg->v.string);
	return 0;
}

struct mu_cfg_param resolver_section_param[] = {
	{ "config", mu_cfg_callback,
	  NULL, 0, cb_resolv_conf,
	  N_("Read DNS configuration from this file (default: /etc/resolv.conf)"),
	  N_("name: string") },
	{ "max-cname-chain", mu_c_size,
	  &dns_max_cname_chain, 0, NULL,
	  N_("Maximum allowed length of CNAME chains") },
	{ NULL }
};

void
dnsbase_init(void)
{
	struct mu_cfg_section *section;
	if (!debug_handle)
		debug_handle = mu_debug_register_category("dns");
	if (mu_create_canned_section("resolver", &section) == 0) {
		section->label = NULL;
		section->parser = NULL;
		section->docstring = N_("DNS resolver configuration.");
		mu_cfg_section_add_params(section, resolver_section_param);
	}
}
