/* $Cambridge: hermes/src/prayer/session/sieve.c,v 1.4 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 sieve service */

#include "prayer_session.h"

/* sieve_create() *****************************************************
 *
 * Create a fresh sieve structure based on session defaults.
 * Returns: sieve structure
 ***********************************************************************/

struct sieve *sieve_create(struct pool *pool, struct session *session)
{
    struct sieve *result = pool_alloc(pool, sizeof(struct sieve));

    result->stream = NIL;
    result->last_ping = (time_t) 0L;

    result->checked = NIL;
    result->current = NIL;
    result->live    = NIL;
    result->have_message = NIL;
    result->message = memblock_create(pool, 64);

    return (result);
}

/* sieve_free() *********************************************************
 *
 * Free sieve strucure
 *   Closes down main and NIS streams if active.
 * 
 ***********************************************************************/

void sieve_free(struct sieve *sieve)
{
    if (sieve->stream)
        iostream_close(sieve->stream);

    if (sieve->pool)
        return;

    memblock_free(sieve->message);
}

/* sieve_current_record() ***********************************************
 *
 * Record current current live sieve script
 * 
 ***********************************************************************/

void
sieve_current_record(struct sieve *sieve, char *script)
{
    string_strdup(&sieve->current, script);
}

/* sieve_current() *******************************************************
 *
 * Retrieve current live sieve script
 ***********************************************************************/


char *
sieve_current(struct sieve *sieve)
{
    return(sieve->current);
}

/* sieve_live_record() **************************************************
 *
 * Record live sieve script
 * 
 ***********************************************************************/

void
sieve_live_record(struct sieve *sieve, char *script)
{
    string_strdup(&sieve->live, script);
}

/* sieve_live() *********************************************************
 *
 * Retrieve current live sieve script
 ***********************************************************************/

char *
sieve_live(struct sieve *sieve)
{
    return(sieve->live);
}

/* sieve_close() *********************************************************
 *
 * Close down connections to sieve servers
 ************************************************************************/

void
sieve_close(struct sieve *sieve)
{
    if (sieve->stream)
        iostream_close(sieve->stream);

    sieve->stream = NIL;
}

/* sieve_timeout_close() *************************************************
 *
 * Close down connections to sieve servers
 ************************************************************************/

void
sieve_timeout_close(struct sieve *sieve, struct session *session)
{
    struct config *config = session->config;
    time_t now = time(NIL);

    if (sieve->stream && (config->sieved_timeout > 0) &&
        ((sieve->last_ping + config->sieved_timeout) < now)) {
        iostream_close(sieve->stream);
        sieve->stream = NIL;
    }
}

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

/* sieve_message() ******************************************************
 *
 * Record status message for later feedback to user.
 * sieve:
 *     fmt: printf format string, followed by arguments.
 * 
 ***********************************************************************/

void sieve_message(struct sieve *sieve, char *fmt, ...)
{
    struct memblock *m = sieve->message;
    va_list ap;
    unsigned long size;

    va_start(ap, fmt);
    size = memblock_vprintf_size(m, fmt, ap);
    va_end(ap);

    memblock_resize(m, size + 1);

    va_start(ap, fmt);
    memblock_vprintf(m, fmt, ap);
    va_end(ap);

    sieve->have_message = T;
}

/* sieve_fetch_message() **********************************************
 *
 * Record status message for later feedback to user.
 * sieve:
 *     fmt: printf format string, followed by arguments.
 * 
 ***********************************************************************/

char *sieve_fetch_message(struct sieve *sieve)
{
    if (sieve->have_message) {
        sieve->have_message = NIL;
        return (memblock_data(sieve->message));
    }

    return (NIL);
}

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

/* XXX Comment */

static char *
sieve_get_token(struct iostream *stream, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 64);
    int c;

    /* Skip leading whitespace */
    while (((c = iogetc(stream)) != EOF) && string_isspace(c))
        ;

    if (c == EOF)
        return(NIL);

    bputc(b, c);

    while (((c = iogetc(stream)) != EOF) &&
           (c != '\015') && (c != '\012') && !string_isspace(c))
        bputc(b, c);

    if (c == EOF)
        return(NIL);

    ioungetc(c, stream);

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

static char *
sieve_get_string(struct iostream *stream, struct pool *pool)
{
    struct buffer *b;
    char *result, *s;
    unsigned long size = 0;
    int c;

    /* Skip leading whitespace */
    while (((c = iogetc(stream)) != EOF) && string_isspace(c))
        ;

    switch (c) {
    case EOF:
        return(NIL);
    case '{':
        while (((c = iogetc(stream)) != EOF) && Uisdigit(c))
            size = (size*10) + (c - '0');

        if (c != '}')
            return(NIL);

        if ((c=iogetc(stream)) == '\015')
            c=iogetc(stream);

        if (c != '\012')
            return(NIL);

        s = result = pool_alloc(pool, size+1);

        while ((size > 0) && ((c = iogetc(stream)) != EOF)) {
            *s++ = c;
            size--;
        }
        *s = '\0';
        return((c == EOF) ? NIL : result);
    case '"':
        b = buffer_create(pool, 64);

        while (((c = iogetc(stream)) != EOF) && (c != '"'))
            bputc(b, c);

        if (c != '"')
            return(NIL);

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

    default:
        ioungetc(c, stream);
        return(sieve_get_token(stream, pool));
    }
    return(NIL);
}

static BOOL
sieve_eatline(struct iostream *stream)
{
    int c;

    while ((c=iogetc(stream)) != EOF) {
        if ((c == '\015') || (c == '\012'))
            break;
    }                                         

    if (c == '\015')
        c = iogetc(stream);

    return((c == '\012') ? T : NIL);
}

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

/* sieve_get_line() *****************************************************
 *
 * Get a line (of arbitary length) from input stream.
 * stream: iostream linked to one of the sieve servers
 *   pool: Target pool (typically scratch)
 *
 * Returns: NUL terminated string
 ***********************************************************************/

static char *sieve_get_line(struct iostream *stream, struct pool *pool)
{
    char *result;

    do {
        struct buffer *b = buffer_create(pool, 64);
        int c;

        while ((c = iogetc(stream)) != EOF) {
            if (c == '\015')
                continue;
            if (c == '\012')
                break;

            bputc(b, c);
        }
        if (c == EOF)
            return (NIL);

        result = buffer_fetch(b, 0, buffer_size(b), NIL);
    }
    while (!(result && result[0] && (result[0] != ' ')));

    return (result);
}

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

#if 0
/* Rudimentary conversion ISO-8859-1 --> UTF-8
 *
 * Characters 0x00 -> 0x7f encode as single UTF-8 byte as is.
 * Characters 0x80 -> 0xff encode as two byte sequence using following rule:
 *
 *   U-00000080 - U-000007FF:  110xxxxx 10xxxxxx
 *
*/

static char *
sieve_latin1_to_utf8(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 1024);
    unsigned char *t = s;
    unsigned char c;

    while ((c=*t++)) {
        if (c & 0x80) {
            bputc(b, ((c & 0x40) ? 0xc3 : 0xc2));
            bputc(b, (0x80 + (c & 0x3f)));
        } else
            bputc(b, c);
    }
    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}

/* Decode UTF-8 string. NIL => not valid ISO-8859-1 result */

static char *
sieve_utf8_to_latin1(char *s, struct pool *pool)
{
    struct buffer *b = buffer_create(pool, 1024);
    unsigned char *t = s;
    unsigned char c, d;

    while ((c=*t++)) {
        if (c & 0x80) {
            if ((d = *t++) == 0x00) return(NIL);
            if ((c & 0xe0) != 0xc0) return(NIL);
            if ((d & 0xc0) != 0x80) return(NIL);

            d &= 0x3f;

            if (c == 0xc3) {
                bputc(b, ((0x80 + 0x40) + d)); /* Chars 224 through 255 */
            } else if (c == 0xc2) {
                bputc(b, (0x80 +g d));          /* Chars 128 through 223 */
            } else 
                return(NIL);
        } else
            bputc(b, c);                       /* Chars   0 through 127 */
    }
    return(buffer_fetch(b, 0, buffer_size(b), NIL));
}
#endif

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

/* Utility routine stolen from c-client (rfc822_binary()).
 *
 * Convert binary contents to BASE64
 * Accepts: source
 *	    length of source
 *	    pointer to return destination length
 * Returns: destination as BASE64
 */

static char *my_base64 (struct pool *pool, void *src,unsigned long srcl)
{
    char *ret;
    unsigned char *d;
    unsigned char *s = (unsigned char *) src;
    char *v = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    unsigned long i = ((srcl + 2) / 3) * 4;

  d = (unsigned char *) pool_alloc (pool, (size_t) ++i);
  ret = (char *)d;
				/* process tuplets */
  for (i = 0; srcl >= 3; s += 3, srcl -= 3) {
    *d++ = v[s[0] >> 2];	/* byte 1: high 6 bits (1) */
				/* byte 2: low 2 bits (1), high 4 bits (2) */
    *d++ = v[((s[0] << 4) + (s[1] >> 4)) & 0x3f];
				/* byte 3: low 4 bits (2), high 2 bits (3) */
    *d++ = v[((s[1] << 2) + (s[2] >> 6)) & 0x3f];
    *d++ = v[s[2] & 0x3f];	/* byte 4: low 6 bits (3) */
  }
  if (srcl) {
    *d++ = v[s[0] >> 2];	/* byte 1: high 6 bits (1) */
				/* byte 2: low 2 bits (1), high 4 bits (2) */
    *d++ = v[((s[0] << 4) + (--srcl ? (s[1] >> 4) : 0)) & 0x3f];
				/* byte 3: low 4 bits (2), high 2 bits (3) */
    *d++ = srcl ? v[((s[1] << 2) + (--srcl ? (s[2] >> 6) : 0)) & 0x3f] : '=';
				/* byte 4: low 6 bits (3) */
    *d++ = srcl ? v[s[2] & 0x3f] : '=';
    if (srcl) srcl--;		/* count third character if processed */
  }
  *d = '\0';			/* tie off string */
  return ret;			/* return the resulting string */
}

/* sieve_encode_userpass() ***********************************************
 *
 * Encode Username and password into slightly peculiar format used
 * by SASL "PLAIN" mechanism: "\0$username\0$password" encoded BASE64
 *
 *     pool: scratch pool
 * username: } Information to encode
 * password: }
 *
 * Returns: encoded string allocated from scratch pool.
 ************************************************************************/

static char *
sieve_encode_userpass(struct pool *pool, char *username, char *password)
{
    unsigned long src_len = strlen(username)+strlen(password)+2;
    char *src = pool_alloc(pool, src_len+1);
    
    sprintf(src, "%c%s%c%s", '\0', username, '\0', password);
    
    return(my_base64(pool, src, src_len));
}

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

/* sieve_connect() ******************************************************
 *
 * Connect to nominated server
 * sieve:
 * session:
 *  server: Name of server with port and SSL modifiers.
 *          e.g: prism-intramail:145 or server:8082/ssl
 *
 * Returns: iostream to server
 *          NIL if connection attempt failed. (reason to session_message)
 ***********************************************************************/

static struct iostream *
sieve_connect(struct sieve *sieve, struct session *session,
              char *server)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;  /* Scratch space */
    unsigned long port = SIEVE_DEFAULT_PORT;
    int sockfd;
    char *line, *rcode, *text, *s, *t, c;
    BOOL use_ssl = NIL;
    struct iostream *stream;
    char *token;

    if (server == NIL)
        return (NIL);

    server = pool_strdup(pool, server);

    /* Split up string of form server:port/ssl or server/ssl:port */
    s = server;
    while (*s) {
        switch (*s) {
        case ':':
            *s++ = '\0';
            port = atoi(s);
            while (Uisdigit(*s))
                s++;
            break;
        case '/':
            *s++ = '\0';

            /* Isolate the next token */
            t = s;
            while (*t && (*t != '/') && (*t != ':'))
                t++;

            c = *t;
            *t = '\0';
            if (!strcmp(s, "ssl"))
                use_ssl = T;
            *t = c;
            s = t;
            break;
        default:
            s++;
        }
    }

    if (port == 0L)
        return (NIL);

    /* Bind connection to sieve server */
    if ((sockfd = os_connect_inet_socket(server, port)) < 0) {
        session_message(session, "Can't communicate with sieve server");
        session_log(session,
                    "[sieve_connect]: os_connect_inet_socket() failed");
        return (NIL);
    }

    /* Bind the iostream */
    if (!(stream = iostream_create(NIL, sockfd, SIEVE_BLOCKSIZE))) {
        session_message(session, "Can't communicate with sieve system");
        session_log(session,
                    "[sieve_connect]: iostream_create() failed");
        return (NIL);
    }

    if (config->sieved_timeout)
        iostream_set_timeout(stream, config->sieved_timeout);

    /* Start SSL if requested */
    if (use_ssl)
        iostream_ssl_start_client(stream);

    /* Munch initial capanility stuff */
    line = sieve_get_line(stream, pool);
    token = string_get_token(&line);

    while (token && (strcmp(token, "OK") != 0)) {
        line = sieve_get_line(stream, pool);
        token = string_get_token(&line);
    }

    if (!token)
        return (NIL);

    /* Initial login attempt */
    ioprintf(stream, "AUTHENTICATE \"PLAIN\" \"%s\"" CRLF,
             sieve_encode_userpass(request->pool,
                                   session->username, session->password));
    if (!ioflush(stream))
        return (NIL);

    /* Get result line */
    if ((line = sieve_get_line(stream, pool)) == NIL)
        return (NIL);

    rcode = string_get_token(&line);
    text = line;
    string_ucase(rcode);

    if (!strcmp(rcode, "OK"))
        return (stream);

    if (!strcmp(rcode, "NO") && session->newpassword) {
        /* Try replacement password */
        ioprintf(stream, "AUTHENTICATE \"PLAIN\" \"%s\"" CRLF,
                 sieve_encode_userpass(request->pool,
                                       session->username,
                                       session->newpassword));
        if (!ioflush(stream))
            return (NIL);

        /* Get result line */
        if ((line = sieve_get_line(stream, pool)) == NIL)
            return (NIL);

        rcode = string_get_token(&line);
        text = line;
        string_ucase(rcode);

        if (!strcmp(rcode, "OK")) {
            session->password = session->newpassword;
            session->newpassword = NIL;
            return (stream);
        }
    }

    if (!strcmp(rcode, "NO")) {
        session_message(session, "Login error: %s", text);
        return (NIL);
    }

    session_message(session, "Can't communicate with sieve system");

    session_log(session,
                "[sieve_connect]: Protocol failure: %s %s", rcode, text);

    return (NIL);
}

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

/* sieve_check() *********************************************************
 *
 * Fetch sieve script.
 ************************************************************************/

BOOL
sieve_fetch(struct sieve *sieve, struct session *session,
            char *script_name, char **scriptp)
{
    struct pool *pool = session->request->pool;
    struct iostream *stream;
    char *response, *value;

    *scriptp = NIL;

    if (!session->sieved_server)
        return(NIL);

    sieve_message(sieve, "");

    if (!sieve->stream) {
        sieve->stream = sieve_connect(sieve, session, session->sieved_server);
    
        if (!sieve->stream)
            return(NIL);

        sieve->last_ping = time(NIL);
    }

    stream = sieve->stream;

    ioprintf(stream, "GETSCRIPT \"%s\""CRLF, script_name); 
    ioflush(stream);

    /* Sieve upload protocol utterly bizarre */
    if (!(response = sieve_get_string(stream, pool)))
        return(NIL);

    if (!strcasecmp(response, "BAD")) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        session_message(session, "Protocol error talking to sieve: %s",
                        response);
        return(NIL);
    }

    if (!strcasecmp(response, "NO")) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        value = NIL;
    } else {
        value = response;

        if (!sieve_eatline(stream))
            return(NIL);

        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (strcmp(response, "OK") != 0)
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);
    }

#if 0
    /* Attempt UTF-8 -> Latin 1 translation.
     *
     * If translation fails, return the UTF-* script as an approximation
     * (should be impossible if Prayer is the only interface to Sieve)
     */
    {
        char *s;

        if ((s=sieve_utf8_to_latin1(value, pool)))
            return(s);
    }
#endif

    *scriptp = value;
    return(T);
}

/* Worker routine */

BOOL
sieve_upload_work(struct sieve *sieve, struct session *session,
                  char *script_name, char *script)
{
    struct pool *pool = session->request->pool;
    struct iostream *stream;
    char *response;

    if (!session->sieved_server)
        return(NIL);

    sieve_message(sieve, "");

    if (!sieve->stream) {
        sieve->stream = sieve_connect(sieve, session, session->sieved_server);
    
        if (!sieve->stream)
            return(NIL);

        sieve->last_ping = time(NIL);
    }
    stream = sieve->stream;

#if 0
    /* Convert sieve script to UTF-8 (canonical format on sieve server) */
    script = sieve_latin1_to_utf8(script, pool);
#endif

    ioprintf(stream, "PUTSCRIPT \"%s\" {%lu+}"CRLF,
             script_name, strlen(script)); 

    ioputs(stream, script);
    ioputs(stream, ""CRLF);
    ioflush(stream);

    /* Sieve upload protocol utterly bizarre */
    if (!(response = sieve_get_string(stream, pool)))
        return(NIL);

    if (!strcasecmp(response, "BAD")) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        session_message(session, "Protocol error talking to sieve: %s",
                        response);
        return(NIL);
    }

    if (strcasecmp(response, "OK") != 0) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        sieve_message(sieve, response);
        return(NIL);
    }

    if (!sieve_eatline(stream))
        return(NIL);

    return(T);
}

/* sieve_upload() ********************************************************
 *
 * Attempt to upload new script file
 ************************************************************************/

BOOL
sieve_upload(struct sieve *sieve, struct session *session,
             char *script_name, char *script)
{
    if (sieve_upload_work(sieve, session, script_name, script))
        return(T);

    if (sieve->stream && (sieve->stream->ieof || sieve->stream->oerror)) {
        /* Have another go on obvious iostream error */
        sieve_close(sieve);
        return(sieve_upload_work(sieve, session, script_name, script));
    }
    return(NIL);
}

/* sieve_activate() ******************************************************
 *
 * Activate Sieve file. script_name="" deactivates.
 ************************************************************************/

BOOL
sieve_activate(struct sieve *sieve, struct session *session,
              char *script_name)
{
    struct pool *pool = session->request->pool;
    struct iostream *stream;
    char *response;

    if (!session->sieved_server)
        return(NIL);

    if (!sieve->stream) {
        sieve->stream = sieve_connect(sieve, session, session->sieved_server);
    
        if (!sieve->stream)
            return(NIL);
        
        sieve->last_ping = time(NIL);
    }
    stream = sieve->stream;

    ioprintf(stream, "SETACTIVE \"%s\""CRLF, script_name);
    ioflush(stream);

    /* Sieve upload protocol utterly bizarre */
    if (!(response = sieve_get_string(stream, pool)))
        return(NIL);

    if (!strcasecmp(response, "BAD")) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        session_message(session, "Protocol error talking to sieve: %s",
                        response);
        return(NIL);
    }

    if (strcasecmp(response, "OK") != 0) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        sieve_message(sieve, response);
        return(NIL);
    }

    if (!sieve_eatline(stream))
        return(NIL);

    return(T);
}

/* sieve_delete() ********************************************************
 *
 * Delete a sieve file. (Removes active link if current)
 ************************************************************************/

BOOL
sieve_delete(struct sieve *sieve, struct session *session, char *script_name)
{
    struct pool *pool = session->request->pool;
    struct iostream *stream;
    char *response;

    if (!session->sieved_server)
        return(NIL);

    if (!sieve->stream) {
        sieve->stream = sieve_connect(sieve, session, session->sieved_server);
    
        if (!sieve->stream)
            return(NIL);
        
        sieve->last_ping = time(NIL);
    }
    stream = sieve->stream;

    ioprintf(stream, "DELETESCRIPT \"%s\""CRLF, script_name);
    ioflush(stream);

    /* Sieve upload protocol utterly bizarre */
    if (!(response = sieve_get_string(stream, pool)))
        return(NIL);

    if (!strcasecmp(response, "BAD")) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        session_message(session, "Protocol error talking to sieve: %s",
                        response);
        return(NIL);
    }

    if (strcasecmp(response, "OK") != 0) {
        if (!(response = sieve_get_string(stream, pool)))
            return(NIL);

        if (!sieve_eatline(stream))
            return(NIL);

        sieve_message(sieve, response);
        return(NIL);
    }

    if (!sieve_eatline(stream))
        return(NIL);

    return(T);
}
