/* Copyright (C) 2005-2006 Timo Sirainen */

#include "lib.h"
#include "ioloop.h"
#include "array.h"
#include "hostpid.h"
#include "str.h"
#include "str-sanitize.h"
#include "write-full.h"
#include "message-date.h"
#include "mail-storage.h"
#include "deliver.h"
#include "duplicate.h"
#include "mail-send.h"
#include "smtp-client.h"
#include "libsieve/sieve_interface.h"
#include "cmusieve-plugin.h"

#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

/* data per script */
typedef struct script_data {
	const char *username;
	struct mail_storage *storage;
	string_t *errors;
} script_data_t;

typedef struct {
	struct mail *mail;
	const char *mailbox;
	const char *id;
	const char *return_path;
	const char *authuser;

	const char *temp[10];
} sieve_msgdata_t;

/* gets the header "head" from msg. */
static int getheader(void *v, const char *phead, const char ***body)
{
    sieve_msgdata_t *m = v;

    if (phead==NULL) return SIEVE_FAIL;
    *body = (const char **)mail_get_headers(m->mail, phead);

    if (*body) {
	return SIEVE_OK;
    } else {
	return SIEVE_FAIL;
    }
}

static int getsize(void *mc, int *size)
{
    sieve_msgdata_t *md = mc;
    uoff_t psize;

    psize = mail_get_physical_size(md->mail);
    if (psize == (uoff_t)-1)
	    return SIEVE_FAIL;

    *size = psize;
    return SIEVE_OK;
}

/* we use the temp field in message_data to avoid having to malloc memory
   to return, and we also can't expose our the receipients to the message */
static int getenvelope(void *mc, const char *field, const char ***contents)
{
    sieve_msgdata_t *m = (sieve_msgdata_t *) mc;

    if (!strcasecmp(field, "from")) {
	if (m->return_path == NULL) {
	    /* invalid or missing return path */
	    *contents = NULL;
	    return SIEVE_FAIL;
	}
	*contents = m->temp;
	m->temp[0] = m->return_path;
	m->temp[1] = NULL;
	return SIEVE_OK;
    } else if (!strcasecmp(field, "to")) {
	*contents = m->temp;
	m->temp[0] = /*FIXME:msg_getrcptall(m, m->cur_rcpt)*/m->authuser;
	m->temp[1] = NULL;
	return SIEVE_OK;
    } else if (!strcasecmp(field, "auth") && m->authuser) {
	*contents = m->temp;
	m->temp[0] = m->authuser;
	m->temp[1] = NULL;
	return SIEVE_OK;
    } else {
	*contents = NULL;
	return SIEVE_FAIL;
    }
}

static int sieve_redirect(void *ac, 
			  void *ic __attr_unused__, 
			  void *sc, void *mc, const char **errmsg)
{
    sieve_redirect_context_t *rc = (sieve_redirect_context_t *) ac;
    script_data_t *sd = (script_data_t *) sc;
    sieve_msgdata_t *m = mc;
    const char *dupeid;
    int res;

    /* if we have a msgid, we can track our redirects */
    dupeid = m->id == NULL ? NULL : t_strdup_printf("%s-%s", m->id, rc->addr);
    if (dupeid != NULL) {
	/* ok, let's see if we've redirected this message before */
	if (duplicate_check(dupeid, strlen(dupeid), sd->username)) {
	    /*duplicate_log(m->id, sd->username, "redirect");*/
	    i_info("msgid=%s: discarded duplicate forward to <%s>",
		   str_sanitize(m->id, 80), str_sanitize(rc->addr, 80));
            return SIEVE_OK;
	}
    }

    if ((res = mail_send_forward(m->mail, rc->addr)) == 0) {
	/* mark this message as redirected */
	i_info("msgid=%s: forwarded to <%s>",
	       m->id == NULL ? "" : str_sanitize(m->id, 80),
	       str_sanitize(rc->addr, 80));
        if (dupeid != NULL) {
            duplicate_mark(dupeid, strlen(dupeid), sd->username,
                           ioloop_time + DUPLICATE_DEFAULT_KEEP);
        }
	return SIEVE_OK;
    } else {
	*errmsg = "Error sending mail";
	return SIEVE_FAIL;
    }
}

static int sieve_discard(void *ac __attr_unused__, 
			 void *ic __attr_unused__, 
			 void *sc __attr_unused__, void *mc,
			 const char **errmsg __attr_unused__)
{
    sieve_msgdata_t *md = mc;

    /* ok, we won't file it, but log it */
    i_info("msgid=%s: discarded",
	   md->id == NULL ? "" : str_sanitize(md->id, 80));
    return SIEVE_OK;
}

static int sieve_reject(void *ac, 
			void *ic __attr_unused__, 
			void *sc, void *mc, const char **errmsg)
{
    sieve_reject_context_t *rc = (sieve_reject_context_t *) ac;
    script_data_t *sd = (script_data_t *) sc;
    sieve_msgdata_t *md = (sieve_msgdata_t *) mc;
    int res;

    if (md->return_path == NULL) {
	/* return message to who?!? */
	*errmsg = "No return-path for reply";
	return SIEVE_FAIL;
    }

    if (strlen(md->return_path) == 0) {
        i_info("msgid=%s: discarded reject to <>",
	       md->id == NULL ? "" : str_sanitize(md->id, 80));
        return SIEVE_OK;
    }

    if ((res = mail_send_rejection(md->mail, sd->username, rc->msg)) == 0) {
        i_info("msgid=%s: rejected",
	       md->id == NULL ? "" : str_sanitize(md->id, 80));
	return SIEVE_OK;
    } else {
	*errmsg = "Error sending mail";
	return SIEVE_FAIL;
    }
    return SIEVE_FAIL;
}

static void get_flags(const sieve_imapflags_t *sieve_flags,
		      enum mail_flags *flags_r, const char *const **keywords_r)
{
	array_t ARRAY_DEFINE(keywords, const char *);
        const char *name;
	int i;

	*flags_r = 0;

	ARRAY_CREATE(&keywords, default_pool, const char *, 16);
	for (i = 0; i < sieve_flags->nflags; i++) {
		name = sieve_flags->flag[i];

		if (name != NULL && *name != '\\') {
			/* keyword */
			array_append(&keywords, &name, 1);
		} else {
			/* system flag */
			if (name == NULL || strcasecmp(name, "\\flagged") == 0)
				*flags_r |= MAIL_FLAGGED;
			else if (strcasecmp(name, "\\answered") == 0)
				*flags_r |= MAIL_ANSWERED;
			else if (strcasecmp(name, "\\deleted") == 0)
				*flags_r |= MAIL_DELETED;
			else if (strcasecmp(name, "\\seen") == 0)
				*flags_r |= MAIL_SEEN;
			else if (strcasecmp(name, "\\draft") == 0)
				*flags_r |= MAIL_DRAFT;
		}
	}

	name = NULL;
	array_append(&keywords, &name, 1);

	*keywords_r = array_count(&keywords) == 1 ? NULL :
		array_get(&keywords, 0);
}

static int sieve_fileinto(void *ac, 
			  void *ic __attr_unused__,
			  void *sc, 
			  void *mc,
			  const char **errmsg __attr_unused__)
{
    sieve_fileinto_context_t *fc = (sieve_fileinto_context_t *) ac;
    script_data_t *sd = (script_data_t *) sc;
    sieve_msgdata_t *md = (sieve_msgdata_t *) mc;
    enum mail_flags flags;
    const char *const *keywords;

    get_flags(fc->imapflags, &flags, &keywords);

    if (deliver_save(sd->storage, fc->mailbox, md->mail, flags, keywords) < 0)
	    return SIEVE_FAIL;

    return SIEVE_OK;
}

static int sieve_keep(void *ac, 
		      void *ic __attr_unused__,
		      void *sc, void *mc, const char **errmsg __attr_unused__)
{
    sieve_keep_context_t *kc = (sieve_keep_context_t *) ac;
    script_data_t *sd = (script_data_t *) sc;
    sieve_msgdata_t *md = (sieve_msgdata_t *) mc;
    enum mail_flags flags;
    const char *const *keywords;

    get_flags(kc->imapflags, &flags, &keywords);

    if (deliver_save(sd->storage, md->mailbox, md->mail, flags, keywords) < 0)
	    return SIEVE_FAIL;

    return SIEVE_OK;
}

static bool contains_8bit(const char *msg)
{
	const unsigned char *s = (const unsigned char *)msg;

	for (; *s != '\0'; s++) {
		if ((*s & 0x80) != 0)
			return TRUE;
	}
	return FALSE;
}

static int sieve_notify(void *ac,
			void *ic __attr_unused__,
			void *sc __attr_unused__,
			void *mc,
			const char **errmsg)
{
    sieve_notify_context_t *nc = (sieve_notify_context_t *) ac;
    sieve_msgdata_t *m = mc;

    int nopt = 0;
    FILE *f;
    struct smtp_client *smtp_client;
    const char *outmsgid;

    /* "default" is "mailto" as only one... */
    if (!strcasecmp(nc->method, "default")) nc->method = "mailto";
    /* check method */
    if (strcasecmp(nc->method, "mailto")) { 
        *errmsg = "Unknown [unimplemented] notify method";
	/* just log error, failed notify is not reason to abort all script. */
        i_info("SIEVE ERROR: Unknown [unimplemented] notify method <%s>", 
	nc->method);
	return SIEVE_OK;
    }    
    /* count options */
    while (nc->options[nopt]) {
	smtp_client = smtp_client_open(nc->options[nopt], NULL, &f);
	outmsgid = deliver_get_new_message_id();
	fprintf(f, "Message-ID: %s\r\n", outmsgid);
	fprintf(f, "Date: %s\r\n", message_date_create(ioloop_time));
	fprintf(f, "X-Sieve: %s\r\n", SIEVE_VERSION);
	if ( strcasecmp(nc->priority, "high") == 0 ) {
            fprintf(f, "X-Priority: 1 (Highest)\r\n");
	    fprintf(f, "Importance: High\r\n");
        } else if ( strcasecmp(nc->priority, "normal") == 0 ) {
            fprintf(f, "X-Priority: 3 (Normal)\r\n");
	    fprintf(f, "Importance: Normal\r\n");
	} else if ( strcasecmp(nc->priority, "low") == 0 ) {
	    fprintf(f, "X-Priority: 5 (Lowest)\r\n");
	    fprintf(f, "Importance: Low\r\n");
	/* RFC: If no importance is given, the default value is "2 (Normal)" */
	} else {
	    fprintf(f, "X-Priority: 3 (Normal)\r\n");
	    fprintf(f, "Importance: Normal\r\n");
	} 
	fprintf(f, "From: Postmaster <%s>\r\n",
		deliver_set->postmaster_address);
	fprintf(f, "To: <%s>\r\n", nc->options[nopt]);
	fprintf(f, "Subject: [SIEVE] New mail notification\r\n");
        fprintf(f, "Auto-Submitted: auto-generated (notify)\r\n");
	fprintf(f, "Precedence: bulk\r\n");
        if (contains_8bit(nc->message)) {
            fprintf(f, "MIME-Version: 1.0\r\n");
	    fprintf(f, "Content-Type: text/plain; charset=UTF-8\r\n");
	    fprintf(f, "Content-Transfer-Encoding: 8bit\r\n");
	}
	fprintf(f, "\r\n");
	fprintf(f, "%s\r\n", nc->message);
	if (smtp_client_close(smtp_client) == 0) {
		i_info("msgid=%s: sent notification to <%s> (method=%s)",
		       m->id == NULL ? "" : str_sanitize(m->id, 80),
		       str_sanitize(nc->options[nopt], 80), nc->method);
	} else {
		i_info("msgid=%s: ERROR sending notification to <%s> "
		       "(method=%s)",
		       m->id == NULL ? "" : str_sanitize(m->id, 80),
		       str_sanitize(nc->options[nopt], 80), nc->method);
		*errmsg = "Error sending notify mail";
	}
	nopt = nopt + 1;
    }
    return SIEVE_OK;
}

static int autorespond(void *ac, 
		       void *ic __attr_unused__,
		       void *sc,
		       void *mc __attr_unused__,
		       const char **errmsg __attr_unused__)
{
    sieve_autorespond_context_t *arc = (sieve_autorespond_context_t *) ac;
    script_data_t *sd = (script_data_t *) sc;
    int ret;

    /* ok, let's see if we've responded before */
    ret = duplicate_check(arc->hash, arc->len,  sd->username) ?
	    SIEVE_DONE : SIEVE_OK;

    if (ret == SIEVE_OK) {
	duplicate_mark(arc->hash, arc->len, sd->username,
		       ioloop_time + arc->days * (24 * 60 * 60));
    }

    return ret;
}

static int send_response(void *ac, 
			 void *ic __attr_unused__, 
			 void *sc, void *mc,
			 const char **errmsg)
{
    struct smtp_client *smtp_client;
    FILE *f;
    const char *outmsgid;
    sieve_send_response_context_t *src = (sieve_send_response_context_t *) ac;
    script_data_t *sdata = (script_data_t *) sc;
    sieve_msgdata_t *md = mc;

    smtp_client = smtp_client_open(src->addr, NULL, &f);

    outmsgid = deliver_get_new_message_id();
    fprintf(f, "Message-ID: %s\r\n", outmsgid);
    fprintf(f, "Date: %s\r\n", message_date_create(ioloop_time));
    
    fprintf(f, "X-Sieve: %s\r\n", SIEVE_VERSION);
    fprintf(f, "From: <%s>\r\n", src->fromaddr);
    fprintf(f, "To: <%s>\r\n", src->addr);
    fprintf(f, "Subject: %s\r\n", str_sanitize(src->subj, 80));
    if (md->id) fprintf(f, "In-Reply-To: %s\r\n", md->id);
    fprintf(f, "Auto-Submitted: auto-replied (vacation)\r\n");
    fprintf(f, "Precedence: bulk\r\n");
    fprintf(f, "MIME-Version: 1.0\r\n");
    if (src->mime) {
	fprintf(f, "Content-Type: multipart/mixed;"
		"\r\n\tboundary=\"%s/%s\"\r\n", my_pid, deliver_set->hostname);
	fprintf(f, "\r\nThis is a MIME-encapsulated message\r\n\r\n");
	fprintf(f, "--%s/%s\r\n", my_pid, deliver_set->hostname);
    } else {
	fprintf(f, "Content-Type: text/plain; charset=utf-8\r\n");
	fprintf(f, "Content-Transfer-Encoding: 8bit\r\n");
	fprintf(f, "\r\n");
    }

    fprintf(f, "%s\r\n", src->msg);
    if (src->mime)
	fprintf(f, "\r\n--%s/%s--\r\n", my_pid, deliver_set->hostname);

    if (smtp_client_close(smtp_client) == 0) {
        duplicate_mark(outmsgid, strlen(outmsgid),
                       sdata->username, ioloop_time + DUPLICATE_DEFAULT_KEEP);
	return SIEVE_OK;
    } else {
	*errmsg = "Error sending mail";
	return SIEVE_FAIL;
    }
}

/* vacation support */
sieve_vacation_t vacation = {
    1,				/* min response */
    31,				/* max response */
    &autorespond,		/* autorespond() */
    &send_response		/* send_response() */
};

/* imapflags support */
static char *markflags[] = { "\\flagged" };
static sieve_imapflags_t mark = { markflags, 1 };

static int sieve_parse_error_handler(int lineno, const char *msg, 
				     void *ic __attr_unused__,
				     void *sc)
{
    script_data_t *sd = (script_data_t *) sc;

    if (sd->errors == NULL) {
	    sd->errors = str_new(default_pool, 1024);
	    i_info("sieve parse error: line %d: %s", lineno, msg);
    }

    str_printfa(sd->errors, "line %d: %s\n", lineno, msg);
    return SIEVE_OK;
}

static int sieve_execute_error_handler(const char *msg, 
				       void *ic __attr_unused__,
				       void *sc __attr_unused__,
				       void *mc __attr_unused__)
{
    i_info("sieve runtime error: %s", msg);
    return SIEVE_OK;
}
 
static sieve_interp_t *setup_sieve(void)
{
    sieve_interp_t *interp = NULL;
    int res;

    res = sieve_interp_alloc(&interp, NULL);
    if (res != SIEVE_OK)
	i_fatal("sieve_interp_alloc() returns %d\n", res);

    res = sieve_register_redirect(interp, &sieve_redirect);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_redirect() returns %d\n", res);
    res = sieve_register_discard(interp, &sieve_discard);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_discard() returns %d\n", res);
    res = sieve_register_reject(interp, &sieve_reject);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_reject() returns %d\n", res);
    res = sieve_register_fileinto(interp, &sieve_fileinto);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_fileinto() returns %d\n", res);
    res = sieve_register_keep(interp, &sieve_keep);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_keep() returns %d\n", res);
    res = sieve_register_imapflags(interp, &mark);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_imapflags() returns %d\n", res);
    res = sieve_register_notify(interp, &sieve_notify);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_notify() returns %d\n", res);
    res = sieve_register_size(interp, &getsize);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_size() returns %d\n", res);
    res = sieve_register_header(interp, &getheader);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_header() returns %d\n", res);

    res = sieve_register_envelope(interp, &getenvelope);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_envelope() returns %d\n", res);
    res = sieve_register_vacation(interp, &vacation);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_vacation() returns %d\n", res);
    res = sieve_register_parse_error(interp, &sieve_parse_error_handler);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_parse_error() returns %d\n", res);
    res = sieve_register_execute_error(interp,  &sieve_execute_error_handler);
    if (res != SIEVE_OK)
	i_fatal("sieve_register_execute_error() returns %d\n", res);

    return interp;
}

static int
dovecot_sieve_compile(sieve_interp_t *interp, script_data_t *sdata,
		      const char *script_path, const char *compiled_path)
{
	struct stat st, st2;
	sieve_script_t *script;
	bytecode_info_t *bc;
	const char *temp_path;
	FILE *f;
	int fd, ret;

	if (stat(script_path, &st) < 0) {
		if (errno == ENOENT) {
			if (getenv("DEBUG") != NULL) {
				i_info("cmusieve: Script not found: %s",
				       script_path);
			}
			return 0;
		}
		i_error("stat(%s) failed: %m", script_path);
		return -1;
	}
	if (stat(compiled_path, &st2) < 0) {
		if (errno != ENOENT) {
			i_error("stat(%s) failed: %m", script_path);
			return -1;
		}
	} else {
		if (st.st_mtime < st2.st_mtime)
			return 1;
	}

	/* need to compile */
	f = fopen(script_path, "r");
	if (f == NULL) {
		i_error("fopen(%s) failed: %m", script_path);
		return -1;
	}

	ret = sieve_script_parse(interp, f, sdata, &script);
	if (ret != SIEVE_OK) {
		if (sdata->errors == NULL) {
			sdata->errors = str_new(default_pool, 128);
			str_printfa(sdata->errors, "parse error %d", ret);
		}
		return -1;
	}

	if (sieve_generate_bytecode(&bc, script) < 0) {
		i_error("sieve_generate_bytecode() failed");
		return -1;
	}

	/* write to temp file */
	temp_path = t_strconcat(compiled_path, ".tmp", NULL);
	fd = open(temp_path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
	if(fd == -1) {
		i_error("open(%s) failed: %m", temp_path);
		return -1;
	}

	if (sieve_emit_bytecode(fd, bc) < 0) {
		i_error("sieve_emit_bytecode() failed");
		return -1;
	}

	if (close(fd) < 0)
		i_error("close() failed: %m");

	/* and finally replace the script */
	if (rename(temp_path, compiled_path) < 0) {
		i_error("rename(%s, %s) failed: %m", temp_path, compiled_path);
		return -1;
	}
	return 1;
}

static void
dovecot_sieve_write_error_file(script_data_t *sdata, const char *path)
{
	int fd;

	fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
	if (fd == -1) {
		i_error("open(%s) failed: %m", path);
		return;
	}

	if (write_full(fd, str_data(sdata->errors), str_len(sdata->errors)) < 0)
		i_error("write_full(%s) failed: %m", path);

	if (close(fd) < 0)
		i_error("close() failed: %m");
}

int cmu_sieve_run(struct mail_storage *storage, struct mail *mail,
		  const char *script_path, const char *username,
		  const char *mailbox)
{
	sieve_interp_t *interp;
	sieve_bytecode_t *bytecode;
	script_data_t sdata;
	sieve_msgdata_t mdata;
	const char *compiled_path, *path;
	int ret;

	interp = setup_sieve();

	memset(&sdata, 0, sizeof(sdata));
	sdata.username = username;
	sdata.storage = storage;

	compiled_path = t_strconcat(script_path, "c", NULL);
	ret = dovecot_sieve_compile(interp, &sdata, script_path, compiled_path);

	if (sdata.errors != NULL) {
		if (getenv("DEBUG") != NULL) {
			i_info("cmusieve: Compilation failed for %s: %s",
			       script_path,
			       str_sanitize(str_c(sdata.errors), 80));
		}
		path = t_strconcat(script_path, ".err", NULL);
		dovecot_sieve_write_error_file(&sdata, path);
		str_free(&sdata.errors);
	}
	if (ret <= 0)
		return ret;

	memset(&mdata, 0, sizeof(mdata));
	mdata.mail = mail;
	mdata.mailbox = mailbox;
	mdata.authuser = username;
	mdata.id = mail_get_first_header(mail, "Message-ID");
	mdata.return_path = deliver_get_return_address(mail);

	if ((ret = sieve_script_load(compiled_path, &bytecode)) != SIEVE_OK) {
		i_error("sieve_script_load(%s) failed: %d", compiled_path, ret);
		return -1;
	}

	if (getenv("DEBUG") != NULL)
		i_info("cmusieve: Executing script %s", compiled_path);

	if (sieve_execute_bytecode(bytecode, interp,
				   &sdata, &mdata) != SIEVE_OK) {
		i_error("sieve_execute_bytecode(%s) failed", compiled_path);
		return -1;
	}

	return 1;
}


syntax highlighted by Code2HTML, v. 0.9.1