/* $Cambridge: hermes/src/prayer/accountd/filter.c,v 1.3 2003/05/13 12:53:30 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

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

#include "accountd.h"

/* Class for converting .MSforward rules into an Exim filter .forward file
 * Transliteration of some Perl that is used in our interactive menu system
 * interface, including a number of rather dubious rules */

struct filter {
    enum { CO_UNKNOWN, CO_VACATION, CO_SPAM,
        CO_SENDER, CO_RECIPIENT, CO_BLOCK, CO_SUBJECT,
        CO_REDIRECT
    } type;
    char *local_part;
    char *domain;
    char *subject;
    char *mailbox;
    char *address;
    char *threshold;
    BOOL copy;
};

/* Quoting for metacharacters: two levels of quoting required */
#define QMETA "\\\\"

/* Quoting for for wildcard characters: four levels of quoting required */
#define QWILD "\\\\\\\\"

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

/* Some Support routines for processing wildcards */

/* filter_quote_double() *************************************************
 *
 * Print string to file, quoting '"' characters.
 *   file: Output file
 *      s: String to quote.
 ************************************************************************/

static void filter_quote_double(FILE * file, char *s)
{
    char c;

    while ((c = *s++)) {
        if (c == '"')
            putc('\\', file);
        putc(c, file);
    }
}

/* filter_has_wildcard() *************************************************
 *
 * Check whether string contains wildcard character.
 *      s: String to check.
 *
 * Returns: T => string contains wildcard. NIL otherwise.
 ************************************************************************/

static BOOL filter_has_wildcard(char *s)
{
    char c;

    while ((c = *s++)) {
        if ((c == '?') || (c == '*') || (c == '"'))
            return (T);
    }
    return (NIL);
}

/* filter_quote_wildcard() ***********************************************
 *
 * Print string to file, quoting wildcard characters.
 *   file: Output file
 *      s: String to quote.
 ************************************************************************/

static void filter_quote_wildcard(FILE * file, char *s)
{
    char c;

    while ((c = *s++)) {
        switch (c) {
        case '[':
        case ']':
        case '\'':
        case '|':
        case '.':
        case '+':
        case '^':
        case '$':
        case '?':
        case '*':
            fprintf(file, "%s", QWILD);
            putc(c, file);
            break;
        case '"':
            fputc('\\', file);
            putc(c, file);
            break;
        default:
            putc(c, file);
        }
    }
}

/* filter_expand_wildcard() **********************************************
 *
 * Print string to file, expanding simple wildcard characters (? and *)
 * into Perl 5 compatible regexps used by Exim.
 *   file: Output file
 *      s: String to quote.
 ************************************************************************/

static void filter_expand_wildcard(FILE * file, char *s)
{
    char c;

    while ((c = *s++)) {
        switch (c) {
        case '[':
        case ']':
        case '\'':
        case '|':
        case '.':
        case '+':
        case '^':
        case '$':
            /* Quota regexp significant character */
            fprintf(file, "%s", QWILD);
            putc(c, file);
            break;
        case '?':
            /* Translate '?' wildcard into appropriate Exim filter file syntax */
            fprintf(file, "%s.", QMETA);
            break;
        case '*':
            /* Translate '*' wildcard into appropriate Exim filter file syntax */
            fprintf(file, "%s.%s*", QMETA, QMETA);
            break;
        case '"':
            fputc('\\', file);
            putc(c, file);
            break;
        default:
            putc(c, file);
        }
    }
}

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

/* filter_expand_local() *************************************************
 *
 * Print local part to file, expanding simple wildcard characters (? and
 * into Perl 5 compatible regexps used by Exim.
 *   file: Output file
 *      s: String to quote.
 ************************************************************************/

static void filter_expand_local(FILE * file, char *s)
{
    if (*s == '"')
        filter_quote_wildcard(file, s);
    else
        filter_expand_wildcard(file, s);
}

/* filter_expand_domain() *************************************************
 *
 * Print domain to file, expanding simple wildcard characters (? and into
 * Perl 5 compatible regexps used by Exim.
 *   file: Output file
 *      s: String to quote.
 ************************************************************************/

static void filter_expand_domain(FILE * file, char *s)
{
    filter_expand_wildcard(file, s);
}

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

/* Routines for generating parts of filter file */

/* filter_write_to() *****************************************************
 *
 * Write target mailbox
 *   config: Gloabl state
 *     file: Output file
 *  mailbox: Target mailbox
 *     copy: Store copy in inbox
 ************************************************************************/

static void
filter_write_to(struct config *config, FILE * file, char *mailbox,
                BOOL copy)
{
    if (copy)
        fprintf(file, "    unseen save \"");
    else
        fprintf(file, "    save \"");

    /* Ghastly restriction on Hermes for historical reasons. Yuck! */
    if (config->filter_restricted)
        fprintf(file, "mail/");

    filter_quote_double(file, mailbox);
    fprintf(file, "\"\n");
    fprintf(file, "    finish\n");
    fprintf(file, "endif\n");
}

/* filter_print_aliases() *************************************************
 *
 * Extract aliases list from vacation.aliases file and include in forward
 * file.
 *   config: Global state
 *     file: Output file
 ************************************************************************/

static BOOL filter_print_aliases(struct config *config, FILE * file)
{
    FILE *aliases;
    char buffer[MAXLENGTH];

    if ((aliases = fopen(config->aliases_name, "r")) == NIL)
        return (T);

    while (fgets(buffer, MAXLENGTH, aliases)) {
        unsigned long len = strlen(buffer);

        /* Chomp: buffer may or may not include a '\n' */
        if ((len > 0) && (buffer[len - 1] == '\n'))
            buffer[--len] = '\0';

        fprintf(file, "   alias %s\n", buffer);
    }
    fclose(aliases);

    return (T);
}

/* filter_print_vacation() ***********************************************
 *
 * Add vacation message to filter file
 *   config: Global state
 *   filter: Filter action structure (actually unused)
 *     file: Output file
 ************************************************************************/

static BOOL
filter_print_vacation(struct config *config, struct filter *filter,
                      FILE * file)
{
    struct passwd *pwd;

    if (!(pwd = getpwuid(getuid())))
        return (NIL);

    fprintf(file, "# MSshell :: vacation\n");
    fprintf(file, "if personal\n");
    fprintf(file, "   alias %s@cam.ac.uk\n", pwd->pw_name);

    if (!filter_print_aliases(config, file))
        return (NIL);

    fprintf(file, "then\n");
    fprintf(file, "   mail\n");
    fprintf(file, "   subject \"Auto reply Re: $h_subject\"\n");
    fprintf(file, "   text \"\\\n");
    fprintf(file, ("This message is automatically generated "
                   "in response to your mail\\n\\" "\n"));
    fprintf(file, ("message (perhaps re-directed) to "
                   "$local_part@hermes.cam.ac.uk.\\n\\n\"\n"));
    fprintf(file, "    file ${home}/vacation.message\n");
    fprintf(file, "    log  ${home}/vacation.log\n");
    fprintf(file, "    once ${home}/vacation.once\n");
    fprintf(file, "endif\n");
    fprintf(file, "\n");

    return (T);
}

/* filter_print_spam() ***************************************************
 *
 * Add spam clause to filter file
 *   config: Global state
 *   filter: Filter action structure (actually unused)
 *     file: Output file
 ************************************************************************/

static BOOL
filter_print_spam(struct config *config, struct filter *filter, FILE *file)
{
    int threshold, i;

    if (!(filter->threshold && string_isnumber(filter->threshold)))
        return(NIL);

    threshold = atoi(filter->threshold);

    /* XXX (threshold == 0) okay? */
    fprintf(file, "if $h_X-Cam-SpamScore contains \"");

    for (i=0 ; i < threshold; i++)
        fputc('s', file);

    fprintf(file, "\" then\n");
    fprintf(file, "    save mail/spam\n");
    fprintf(file, "    finish\n");
    fprintf(file, "endif\n");
    fprintf(file, "\n");

    return(T);
}

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

/* filter_print_sender() *************************************************
 *
 * Print SENDER filter
 *  config: Global state
 *  filter: Filter action structure
 *    file: Output file
 ************************************************************************/

static BOOL
filter_print_sender(struct config *config, struct filter *filter,
                    FILE * file)
{
    if (!((filter->local_part && filter->domain)))
        return (NIL);

    fprintf(file, "# MSshell :: sender\n");
    if (filter_has_wildcard(filter->local_part) ||
        filter_has_wildcard(filter->domain)) {
        fprintf(file, "if $sender_address matches \"%s^", QMETA);
        filter_expand_local(file, filter->local_part);
        fprintf(file, "@");
        filter_expand_domain(file, filter->domain);
        fprintf(file, "%s$\" then\n", QMETA);
    } else {
        fprintf(file, "if $sender_address is \"%s@%s\" then\n",
                filter->local_part, filter->domain);
    }
    filter_write_to(config, file, filter->mailbox, filter->copy);
    fprintf(file, "\n");

    return (T);
}

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

/* filter_print_recipient() **********************************************
 *
 * Print RECIPIENT filter
 *  config: Global state
 *  filter: Filter action structure
 *    file: Output file
 ************************************************************************/

static BOOL
filter_print_recipient(struct config *config,
                       struct filter *filter, FILE * file)
{
    if (!((filter->local_part && filter->domain)))
        return (NIL);

    fprintf(file, "# MSshell :: recip\n");
    if (filter_has_wildcard(filter->local_part) ||
        filter_has_wildcard(filter->domain)) {
        fprintf(file, "if foranyaddress $h_to:,$h_cc:\n");
        fprintf(file, "   ($thisaddress matches \"");
        filter_expand_local(file, filter->local_part);
        fprintf(file, "@");
        filter_expand_domain(file, filter->domain);
        fprintf(file, "\") then\n");
    } else {
        fprintf(file, "if foranyaddress $h_to:,$h_cc:\n");
        fprintf(file, "   ($thisaddress is \"%s@%s\") then\n",
                filter->local_part, filter->domain);
    }
    filter_write_to(config, file, filter->mailbox, filter->copy);
    fprintf(file, "\n");

    return (T);
}

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

/* filter_print_block() **************************************************
 *
 * Print BLOCK filter
 *    config: Global state
 *    filter: Filter action structure
 *      file: Output file
 ************************************************************************/

static BOOL
filter_print_block(struct config *config, struct filter *filter,
                   FILE * file)
{
    if (!((filter->local_part && filter->domain)))
        return (NIL);

    fprintf(file, "# MSshell :: block\n");
    if (filter_has_wildcard(filter->local_part) ||
        filter_has_wildcard(filter->domain)) {
        fprintf(file, "if $sender_address matches \"%s^", QMETA);
        filter_expand_local(file, filter->local_part);
        fprintf(file, "@");
        filter_expand_domain(file, filter->domain);
        fprintf(file, "%s$\" then\n", QMETA);
    } else {
        fprintf(file, "if $sender_address is \"%s@%s\" then\n",
                filter->local_part, filter->domain);
    }
    fprintf(file, "    seen finish\n");
    fprintf(file, "endif\n");
    fprintf(file, "\n");

    return (T);
}

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

/* filter_print_subject() ************************************************
 *
 * Print SUBJECT filter
 *   config: Global state
 *   filter: Filter action structure
 *     file: Output file
 ************************************************************************/

static BOOL
filter_print_subject(struct config *config, struct filter *filter,
                     FILE * file)
{
    if (!filter->subject)
        return (NIL);

    fprintf(file, "# MSshell :: subject\n");
    if (strchr(filter->subject, '?') || strchr(filter->subject, '*')) {
        fprintf(file, "if $h_subject: matches \"(?s)%s^", QMETA);
        filter_expand_wildcard(file, filter->subject);
        fprintf(file, "%s$\" then\n", QMETA);
    } else {
        fprintf(file, "if $h_subject: is \"");
        filter_quote_double(file, filter->subject);
        fprintf(file, "\" then\n");
    }

    filter_write_to(config, file, filter->mailbox, filter->copy);
    fprintf(file, "\n");
    return (T);
}

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

/* filter_print_redirect() ***********************************************
 *
 * Print REDIRECT filter
 *   config: Global state
 *   filter: Filter action structure
 *     file: Output file
 ************************************************************************/

static BOOL
filter_print_redirect(struct config *config, struct filter *filter,
                      FILE * file)
{
    fprintf(file, "# MSshell :: redirect\n");
    if (filter->copy)
        fprintf(file, "unseen deliver %s", filter->address);
    else
        fprintf(file, "deliver %s", filter->address);
    fprintf(file, "\n");

    return (T);
}

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

/* filter_clear() ********************************************************
 *
 * Clear out filter structure.
 *      filter: Filter action structure
 ************************************************************************/

static void filter_clear(struct filter *filter)
{
    filter->type = CO_UNKNOWN;
    filter->local_part = NIL;
    filter->domain = NIL;
    filter->subject = NIL;
    filter->mailbox = NIL;
    filter->address = NIL;
    filter->copy = NIL;
    filter->threshold = NIL;
}

/* filter_print() ********************************************************
 *
 * Print a single filter item, using the static support routines above.
 *   config: Global configuration
 *   filter: Filter action structure
 ************************************************************************/

static BOOL
filter_print(struct config *config, struct filter *filter, FILE * file)
{
    switch (filter->type) {
    case CO_UNKNOWN:
        return (NIL);
    case CO_VACATION:
        return (filter_print_vacation(config, filter, file));
    case CO_SPAM:
        return (filter_print_spam(config, filter, file));
    case CO_SENDER:
        return (filter_print_sender(config, filter, file));
    case CO_RECIPIENT:
        return (filter_print_recipient(config, filter, file));
    case CO_BLOCK:
        return (filter_print_block(config, filter, file));
    case CO_SUBJECT:
        return (filter_print_subject(config, filter, file));
    case CO_REDIRECT:
        return (filter_print_redirect(config, filter, file));
    }
    return (NIL);
}

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

/* Some simple string tokenisation routines stolen from prayer string.c */

/* filter_get_token() ****************************************************
 *
 * Get a token from current line
 *     sp:  Ptr to string. Returns ptr to following token
 *
 * Returns: ptr to next token
 ************************************************************************/

static char *filter_get_token(char **sp)
{
    char *result;
    char *s = *sp;

    /* Isolate first token */
    while ((*s == ' ') || (*s == '\t'))
        s++;
    result = s;

    while (*s && (*s != ' ') && (*s != '\t'))
        s++;

    if (*s)
        *s++ = '\0';

    while ((*s == ' ') || (*s == '\t'))
        s++;

    *sp = s;
    return (result);
}

/* filter_next_token() ****************************************************
 *
 * Move to next token (skips whitespace)
 *     sp:  Ptr to string. Returns ptr to next token
 *
 * Returns: ptr to next token
 ************************************************************************/

static char *filter_next_token(char **sp)
{
    char *s = *sp;

    /* Isolate first token */
    while ((*s == ' ') || (*s == '\t'))
        s++;

    *sp = s;
    return (s);
}

/* filter_trim_whitespace() ***********************************************
 *
 * Trim leading and trailing whitespace from a string
 *    string: String to trim
 *
 * Returns: Ptr to trimmed string
 *************************************************************************/

char *filter_trim_whitespace(char *string)
{
    unsigned long len;

    /* Remove leading whitespace */
    while ((string[0] == ' ') ||
           (string[0] == '\t') ||
           (string[0] == '\015') || (string[0] == '\012'))
        string++;

    /* Remove traiing whitespace */
    len = strlen(string);
    while ((len > 0) &&
           ((string[len - 1] == ' ') ||
            (string[len - 1] == '\t') ||
            (string[len - 1] == '\015') || (string[len - 1] == '\012')))
        len--;

    /* Tie off the string */
    string[len] = '\0';

    return (string);
}


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

/* filter_write() ********************************************************
 *
 * Write out an entire filter file
 *  config: Global configuration
 *    file: Output file
 *    text: Msforward contains to parse and translate into Exim .forward file
 *
 * Returns: T on sucess. NIL otherwise.
 ************************************************************************/

static BOOL filter_write(struct config *config, FILE * file, char *text)
{
    struct filter filter_space;
    struct filter *filter = &filter_space;
    char *s = text;
    char *t;

    fprintf(file, "# Exim Filter <-- don't change this line\n");
    fprintf(file,
            "# This file automatically generated. Do not edit!\n\n");;

    filter_clear(filter);
    for (; *s; s = t) {
        t = s;

        /* Get a line and tie it off */
        while (*t && (*t != '\n'))
            t++;

        if (*t)
            *t++ = '\0';

        /* Ignore comment lines */
        if (*s == '#')
            continue;

        /* End of block: process filter */
        if (*s == '\0') {
            if (!filter_print(config, filter, file))
                return (NIL);
            filter_clear(filter);
            continue;
        }

        if ((*s == ' ') || (*s == '\t')) {
            char *arg = filter_get_token(&s);
            char *value = filter_next_token(&s);

            if (!strcmp(arg, "local_part"))
                filter->local_part = value;
            else if (!strcmp(arg, "domain"))
                filter->domain = value;
            else if (!strcmp(arg, "subject"))
                filter->subject = value;
            else if (!strcmp(arg, "mailbox"))
                filter->mailbox = value;
            else if (!strcmp(arg, "address"))
                filter->address = value;
            else if (!strcmp(arg, "copy"))
                filter->copy = !strcmp(value, "true") ? T : NIL;
            else if (!strcmp(arg, "threshold"))
                filter->threshold = value;
            else
                return (NIL);
            continue;
        }

        if ((filter->type != CO_UNKNOWN)
            && !filter_print(config, filter, file))
            return (NIL);
        filter_clear(filter);

        s = filter_trim_whitespace(s);

        if (!strcmp(s, "vacation"))
            filter->type = CO_VACATION;
        else if (!strcmp(s, "spam"))
            filter->type = CO_SPAM;
        else if (!strcmp(s, "sender"))
            filter->type = CO_SENDER;
        else if (!strcmp(s, "recip"))
            filter->type = CO_RECIPIENT;
        else if (!strcmp(s, "recipient"))
            filter->type = CO_RECIPIENT;
        else if (!strcmp(s, "block"))
            filter->type = CO_BLOCK;
        else if (!strcmp(s, "subject"))
            filter->type = CO_SUBJECT;
        else if (!strcmp(s, "redirect"))
            filter->type = CO_REDIRECT;
    }
    if ((filter->type != CO_UNKNOWN)
        && !filter_print(config, filter, file))
        return (NIL);

    return (T);
}

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

/* filter_write_forward() ************************************************
 *
 * Write out .forward file. This is external interface to filter module
 *     config: Global configuration
 *       text: MSforward text to parse and translate into Exim .forward
 *
 * Returns: T on success. NIL otherwise
 ************************************************************************/

BOOL filter_write_forward(struct config * config, char *text)
{
    FILE *file;
    BOOL rc;

    if ((file = fopen(config->forward_name, "w")) == NIL)
        return (NIL);

    rc = filter_write(config, file, (char *) text);

    if (fclose(file))           /* Probable quota error */
        rc = NIL;

    /* Better than leaving it in an inconsistent state */
    if (!rc)
        unlink(config->forward_name);

    if (!checksum_generate(config->forward_name))
        unlink(config->forward_name);

    return (rc);
}

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

/* filter_empty() ********************************************************
 *
 * Check whether filter text is empty (approximation: contains alphanumeric
 * characters outside comment line).
 *    text: Filter file to test
 *
 * Returns: T => no actions defined by filter file.
 ************************************************************************/

BOOL filter_empty(char *s)
{
    char c;

    while ((c = *s++)) {
        if (c == '#') {
            /* Skip comment line */
            while ((c = *s) && (c != '\015') && (c != '\012'))
                s++;
            continue;
        }
        if (Uisalpha(c))
            return (NIL);
    }
    return (T);
}
