/* $Cambridge: hermes/src/prayer/servers/session_streams.c,v 1.3 2008/09/16 09:59:57 dpc22 Exp $ */
/************************************************
 *    Prayer - a Webmail Interface              *
 ************************************************/

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

/* session stream manipulation stuff amalgamated here. This is a bit of a
 * mess simply because struct session defines half a dozen independant
 * streams, and session idle mode, stream checking and folder change
 * affect a number of different streams.
 *
 * Putting it all here in one place to encourage rationalisation/cleanup at
 * some future point. However this probably isn't a high priority as the
 * mess is more or less invisible to the main body of the code which only
 * cares about session->stream. Thought: define abstraction, have ptrs from
 * struct session into abstraction so that all changes within this
 * abstraction transparent to other code
 */

#include "server.h"

/* session_streams_idle() ************************************************
 *
 * Shut down transient resources if session is idle
 ************************************************************************/

BOOL session_streams_idle(struct session *session)
{
    unsigned long marked = msgmap_marked_count(session->zm);

    /* Will be reassigned when we wake up */
    session->stream = NIL;

    if (session->inbox_stream) {
        if ((session->stream == session->inbox_stream) && (marked > 0))
            /* Garbage collect inbox stream */
            mail_gc(session->inbox_stream, GC_ENV | GC_TEXTS);
        else {
            /* Shut down inbox stream */
            ml_close(session, session->inbox_stream);
            session->inbox_stream = NIL;
        }
    }

    if (session->other_stream) {
        if ((session->stream == session->other_stream) && (marked > 0)) {
            /* Garbage collect other stream */
            mail_gc(session->other_stream, GC_ENV | GC_TEXTS);
        } else {
            /* Shut down other stream */
            ml_close(session, session->other_stream);
            session->other_stream = NIL;
        }
    }

    if (session->draft_stream) {
        if ((session->stream == session->draft_stream) && (marked > 0)) {
            /* Garbage collect draft stream */
            mail_gc(session->draft_stream, GC_ENV | GC_TEXTS);
        } else {
            /* Shut down other stream */
            ml_close(session, session->draft_stream);
            session->draft_stream = NIL;
        }
    }

    /* Shut down preferences stream */
    if (session->prefs_stream) {
        ml_close(session, session->prefs_stream);
        session->prefs_stream = NIL;
    }

    /* Shut down transfer stream */
    if (session->xfer_stream) {
        ml_close(session, session->xfer_stream);
        session->xfer_stream = NIL;
    }

    /* Shut down account connections as well */
    account_close(session->account);
    sieve_close(session->sieve);

    return (T);
}

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

/* Some static utility routines for session_streams_check() */

static BOOL session_reopen_inbox_stream(struct session *session)
{
    struct request *request = session->request;
    struct pool *pool = request->pool;

    session->inbox_stream
        =
        ml_open(session, NIL, session_mailbox(session, pool, "inbox"), 0);

    if (session->inbox_stream == NIL) {
        session_log(session,
                    "[session_reopen_inbox_stream] Failed to reopen inbox");
        return (NIL);
    }

    if (!strcasecmp(session->foldername, "inbox")) {
        session->stream = session->inbox_stream;
        msgmap_associate(session->zm, session, session->inbox_stream);
    }
    session_log(session, "[session_reopen_inbox_stream] Reopened inbox");
    session->inbox_last_ping_time = time(NIL);
    return (T);
}

static BOOL session_reopen_other_stream(struct session *session)
{
    struct request *request = session->request;
    struct pool *pool = request->pool;

    session->other_stream
        = ml_open(session, NIL,
                  session_mailbox(session, pool,
                                  session->other_foldername), 0);

    if (session->other_stream == NIL) {
        session_log(session,
                    "[session_reopen_other_stream] Failed to reopen %s",
                    session->other_foldername);
        return (NIL);
    }

    if (!strcmp(session->foldername, session->other_foldername)) {
        session->stream = session->other_stream;
        msgmap_associate(session->zm, session, session->other_stream);
    }
    session_log(session,
                "[session_reopen_other_stream] Reopened %s",
                session->other_foldername);
    session->other_last_ping_time = time(NIL);
    return (T);
}

static BOOL session_reopen_draft_stream(struct session *session)
{
    struct request *request = session->request;
    struct pool *pool = request->pool;

    session->draft_stream
        = ml_open(session, NIL,
                  session_mailbox(session, pool,
                                  session->draft_foldername), 0);

    if (session->draft_stream == NIL) {
        session_log(session,
                    "[session_reopen_draft_stream] Failed to reopen %s",
                    session->draft_foldername);
        return (NIL);
    }

    if (!strcmp(session->foldername, session->draft_foldername)) {
        session->stream = session->draft_stream;
        msgmap_associate(session->zm, session, session->draft_stream);
    }
    session_log(session, "[session_reopen_draft_stream] Reopened %s",
                session->draft_foldername);
    session->draft_last_ping_time = time(NIL);
    return (T);
}

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

static unsigned long *session_marked_list(struct pool *pool,
                                          MAILSTREAM * stream)
{
    unsigned long count = 0;
    unsigned long msgno;
    unsigned long *result;

    for (msgno = 1; msgno <= stream->nmsgs; msgno++) {
        MESSAGECACHE *elt = mail_elt(stream, msgno);

        if (elt->spare)
            count++;
    }
    result = pool_alloc(pool, (count + 1) * sizeof(unsigned long));

    count = 0;
    for (msgno = 1; msgno <= stream->nmsgs; msgno++) {
        MESSAGECACHE *elt = mail_elt(stream, msgno);

        if (elt->spare)
            result[count++] = mail_uid(stream, msgno);
    }

    result[count] = 0;
    return (result);
}

unsigned long
session_apply_marked_list(MAILSTREAM * stream, unsigned long *list)
{
    unsigned long count = 0;
    unsigned long msgno;

    for (msgno = 1; msgno <= stream->nmsgs; msgno++) {
        unsigned long uid = mail_uid(stream, msgno);

        if (uid == *list) {
            mail_elt(stream, msgno)->spare = T;
            count++;
            list++;
        } else if (uid > (*list)) {
            list++;
            if (*list == 0L)
                break;
        }
    }
    return (count);
}

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

static BOOL session_check_inbox_stream(struct session *session)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;
    time_t now = time(NIL);
    BOOL use_check = config->stream_checkpoint;
    BOOL current = (session->stream == session->inbox_stream);
    unsigned long *marklist = NIL;

    if (use_check) {
        if (ml_check(session, session->inbox_stream)) {
            session_log(session,
                        "[session_check_inbox_stream] Checkpoint inbox");
            session->inbox_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    } else {
        if (ml_ping(session, session->inbox_stream)) {
            session_log(session,
                        "[session_check_inbox_stream] Pinged inbox");
            session->inbox_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    }

    /* Need to reopen inbox. First record current marked messages */
    if (current)
        marklist = session_marked_list(pool, session->inbox_stream);

    ml_close(session, session->inbox_stream);
    session->inbox_stream
        =
        ml_open(session, NIL, session_mailbox(session, pool, "INBOX"), 0);

    if (session->inbox_stream == NIL) {
        session_log(session,
                    "[session_check_inbox_stream] Failed to reopen inbox");
        return (NIL);
    }

    if (current) {
        session->stream = session->inbox_stream;

        if (marklist)
            msgmap_marked_set(session->zm,
                              session_apply_marked_list(session->stream,
                                                        marklist));
        msgmap_associate(session->zm, session, session->inbox_stream);
    }

    if (msgmap_marked_count(session->zm) > 0)
        session_log(session, ("[session_check_inbox_stream] "
                              "Reopened inbox with %lu marked msgs"),
                    msgmap_marked_count(session->zm));
    else
        session_log(session,
                    "[session_check_inbox_stream] Reopened inbox");
    return (T);
}

static BOOL session_check_other_stream(struct session *session)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;
    time_t now = time(NIL);
    BOOL use_check = config->stream_checkpoint;
    BOOL current = (session->stream == session->other_stream);
    unsigned long *marklist = NIL;

    if (use_check) {
        if (ml_check(session, session->other_stream)) {
            session_log(session,
                        "[session_check_other_stream] Checkpoint %s",
                        session->other_foldername);
            session->other_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    } else {
        if (ml_ping(session, session->other_stream)) {
            session_log(session, "[session_check_other_stream] Pinged %s",
                        session->other_foldername);
            session->other_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    }

    /* Need to reopen other stream. First record current marked messages */
    if (current)
        marklist = session_marked_list(pool, session->other_stream);

    ml_close(session, session->other_stream);

    session->other_stream
        = ml_open(session, NIL,
                  session_mailbox(session, pool,
                                  session->other_foldername), 0);

    if (session->other_stream == NIL) {
        session_log(session,
                    "[session_check_other_stream] Failed to reopen %s",
                    session->other_foldername);
        return (NIL);
    }

    if (current) {
        session->stream = session->other_stream;
        if (marklist)
            msgmap_marked_set(session->zm,
                              session_apply_marked_list(session->stream,
                                                        marklist));
        msgmap_associate(session->zm, session, session->other_stream);
    }

    if (msgmap_marked_count(session->zm) > 0)
        session_log(session, ("[session_check_other_stream] "
                              "Reopened %s with %lu marked msgs"),
                    session->other_foldername,
                    msgmap_marked_count(session->zm));
    else
        session_log(session, "[session_check_other_stream] Reopened %s",
                    session->other_foldername);
    session->other_last_ping_time = now;
    return (T);
}

static BOOL session_check_draft_stream(struct session *session)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;
    time_t now = time(NIL);
    BOOL use_check = config->stream_checkpoint;
    BOOL current = (session->stream == session->draft_stream);
    unsigned long *marklist = NIL;

    if (use_check) {
        if (ml_check(session, session->draft_stream)) {
            session_log(session,
                        "[session_check_draft_stream] Checkpoint %s",
                        session->draft_foldername);
            session->draft_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    } else {
        if (ml_ping(session, session->draft_stream)) {
            session_log(session, "[session_check_draft_stream] Pinged %s",
                        session->draft_foldername);
            session->draft_last_ping_time = now;
            if (current)
                msgmap_check(session->zm);
            return (T);
        }
    }

    /* Need to reopen other stream. First record current marked messages */
    if (current)
        marklist = session_marked_list(pool, session->draft_stream);

    ml_close(session, session->draft_stream);

    session->draft_stream
        = ml_open(session, NIL,
                  session_mailbox(session, pool,
                                  session->draft_foldername), 0);

    if (session->draft_stream == NIL) {
        session_log(session,
                    "[session_check_draft_stream] Failed to reopen %s",
                    session->draft_foldername);
        return (NIL);
    }

    if (current) {
        session->stream = session->draft_stream;
        if (marklist)
            msgmap_marked_set(session->zm,
                              session_apply_marked_list(session->stream,
                                                        marklist));
        msgmap_associate(session->zm, session, session->draft_stream);
    }

    if (msgmap_marked_count(session->zm) > 0)
        session_log(session, ("[session_check_draft_stream] "
                              "Reopened %s with %lu marked msgs"),
                    session->draft_foldername,
                    msgmap_marked_count(session->zm));
    else
        session_log(session, "[session_check_draft_stream] Reopened %s",
                    session->draft_foldername);
    session->draft_last_ping_time = now;
    return (T);
}

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

/* session_streams_check() ***********************************************
 *
 * Check to see if MAILSTREAM connections to IMAP server have timed out
 * Quietly reconnect connections which have timed out
 ************************************************************************/

BOOL session_streams_check(struct session * session)
{
    struct config *config = session->config;
    time_t now = time(NIL);
    time_t interval;

    interval = (time_t) config->stream_ping_interval;
    /* IMAP server may time out after 30 minutes */
    if ((interval == 0) || (interval > SESSION_DEFAULT_IMAP_TIMEOUT))
        interval = SESSION_DEFAULT_IMAP_TIMEOUT;

    /* Check or reopen inbox stream if needed */
    if (session->inbox_stream == NIL) {
        if (!session_reopen_inbox_stream(session))
            return (NIL);
    } else if ((session->inbox_last_ping_time + interval) < now) {
        if (!session_check_inbox_stream(session))
            return (NIL);
    }

    /* Check or reopen other stream if needed */
    if (session->other_stream) {
        if (((session->other_last_ping_time + interval) < now) &&
            !session_check_other_stream(session))
            return (NIL);
    } else if (session->foldername && session->other_foldername &&
               !strcmp(session->foldername, session->other_foldername) &&
               !session_reopen_other_stream(session))
        return (NIL);

    /* Check or reopen draft stream if needed */
    if (session->draft_stream) {
        if (((session->draft_last_ping_time + interval) < now) &&
            !session_check_draft_stream(session))
            return (NIL);
    } else if (session->foldername && session->draft_foldername &&
               !strcmp(session->foldername, session->draft_foldername) &&
               !session_reopen_draft_stream(session))
        return (NIL);

    interval = config->stream_misc_timeout;
    /* IMAP server may time out after 30 minutes */
    if ((interval == 0) || (interval > SESSION_DEFAULT_IMAP_TIMEOUT))
        interval = SESSION_DEFAULT_IMAP_TIMEOUT;

    /* Shut down preferences stream if it has been idle */
    if (session->prefs_stream &&
        ((session->prefs_last_ping_time + interval) < now)) {
        ml_close(session, session->prefs_stream);
        session->prefs_stream = NIL;
    }

    /* Shut down transfer stream if it has been idle */
    if (session->xfer_stream &&
        ((session->xfer_last_ping_time + interval) < now)) {
        ml_close(session, session->xfer_stream);
        session->xfer_stream = NIL;
    }

    /* Shut down account connections if they have been idle */
    account_timeout_close(session->account);
    sieve_timeout_close(session->sieve, session);

    return (T);
}

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

/* session_streams_change() **********************************************
 *
 * Change to named folder, updating session stream stuff as required 
 ************************************************************************/

BOOL session_streams_change(struct session * session, char *name)
{
    struct request *request = session->request;
    struct msgmap *zm = session->zm;
    MAILSTREAM *stream;

    if (!strcasecmp(name, "inbox")) {
        /* Switch back to inbox stream */
        string_strdup(&session->foldername, "INBOX");
        session->stream = session->inbox_stream;
    } else if (session->other_stream && session->other_foldername &&
               !strcmp(name, session->other_foldername)) {
        /* Switch to other stream */
        string_strdup(&session->foldername, session->other_foldername);
        session->stream = session->other_stream;
    } else if (!strcmp(name, session->draft_foldername)) {
        /* Switch to draft stream, opening if required */
        if (!session->draft_stream) {
            if (!(stream = ml_open(session, NIL,
                                   session_mailbox(session, request->pool,
                                                   session->
                                                   draft_foldername), 0)))
                return (NIL);

            session->draft_stream = session->stream = stream;
            session->draft_last_ping_time = time(NIL);
        } else
            session->stream = stream = session->draft_stream;

        string_strdup(&session->foldername, session->draft_foldername);
    } else {                    /* Open fresh other_stream */
        /* Just in case ml_open() fails */
        stream = session->other_stream;
        session->other_stream = NIL;

        /* Open stream, reusing existing other stream if possible */
        if (!(stream = ml_open(session, stream,
                               session_mailbox(session, request->pool,
                                               name), 0)))
            return (NIL);

        string_strdup(&session->other_foldername, name);
        string_strdup(&session->foldername, name);
        session->other_stream = session->stream = stream;
        session->other_last_ping_time = time(NIL);
    }

    stream = session->stream;

    msgmap_associate(zm, session, stream);
    msgmap_update(zm);

    if (zm->nmsgs > 0) {
        if (zm->sort_reverse)
            session->current = msgmap_value(zm, 1);
        else
            session->current = msgmap_value(zm, zm->nmsgs);
    } else
        session->current = 0;

    session->last_displayed = session->current;

    /* Record current session sequence number: intercept browser back button */
    session->sequence_last_change = session->sequence;

    return (T);
}

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

/* session_streams_find() ************************************************
 *
 * Work out if we have open connection to nominated mail folder already.
 ************************************************************************/

MAILSTREAM *session_streams_find(struct session * session, char *name)
{
    if (!strcasecmp(name, "INBOX"))
        return (session->inbox_stream);

    if (session->other_foldername
        && !strcmp(name, session->other_foldername))
        return (session->other_stream);

    if (session->draft_foldername
        && !strcmp(name, session->draft_foldername))
        return (session->draft_stream);

    return (NIL);
}

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

/* session_streams_ping() ************************************************
 *
 * Ping stream associated with mailbox name if active
 ************************************************************************/

BOOL session_streams_ping(struct session * session, char *mailbox)
{
    if (!strcasecmp(mailbox, "inbox") && session->inbox_stream) {
        if (!ml_ping(session, session->inbox_stream))
            return (NIL);
        session->inbox_last_ping_time = time(NIL);
        if (session->stream == session->inbox_stream)
            msgmap_check(session->zm);
        return (T);
    }

    if (session->other_foldername && session->other_stream &&
        !strcmp(mailbox, session->other_foldername)) {
        if (!ml_ping(session, session->other_stream))
            return (NIL);
        session->other_last_ping_time = time(NIL);
        if (session->stream == session->other_stream)
            msgmap_check(session->zm);
        return (T);
    }

    if (!strcmp(mailbox, session->draft_foldername)
        && session->draft_stream) {
        if (!ml_ping(session, session->draft_stream))
            return (NIL);
        session->draft_last_ping_time = time(NIL);
        if (session->stream == session->draft_stream)
            msgmap_check(session->zm);
        return (T);
    }

    return (T);
}

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

/* session_save_options_to_stream() **************************************
 *
 * Utility routine to help save user preferences.
 *   session:  Global state
 *   stream    Connection to preferences folder
 ************************************************************************/

static BOOL
session_save_options_to_stream(struct session *session,
                               MAILSTREAM * stream)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;
    struct options *options = session->options;
    unsigned long nmsgs = stream->nmsgs;
    STRING ms;
    char *data;
    unsigned long size;
    char *range;
    char *prayer_name = session_mailbox_prefs(session, pool);

    /* Create new status message */
    data = options_create_message(options, config, pool);

    /* Convert simple "char *" string into c-client "STRING *" string */
    INIT(&ms, mail_string, data, size = strlen(data));

    /* Append new preferences message to end of folder */
    if (!ml_append(session, stream, prayer_name, &ms))
        return (NIL);

    /* Double check that append was successful: c-client ignores errors :( */
    if (!ml_ping(session, stream) || (stream->nmsgs == nmsgs))
        return (NIL);

    /* Clear out the old preferences if append was successful */
    if (nmsgs > 0) {
        range = pool_strcat(pool, "1:", string_itoa_tmp(nmsgs));
        if (!ml_flag(session, stream, range, "\\DELETED", ST_SET)) {
            session_log(session, ("[session_save_options_to_stream] "
                                  "Failed to clear out prefs file: %s"),
                        ml_errmsg());
            return (NIL);
        }
    }
    if (!ml_expunge(session, stream))
        return (NIL);

    options->save = NIL;
    session->prefs_last_ping_time = time(NIL);
    return (T);
}

/* session_streams_save_options() ****************************************
 *
 * Save user preferences. Catch and report error condition.
 ************************************************************************/

BOOL session_streams_save_options(struct session * session)
{
    struct request *request = session->request;
    struct pool *pool = request->pool;
    char *prayer_name = session_mailbox_prefs(session, pool);

    ml_clear_error();
    if (!session->prefs_stream)
        session->prefs_stream = ml_open(session, NIL, prayer_name, 0);

    if (!session->prefs_stream || ml_have_error()) {
        ml_clear_error();
        ml_create(session, session->prefs_stream, prayer_name);
        if (!ml_have_error())
            session->prefs_stream
                = ml_open(session, session->prefs_stream, prayer_name, 0);
    }

    if (session->prefs_stream && !ml_have_error() && session->prefs_stream
        && session_save_options_to_stream(session, session->prefs_stream))
        return (T);

    session_message(session, "Failed to save preferences: %s",
                    ml_errmsg());
    session_log(session,
                "[session_save_options] Failed to save preferences: %s",
                ml_errmsg());

    if (session->prefs_stream) {
        ml_close(session, session->prefs_stream);
        session->prefs_stream = NIL;
    }
    return (T);
}

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

/* session_save_options_and_close_inbox() *******************************
 *
 * Shut down inbox stream, saving options if needed.
 ***********************************************************************/

static void session_save_options_and_close_inbox(struct session *session)
{
    struct config *config = session->config;
    struct request *request = session->request;
    struct pool *pool = request->pool;
    MAILSTREAM *stream;
    char *name;
    char *prefs_folder_name = config->prefs_folder_name;
    char *prayer_name = session_mailbox_prefs(session, pool);

    /* Checkpoint inbox stream before recycle attempt */
    ml_check(session, session->inbox_stream);

    ml_clear_error();
    stream = ml_open(session, session->inbox_stream, prayer_name, 0);

    if (!stream) {
        if (ml_have_error()) {
            ml_clear_error();
            ml_create(session, stream, prayer_name);
            if (!ml_have_error())
                stream = ml_open(session, stream, prayer_name, 0);
        }

        if (!stream || ml_have_error() ||
            !((name = stream->mailbox) &&
              (strlen(name) >= strlen(prefs_folder_name)) &&
              (!strcmp(name + strlen(name) - strlen(prefs_folder_name),
                       prefs_folder_name)))) {
            session_log(session,
                        ("[session_save_options_and_close_inbox]"
                         " Unable to select/create prefs file on exit"));
            ml_close(session, stream);
            return;
        }
    }

    if (stream) {
        session_save_options_to_stream(session, stream);
        ml_close(session, stream);
    }
}

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

/* session_streams_close() **********************************************
 *
 * Shut down IMAP mailstreams associated with a login session
 ***********************************************************************/

void session_streams_close(struct session *session)
{
    struct request *request = session->request;
    struct options *options = session->options;
    BOOL remove_drafts = NIL;

    /* Shut down transfer stream */
    if (session->xfer_stream) {
        ml_close(session, session->xfer_stream);
        session->xfer_stream = NIL;
    }

    /* Shut down other stream */
    if (session->other_stream) {
        ml_close(session, session->other_stream);
        session->other_stream = NIL;
    }

    /* Shut down drafts stream */
    if (session->draft_stream) {
        if (session->draft_foldername
            && (session->draft_stream->nmsgs == 0))
            remove_drafts = T;

        ml_close(session, session->draft_stream);
        session->draft_stream = NIL;
    }

    /* Shut down preferences stream, flushing options if dirty */
    if (session->prefs_stream) {
        if (options->save)
            session_streams_save_options(session);
        ml_close(session, session->prefs_stream);
        session->prefs_stream = NIL;
    }

    /* Remove draft messages folder if empty */
    if (remove_drafts)
        ml_delete(session, session->inbox_stream,
                  session_mailbox(session, request->pool,
                                  session->draft_foldername));

    /* Shut down inbox stream, flushing options if dirty (and no prefs stream) */
    if (session->inbox_stream) {
        if (options->save)
            session_save_options_and_close_inbox(session);
        else
            ml_close(session, session->inbox_stream);
        session->inbox_stream = NIL;
    }
}
