/* Copyright (c) 2002-2009 Dovecot Sieve authors, see the included COPYING file 
 */

#include "lib.h"
#include "strfuncs.h"
#include "str-sanitize.h"
#include "mail-storage.h"
#include "mail-namespace.h"

#include "sieve-code.h"
#include "sieve-extensions.h"
#include "sieve-binary.h"
#include "sieve-interpreter.h"
#include "sieve-dump.h"
#include "sieve-result.h"
#include "sieve-actions.h"

/*
 * Action execution environment
 */

const char *sieve_action_get_location(const struct sieve_action_exec_env *aenv)
{
	return t_strdup_printf("msgid=%s", aenv->msgdata->id == NULL ?
		"unspecified" : str_sanitize(aenv->msgdata->id, 80));
}

/*
 * Side-effect operand
 */
 
const struct sieve_operand_class sieve_side_effect_operand_class = 
	{ "SIDE-EFFECT" };

bool sieve_opr_side_effect_dump
(const struct sieve_dumptime_env *denv, sieve_size_t *address)
{
	const struct sieve_object *obj;
	const struct sieve_side_effect *seffect;
	
	if ( !sieve_opr_object_dump
		(denv, &sieve_side_effect_operand_class, address, &obj) )
		return FALSE;
	
	seffect = (const struct sieve_side_effect *) obj;

	if ( seffect->dump_context != NULL ) {
		sieve_code_descend(denv);
		if ( !seffect->dump_context(seffect, denv, address) ) {
			return FALSE;	
		}
		sieve_code_ascend(denv);
	}

	return TRUE;
}

/*
 * Store action
 */
 
/* Forward declarations */

static bool act_store_equals
	(const struct sieve_script_env *senv, const void *ctx1, const void *ctx2);
	
static int act_store_check_duplicate
	(const struct sieve_runtime_env *renv, 
		const struct sieve_action_data *act, 
		const struct sieve_action_data *act_other);
static void act_store_print
	(const struct sieve_action *action, 
		const struct sieve_result_print_env *rpenv, void *context, bool *keep);

static bool act_store_start
	(const struct sieve_action *action,
		const struct sieve_action_exec_env *aenv, void *context, void **tr_context);
static bool act_store_execute
	(const struct sieve_action *action, 
		const struct sieve_action_exec_env *aenv, void *tr_context);
static bool act_store_commit
	(const struct sieve_action *action, 
		const struct sieve_action_exec_env *aenv, void *tr_context, bool *keep);
static void act_store_rollback
	(const struct sieve_action *action, 
		const struct sieve_action_exec_env *aenv, void *tr_context, bool success);
		
/* Action object */

const struct sieve_action act_store = {
	"store",
	SIEVE_ACTFLAG_TRIES_DELIVER,
	act_store_equals,
	act_store_check_duplicate, 
	NULL, 
	act_store_print,
	act_store_start,
	act_store_execute,
	act_store_commit,
	act_store_rollback,
};

/* API */

int sieve_act_store_add_to_result
(const struct sieve_runtime_env *renv, 
	struct sieve_side_effects_list *seffects, const char *folder,
	unsigned int source_line)
{
	pool_t pool;
	struct act_store_context *act;
	
	/* Add redirect action to the result */
	pool = sieve_result_pool(renv->result);
	act = p_new(pool, struct act_store_context, 1);
	act->folder = p_strdup(pool, folder);

	return sieve_result_add_action(renv, &act_store, seffects, 
		source_line, (void *) act, 0);
}

/* Equality */

static bool act_store_equals
(const struct sieve_script_env *senv, const void *ctx1, const void *ctx2)
{
	struct act_store_context *st_ctx1 = (struct act_store_context *) ctx1;
	struct act_store_context *st_ctx2 = (struct act_store_context *) ctx2;
	const char *folder1, *folder2;
	
	if ( st_ctx1 == NULL && st_ctx2 == NULL )
		return TRUE;
		
	folder1 = ( st_ctx1 == NULL ? 
		SIEVE_SCRIPT_DEFAULT_MAILBOX(senv) : st_ctx1->folder );
	folder2 = ( st_ctx2 == NULL ? 
		SIEVE_SCRIPT_DEFAULT_MAILBOX(senv) : st_ctx2->folder );
	
	if ( strcmp(folder1, folder2) == 0 ) 
		return TRUE;
		
	return 
		( strcasecmp(folder1, "INBOX") == 0 && strcasecmp(folder2, "INBOX") == 0 ); 

}

/* Result verification */

static int act_store_check_duplicate
(const struct sieve_runtime_env *renv,
	const struct sieve_action_data *act, 
	const struct sieve_action_data *act_other)
{
	return ( act_store_equals(renv->scriptenv, act->context, act_other->context)
		? 1 : 0 );
}

/* Result printing */

static void act_store_print
(const struct sieve_action *action ATTR_UNUSED, 
	const struct sieve_result_print_env *rpenv, void *context, bool *keep)	
{
	struct act_store_context *ctx = (struct act_store_context *) context;
	const char *folder;

	folder = ( ctx == NULL ? 
		SIEVE_SCRIPT_DEFAULT_MAILBOX(rpenv->scriptenv) : ctx->folder );	

	sieve_result_action_printf(rpenv, "store message in folder: %s", 
		str_sanitize(folder, 128));
	
	*keep = FALSE;
}

/* Action implementation */

static void act_store_get_storage_error
(const struct sieve_action_exec_env *aenv, struct act_store_transaction *trans)
{
	enum mail_error error;
	pool_t pool = sieve_result_pool(aenv->result);
	
	trans->error = p_strdup(pool, 
		mail_storage_get_last_error(trans->namespace->storage, &error));
}

static struct mailbox *act_store_mailbox_open
(const struct sieve_action_exec_env *aenv, const char **name,
	struct mail_namespace **ns_r)
{
	struct mail_storage **storage = &(aenv->exec_status->last_storage);
	enum mailbox_flags flags =
		MAILBOX_FLAG_KEEP_RECENT | MAILBOX_FLAG_SAVEONLY |
		MAILBOX_FLAG_POST_SESSION;
	struct mailbox *box;
	enum mail_error error;
	const char *folder = *name;

	if (strcasecmp(folder, "INBOX") == 0) {
		/* Deliveries to INBOX must always succeed, regardless of ACLs */
		flags |= MAILBOX_FLAG_IGNORE_ACLS;
	}

	*ns_r = mail_namespace_find(aenv->scriptenv->namespaces, &folder);
	if ( *ns_r == NULL) {
		*storage = NULL;
		return NULL;
	}
	
	if ( *folder == '\0' ) {
		/* delivering to a namespace prefix means we actually want to
		 * deliver to the INBOX instead
		 */
		folder = *name = "INBOX";
		flags |= MAILBOX_FLAG_IGNORE_ACLS;

		*ns_r = mail_namespace_find(aenv->scriptenv->namespaces, &folder);
    	if ( *ns_r == NULL) {
			*storage = NULL;
			return NULL;
		}

		*storage = (*ns_r)->storage;
	}

	/* First attempt at opening the box */
	box = mailbox_alloc((*ns_r)->list, folder, NULL, flags);
	if ( mailbox_open(box) == 0 ) {
		/* Success */
		return box;
	}

	/* Failed */

	*storage = mailbox_get_storage(box);
	(void)mail_storage_get_last_error(*storage, &error);
	
	/* Only continue when the mailbox is missing and when we are allowed to
	 * create it.
	 */
	if ( !aenv->scriptenv->mailbox_autocreate || error != MAIL_ERROR_NOTFOUND ) {
		mailbox_close(&box);
		return NULL;
	}

	/* Try creating it. */
    if ( mailbox_create(box, NULL, FALSE) < 0 ) {
        (void)mail_storage_get_last_error(*storage, &error);
        mailbox_close(&box);
        return NULL;
    }

	/* Subscribe to it if required */
	if ( aenv->scriptenv->mailbox_autosubscribe ) {
		(void)mailbox_list_set_subscribed((*ns_r)->list, folder, TRUE);
	}

	/* Try opening again */
	if ( mailbox_open(box) < 0 || mailbox_sync(box, 0, 0, NULL) < 0 ) {
		/* Failed definitively */
		mailbox_close(&box);
		return NULL;
	}

	return box;
}

static bool act_store_start
(const struct sieve_action *action ATTR_UNUSED, 
	const struct sieve_action_exec_env *aenv, void *context, void **tr_context)
{  
	struct act_store_context *ctx = (struct act_store_context *) context;
	struct act_store_transaction *trans;
	struct mail_namespace *ns = NULL;
	struct mailbox *box = NULL;
	pool_t pool = sieve_result_pool(aenv->result);

	/* If context is NULL, the store action is the result of (implicit) keep */	
	if ( ctx == NULL ) {
		ctx = p_new(pool, struct act_store_context, 1);
		ctx->folder = p_strdup(pool, SIEVE_SCRIPT_DEFAULT_MAILBOX(aenv->scriptenv));
	}

	/* Open the requested mailbox */

	/* NOTE: The caller of the sieve library is allowed to leave namespaces set 
	 * to NULL. This implementation will then skip actually storing the message.
	 */
	if ( aenv->scriptenv->namespaces != NULL ) {
		box = act_store_mailbox_open(aenv, &ctx->folder, &ns);
	}
				
	/* Create transaction context */
	trans = p_new(pool, struct act_store_transaction, 1);
	trans->context = ctx;
	trans->namespace = ns;
	trans->box = box;
	trans->flags = 0;
	
	*tr_context = (void *)trans;

	return ( (aenv->scriptenv->namespaces == NULL) || (box != NULL) );
}

static bool act_store_execute
(const struct sieve_action *action ATTR_UNUSED, 
	const struct sieve_action_exec_env *aenv, void *tr_context)
{   
	struct act_store_transaction *trans = 
		(struct act_store_transaction *) tr_context;
	struct mail_keywords *keywords = NULL;
	struct mail_save_context *save_ctx;
	
	/* Verify transaction */
	if ( trans == NULL ) return FALSE;

	/* Exit early if namespace is not available */
	if ( trans->namespace == NULL ) {
		if ( aenv->scriptenv->namespaces == NULL )
			return TRUE;

		return FALSE;
	} else if ( trans->box == NULL ) return FALSE;

	/* Mark attempt to store in default mailbox */
	if ( strcmp(trans->context->folder, 
		SIEVE_SCRIPT_DEFAULT_MAILBOX(aenv->scriptenv)) == 0 ) 
		aenv->exec_status->tried_default_save = TRUE;

	/* Mark attempt to use storage. Can only get here when all previous actions
	 * succeeded. 
	 */
	aenv->exec_status->last_storage = mailbox_get_storage(trans->box);
	
	/* Start mail transaction */
	trans->mail_trans = mailbox_transaction_begin
		(trans->box, MAILBOX_TRANSACTION_FLAG_EXTERNAL);

	/* Create mail object for stored message */
	trans->dest_mail = mail_alloc(trans->mail_trans, 0, NULL);

	/* Collect keywords added by side-effects */
	if ( array_is_created(&trans->keywords) && array_count(&trans->keywords) > 0 ) 
	{
		const char *const *kwds;
		
		(void)array_append_space(&trans->keywords);
		kwds = array_idx(&trans->keywords, 0);
				
		/* FIXME: Do we need to clear duplicates? */
		
		if ( mailbox_keywords_create(trans->box, kwds, &keywords) < 0) {
			sieve_result_error(aenv, "invalid keywords set for stored message");
			keywords = NULL;
		}
	}
	
	/* Store the message */
	save_ctx = mailbox_save_alloc(trans->mail_trans);
	mailbox_save_set_flags(save_ctx, trans->flags, keywords);
	mailbox_save_set_dest_mail(save_ctx, trans->dest_mail);
	if ( mailbox_copy(&save_ctx, aenv->msgdata->mail) < 0 ) {
		act_store_get_storage_error(aenv, trans);
 		return FALSE;
 	}
 	
 	if ( keywords != NULL ) {
 		mailbox_keywords_unref(trans->box, &keywords);
 	}
 		 	
	return TRUE;
}

static void act_store_log_status
(struct act_store_transaction *trans, 
	const struct sieve_action_exec_env *aenv, bool rolled_back, bool status )
{
	const char *mailbox_name;
	
	mailbox_name = str_sanitize(trans->context->folder, 128);

	if ( trans->namespace == NULL ) {
		if ( aenv->scriptenv->namespaces == NULL )
			sieve_result_log(aenv, "store into mailbox '%s' skipped", mailbox_name);
		else
			sieve_result_error
				(aenv, "failed to find namespace for mailbox '%s'", mailbox_name);
	} else {	
		if ( !rolled_back && status ) {
			sieve_result_log(aenv, "stored mail into mailbox '%s'", mailbox_name);
		} else {
			const char *errstr;
			enum mail_error error;
		
			if ( trans->error != NULL )
				errstr = trans->error;
			else
				errstr = mail_storage_get_last_error(trans->namespace->storage, &error);
			
			if ( status )
				sieve_result_log(aenv, "store into mailbox '%s' aborted", mailbox_name);
			else
				sieve_result_error(aenv, "failed to store into mailbox '%s': %s", 
					mailbox_name, errstr);
		}
	}
}

static bool act_store_commit
(const struct sieve_action *action ATTR_UNUSED, 
	const struct sieve_action_exec_env *aenv, void *tr_context, bool *keep)
{  
	struct act_store_transaction *trans = 
		(struct act_store_transaction *) tr_context;
	bool status = TRUE;

	/* Verify transaction */
	if ( trans == NULL ) return FALSE;

	/* Exit early if namespace is not available */
	if ( trans->namespace == NULL ) {
		if ( aenv->scriptenv->namespaces == NULL ) {
			act_store_log_status(trans, aenv, FALSE, status);
			*keep = FALSE;
			return TRUE;
		}

		return FALSE;
	} else if ( trans->box == NULL ) return FALSE;

	/* Mark attempt to use storage. Can only get here when all previous actions
	 * succeeded. 
	 */
	aenv->exec_status->last_storage = trans->namespace->storage;

	/* Free mail object for stored message */
	if ( trans->dest_mail != NULL ) 
		mail_free(&trans->dest_mail);	

	/* Commit mailbox transaction */	
	status = mailbox_transaction_commit(&trans->mail_trans) == 0;

	/* Note the fact that the message was stored at least once */
	if ( status )
		aenv->exec_status->message_saved = TRUE;
	
	/* Log our status */
	act_store_log_status(trans, aenv, FALSE, status);
	
	/* Cancel implicit keep if all went well */
	*keep = !status;
	
	/* Close mailbox */	
	if ( trans->box != NULL )
		mailbox_close(&trans->box);

	return status;
}

static void act_store_rollback
(const struct sieve_action *action ATTR_UNUSED, 
	const struct sieve_action_exec_env *aenv, void *tr_context, bool success)
{
	struct act_store_transaction *trans = 
		(struct act_store_transaction *) tr_context;

	/* Log status */
	act_store_log_status(trans, aenv, TRUE, success);

	/* Free mailobject for stored message */
	if ( trans->dest_mail != NULL ) 
		mail_free(&trans->dest_mail);	

	/* Rollback mailbox transaction */
	if ( trans->mail_trans != NULL )
		mailbox_transaction_rollback(&trans->mail_trans);
  
	/* Close the mailbox */
	if ( trans->box != NULL )  
		mailbox_close(&trans->box);
}

/*
 * Action utility functions
 */

bool sieve_action_duplicate_check_available
(const struct sieve_script_env *senv)
{
	return ( senv->duplicate_check != NULL && senv->duplicate_mark != NULL );
}

int sieve_action_duplicate_check
(const struct sieve_script_env *senv, const void *id, size_t id_size)
{
	if ( senv->duplicate_check == NULL || senv->duplicate_mark == NULL)
		return 0;

	return senv->duplicate_check
		(id, id_size, senv->username); 
}

void sieve_action_duplicate_mark
(const struct sieve_script_env *senv, const void *id, size_t id_size,
	time_t time)
{
	if ( senv->duplicate_check == NULL || senv->duplicate_mark == NULL)
		return;

	senv->duplicate_mark
		(id, id_size, senv->username, time);
}