/*
 * antispam plugin for dovecot
 *
 * Copyright (C) 2004-2007  Johannes Berg <johannes@sipsolutions.net>
 *                    2006  Frank Cusack
 *
 * 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
 *
 * based on the original framework http://www.dovecot.org/patches/1.0/copy_plugin.c
 *
 * Please see http://johannes.sipsolutions.net/wiki/Projects/dovecot-dspam-integration
 * for more information on this code.
 *
 * Install the plugin in the usual dovecot module location.
 */

#include <stdlib.h>
#include <ctype.h>

/* dovecot headers we need */
#include "lib.h"
#include "str.h"
#include "mail-storage-private.h"
#include "antispam-version.h"

/* internal stuff we need */
#include "antispam-plugin.h"


static char *default_spam_folders[] = {
	"SPAM",
	NULL
};

enum match_type {
	MT_REG,
	MT_PATTERN,
	MT_PATTERN_IGNCASE,

	/* keep last */
	NUM_MT
};

/* lower-case string, but keep modified UTF7 unchanged */
static void lowercase_string(const char *in, char *out)
{
	char ch;

	while ((ch = *out++ = i_tolower(*in++))) {
		/* lower case */
		if (ch == '&') {
			/* modified UTF7 -- find end of sequence ('-') */
			while ((ch = *out++ = *in++)) {
				if (ch == '-')
					break;
			}
		}
	}
}

static bool mailbox_patternmatch(struct mailbox *box,
#if DOVECOT_IS_GE(2,0)
				 const struct mail_namespace *ns,
#else
				 struct mail_storage *storage,
#endif
				 const char *name, bool lowercase)
{
	const char *boxname;
	char *lowerboxname;
	int len;
	int rc;

#if DOVECOT_IS_GE(2,0)
	if (ns && mailbox_get_namespace(box) != ns)
		return FALSE;
#else
	if (storage && mailbox_get_storage(box) != storage)
		return FALSE;
#endif

	T_BEGIN {

	boxname = mailbox_get_name(box);
	if (lowercase) {
		lowerboxname = t_buffer_get(strlen(boxname) + 1);
		lowercase_string(boxname, lowerboxname);
		boxname = lowerboxname;
	}

	len = strlen(name);
	if (len && name[len - 1] == '*') {
		/* any wildcard */
		--len;
	} else {
		/* compare EOS too */
		++len;
	}

	rc = memcmp(name, boxname, len) == 0;

	} T_END;

	return rc;
}

static bool mailbox_patternmatch_case(struct mailbox *box,
#if DOVECOT_IS_GE(2,0)
				      const struct mail_namespace *ns,
#else
				      struct mail_storage *ns,
#endif
				      const char *name)
{
	return mailbox_patternmatch(box, ns, name, FALSE);
}

static bool mailbox_patternmatch_icase(struct mailbox *box,
#if DOVECOT_IS_GE(2,0)
				       const struct mail_namespace *ns,
#else
				       struct mail_storage *ns,
#endif
				       const char *name)
{
	return mailbox_patternmatch(box, ns, name, TRUE);
}

static bool _mailbox_equals(struct mailbox *box,
#if DOVECOT_IS_GE(2,0)
			    const struct mail_namespace *ns,
#else
			    struct mail_storage *ns,
#endif
			    const char *name)
{
	return mailbox_equals(box, ns, name);
}

typedef bool (*match_fn_t)(struct mailbox *,
#if DOVECOT_IS_GE(2,0)
			   const struct mail_namespace *,
#else
			   struct mail_storage *,
#endif
			   const char *);

/* match information */
static const struct {
	const char *human, *suffix;
	match_fn_t fn;
} match_info[NUM_MT] = {
	[MT_REG]		= { .human  = "exact match",
				    .suffix = "",
				    .fn     = _mailbox_equals, },
	[MT_PATTERN]		= { .human  = "wildcard match",
				    .suffix = "_PATTERN",
				    .fn     = mailbox_patternmatch_case, },
	[MT_PATTERN_IGNCASE]	= { .human  = "case-insensitive wildcard match",
				    .suffix = "_PATTERN_IGNORECASE",
				    .fn     = mailbox_patternmatch_icase, },
};

static bool mailbox_in_list(struct mailbox *box, char ** const * patterns)
{
	enum match_type i;
	char **list;

	if (!patterns)
		return FALSE;

	for (i = 0; i < NUM_MT; i++) {
		list = patterns[i];
		if (!list)
			continue;

		while (*list) {
			if (match_info[i].fn(box,
#if DOVECOT_IS_GE(2,0)
					     mailbox_get_namespace(box),
#else
					     mailbox_get_storage(box),
#endif
					     *list))
				return TRUE;
			list++;
		}
	}

	return FALSE;
}

bool mailbox_is_spam(const struct antispam_config *cfg, struct mailbox *box)
{
	bool ret;

	ret = mailbox_in_list(box, cfg->spam_folders);
	debug_verbose(&cfg->dbgcfg, "mailbox_is_spam(%s): %d\n",
		      mailbox_get_name(box), ret);
	return ret;
}

bool mailbox_is_trash(const struct antispam_config *cfg, struct mailbox *box)
{
	bool ret;

	ret = mailbox_in_list(box, cfg->trash_folders);
	debug_verbose(&cfg->dbgcfg, "mailbox_is_trash(%s): %d\n",
		      mailbox_get_name(box), ret);
	return ret;
}

bool mailbox_is_unsure(const struct antispam_config *cfg, struct mailbox *box)
{
	bool ret;

	ret = mailbox_in_list(box, cfg->unsure_folders);
	debug_verbose(&cfg->dbgcfg, "mailbox_is_unsure(%s): %d\n",
		      mailbox_get_name(box), ret);
	return ret;
}

bool keyword_is_spam(const struct antispam_config *cfg, const char *keyword)
{
	char **k = cfg->spam_keywords;

	if (!cfg->spam_keywords)
		return FALSE;

	while (*k) {
		if (strcmp(*k, keyword) == 0)
			return TRUE;
		k++;
	}

	return FALSE;
}

static int parse_folder_setting(const struct antispam_config *cfg,
				const char *setting, char ***strings,
				const char *display_name,
				const char *(getenv)(const char *env, void *data),
				void *getenv_data)
{
	const char *tmp;
	int cnt = 0;
	enum match_type i;

	T_BEGIN {

	for (i = 0; i < NUM_MT; ++i) {
		tmp = getenv(t_strconcat(setting, match_info[i].suffix, NULL),
			     getenv_data);
		if (tmp) {
			strings[i] = p_strsplit(cfg->mem_pool, tmp, ";");
			if (i == MT_PATTERN_IGNCASE) {
				/* lower case the string */
				char **list = strings[i];
				while (*list) {
					lowercase_string(*list, *list);
					++list;
				}
			}
		}

		if (strings[i]) {
			char **iter = strings[i];
			while (*iter) {
				++cnt;
				debug(&cfg->dbgcfg, 
					"\"%s\" is %s %s folder\n", *iter,
					match_info[i].human, display_name);
				iter++;
			}
		}
	}

	} T_END;

	if (!cnt)
		debug(&cfg->dbgcfg, "no %s folders\n", display_name);

	return cnt;
}

struct antispam_config *
antispam_setup_config(const char *(getenv)(const char *env, void *data),
		      void *getenv_data)
{
	struct antispam_config *cfg;
	const char *tmp;
	char * const *iter;
	int spam_folder_count;

	cfg = i_new(struct antispam_config, 1);

	cfg->mem_pool = pool_alloconly_create("antispam-pool", 1024);

	if (debug_init(&cfg->dbgcfg, getenv, getenv_data) < 0)
		goto error;

	cfg->spam_folders[0] = default_spam_folders;
	spam_folder_count = parse_folder_setting(cfg, "SPAM", cfg->spam_folders,
						 "spam", getenv, getenv_data);
	parse_folder_setting(cfg, "UNSURE", cfg->unsure_folders, "unsure",
			     getenv, getenv_data);
	parse_folder_setting(cfg, "TRASH", cfg->trash_folders, "trash",
			     getenv, getenv_data);

	tmp = getenv("ALLOW_APPEND_TO_SPAM", getenv_data);
	if (tmp && strcasecmp(tmp, "yes") == 0) {
		cfg->can_append_to_spam = TRUE;
		debug(&cfg->dbgcfg, "allowing APPEND to spam folders");
	}

	tmp = getenv("SPAM_KEYWORDS", getenv_data);
	if (tmp)
		cfg->spam_keywords = p_strsplit(cfg->mem_pool, tmp, ";");

	if (cfg->spam_keywords) {
		iter = cfg->spam_keywords;
		while (*iter) {
			debug(&cfg->dbgcfg, "\"%s\" is spam keyword\n", *iter);
			iter++;
		}
	}

	tmp = getenv("BACKEND", getenv_data);
	if (tmp) {
		if (strcmp(tmp, "crm114") == 0)
			cfg->backend = &crm114_backend;
		else if (strcmp(tmp, "dspam") == 0)
			cfg->backend = &dspam_backend;
		else if (strcmp(tmp, "pipe") == 0)
			cfg->backend = &pipe_backend;
		else if (strcmp(tmp, "spool2dir") == 0)
			cfg->backend = &spool2dir_backend;
		else {
			debug(&cfg->dbgcfg, "selected invalid backend!\n");
			exit(3);
		}
	} else {
		debug(&cfg->dbgcfg, "no backend selected!\n");
		goto error;
	}

	/* set spam_folders to empty to only allow keywords */
	cfg->need_folder_hook = spam_folder_count > 0;
	cfg->need_keyword_hook = !!cfg->spam_keywords;

	cfg->backend->init(cfg, getenv, getenv_data);

	return cfg;
 error:
	mempool_unref(&cfg->mem_pool);
	i_free(cfg);
	return NULL;
}

void antispam_free_config(struct antispam_config *cfg)
{
	mempool_unref(&cfg->mem_pool);
	i_free(cfg);
}

#if DOVECOT_IS_GE(2,0)
void PLUGIN_FUNCTION(init)(struct module *module)
{
	antispam_storage_init(module);
}
#else
void PLUGIN_FUNCTION(init)(void)
{
	antispam_storage_init();
}
#endif

void PLUGIN_FUNCTION(deinit)(void)
{
	antispam_storage_deinit();
}

/* put dovecot version we built against into plugin for checking */
#if DOVECOT_IS_GE(2,2)
const char *PLUGIN_FUNCTION(version) = DOVECOT_ABI_VERSION;
#else
const char *PLUGIN_FUNCTION(version) = PACKAGE_VERSION;
#endif
