/*
 * mailing backend for dovecot antispam plugin
 *
 * Copyright (C) 2025       Johannes Berg <johannes@sipsolutions.net>
 * Copyright (C) 2008       Steffen Kaiser <skdovecot@smail.inf.fh-brs.de>
 * this backend "spool2dir" bases on "mailtrain" backend of
 * Copyright (C) 2007       Johannes Berg <johannes@sipsolutions.net>
 *
 * 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.
 */


/*
 * spool2dir antispam backend / plugin
 *
 * Any modification of SPAM status is recorded into a directory.
 *
 * Configuration
 *
 * Via settings similiar to the other antispam backends
 *	antispam_spool2dir_spam :- filename tempate for SPAM messages
 *	antispam_spool2dir_notsam :- filename tempate for HAM messages
 *
 * The templates _must_ provide two arguments:
 *   1. %%lu - the current unix time (lowercase L, lowercase U)
 *   2. %%lu - a counter to create different temporary files
 * Note: The %-sign must be given two times to protect agains
 *  the expansion by Dovecot itself. You can put any legal
 *  format modification character of C's printf() function between
 *  '%%' and 'lu'.
 *
 * e.g.:
 *	antispam_spool2dir_spam = /tmp/spam/%%020lu-%%05lu-%u-S
 *	antispam_spool2dir_ham  = /tmp/spam/%%020lu-%%05lu-%u-H
 *
 * This example will spool the messages into the directory
 * /tmp/spam. The individual files start with 20 digits,
 * followe by a dash, 5 digits, the current username and S or H,
 * indicating Spam or Ham messages.
 * The first %%lu placeholder is replace by the current unix time,
 * the second %%lu with the counter. That way, if the same user
 * trains the same message twice, the filename indicates the order
 * in which it was done. So if the message was trained as SPAM first,
 * as HAM later, HAM superceeds SPAM.
 *
 * Operation
 *
 * When the antispam plugin identifies detects a SPAM status change,
 * e.g. moving/copying a message from any antispam_spam folder into
 * a folder _not_ listed in antispam_spam or antispam_trash, this
 * backend spools the complete message into antispam_mail_basedir.
 * If there is an error copying _all_ messages around, old spools
 * are kept, but the current one is deleted. For instance, if the
 * user is copying 15 messages, but only 10 succeed, the 10 would
 * be usually deleted. In this backend there is no rollback of
 * successfully spooled message, only the failed message is
 * deleted.
 *
 * Possible usage models
 *
 * A)
 *   I use spool2dir for training the Bayes database as follows:
 *
 *   Every 10 seconds a service invokes the training program, unless
 *   it already runs.
 *
 *   The training progran reads the content of the spool directory, sorts
 *   the filenames alphanumerically, waits two seconds to allow any current
 *   spool2dir processes to finish currently open files.
 *   Then one message at a time is read and identified, if it contains
 *   local modifications, e.g. user-visible SPAM reports, which are removed.
 *   Furthermore, reports of untrustworthy people are discarded.
 *   This process continues until either all messages are processed or
 *   the next message would have another SPAM report type (HAM or SPAM). The
 *   file names of the messages processed til now are passed to the Bayes
 *   trainer to be processed within one run. Then those messages are removed.
 *
 * B)
 *
 *   An Inotify server watches the spam directory and passes the messages
 *   to spamd. No need for the filenames to indicate the order anymore, unless
 *   the inotify server is not fast enough.
 */

#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

#include "lib.h"
#include "mail-storage-private.h"
#include "ostream.h"
#include "istream.h"

#include "antispam-plugin.h"


struct antispam_transaction_context {
	int count;
};


static void backend_rollback(const struct antispam_settings *cfg ATTR_UNUSED,
			     struct antispam_transaction_context *ast)
{
	i_free(ast);
}

static int backend_commit(const struct antispam_settings *cfg ATTR_UNUSED,
			  struct mailbox_transaction_context *ctx ATTR_UNUSED,
			  struct antispam_transaction_context *ast)
{
	i_free(ast);

	return 0;
}

static int backend_handle_mail(const struct antispam_settings *cfg,
			       struct mailbox_transaction_context *t,
			       struct antispam_transaction_context *ast,
			       struct mail *mail, enum classification wanted)
{
	struct istream *mailstream;
	struct ostream *outstream;
	int ret = -1;
	const char *dest, *buf;
	const unsigned char *beginning;
	size_t size;
	int fd = -1;

	i_assert(ast);

	switch (wanted) {
	case CLASS_SPAM:
		dest = cfg->spool2dir_spam;
		break;
	case CLASS_NOTSPAM:
		dest = cfg->spool2dir_notspam;
		break;
	default:	/* cannot handle this */
		return -1;
	}

	if (mail_get_stream(mail, NULL, NULL, &mailstream) < 0) {
		mail_storage_set_error(t->box->storage,
				       MAIL_ERROR_EXPUNGED,
				       "Failed to get mail contents");
		return -1;
	}

	T_BEGIN {

	/* atomically create a _new_ file */
	while (ast->count <= 9999) {
		buf = t_strdup_printf(dest, (long)time(0), (long)++ast->count);
		fd = open(buf, O_CREAT | O_EXCL | O_WRONLY, 0600);
		if (fd >= 0 || errno != EEXIST)
			break;
		/* current filename in buf already exists, zap it */
	}

	if (fd < 0) {
		debug(cfg, "spool2dir backend: Failed to create spool file %s: %s\n",
		      dest, strerror(errno));
		mail_storage_set_error(t->box->storage,
				       MAIL_ERROR_NOTPOSSIBLE,
				       "Failed to create spool file");
		goto out;
	}

	/* buf still points to allocated memory, because fd >= 0 */

	outstream = o_stream_create_fd_autoclose(&fd, 0);
	if (!outstream) {
		mail_storage_set_error(t->box->storage,
				       MAIL_ERROR_NOTPOSSIBLE,
				       "Failed to stream spool file");
		goto out_close;
	}

	if (i_stream_read_data(mailstream, &beginning, &size, 5) < 0 ||
	    size < 5) {
		mail_storage_set_error	(t->box->storage,
				       MAIL_ERROR_NOTPOSSIBLE,
				       "Failed to read mail beginning");
		goto failed_to_copy;
	}

	/* "From "? skip line */
	if (memcmp("From ", beginning, 5) == 0)
		i_stream_read_next_line(mailstream);

	if (o_stream_send_istream(outstream, mailstream) < 0) {
		mail_storage_set_error(t->box->storage,
				       MAIL_ERROR_NOTPOSSIBLE,
				       "Failed to copy to spool file");
		goto failed_to_copy;
	}

	ret = 0;

 failed_to_copy:
	o_stream_destroy(&outstream);
 out_close:
	close(fd);
	if (ret)
		unlink(buf);
 out:	;
	} T_END;

	return ret;
}

static bool backend_settings(struct antispam_settings *cfg,
			     pool_t pool ATTR_UNUSED, const char **error_r)
{
	if (!cfg->spool2dir_spam) {
		*error_r = t_strdup("spool2dir: must configure spam spooldir");
		return false;
	}

	if (!cfg->spool2dir_notspam) {
		*error_r = t_strdup("spool2dir: must configure notspam spooldir");
		return false;
	}

	debug(cfg, "spool2dir spam %s\n", cfg->spool2dir_spam);
	debug(cfg, "spool2dir notspam %s\n", cfg->spool2dir_notspam);
	return true;
}

static struct antispam_transaction_context *
backend_start(const struct antispam_settings *cfg ATTR_UNUSED,
	      struct mailbox *box ATTR_UNUSED)
{
	struct antispam_transaction_context *ast;

	ast = i_new(struct antispam_transaction_context, 1);

	ast->count = 0;

	return ast;
}


struct backend spool2dir_backend = {
	.settings = backend_settings,
	.handle_mail = backend_handle_mail,
	.start = backend_start,
	.rollback = backend_rollback,
	.commit = backend_commit,
};
