/*
 * $Id: 016de46c39c990f92c99dc2279510c393e05fba6 $
 *
 * 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 <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <errno.h>

#include <sys/socket.h>
#include <arpa/inet.h>

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

#define E_OK 0
#define E_SPAM 1
#define E_RES -1
#define E_CON -2
#define E_LARGE -3
#define E_SPAMD -4

#define MAX_SCANSIZE	1*1024*1024	/* 1 MB */
#define SPAMD_PORT	783
#define PROCESS_CMD	"PROCESS SPAMC/1.2\r\n"
#define SPAM_HEADER	"X-Spam-Flag: YES"
#define MAX_SMALLER	512

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

/*
 * return codes:
 * [see E_* above]
 * < 0 - unable to check atm
 * 0   - okay
 * 1   - spam
 */
int
spamscan(const char *user, const char *databuf, size_t datalen, char **newdatabuf, size_t *newdatalen)
{
	struct sockaddr_in addr;
	unsigned long ip;
	int foo;
	int s;
	char strbuf[2048];
	unsigned char readbuf[8096];
	ssize_t r, len, rlen;
	int i;

	if (nsmc_spamscan_hosts == NULL || nsmc_spamscan_hosts[0] == NULL) {
		xinfo("info: spamscan() => no spamscan hosts defined");
		return -1;
	}

	DEBUG(("spamscan() => scanning (%lu) chars of data", (unsigned long)datalen));

	if (datalen > MAX_SCANSIZE) {
		xwarn("warn: spamscan() => data to be scanned is too large (%u > %u bytes), not scanning", datalen, MAX_SCANSIZE);
		return E_LARGE;
	}

	if ((ip = resolve(nsmc_spamscan_hosts[0])) == INADDR_NONE) {
		xwarn("warn: spamscan() => unable to resolve first spamd host <%s>", nsmc_spamscan_hosts[0]);
		return E_RES;
	}

	/*
	 * (1) Open connection to spamd server
	 */
	(void)memset(&addr, 0, sizeof(addr));
	addr.sin_addr.s_addr = ip;
	addr.sin_port = htons(SPAMD_PORT);
	addr.sin_family = AF_INET;
	foo = 1;

	if (
		((s = socket(AF_INET, SOCK_STREAM, 0)) == -1)
		|| (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &foo, sizeof(foo)) != 0)
		|| (connect(s, (struct sockaddr *) & addr, sizeof(addr)) == -1)
	) {
		xwarn("warn: spamscan() => unable to connect to spamd server <%s> port (%d)", nsmc_spamscan_hosts[0], SPAMD_PORT);
		return E_CON;
	}

	/*
	 * (2) Write length and user followed by \r\n\r\n followed by data to spamd server,
	 *     that is, HTTP-style
	 */
	#define WOD(buffer, bufferlen)	if (write(s, buffer, bufferlen) == -1) { \
						xwarn("warn: spamscan() => write error (len = %u) while writing to spamd host <%s> port (%d): \"%s\"", \
						    bufferlen, nsmc_spamscan_hosts[0], SPAMD_PORT, strerror(errno)); \
						(void)close(s); \
						return E_SPAMD; \
					}
	len = datalen;
	(void)snprintf(strbuf, sizeof(strbuf), "Content-length: %ld\r\nUser: %s\r\n\r\n", (long)len, user);
	WOD(PROCESS_CMD, strlen(PROCESS_CMD));
	WOD(strbuf, strlen(strbuf));
	WOD(databuf, (size_t)len);

	(void)shutdown(s, SHUT_WR);

	/*
	 * (3) Read result from server
	 */

	#define GETLINE()	for (i = 0; i < (int)sizeof(strbuf) - 1; i++) \
					if (read(s, &strbuf[i], 1) != 1 || strbuf[i] == '\n') \
						break; \
				strbuf[i] = '\0'; \
				if (i > 0) \
					strbuf[--i] = '\0';

	/*
	 * (3)(a) Status
	 */
	GETLINE();
	if (i < 8 || strncmp(strbuf, "SPAMD/1.", strlen("SPAMD/1.")) != 0
	    || strbuf[strlen(strbuf)-1] != 'K'
	    || strbuf[strlen(strbuf)-2] != 'O'
	    || strbuf[strlen(strbuf)-3] != '_'
	) {
		xwarn("warn: spamscan() => spamd host <%s> port (%d) did not return \"OK\" status", nsmc_spamscan_hosts[0], SPAMD_PORT);
		(void)close(s);
		return E_SPAMD;
	}

	DEBUG(("spamscan() => server said <%s> about our data", strbuf));

	/*
	 * (3)(b) New content length after tagging
	 */
	GETLINE();
	if (i < 16 || strncmp(strbuf, "Content-length: ", strlen("Content-length: ")) != 0) {
		xwarn("warn: spamscan() => spamd host <%s> port (%d) did not supply new content length", nsmc_spamscan_hosts[0], SPAMD_PORT);
		(void)close(s);
		return E_SPAMD;
	}
	rlen = atol(strchr(strbuf, ':') + 1);
	/*
	 * NOTE:  It might very well be that we receive a
	 *        size smaller than our input.
	 *        SpamAssassin might have removed foreign
	 *        X-Spam headers to insert its own.
	 *
	 * We use the heuristic that the SpamAssassin
	 * idea of the new size may be just MAX_SMALLER
	 * smaller.
	 */
	if (rlen + MAX_SMALLER < (ssize_t)datalen) {
		xwarn("warn: spamscan() => spamd host <%s> port (%d) specified (rlen %d + %d < datalen %u), which we don't believe", nsmc_spamscan_hosts[0], SPAMD_PORT, rlen, MAX_SMALLER, datalen);
		(void)close(s);
		return E_SPAMD;
	}

	/*
	 * (3)(c) New data
	 */
	GETLINE();
	if (i != 0) {
		xwarn("warn: spamscan() => unable to read reply from spamd host <%s> port (%d)", nsmc_spamscan_hosts[0], SPAMD_PORT);
		(void)close(s);
		return E_SPAMD;
	}

	*newdatabuf = nsm_calloc((size_t)rlen, sizeof(char));
	*newdatalen = rlen;
	len = 0;
	while ((r = read(s, readbuf, sizeof(readbuf))) > 0 && len < rlen) {
		if (r + len > rlen) {
			len = r + len;
			break;
		}
		(void)memcpy(*newdatabuf + len, readbuf, (size_t)r);
		len += r;
	}
	if (len != rlen) {
		xwarn("warn: spamscan() => spamd host <%s> port (%d) returned (len %d != rlen %u), which indicates sth. went wrong", nsmc_spamscan_hosts[0], SPAMD_PORT, len, rlen);
		nsm_free(*newdatabuf);
		(void)close(s);
		return E_SPAMD;
	}

	(void)close(s);

	if (*newdatalen > strlen(SPAM_HEADER) && strncasecmp(*newdatabuf, SPAM_HEADER, strlen(SPAM_HEADER)) == 0)
		return E_SPAM;

	return E_OK;
}

/*
 * return codes:
 * -1 - unable to determine atm
 * 0  - no
 * 1  - yep, *sigh*
 */
int
want_onlinespamscan(const char *address, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	const char *attrs[] = { "onlineSpamscan", NULL };
	int e;

	if ((e = want_generic("onlinespamscan", address, attrs, d_attrs, u_attrs, ret)) == 2)
		return 0;
	return e;
}

int
want_spammailbox(const char *address, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	const char *attrs[] = { "spamMailbox", NULL };
	int e;

	if ((e = want_generic("spammailbox", address, attrs, d_attrs, u_attrs, ret)) == 2)
		return 0;
	return e;
}

char *
get_spamassassin_user(const char *address, NTL_ATTR **d_attrs, NTL_ATTR **u_attrs, int ret)
{
	char buf[128];
	char *u, *p;

	if (*d_attrs == NULL || *u_attrs == NULL)
		want_generic("spamassasin_user", address, NULL, d_attrs, u_attrs, 1);

	if (*ntl_lookup(*u_attrs, "spamassassin") == NULL) {
		(void)snprintf(buf, sizeof(buf), "%s", address);
		if ((p = strchr(buf, '@')) == NULL) {
			xfatal("fatal: internal error: get_spamassassin_user() => address <%s> does not contain domain part, delimited by '@'", address);
			u = NULL;
		} else {
			*p = '\0';
			u = nsm_strdup(p + 1);
		}
	} else
		u = nsm_strdup(address);

	if (!ret) {
		ntl_entry_free(*d_attrs);
		ntl_entry_free(*u_attrs);
		*d_attrs = *u_attrs = NULL;
	}

	return u;
}
