/* $Id: sortsieve.c 1912 2005-11-19 02:29:41Z aaron $

 Copyright (C) 1999-2004 Aaron Stone aaron at serendipity dot cx

 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 2 of the License, 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, write to the Free Software
 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

 *
 * Functions for running user defined sorting rules
 * on a message in the temporary store, usually
 * just delivering the message to the user's INBOX
 * ...unless they have fancy rules defined, that is :-)
 * 
 */

#include "dbmail.h"

#define THIS_MODULE "sieve"

/* Used by us to keep track of libSieve. */
struct sort_context {
	char *s_buf;
	char *script;
	u64_t user_idnr;
	struct DbmailMessage *message;
	struct sort_result *result;
	struct dm_list freelist;
};

/* Returned opaquely as type sort_result_t. */
struct sort_result {
	int cancelkeep;
	const char *mailbox;
	int reject;
	GString *rejectmsg;
	int error_runtime;
	int error_parse;
	GString *errormsg;
};

struct sort_sieve_config {
	int vacation;
	int notify;
} sort_sieve_config;


/* SIEVE CALLBACKS */

/*
From http://www.ietf.org/internet-drafts/draft-ietf-sieve-vacation-06.txt

   Usage:   vacation [":days" number] [":subject" string]
                     [":from" string] [":addresses" string-list]
                     [":mime"] [":handle" string] <reason: string>

   The parameters that an implementation needs to know about are:
   days, subject, from, mime, handle, reason. Addresses is used
   internally by libSieve implementation to comply with RFCs.

We need to make sure to respect the implementation requirements.
*/
int sort_vacation(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	int days = 1, mime = 0;
	const char *message, *subject, *fromaddr, *handle;
	const char *rc_to, *rc_from, *rc_handle;
	char *md5_handle = NULL;

	days = sieve2_getvalue_int(s, "days"); // days: min 1, max 30, default 7.
	mime = sieve2_getvalue_int(s, "mime"); // mime: 1 if message is mime coded. FIXME.
	message = sieve2_getvalue_string(s, "message");
	subject = sieve2_getvalue_string(s, "subject");
	fromaddr = sieve2_getvalue_string(s, "fromaddr"); // From: specified by the script.
	handle = sieve2_getvalue_string(s, "handle");

	if (days < 1) days = 1;
	if (days > 30) days = 30;

	if (handle) {
		rc_handle = handle;
	} else {
		char *tmp;
		tmp = g_strconcat(subject, message, NULL);
		rc_handle = md5_handle = dm_md5((const unsigned char * const) tmp);
		g_free(tmp);
	}

	// FIXME: should be validated as a user might try
	// to forge an address from their script.
	rc_from = fromaddr;
	if (!rc_from)
		rc_from = dbmail_message_get_header(m->message, "Delivered-To");
	if (!rc_from)
		rc_from = m->message->envelope_recipient->str;

	rc_to = dbmail_message_get_header(m->message, "Reply-To");
	if (!rc_to)
		rc_to = dbmail_message_get_header(m->message, "Return-Path");

	if (db_replycache_validate(rc_to, rc_from, rc_handle, days) == DM_SUCCESS) {
		if (send_vacation(m->message, rc_to, rc_from, subject, message, rc_handle) == 0)
			db_replycache_register(rc_to, rc_from, rc_handle);
		TRACE(TRACE_INFO, "Sending vacation to [%s] from [%s] handle [%s] repeat days [%d]",
			rc_to, rc_from, rc_handle, days);
	} else {
		TRACE(TRACE_INFO, "Vacation suppressed to [%s] from [%s] handle [%s] repeat days [%d]",
			rc_to, rc_from, rc_handle, days);
	}

	if (md5_handle)
		dm_free(md5_handle);

	m->result->cancelkeep = 0;
	return SIEVE2_OK;
}

int sort_redirect(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char *to;
	const char *from;

	to = sieve2_getvalue_string(s, "address");

	TRACE(TRACE_INFO, "Action is REDIRECT: REDIRECT destination is [%s].",
		to);

	/* According to a clarification from the ietf-mta-filter mailing list,
	 * the redirect is supposed to be absolutely transparent: the envelope
	 * sender is the original envelope sender, with only the envelope
	 * recipient changed. As a fallback, we'll use the redirecting user. */
	from = dbmail_message_get_header(m->message, "Return-Path");
	if (!from)
		from = m->message->envelope_recipient->str;

	if (send_redirect(m->message, to, from) != 0) {
		return SIEVE2_ERROR_FAIL;
	}

	m->result->cancelkeep = 1;
	return SIEVE2_OK;
}

int sort_reject(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char *message;

	message = sieve2_getvalue_string(s, "message");

	TRACE(TRACE_INFO, "Action is REJECT: REJECT message is [%s].", message);

	m->result->rejectmsg = g_string_new(message);

	/* Reject also discards. */
	m->result->cancelkeep = 1;
	m->result->reject = 1;
	return SIEVE2_OK;
}

int sort_discard(sieve2_context_t *s UNUSED, void *my)
{
	struct sort_context *m = (struct sort_context *)my;

	TRACE(TRACE_INFO, "Action is DISCARD.");

	m->result->cancelkeep = 1;
	return SIEVE2_OK;
}

int sort_fileinto(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	extern const char * imap_flag_desc[];
	const char * const * flags;
	const char * mailbox;
	int *msgflags = NULL;

	mailbox = sieve2_getvalue_string(s, "mailbox");
	flags = sieve2_getvalue_stringlist(s, "imapflags"); // TODO

	/* This condition exists for the KEEP callback. */
	if (! mailbox) {
		mailbox = "INBOX";
	}

	/* If there were any imapflags, set them. */
	if (flags) {
		int i, j;
		msgflags = g_new0(int, IMAP_NFLAGS);

		for (i = 0; flags[i]; i++) { // Loop through all script/user-specified flags.
			for (j = 0; imap_flag_desc[j]; i++) { // Find the ones we support.
				if (g_strcasestr(imap_flag_desc[j], flags[i])) {
					msgflags[i] = 1;
				}
			}
		}
	}

	TRACE(TRACE_INFO, "Action is FILEINTO: mailbox is [%s]", mailbox);

	/* Don't cancel the keep if there's a problem storing the message. */
	if (sort_deliver_to_mailbox(m->message, m->user_idnr,
			mailbox, BOX_SORTING, msgflags) != DSN_CLASS_OK) {
		TRACE(TRACE_ERROR, "Could not file message into mailbox; not cancelling keep.");
		m->result->cancelkeep = 0;
	} else {
		m->result->cancelkeep = 1;
	}

	if (msgflags)
		g_free(msgflags);

	return SIEVE2_OK;
}

int sort_errparse(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char *message;
	int lineno;

	lineno = sieve2_getvalue_int(s, "lineno");
	message = sieve2_getvalue_string(s, "message");

	TRACE(TRACE_INFO, "Error is PARSE: Line is [%d], Message is [%s]", lineno, message);

	g_string_append_printf(m->result->errormsg, "Parse error on line [%d]: %s", lineno, message);

//	TODO: generate a message and put it into the INBOX
//	of the script's owner, probably with an Urgent flag.

	m->result->error_parse = 1;
	return SIEVE2_OK;
}

int sort_errexec(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char *message;

	message = sieve2_getvalue_string(s, "message");

	TRACE(TRACE_INFO, "Error is EXEC: Message is [%s]", message);

	g_string_append_printf(m->result->errormsg, "Execution error: %s", message);

//	TODO: generate a message and put it into the INBOX
//	of the script's owner, probably with an Urgent flag.

	m->result->error_runtime = 1;
	return SIEVE2_OK;
}

int sort_getscript(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char * path, * name;
	int res;

	/* Path could be :general, :personal, or empty. */
	path = sieve2_getvalue_string(s, "path");

	/* If no file is named, we're looking for the main file. */
	name = sieve2_getvalue_string(s, "name");

	if (path == NULL || name == NULL)
		return SIEVE2_ERROR_BADARGS;

	if (strlen(path) && strlen(name)) {
		/* TODO: handle included files. */
		TRACE(TRACE_INFO, "Include requested from [%s] named [%s]", path, name);
	} else
	if (!strlen(path) && !strlen(name)) {
		/* Read the script file given as an argument. */
		TRACE(TRACE_INFO, "Getting default script named [%s]", m->script);
		res = db_get_sievescript_byname(m->user_idnr, m->script, &m->s_buf);
		if (res != SIEVE2_OK) {
			TRACE(TRACE_ERROR, "sort_getscript: read_file() returns %d\n", res);
			return SIEVE2_ERROR_FAIL;
		}
		sieve2_setvalue_string(s, "script", m->s_buf);
	} else {
		return SIEVE2_ERROR_BADARGS;
	}

	return SIEVE2_OK;
}

int sort_getheader(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	char *header;
	char **bodylist;
	GTuples *headers;
	unsigned i;

	header = (char *)sieve2_getvalue_string(s, "header");
	
	headers = dbmail_message_get_header_repeated(m->message, header);
	
	bodylist = g_new0(char *,headers->len+1);
	for (i=0; i<headers->len; i++)
		bodylist[i] = (char *)g_tuples_index(headers,i,1);
	g_tuples_destroy(headers);

	/* We have to free the header array, but not its contents. */
	dm_list_nodeadd(&m->freelist, &bodylist, sizeof(char **));

	for (i = 0; bodylist[i] != NULL; i++) {
		TRACE(TRACE_INFO, "Getting header [%s] returning value [%s]",
			header, bodylist[i]);
	}

	sieve2_setvalue_stringlist(s, "body", bodylist);

	return SIEVE2_OK;
}

/* Return both the to and from headers. */
int sort_getenvelope(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;

	sieve2_setvalue_string(s, "to",
		m->message->envelope_recipient->str);
	sieve2_setvalue_string(s, "from",
		(char *)dbmail_message_get_header(m->message, "Return-Path"));

	return SIEVE2_OK;
}

int sort_getbody(sieve2_context_t *s UNUSED, void *my UNUSED)
{
	return SIEVE2_ERROR_UNSUPPORTED;
}

int sort_getsize(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	int rfcsize;

	rfcsize = dbmail_message_get_rfcsize(m->message);

	TRACE(TRACE_INFO, "Getting message size [%d]", rfcsize);

	sieve2_setvalue_int(s, "size", rfcsize);

	return SIEVE2_OK;
}

int sort_getsubaddress(sieve2_context_t *s, void *my)
{
	struct sort_context *m = (struct sort_context *)my;
	const char *address;
	char *user = NULL, *detail = NULL,
	     *localpart = NULL, *domain = NULL;

	address = sieve2_getvalue_string(s, "address");

	/* Simple address parsing. libSieve only shows us
	 * the localpart@domain portion, so we don't need
	 * to handle anything exciting or exotic here. */
	// TODO: Unify with the delivery chain subaddress code.

	localpart = strdup(address);
	domain = strchr(localpart, '@');
	if (domain) {
		*domain = '\0';
		domain++;
	} else {
		// Malformed address.
	}

	user = strdup(localpart);
	detail = strchr(user, '+');
	if (detail) {
		*detail = '\0';
		detail++;
	} else {
		// No detail present.
	}

	sieve2_setvalue_string(s, "user", user);
	sieve2_setvalue_string(s, "detail", detail);
	sieve2_setvalue_string(s, "localpart", localpart);
	sieve2_setvalue_string(s, "domain", domain);

	dm_list_nodeadd(&m->freelist, &user, sizeof(char *));
	dm_list_nodeadd(&m->freelist, &localpart, sizeof(char *));

	return SIEVE2_OK;
}

int sort_debugtrace(sieve2_context_t *s, void *my UNUSED)
{
	int trace_level;

	switch (sieve2_getvalue_int(s, "level")) {
	case 0:
	case 1:
	case 2:
		trace_level = TRACE_INFO;
		break;
	case 3:
	case 4:
	case 5:
	default:
		trace_level = TRACE_DEBUG;
		break;
	}

	TRACE(trace_level, "libSieve: module [%s] file [%s] function [%s] message [%s]\n",
			sieve2_getvalue_string(s, "module"),
			sieve2_getvalue_string(s, "file"),
			sieve2_getvalue_string(s, "function"),
			sieve2_getvalue_string(s, "message"));

	return SIEVE2_OK;
}

/* END OF CALLBACKS */


sieve2_callback_t sort_callbacks[] = {
	{ SIEVE2_ERRCALL_RUNTIME,       sort_errexec       },
	{ SIEVE2_ERRCALL_PARSE,         sort_errparse      },

	{ SIEVE2_ACTION_VACATION,       sort_vacation      },
	{ SIEVE2_ACTION_REDIRECT,       sort_redirect      },
	{ SIEVE2_ACTION_DISCARD,        sort_discard       },
	{ SIEVE2_ACTION_REJECT,         sort_reject        },
	{ SIEVE2_ACTION_FILEINTO,       sort_fileinto      },
	{ SIEVE2_ACTION_KEEP,           sort_fileinto      },

// TODO: Register this, but only if we're at a high debug level.
//	{ SIEVE2_DEBUG_TRACE,           sort_debugtrace    },
	{ SIEVE2_SCRIPT_GETSCRIPT,      sort_getscript     },
	{ SIEVE2_MESSAGE_GETHEADER,     sort_getheader     },
	{ SIEVE2_MESSAGE_GETENVELOPE,   sort_getenvelope   },
	{ SIEVE2_MESSAGE_GETBODY,       sort_getbody       },
	{ SIEVE2_MESSAGE_GETSIZE,       sort_getsize       },
	{ SIEVE2_MESSAGE_GETSUBADDRESS, sort_getsubaddress },
	{ 0, 0 } };


static int sort_teardown(sieve2_context_t **s2c,
		struct sort_context **sc)
{
	assert(s2c != NULL);
	assert(sc != NULL);

	sieve2_context_t *sieve2_context = *s2c;
	struct sort_context *sort_context = *sc;
	int res;

	dm_list_free(&sort_context->freelist.start);

	if (sort_context) {
		dm_free(sort_context);
	}

	res = sieve2_free(&sieve2_context);
	if (res != SIEVE2_OK) {
		TRACE(TRACE_ERROR, "Error [%d] when calling sieve2_free: [%s]",
			res, sieve2_errstr(res));
		return DM_EGENERAL;
	}

	*s2c = NULL;
	*sc = NULL;

	return DM_SUCCESS;
}

static int sort_startup(sieve2_context_t **s2c,
		struct sort_context **sc)
{
	assert(s2c != NULL);
	assert(sc != NULL);

	sieve2_context_t *sieve2_context = NULL;
	struct sort_context *sort_context = NULL;
	int res;

	res = sieve2_alloc(&sieve2_context);
	if (res != SIEVE2_OK) {
		TRACE(TRACE_ERROR, "Error [%d] when calling sieve2_alloc: [%s]",
			res, sieve2_errstr(res));
		return DM_EGENERAL;
	}

	res = sieve2_callbacks(sieve2_context, sort_callbacks);
	if (res != SIEVE2_OK) {
		TRACE(TRACE_ERROR, "Error [%d] when calling sieve2_callbacks: [%s]",
			res, sieve2_errstr(res));
		sort_teardown(&sieve2_context, &sort_context);
		return DM_EGENERAL;
	}

	sort_context = dm_malloc(sizeof(struct sort_context));
	if (!sort_context) {
		sort_teardown(&sieve2_context, &sort_context);
		return DM_EGENERAL;
	}
	memset(sort_context, 0, sizeof(struct sort_context));

	dm_list_init(&sort_context->freelist);

	*s2c = sieve2_context;
	*sc = sort_context;

	return DM_SUCCESS;
}

/* The caller is responsible for freeing memory here. */
// TODO: Support SIEVE_{extension} settings in dbmail.conf
const char * sort_listextensions(void)
{
	sieve2_context_t *sieve2_context;
	const char * extensions;

	if (sieve2_alloc(&sieve2_context) != SIEVE2_OK) 
		return NULL;

	if (sieve2_callbacks(sieve2_context, sort_callbacks))
			return NULL;

	/* This will be freed by sieve2_free. */
	extensions = sieve2_listextensions(sieve2_context);

	/* So we'll make our own copy. */
	if (extensions)
		extensions = dm_strdup(extensions);

	/* If this fails, then we don't care about the
	 * memory leak, because the program has to bomb out.
	 * It will not be possible to start libSieve up again
	 * if it does not free properly. */
	if (sieve2_free(&sieve2_context) != SIEVE2_OK)
		return NULL;

	return extensions;
}

/* Return 0 on script OK, 1 on script error, 2 on misc error. */
sort_result_t *sort_validate(u64_t user_idnr, char *scriptname)
{
	int res, exitnull = 0;
	struct sort_result *result = NULL;
	sieve2_context_t *sieve2_context;
	struct sort_context *sort_context;

	/* The contents of this function are taken from
	 * the libSieve distribution, sv_test/example.c,
	 * and are provided under an "MIT style" license.
	 * */

	if (sort_startup(&sieve2_context, &sort_context) != DM_SUCCESS) {
		return NULL;
	}

	sort_context->script = scriptname;
	sort_context->user_idnr = user_idnr;
	sort_context->result = g_new0(struct sort_result, 1);
	if (! sort_context->result) {
		return NULL;
	}
	sort_context->result->errormsg = g_string_new("");

	res = sieve2_validate(sieve2_context, sort_context);
	if (res != SIEVE2_OK) {
		TRACE(TRACE_ERROR, "Error %d when calling sieve2_validate: %s",
			res, sieve2_errstr(res));
		exitnull = 1;
		goto freesieve;
	}

	/* At this point the callbacks are called from within libSieve. */

freesieve:
	if (sort_context->s_buf)
		dm_free(sort_context->s_buf);

	if (exitnull)
		result = NULL;
	else
		result = sort_context->result;

	sort_teardown(&sieve2_context, &sort_context);

	return result;
}

/* Pull up the relevant sieve scripts for this
 * user and begin running them against the header
 * and possibly the body of the message.
 *
 * Returns 0 on success, -1 on failure,
 * and +1 on success but with memory leaking.
 * In the +1 case, if called from a daemon
 * such as dbmail-lmtpd, the daemon should
 * finish storing the message and restart.
 * */
sort_result_t *sort_process(u64_t user_idnr, struct DbmailMessage *message)
{
	int res, exitnull = 0;
	struct sort_result *result = NULL;
	sieve2_context_t *sieve2_context;
	struct sort_context *sort_context;

	/* The contents of this function are taken from
	 * the libSieve distribution, sv_test/example.c,
	 * and are provided under an "MIT style" license.
	 * */

	if (sort_startup(&sieve2_context, &sort_context) != DM_SUCCESS) {
		return NULL;
	}

	sort_context->message = message;
	sort_context->user_idnr = user_idnr;
	sort_context->result = g_new0(struct sort_result, 1);
	if (! sort_context->result) {
		exitnull = 1;
		goto freesieve;
	}
	sort_context->result->errormsg = g_string_new("");

	res = db_get_sievescript_active(user_idnr, &sort_context->script);
	if (res != 0) {
		TRACE(TRACE_ERROR, "Error [%d] when calling db_getactive_sievescript", res);
		exitnull = 1;
		goto freesieve;
	}
	if (sort_context->script == NULL) {
		TRACE(TRACE_INFO, "User doesn't have any active sieve scripts.");
		exitnull = 1;
		goto freesieve;
	}

	res = sieve2_execute(sieve2_context, sort_context);
	if (res != SIEVE2_OK) {
		TRACE(TRACE_ERROR, "Error [%d] when calling sieve2_execute: [%s]",
			res, sieve2_errstr(res));
		exitnull = 1;
	}
	if (! sort_context->result->cancelkeep) {
		TRACE(TRACE_INFO, "No actions taken; message must be kept.");
	}

	/* At this point the callbacks are called from within libSieve. */

freesieve:
	if (sort_context->s_buf)
		dm_free(sort_context->s_buf);
	if (sort_context->script)
		dm_free(sort_context->script);

	if (exitnull)
		result = NULL;
	else
		result = sort_context->result;

	sort_teardown(&sieve2_context, &sort_context);

	return result;
}

/* SORT RESULT INTERFACE */

void sort_free_result(sort_result_t *result)
{
	if (result == NULL) return;
	if (result->errormsg != NULL) 
		g_string_free(result->errormsg, TRUE);
	if (result->rejectmsg != NULL) 
		g_string_free(result->rejectmsg, TRUE);
	dm_free(result);
}

int sort_get_cancelkeep(sort_result_t *result)
{
	if (result == NULL) return 0;
	return result->cancelkeep;
}

const char * sort_get_mailbox(sort_result_t *result)
{
	if (result == NULL) return NULL;
	return result->mailbox;
}

int sort_get_reject(sort_result_t *result)
{
	if (result == NULL) return 0;
	return result->reject;
}

const char *sort_get_rejectmsg(sort_result_t *result)
{
	if (result == NULL) return NULL;
	return result->rejectmsg->str;
}

int sort_get_error(sort_result_t *result)
{
	if (result == NULL) return 0;
	return result->errormsg->len;
}

const char * sort_get_errormsg(sort_result_t *result)
{
	if (result == NULL) return NULL;
	return result->errormsg->str;
}



syntax highlighted by Code2HTML, v. 0.9.1