/* $Cambridge: hermes/src/prayer/session/account_sieve.c,v 1.7 2011/09/09 16:34:36 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

/* Copyright (c) University of Cambridge 2000 - 2008 */
/* See the file NOTICE for conditions of use and distribution. */

/* Auxillary routines for accountd service */

#include "prayer_session.h"

#define LOCAL_BLOCK_SIZE (512)

/* ====================================================================== */

/* auto file has following format:
 *
 * vacation_aliases:
 * dpc22@cam.ac.uk, david.carter@ucs.cam.ac.uk, ...
 * vacation_msg:
 * Arbitary block of text with '.' stuffing like this
 * .here, ending with a line with a single .
 * .
 * filters:
 * Old fashioned MSforward style block, but with '.' stuffing so we
 * add additional parts later. '.' stuffing should be NOOP.
 *
 * Three files combined into single file to reduce transaction count.
 * Each line quoted with '#' to supress Sieve syntax check.
 */

/* ====================================================================== */

/* account_sieve_checksum() ***********************************************
 *
 * Tag a checksum string onto the end of an arbitary string
 *
 *************************************************************************/

static char *
account_sieve_checksum(char *s, struct pool *pool)
{
    unsigned long checksum = checksum_calculate(s, pool);
    struct buffer *b = buffer_create(pool, 1024);

    while (*s) {
        bputc(b, *s);
        s++;
    }
    bprintf(b, "%s%lu"CRLF, CHECKSUM_PREFIX, checksum);

    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* account_sieve_quote() *************************************************
 *
 * Quote arbitary text (by adding '#' to start of each line) so that
 * it can be uploaded to Sieve server bypassing syntax checks.
 *
 ************************************************************************/

static char *
account_sieve_quote(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, LOCAL_BLOCK_SIZE);

    bputc(b, '#');
    while (*s) {
        if ((s[0] != '\015') && (s[0] != '\012')) {
            bputc(b, *s);
            s++;
            continue;
        }

        s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;
        
        bputs(b, ""CRLF);
        if (*s)
            bputc(b, '#');
    }
    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* account_sieve_quote() *************************************************
 *
 * Unquote text from sieve server quoted by above
 *
 ************************************************************************/

static char *
account_sieve_unquote(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, LOCAL_BLOCK_SIZE);

    if (*s == '#')
        s++;
    
    while (*s) {
        if ((s[0] != '\015') && (s[0] != '\012')) {
            bputc(b, *s);
            s++;
            continue;
        }
        s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;
        bputs(b, ""CRLF);

        if (*s == '#')
            s++;
    }

    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* account_sieve_stuff_string() ******************************************
 *
 * Stuff string: quote '.' characters at the start of lines, convert all
 * CR, LF and CRLF to CRLF, and add (maybe CRLF)"."CRLF to the end.
 *
 * NIL is encoded as empty string.
 ************************************************************************/

static void
account_sieve_stuff_string(struct buffer *b, char *s)
{
    int col = 0;

    if (!(s && s[0])) {
        bputs(b, "."CRLF);
        return;
    }

    /* Deal with rare leading '.' as special case */
    if (*s == '.') {
        bputs(b, "..");
        s++;
    }

    while (*s) {
        if ((s[0] == '\015') || (s[0] == '\012')) {
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;
            bputs(b, ""CRLF);
            col = 0;

            /* Quote '.' chars at the start of the line SMTP style */
            if (*s == '.') {
                bputs(b, "..");
                s++;
                col++;
            }
        } else {
            bputc(b, *s);
            s++;
            col++;
        }
    }

    if (col > 0)
        bputs(b, ""CRLF);

    bputs(b, "."CRLF);
}

/* account_sieve_unstuff_string() ****************************************
 *
 * Remove '.' stuffing from string.
 ************************************************************************/

static char *
account_sieve_unstuff_string(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, LOCAL_BLOCK_SIZE);
    
    while (*s) {
        if ((s[0] != '\015') && (s[0] != '\012')) {
            bputc(b, *s);
            s++;
            continue;
        }

        s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

        if (*s == '.')
            s++;

        bputs(b, ""CRLF);
    }

    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* account_sieve_extract_stuffed_string() ********************************
 *
 * Isolate a stuffed string (terminated by single '.' on line by itself)
 ************************************************************************/

static char *
account_sieve_extract_stuffed_string(char **sp)
{
    char *result, *s;
    int col = 0;

    s = result = *sp;

    /* Find '.' on line by itself */
    while (*s) {
        if ((col == 0) &&
            (s[0] == '.') && ((s[1] == '\015') || (s[1] == '\012'))) {
            *s++ = '\0';
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

            *sp = s;
            return(result);
        }
        if ((s[0] == '\015') || (s[0] == '\012')) {
            s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;
            col = 0;
        } else {
            s++;
            col++;
        }
    }
    return(NIL);
}

/* account_sieve_get_stuffed_string() ************************************
 *
 * Combination of the above.
 ************************************************************************/

static char *
account_sieve_get_stuffed_string(char **sp, struct pool *pool)
{
    char *tmp;

    if (!(tmp=account_sieve_extract_stuffed_string(sp)))
        return(NIL);

    return(account_sieve_unstuff_string(tmp, pool));
}

/* account_sieve_break_lines() *******************************************
 *
 * Break list of addresses value, value, value into one address per line.
 ************************************************************************/

static char *
account_sieve_break_lines(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 128); /* Likely small */
    ADDRESS *addr = NIL;
    ADDRESS *a = NIL;

    s = string_trim_whitespace(pool_strdup(pool, s));

    if (!(s && s[0]))
        return(pool_strdup(pool, ""));

    if (!(addr=addr_parse_destructive(s, "")))
        return (NIL);  /* Shouldn't be possible in normal operation */
    
    for (a = addr; a; a = a->next) {
        if (a->mailbox && a->host)
            bprintf(b, "%s@%s"CRLF, a->mailbox, a->host);
    }
    mail_free_address(&addr);
    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* account_sieve_combine_lines() *****************************************
 *
 * Fold a CRLF string into value, value, value.
 *
 ************************************************************************/

static char *
account_sieve_combine_lines(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 128);

    if (*s == '#')
        s++;
     while (*s) {
        if ((s[0] != '\015') && (s[0] != '\012')) {
            bputc(b, *s);
            s++;
            continue;
        }

        s += ((s[0] == '\015') && (s[1] == '\012')) ? 2 : 1;

        if (*s)
            bputs(b, ", ");
    }

    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* ====================================================================== */
/* ====================================================================== */

/* Couple of small support functions for account_sieve_text */

static void
account_sieve_print_local_domains(struct buffer *b,
                                  struct session *session,
                                  BOOL force_comma)
{
    struct config *config = session->config;
    struct list_item *item;
    struct config_local_domain *cld;

    for (item = config->local_domain_list->head; item; item = item->next) {
        cld = (struct config_local_domain *)item;

        bprintf(b, "   \"%s@%s\"", session->username, cld->name);

        /* Rather messy */
        if (item->next || force_comma)
            bputc(b, ',');

        bputs(b, ""CRLF);
    }
}

static BOOL
account_sieve_print_aliases(struct account *account,
                            struct buffer *b, struct pool *pool)
{
    ADDRESS *addr = NIL;
    ADDRESS *a = NIL;
    BOOL rc = T;
    char *s = account->vacation_aliases;

    if (!(s && s[0]))
        return(T);

    if (!(addr=addr_parse(pool, s, "")))
        return (NIL);  /* Shouldn't be possible in normal operation */

    for (a = addr; a; a = a->next) {
        if (!(a->host && a->host[0])) {
            rc = NIL;
            break;
        }
        bprintf(b, "   \"%s@%s\"", a->mailbox, a->host);
        if (a->next)
            bputc(b, ',');
        bputs(b, ""CRLF);
    }

    mail_free_address(&addr);
    return(rc);
}

/* account_sieve_text() ***************************************************
 *
 * Convert account filter list to Sieve text which can be installed on
 * Cyrus server.
 * account:
 *    pool: Scratch pool nominated by client.
 *
 * Returns: Sieve text from buffer
 *          NIL => problem generating sieve text
 ***********************************************************************/


/* Small helper functions */

static BOOL
account_sieve_has_wild(char *list)
{
    return ((strchr(list, '?') || strchr(list, '*')) ? T : NIL);
}

static void
account_sieve_print_list(struct buffer *b, char *list, struct pool *pool)
{
    char *tmp = pool_strdup(pool, list); /* Scratch copy */
    char *s, c;

    while ((s=string_get_line(&tmp))) {
        if ((s=string_trim_whitespace(s)) && s[0]) {
            bputs(b, "      \"");
            while ((c=*s++)) {
                if (c == '\"')
                    bputc(b, '\\');
                bputc(b, c);
            }
            if (tmp && tmp[0])
                bputs(b, "\","CRLF);
            else
                bputs(b, "\""CRLF);
        }
    }
}

static char *
account_sieve_text(struct account *account, struct pool *pool)
{
    struct session *session = account->session;
    struct config *config = session->config;
    struct buffer *b = buffer_create(pool, ACCOUNT_BLOCKSIZE);
    struct list_item *item;
    int i;

    bputs(b, "# Automatically maintained sieve: do not edit directly"CRLF);
    bputs(b, "#"CRLF);
    bputs(b, ""CRLF);
    bputs(b, "require [ \"fileinto\", \"envelope\", \"vacation\" ];"CRLF);
    bputs(b, ""CRLF);

    for (item = account->block_list->head; item; item = item->next)
        filter_sieve_print((struct filter *) item, b);

    if (account->spam_enabled) {
        /* XXX Need config options here! */
        bputs(b, "if header :contains \"X-Cam-SpamScore\" \"");
        for (i=0 ; i < account->spam_threshold; i++)
            bputc(b, 's');
        bputs(b, "\" {"CRLF);
        if (account->spam_whitelist && account->spam_whitelist[0]) {
            BOOL need_match = account_sieve_has_wild(account->spam_whitelist);
            
            /* Whitelist should check both envelope and "From: " headers */
            /* Test envelope from address */
            bprintf(b, "   if envelope %s \"from\" ["CRLF,
                    (need_match) ? ":matches" : ":is");
            account_sieve_print_list(b, account->spam_whitelist, pool);
                                     
            bputs(b, "   ] {"CRLF);
            bputs(b, "      # Address on whitelist"CRLF);

            /* Test header from address */
            bprintf(b, "   } elsif address %s \"from\" ["CRLF,
                    (need_match) ? ":matches" : ":is");
            account_sieve_print_list(b, account->spam_whitelist, pool);

            /* Otherwise its spam */
            bputs(b, "   ] {"CRLF);
            bputs(b, "      # Address on whitelist"CRLF);
            bputs(b, "   } else {"CRLF);
            bputs(b, "      fileinto \"spam\";"CRLF);
            bputs(b, "      stop;"CRLF);
            bputs(b, "   }"CRLF);
        } else {
            bputs(b, "   fileinto \"spam\";"CRLF);
            bputs(b, "   stop;"CRLF);
        }
        bputs(b, "}"CRLF""CRLF);
    }

    if (account->vacation_enabled) {
        bputs(b, "vacation"CRLF);
        bprintf(b, "  :days %d"CRLF, account->vacation_days);
        if (account->vacation_subject && account->vacation_subject[0]) {
            char *s = account->vacation_subject;
            char c;

            bputs(b, "  :subject \"");
            while ((c=*s++)) {
                if (c == '\"')
                    bputc(b, '\\');
                bputc(b, c);
            }
            bputs(b, "\""CRLF);
        } else
            bputs(b, "  :subject \"Auto-Response: Vacation Message\""CRLF);

        if (account->vacation_aliases && account->vacation_aliases[0]) {
            bputs(b, "  :addresses ["CRLF);
            account_sieve_print_local_domains(b, session, T);
            account_sieve_print_aliases(account, b, pool);
            bputs(b, "  ]"CRLF);
        } else if (config->local_domain_list->head) {
            bputs(b, "  :addresses ["CRLF);
            account_sieve_print_local_domains(b, session, NIL);
            bputs(b, "  ]"CRLF);
        }

        bputs(b, "  text:"CRLF);
        account_sieve_stuff_string(b, account->vacation_msg);
        bputs(b, ";"CRLF""CRLF);
    }

    for (item = account->filter_list->head; item; item = item->next)
        filter_sieve_print((struct filter *) item, b);

    if (account->redirect_enabled) {
        bprintf(b, "redirect \"%s\";"CRLF, account->redirect_address);
        if (account->redirect_copy)
            bputs(b, "keep;"CRLF);
    }
    return (buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* ====================================================================== */

/* account_sieve_purge_text() *******************************************
 *
 * Generate spam_purge file text for Cyrus server.
 * account:
 *    pool: Scratch pool nominated by client.
 *
 * Returns: Sieve text from buffer
 *          NIL => problem generating sieve text
 ***********************************************************************/

static char *
account_sieve_purge_text(struct account *account, struct pool *pool)
{
    struct config *config = account->session->config;
    struct buffer *b = buffer_create(pool, 64);
    char *spam_purge_prefix = config->spam_purge_prefix;

    if (!(spam_purge_prefix && spam_purge_prefix[0]))
        spam_purge_prefix = "# Spam Purge Timeout:";

    if (account->spam_purge_enabled)
        bprintf(b, "%s %d\n", spam_purge_prefix, account->spam_purge_timeout);
    else
        bprintf(b, "%s -1\n", spam_purge_prefix);

    return (buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* ====================================================================== */

/* account_sieve_need_sieve() ********************************************
 *
 * Check whether sieve file required for this account
 *
 * Returns: T => Need sieve file
 *
 ************************************************************************/

static BOOL
account_sieve_need_sieve(struct account *account)
{
    if (account->vacation_enabled || account->redirect_enabled)
        return(T);

    if (account->filter_list->head || account->block_list->head)
        return(T);

    if (account->spam_enabled)
        return(T);

    return(NIL);
}

/* ====================================================================== */
/* ====================================================================== */
/* ====================================================================== */

/* External interface */

/* account_sieve_mail_check() *******************************************
 *
 * Check mail filtering and vacation message using above routines.
 * account:
 *    pool: Scratch stream
 *
 * Returns: T   => success (details recorded in account structure)
 *          NIL => error   (reason sent to account_message).
 *         
 ***********************************************************************/

BOOL
account_sieve_mail_check(struct account * account, struct pool * pool)
{
    struct session *session = account->session;
    struct sieve *sieve = session->sieve;
    char *auto_text;
    char *key = "";
    char *value = "";
    char *sieve_text = "";
    char *vacation_msg = "";
    char *vacation_aliases = "";
    char *vacation_subject = "";
    char *spam_whitelist = "";
    char *filters = "";

    account->sieve_have_auto  = NIL;
    
    if (!sieve_fetch(sieve, session, "sieve", &sieve_text)) {
        account_message(account, "Failed to download active sieve file");
        return(NIL);
    }
    sieve_current_record(sieve, sieve_text);
    sieve_live_record(sieve, sieve_text);

    if (sieve_text && sieve_text[0] && !checksum_test(sieve_text, pool)) {
        account_message(account, "Manually maintained Sieve file");
        return(NIL);
    }

    if (!sieve_fetch(sieve, session, "auto", &auto_text)) {
        account_message(account, "Failed to check mail redirection status");
        return(NIL);
    }

    if (!auto_text)  /* Nothing set up */
        return(T);

    account->sieve_have_auto  = T;

    if (!(auto_text = account_sieve_unquote(auto_text, pool))) {
        account_message(account, "Invalid mail processing state");
        return(NIL);
    }

    while (auto_text && auto_text[0]) {
        if ((key = string_get_line(&auto_text)))
            value = account_sieve_get_stuffed_string(&auto_text, pool);

        if (!(key && value)) {
            account_message(account, "Invalid mail processing state");
            return(NIL);
        }

        if (!strcmp(key, "vacation_aliases:"))
            vacation_aliases = value;
        else if (!strcmp(key, "vacation_subject:"))
            vacation_subject = value;
        else if (!strcmp(key, "vacation_msg:"))
            vacation_msg = value;
        else if (!strcmp(key, "spam_whitelist:"))
             spam_whitelist = value;
        else if (!strcmp(key, "filters:"))
            filters = value;
    }

    /* Fold vacation aliases into single line */
    vacation_aliases = account_sieve_combine_lines(vacation_aliases, pool);

    string_strdup(&account->vacation_msg, vacation_msg);
    string_strdup(&account->vacation_aliases, vacation_aliases);
    string_strdup(&account->vacation_subject, vacation_subject);
    string_strdup(&account->spam_whitelist, spam_whitelist);

    if (!account_support_process_filter(account, filters, pool))
        return(NIL);

    return(T);
}

/* ====================================================================== */

/* account_sieve_strip8bit() *********************************************
 *
 * Useful subset of ISO-8859-1 and UTF-8 is ASCII
 *
 ************************************************************************/

void
account_sieve_strip8bit(struct account *account)
{
    struct list_item *item;

    string_strip8bit(account->vacation_msg);
    string_strip8bit(account->vacation_aliases);
    string_strip8bit(account->spam_whitelist);
    string_strip8bit(account->redirect_address);

    for (item = account->filter_list->head; item; item = item->next)
        filter_strip8bit((struct filter *)item);

    for (item = account->block_list->head; item; item = item->next)
        filter_strip8bit((struct filter *)item);
}


/* account_sieve_mail_update() ******************************************
 *
 * Update mail filtering and vacation message to reflect account structure
 * account:
 *    pool: Scratch stream
 *
 * Returns: T   => success
 *          NIL => error   (reason sent to account_message).
 *         
 ***********************************************************************/

BOOL
account_sieve_mail_update(struct account *account, struct pool *pool)
{
    struct session *session = account->session;
    struct config  *config  = session->config;
    struct sieve *sieve = session->sieve;
    char *aliases, *filters, *auto_text, *sieve_text, *purge_text;
    struct buffer *b = buffer_create(pool, LOCAL_BLOCK_SIZE);
    char *spam_purge_name = config->spam_purge_name;

    account_sieve_strip8bit(account);

    if (!spam_purge_name && spam_purge_name[0])
        spam_purge_name = "spam_purge";

    if (account->vacation_aliases) {
        aliases = account_sieve_break_lines(account->vacation_aliases, pool);

        if (!aliases) {
            account_message(account, "Invalid vacation aliases");
            return(NIL);
        }
    } else
        aliases = "";

    if (account->vacation_aliases && account->vacation_aliases[0]) {
        bputs(b, "vacation_aliases:"CRLF);
        account_sieve_stuff_string(b, aliases);
    }

    if (account->vacation_subject && account->vacation_subject[0]) {
        bputs(b, "vacation_subject:"CRLF);
        account_sieve_stuff_string(b, account->vacation_subject);
    }

    if (account->vacation_msg && account->vacation_msg[0]) {
        bputs(b, "vacation_msg:"CRLF);
        account_sieve_stuff_string(b, account->vacation_msg);
    }

    if (account->spam_whitelist && account->spam_whitelist[0]) {
        bputs(b, "spam_whitelist:"CRLF);
        account_sieve_stuff_string(b, account->spam_whitelist);
    }

    bputs(b, "filters:"CRLF);
    filters = account_support_mail_text(account, pool, T);
    account_sieve_stuff_string(b, filters);

    auto_text = buffer_fetch(b, 0, buffer_size(b), NIL);  
    auto_text = account_sieve_quote(auto_text, pool);

    if (auto_text && auto_text[0]) {
        if (!sieve_upload(sieve, session, "auto", auto_text)) {
            account_message(account, "failed to upload auto file: %s",
                            sieve_fetch_message(sieve));
            return(NIL);
        }

        account->sieve_have_auto = T;
    } else {
        if (account->sieve_have_auto &&
            !sieve_delete(sieve, session, "auto")) {
            account_message(account, "failed to delete auto file: %s",
                            sieve_fetch_message(sieve));
            return(NIL);
        }

        account->sieve_have_auto = NIL;
    }

    if (account_sieve_need_sieve(account)) {
        sieve_text = account_sieve_text(account, pool);
        sieve_text = account_sieve_checksum(sieve_text, pool);

        sieve_current_record(sieve, sieve_text);

        if (!sieve_upload(sieve, session, "sieve", sieve_text)) {
            account_message(account, "failed to upload sieve file: %s",
                            sieve_fetch_message(sieve));
            return(NIL);
        }

        if (!sieve_activate(sieve, session, "sieve")) {
            account_message(account, "failed to activate sieve file: %s",
                            sieve_fetch_message(sieve));
            return(NIL);
        }

        sieve_live_record(sieve, sieve_text);
    } else {
        sieve_current_record(sieve, NIL);

        /* Need to deactivate sieve file before deleting with Cyrus 2.4 */
        if (sieve_live(sieve) && 
            (!sieve_activate(sieve, session, "") ||
             !sieve_delete(sieve, session, "sieve"))) {
            account_message(account, "failed to delete sieve file: %s",
                            sieve_fetch_message(sieve));
            return(NIL);
        }

        sieve_live_record(sieve, NIL);
    }

    /* Update the spam_purge hint */
    if (!account->spam_purge_enabled ||
        (account->spam_purge_timeout != config->spam_purge_timeout)) {
        purge_text = account_sieve_purge_text(account, pool);
        if (!sieve_upload(sieve, session, spam_purge_name, purge_text))
            return(NIL);
    } else
        sieve_delete(sieve, session, spam_purge_name);

    return(T);
}

/* ====================================================================== */

/* account_sieve_vacation_update() ***************************************
 *
 * Update vacation message on sieve server
 ************************************************************************/

BOOL
account_sieve_vacation_update(struct account * account, struct pool * pool)
{
    return(T);   /* NOOP with sieve backend: included in auto file */
}
