/****************************************************************************
 * <Novell-copyright>
 * Copyright (c) 2005, 2006 Novell, Inc. All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public License
 * as published by the Free Software Foundation.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, contact Novell, Inc.
 * 
 * To contact Novell about this file by physical or electronic mail, you 
 * may find current contact information at www.novell.com.
 * </Novell-copyright>
 ****************************************************************************/
#include <config.h>
#include <xpl.h>
#include <bongoutil.h>
#include <assert.h>
#include <limits.h>
#include <time.h>

#include "stored.h"
#include "command-parsing.h"
#include "object-model.h"
#include "calendar.h"

#include "mail.h"
#include "messages.h"
#include "mime.h"

static uint32_t
CommandHash (const void *key)
{
    char *s = (char *) key;
    uint32_t b = 1;
    uint32_t h = 0;

    for (; *s && ' ' != *s; s++) {
        h += b * tolower(*s);
        b = (b << 5) - b;
    }
    return h;
}

// internal prototypes
CCode ReceiveToFile(StoreClient *client, const char *path, uint64_t *size);

/* a strcmp() that ends on first space.  (might be optimizable by word-compares) */

static int
CommandCmp(const void *cmda, const void *cmdb)
{
    char *a = (char *) cmda;
    char *b = (char *) cmdb;

    while (*a) {
        if (tolower(*a) != tolower(*b)) {
            if (!*b && ' ' == *a) {
                return 0;
            } else {
                return -1;
            }
        }
        a++;
        b++;
    }
    return 0;
}


static BongoHashtable *CommandTable;


/* returns 0 on success */

/* I probably should have used ProtocolCommandTree?  The hashtable came
 * about when I tried to do this with with lex yacc code */

int
StoreSetupCommands()
{
    CommandTable = BongoHashtableCreate(64, CommandHash, CommandCmp);

    if (!CommandTable ||

        /* session commands */
        BongoHashtablePutNoReplace(CommandTable, "AUTH", (void *) STORE_COMMAND_AUTH) ||
        BongoHashtablePutNoReplace(CommandTable, "COOKIE", (void *) STORE_COMMAND_COOKIE) ||
        BongoHashtablePutNoReplace(CommandTable, "QUIT", (void *) STORE_COMMAND_QUIT) ||
        BongoHashtablePutNoReplace(CommandTable, "STORE", (void *) STORE_COMMAND_STORE) ||
        BongoHashtablePutNoReplace(CommandTable, "TOKEN", (void *) STORE_COMMAND_TOKEN) ||
        BongoHashtablePutNoReplace(CommandTable, "TOKENS", (void *) STORE_COMMAND_TOKENS) ||
        BongoHashtablePutNoReplace(CommandTable, "USER", (void *) STORE_COMMAND_USER) ||
        
        /* collection commands */
        BongoHashtablePutNoReplace(CommandTable, "COLLECTIONS", 
                         (void *) STORE_COMMAND_COLLECTIONS) ||
        BongoHashtablePutNoReplace(CommandTable, "CREATE", (void *) STORE_COMMAND_CREATE) ||
        BongoHashtablePutNoReplace(CommandTable, "LIST", (void *) STORE_COMMAND_LIST) ||
        BongoHashtablePutNoReplace(CommandTable, "MAILINGLISTS", 
                                  (void *) STORE_COMMAND_MAILINGLISTS) ||
        BongoHashtablePutNoReplace(CommandTable, "RENAME", (void *) STORE_COMMAND_RENAME) ||
        BongoHashtablePutNoReplace(CommandTable, "REMOVE", (void *) STORE_COMMAND_REMOVE) ||
        BongoHashtablePutNoReplace(CommandTable, "SEARCH", (void *) STORE_COMMAND_SEARCH) ||
        BongoHashtablePutNoReplace(CommandTable, "WATCH", (void *) STORE_COMMAND_WATCH) ||

        /* document commands */
        BongoHashtablePutNoReplace(CommandTable, "COPY", (void *) STORE_COMMAND_COPY) ||
        BongoHashtablePutNoReplace(CommandTable, "DELETE", (void *) STORE_COMMAND_DELETE) ||
        BongoHashtablePutNoReplace(CommandTable, "FLAG", (void *) STORE_COMMAND_FLAG) ||
        BongoHashtablePutNoReplace(CommandTable, "INFO", (void *) STORE_COMMAND_INFO) ||
        BongoHashtablePutNoReplace(CommandTable, "LINK", (void *) STORE_COMMAND_LINK) ||
        BongoHashtablePutNoReplace(CommandTable, "LINKS", (void *) STORE_COMMAND_LINKS) ||
        BongoHashtablePutNoReplace(CommandTable, "UNLINK", (void *) STORE_COMMAND_UNLINK) ||
        BongoHashtablePutNoReplace(CommandTable, "MESSAGES ",  (void *) STORE_COMMAND_MESSAGES) ||
        BongoHashtablePutNoReplace(CommandTable, "MFLAG", (void *) STORE_COMMAND_MFLAG) ||
        BongoHashtablePutNoReplace(CommandTable, "MIME ",  (void *) STORE_COMMAND_MIME) ||
        BongoHashtablePutNoReplace(CommandTable, "MOVE ", (void *) STORE_COMMAND_MOVE) ||
        BongoHashtablePutNoReplace(CommandTable, "PROPGET", (void *) STORE_COMMAND_PROPGET) ||
        BongoHashtablePutNoReplace(CommandTable, "PROPSET", (void *) STORE_COMMAND_PROPSET) ||
        BongoHashtablePutNoReplace(CommandTable, "PURGE", (void *) STORE_COMMAND_DELETE) ||
        BongoHashtablePutNoReplace(CommandTable, "READ", (void *) STORE_COMMAND_READ) ||
        BongoHashtablePutNoReplace(CommandTable, "REPLACE", (void *) STORE_COMMAND_REPLACE) ||
        BongoHashtablePutNoReplace(CommandTable, "WRITE", (void *) STORE_COMMAND_WRITE) ||

        /* delivery command */
        BongoHashtablePutNoReplace(CommandTable, "DELIVER", (void *) STORE_COMMAND_DELIVER) ||

        /* conversation commands */
        BongoHashtablePutNoReplace(CommandTable, "CONVERSATION", 
                         (void *) STORE_COMMAND_CONVERSATION) ||
        BongoHashtablePutNoReplace(CommandTable, "CONVERSATIONS", 
                         (void *) STORE_COMMAND_CONVERSATIONS) ||

        /* index commands */
        BongoHashtablePutNoReplace(CommandTable, "ISEARCH", (void *) STORE_COMMAND_ISEARCH) ||

        /* calendar commands */
        BongoHashtablePutNoReplace(CommandTable, "CALENDARS", 
                         (void *) STORE_COMMAND_CALENDARS) ||
        BongoHashtablePutNoReplace(CommandTable, "EVENTS", (void *) STORE_COMMAND_EVENTS) ||

        /* alarm commands */

        BongoHashtablePutNoReplace(CommandTable, "ALARMS", (void *) STORE_COMMAND_ALARMS) ||

        /* other */
        BongoHashtablePutNoReplace(CommandTable, "ACL", (void *) STORE_COMMAND_ACL) ||
        BongoHashtablePutNoReplace(CommandTable, "NOOP", (void *) STORE_COMMAND_NOOP) || 
        BongoHashtablePutNoReplace(CommandTable, "TIMEOUT", (void *) STORE_COMMAND_TIMEOUT) || 

        /* debugging commands */
        
        BongoHashtablePutNoReplace(CommandTable, "REINDEX", (void *) STORE_COMMAND_REINDEX) ||
        BongoHashtablePutNoReplace(CommandTable, "RESET", (void *) STORE_COMMAND_RESET) ||
        BongoHashtablePutNoReplace(CommandTable, "SHUTDOWN", (void *) STORE_COMMAND_SHUTDOWN) ||

        0)
    {
        XplConsolePrintf(AGENT_NAME ": Couldn't setup NMAP command table.\r\n");
        if (CommandTable) {
            BongoHashtableDelete(CommandTable);
            CommandTable = NULL;
        }
        return -1;
    }
    return 0;
}

static CCode
RequireUser(StoreClient *client)
{
    return STORE_PRINCIPAL_USER == client->principal.type 
        ? TOKEN_OK
        : ConnWriteStr(client->conn, MSG3241NOUSER);
}


static CCode
RequireStore(StoreClient *client)
{
	return client->storedb ? TOKEN_OK 
                         : ConnWriteStr(client->conn, MSG3243NOSTORE);
}


static CCode
RequireNoStore(StoreClient *client)
{
	return client->storedb ? ConnWriteStr(client->conn, MSG3247STORE)
                          : TOKEN_OK;
}


static CCode
RequireManager(StoreClient *client)
{
    return IS_MANAGER(client) ? TOKEN_OK
                              : ConnWriteStr(client->conn, MSG3243MANAGERONLY);
}


static CCode
RequireIdentity(StoreClient *client)
{
    return (IS_MANAGER(client) || HAS_IDENTITY(client)) 
        ? TOKEN_OK
        : ConnWriteStr(client->conn, MSG3241NOUSER);
}

#define TOK_ARR_SZ 10
#define PROP_ARR_SZ 10
#define HDR_ARR_SZ 10

CCode
StoreCommandLoop(StoreClient *client)
{
    int ccode = 0;
    StoreCommand command;

    while (-1 != ccode && BONGO_AGENT_STATE_RUNNING == StoreAgent.agent.state) {
        char *tokens[TOK_ARR_SZ];
        int n;
        int i;
        
        BongoMemStackReset(&client->memstack);

        ccode = ConnReadAnswer(client->conn, client->buffer, CONN_BUFSIZE);
        if (-1 == ccode || ccode >= CONN_BUFSIZE) {
            break;
        }
                        
        memset(tokens, 0, sizeof(tokens));
        n = CommandSplit(client->buffer, tokens, TOK_ARR_SZ);
        if (0 == n) {
            command = STORE_COMMAND_NULL;
        } else {
            command = (StoreCommand) BongoHashtableGet(CommandTable, tokens[0]);
        }
                
        if (client->watch.collection) {
            /* out of band events */
            ccode = StoreShowWatcherEvents(client);
            if (-1 == ccode) {
                break;
            }

            /* FIXME: What if there were an event related to a document relevant 
               to the command we just parsed?  (or any event?)  Should we ignore 
               the command? */
        }

        switch (command) {
            StoreDocumentType doctype;
            StoreObject object;
            StoreObject collection;
            int int1;
            int int2;
            int int3;
            int int4;
            unsigned long ulong;
            unsigned long ulong2;
            uint64_t guid;
            uint64_t guid2;
            uint64_t timestamp;
            uint64_t timestamp2;
            const char *filename;
            char *uid;
            char *query;
            StorePropInfo prop;
            StorePropInfo props[PROP_ARR_SZ];
            StoreHeaderInfo hdrs[HDR_ARR_SZ];
            int hdrcnt;
            char dt_start[BONGO_CAL_TIME_BUFSIZE];
            char dt_end[BONGO_CAL_TIME_BUFSIZE];

        case STORE_COMMAND_NULL:
            ccode = ConnWriteStr(client->conn, MSG3000UNKNOWN);
            break;
        
        case STORE_COMMAND_ACL:
            /* ACL GRANT <document> P<priv> T<principal> [W<who>] */
            /* ACL DENY <document> P<priv> T<principal> [W<who>] */
            /* ACL LIST [<document>] */
            /* ACL REMOVE <document> P<priv> [T<principal>] [W<who>] */
            
            int1 = 0; // priv
            int2 = 0; // principal
            query = NULL; // who
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 0, 6))) 
            {
                StorePrincipalType type = 0;
                StorePrivilege priv = 0;
                char *who = NULL;

                for (i = 3; TOKEN_OK == ccode && i < n; i++) {
                    if ('P' == *tokens[i]) {
                        ccode = ParsePrivilege(client, tokens[i] + 1, &priv);
                    } else if ('T' == *tokens[i]) {
                        ccode = ParsePrincipal(client, tokens[i] + 1, &type);
                    } else if ('W' == *tokens[i]) {
                        who = tokens[i] + 1;
                        ccode = TOKEN_OK;
                    } else {
                        // 'bad' token - something we don't understand.
                        ccode = 0;
                    }
                    if (ccode != TOKEN_OK) {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    }
                }
                if (ccode != TOKEN_OK) break;
                
                if (!XplStrCaseCmp(tokens[1], "list")) {
                    if ((n == 1) && ParseDocument(client, tokens[2], &object) == TOKEN_OK) {
                        ccode = StoreObjectListACL(client, &object);
                        break;
                    } else {
                        ccode = StoreObjectListACL(client, NULL);
                        break;
                    }
                } else {
                     if (n < 1) {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                     } else if (ParseDocument(client, tokens[2], &object) != TOKEN_OK) {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                     }
                }
                
                if (!XplStrCaseCmp(tokens[1], "remove")) {
                    if (priv == 0) {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    } else {
                        if (StoreObjectRemoveACL(client, &object, type, priv, who) == 0) {
                            ccode = ConnWriteStr(client->conn, MSG1000OK);
                        } else {
                            ccode = ConnWriteStr(client->conn, MSG5005DBLIBERR);
                        }
                    }
                    break;
                } else {
                    BOOL deny;
                    if (! XplStrCaseCmp(tokens[1], "grant")) {
                        deny = FALSE;
                    } else if (! XplStrCaseCmp(tokens[1], "deny")) {
                        deny = TRUE;
                    } else {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                        break;
                    }
                    if ((priv == 0) || (type == 0) || (who == NULL)) {
                        ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                        break;
                    }
                    if (StoreObjectAddACL(client, &object, type, priv, deny, who) == 0) {
                        ccode = ConnWriteStr(client->conn, MSG1000OK);
                    } else {
                        ccode = ConnWriteStr(client->conn, MSG5005DBLIBERR);
                    }
                    break;
                }
            }
            break;
        case STORE_COMMAND_ALARMS:
            /* ALARMS <starttime> <endtime> */
            
            if (TOKEN_OK == (ccode = RequireManager(client)) &&
                TOKEN_OK == (ccode = RequireNoStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseDateTimeToUint64(client, tokens[1], 
                                                           &timestamp)) &&
                TOKEN_OK == (ccode = ParseDateTimeToUint64(client, tokens[2], 
                                                           &timestamp2)))
            {
                ccode = StoreCommandALARMS(client, timestamp, timestamp2);
            }
            break;

        case STORE_COMMAND_AUTH:
            /* AUTH COOKIE <username> <token> [NOUSER] */
            /* AUTH SYSTEM <md5 hash> */
            /* AUTH USER <username> <password> [NOUSER] */
            
            int1 = 0; /* "NOUSER" flag */

            if (TOKEN_OK != (ccode = CheckTokC(client, n, 3, 5))) {
                break;
            }
            if (5 == n) {
                if (!XplStrCaseCmp(tokens[4], "NOUSER")) {
                    int1 = 1;
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
            }
            if (!XplStrCaseCmp(tokens[1], "COOKIE")) {
                if (TOKEN_OK == (ccode = CheckTokC(client, n, 4, 5)) &&
                    TOKEN_OK == (ccode = ParseUserName(client, tokens[2]))) 
                {
                    ccode = StoreCommandAUTHCOOKIE(client, 
                                                   tokens[2], tokens[3], int1);
                }
            } else if (0 == XplStrCaseCmp(tokens[1], "SYSTEM")) {
                ccode = StoreCommandAUTHSYSTEM(client, tokens[2]);
            } else if (0 == XplStrCaseCmp(tokens[1], "USER")) {
                if (TOKEN_OK == (ccode = CheckTokC(client, n, 4, 5)) &&
                    TOKEN_OK == (ccode = ParseUserName(client, tokens[2]))) 
                {
                    ccode = StoreCommandUSER(client, tokens[2], tokens[3], int1);
                }
            } else {
                ccode = ConnWriteStr(client->conn, MSG3000UNKNOWN);
            }
            break;

        case STORE_COMMAND_CALENDARS:
            /* FIXME: this command is no longer needed */
            /* CALENDARS [F <flags>] [P <proplist>] */

            int1 = 0;          /* propcount */
            ulong = ULONG_MAX; /* flags */
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 1, 3))) 
            {
                break;
            }
            for (i = 1; TOKEN_OK == ccode && i < n; i++) {
                if ('F' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[1] + 1, &ulong);
                } else if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int1, 0);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
                
            if (TOKEN_OK == ccode) {
                ccode = StoreCommandCALENDARS(client, ulong, props, int1);
            }
            break;

        case STORE_COMMAND_COLLECTIONS:
            /* COLLECTIONS [<collection>] */

            if (1 == n) {
                tokens[1] = "/";
            }
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 1, 2)) &&
                TOKEN_OK == (ccode = ParseCollection(client, tokens[1], &object)))
            {
                ccode = StoreCommandCOLLECTIONS(client, &object);
            }
            break;

        case STORE_COMMAND_CONVERSATIONS:
            /* CONVERSATIONS [Ssource] [Rxx-xx] [Q<query>] [P<proplist>] 
                             [C<center document>] [T] [F<flags>] [M<flags mask>]
                             [H<headerlist>]
            */

            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 1, 10)))
            {
                break;
            }

            filename = NULL; /* source */
            query = NULL;
            int1 = int2 = -1; /* range */
            int3 = 0; /* propcount */
            int4 = 0; /* display total */
            guid = 0; /* center document */
            ulong = 0; /* flags mask */
            ulong2 = 0; /* flags */
            hdrcnt = 0;
            for (i = 1; i < n; i++) {
                if ('S' == *tokens[i] && tokens[i][1] != '\0' && filename == NULL) { 
                    filename = tokens[i] + 1;
                } else if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int3, 0);
                } else if ('R' == *tokens[i] && int1 == -1) {
                    ccode = ParseRange(client, tokens[i] + 1, &int1, &int2);
                } else if ('Q' == *tokens[i]) {
                    query = tokens[i] + 1;
                } else if ('C' == *tokens[i] && !guid) {
                    ccode = ParseDocument(client, tokens[i] + 1, &object);
                } else if ('T' == *tokens[i] && !int4 && !tokens[i][1]) {
                    int4 = 1;
                } else if ('M' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong);
                } else if ('F' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong2);
                } else if ('H' == *tokens[i]) {
                    ccode = ParseHeaderList(client, tokens[i] + 1, 
                                            hdrs, HDR_ARR_SZ, &hdrcnt);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
            }

            if (TOKEN_OK != ccode) {
                break;
            }
            
            ccode = StoreCommandCONVERSATIONS(client, filename, query, 
                                              int1, int2, &object, 
                                              (uint32_t) ulong, (uint32_t) ulong2, 
                                              int4,
                                              hdrs, hdrcnt,
                                              props, int3);
            break;

        case STORE_COMMAND_CONVERSATION:
            /* CONVERSATION <conversation> [P<proplist>] */

            guid = 0;
            int3 = 0; /* propcount */
            if (TOKEN_OK != (ccode = RequireStore(client))) break;
            if (TOKEN_OK != (ccode = CheckTokC(client, n, 2, 3))) break;
            if (TOKEN_OK != (ccode = ParseDocument(client, tokens[1], &object))) break;
            
            for (i = 2; i < n; i++) {
                if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int3, 0);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
            }

            if (TOKEN_OK == ccode) {
                ccode = StoreCommandCONVERSATION(client, &object, props, int3);
            }
            break;

        case STORE_COMMAND_COOKIE:
            /* COOKIE [BAKE | NEW] <timeout> */
            /* COOKIE [CRUMBLE | DELETE] [<token>] */
            
            if (TOKEN_OK != (ccode = RequireUser(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 2, 3)))
            {
                break;
            }
            if (!XplStrCaseCmp(tokens[1], "BAKE") || 
                !XplStrCaseCmp(tokens[1], "NEW"))
            {
                if (TOKEN_OK != (ccode = CheckTokC(client, n, 3, 3)) ||
                    TOKEN_OK != (ccode = ParseDateTimeToUint64(client, tokens[2], 
                                                               &timestamp))) 
                {
                    break;
                }
                ccode = StoreCommandCOOKIENEW(client, timestamp); 
            } else if (!XplStrCaseCmp(tokens[1], "CRUMBLE") || 
                       !XplStrCaseCmp(tokens[1], "DELETE"))
            {
                ccode = StoreCommandCOOKIEDELETE(client, 3 == n ? tokens[2] : NULL);
            } else {
                ccode = ConnWriteStr(client->conn, MSG3000UNKNOWN);
            }
            break;

        case STORE_COMMAND_COPY:
            /* COPY <document> <collection> */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                TOKEN_OK == (ccode = ParseCollection(client, tokens[2], &collection)))
            {
                ccode = StoreCommandCOPY(client, &object, &collection);
            }
            break;

        case STORE_COMMAND_CREATE:
            /* CREATE <collection name> [<guid>] */

            guid = 0;
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 3)) &&
                (n <= 2 || TOKEN_OK == (ccode = ParseGUID(client, tokens[2], &guid))))
            {
                ccode = StoreCommandCREATE(client, tokens[1], guid);
            }
            break;

        case STORE_COMMAND_DELETE:
            /* DELETE <document> */
            /* PURGE <document> */
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)))
            {
                ccode = StoreCommandDELETE(client, &object);
            }
            break;

        case STORE_COMMAND_DELIVER:
            /* DELIVER FILE <type> <sender> <authsender> <filename> */
            /* DELIVER STREAM <type> <sender> <authsender> <count> */

            if (TOKEN_OK != (ccode = RequireManager(client)) ||
                TOKEN_OK != (ccode = RequireNoStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 6, 6)) ||
                TOKEN_OK != (ccode = ParseDocType(client, tokens[2], &doctype)))
            {
                break;
            }
            if (0 == XplStrCaseCmp(tokens[1], "FILE")) {
                ccode = StoreCommandDELIVER(client, tokens[3], tokens[4], 
                                            tokens[5], 0);
            } else if (0 == XplStrCaseCmp(tokens[1], "STREAM")) {
                if (TOKEN_OK != (ccode = ParseInt(client, tokens[5], &int1))) {
                    break;
                }
                ccode = StoreCommandDELIVER(client, tokens[3], tokens[4], NULL, int1);
            } else {
                ccode = ConnWriteStr(client->conn, MSG3000UNKNOWN);
            }
            break;

        case STORE_COMMAND_EVENTS:
            /* EVENTS [D<daterange>] [C<calendar> | U<uid>] 
               [F<mask>] [Q<query>] [P<proplist>] */

            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 1, 6)))
            {
                break;
            }

            dt_start[0] = '\0';
            int1 = 0; /* proplist len */
            int2 = 0; /* have coll */
            int3 = int4 = -1; /* range */
            int4 = -1;
            uid = NULL;
            query = NULL; /* uid */
            ulong = ULONG_MAX;
            for (i = 1; TOKEN_OK == ccode && i < n; i++) {
                if ('C' == *tokens[i] && !int2 && !query) {
                    ccode = ParseDocumentInfo(client, tokens[i] + 1, &object);
                    int2++;
                } else if ('D' == *tokens[i] && !dt_start[0]) {
                    ccode = ParseDateRange(client, tokens[i] + 1, dt_start, dt_end);
                } else if ('F' == *tokens[i] && ULONG_MAX == ulong) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong);
                } else if ('P' == *tokens[i] && 0 == int1) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int1, 0);
                } else if ('Q' == *tokens[i] && !query) {
                    query = tokens[i] + 1;
                } else if ('R' == *tokens[i] && -1 == int3) {
                    ccode = ParseRange(client, tokens[i] + 1, &int3, &int4);
                } else if ('U' == *tokens[i] && !uid && !int2) {
                    uid = tokens[i] + 1;
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
                
            if (TOKEN_OK != ccode) {
                break;
            }

            if (!dt_start[0]) {
                strcpy(dt_start, "00000000T000000");
                strcpy(dt_end, "99999999T999999");
            }            
            ccode = StoreCommandEVENTS(client, dt_start, dt_end, 
                                       int2 ? &object : NULL, 
                                       ulong, uid,
                                       query, int3, int4,
                                       props, int1);
            break;

        case STORE_COMMAND_FLAG:
            /* FLAG <document> [[+ | -]<value>] */

            int2 = STORE_FLAG_SHOW;             
            guid = 0;
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                (n < 3 || TOKEN_OK == (ccode = ParseFlag(client, tokens[2], &int1, &int2)))) 
                
            {
                ccode = StoreCommandFLAG(client, &object, int1, int2);
            }
            break;

        case STORE_COMMAND_INFO:
            /* INFO <document> [P<proplist>] */
            
            guid = 0;
            int1 = 0; /* proplist len */
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 2, 3)) ||
                TOKEN_OK != (ccode = ParseDocument(client, tokens[1], &object)))
            {
                break;
            }
            if (n > 2) {
                if ('P' == *tokens[2]) {
                    ccode = ParsePropList(client, tokens[2] + 1, 
                                          props, PROP_ARR_SZ, &int1, 0);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
            if (TOKEN_OK == ccode) {
                ccode = StoreCommandINFO(client, &object, props, int1);
            }
            break;

        case STORE_COMMAND_ISEARCH:
            /* ISEARCH R<range> <query> [P<proplist>] */

            int1 = 0; /* range */
            int2 = 0; /* range */
            int3 = 0; /* proplist len */
            
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 3, 4)))
            {
                break;
            }            
             
            if ('R' != *tokens[1]) {
                ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                break;
            } 
            ccode = ParseRange(client, tokens[1] + 1, &int1, &int2);

            for (i = 3; TOKEN_OK == ccode && i < n; i++) {
                if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int3, 0);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
                
            if (TOKEN_OK != ccode) {
                break;
            }

            ccode = StoreCommandISEARCH(client, tokens[2], int1, int2,
                                        props, int3);
            break;

        case STORE_COMMAND_LINK:
            /* LINK <document> <document> 
             * abuses collection - first argument should be another document*/
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &collection)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[2], &object)))
            {
                ccode = StoreCommandLINK(client, &collection, &object);
            }
            break;
        
        case STORE_COMMAND_LINKS:
            /* LINKS <document> */
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[2], &object)))
            {
                BOOL reverse_links;
                if (!XplStrCaseCmp(tokens[1], "TO")) {
                    reverse_links = TRUE;
                } else if (!XplStrCaseCmp(tokens[1], "FROM")) {
                    reverse_links = FALSE;
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
                
                ccode = StoreCommandLINKS(client, reverse_links, &object);
            }
            break; 

        case STORE_COMMAND_LIST:
            /* LIST <collection> [Rxx-xx] [P<proplist>] [M<flags mask>] [F<flags>] */

            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 2, 6)) ||
                TOKEN_OK != (ccode = ParseCollection(client, tokens[1], &object))) 
            {
                break;
            }

            int1 = int2 = -1; /* range */
            int3 = 0;   /* proplist len */
            ulong = 0;  /* flags mask */
            ulong2 = 0; /* flags */
            for (i = 2; TOKEN_OK == ccode && i < n; i++) {
                if ('R' == *tokens[i] && -1 == int1) {
                    ccode = ParseRange(client, tokens[i] + 1, &int1, &int2);
                } else if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int3, 0);
                } else if ('M' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong);
                } else if ('F' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong2);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
                
            if (TOKEN_OK != ccode) {
                break;
            }
            
            ccode = StoreCommandLIST(client, &object, int1, int2, 
                                     (uint32_t) ulong, (uint32_t) ulong2, 
                                     props, int3);
            break;            

        case STORE_COMMAND_MAILINGLISTS:
            /* MAILINGLISTS <source> */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)))
            {
                ccode = StoreCommandMAILINGLISTS(client, tokens[1]);
            }
            break;

        case STORE_COMMAND_MESSAGES:
            /* MESSAGES [Ssource] [H<headerlist>] [Q<query>] 
                        [F<flags>] [M<flags mask>]
                        [Rxx-xx] [C<center document>] [T] 
                        [P<proplist>] 
            */
            
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 1, 10)))
            {
                break;
            }

            filename = NULL; /* source */
            query = NULL;
            int1 = int2 = -1; /* range */
            int3 = 0; /* propcount */
            int4 = 0; /* display total */
            guid = 0; /* center document */
            ulong = 0; /* flags mask */
            ulong2 = 0; /* flags */
            hdrcnt = 0;
            for (i = 1; i < n; i++) {
                if ('S' == *tokens[i] && tokens[i][1] != '\0' && filename == NULL) { 
                    filename = tokens[i] + 1;
                } else if ('P' == *tokens[i] && 0 == int3) {
                    ccode = ParsePropList(client, tokens[i] + 1, 
                                          props, PROP_ARR_SZ, &int3, 0);
                } else if ('R' == *tokens[i] && int1 == -1) {
                    ccode = ParseRange(client, tokens[i] + 1, &int1, &int2);
                } else if ('Q' == *tokens[i]) {
                    query = tokens[i] + 1;
                } else if ('C' == *tokens[i] && !guid) {
                    ccode = ParseDocument(client, tokens[i] + 1, &object);
                } else if ('T' == *tokens[i] && !int4 && !tokens[i][1]) {
                    int4 = 1;
                } else if ('M' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong);
                } else if ('F' == *tokens[i]) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong2);
                } else if ('H' == *tokens[i]) {
                    ccode = ParseHeaderList(client, tokens[i] + 1, 
                                            hdrs, HDR_ARR_SZ, &hdrcnt);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
            }

            if (TOKEN_OK != ccode) {
                break;
            }
            
            ccode = StoreCommandMESSAGES(client, filename, query, 
                                         int1, int2, &object, 
                                         (uint32_t) ulong, (uint32_t) ulong2, 
                                         int4,
                                         hdrs, hdrcnt,
                                         props, int3);
            break;

        case STORE_COMMAND_MFLAG:
            /* MFLAG [+ | -] <value> */

            int2 = STORE_FLAG_SHOW; /* action */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)) &&
                TOKEN_OK == (ccode = ParseFlag(client, tokens[1], &int1, &int2)))
            {
                ccode = StoreCommandMFLAG(client, int1, int2);
            }
            break;

        case STORE_COMMAND_MIME:
            /* MIME <document> */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)))
            {
                ccode = StoreCommandMIME(client, &object);
            }
            break;

        case STORE_COMMAND_MOVE:
            /* MOVE <document> <collection> [<filename>] */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 4)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                TOKEN_OK == (ccode = ParseCollection(client, tokens[2], &collection)))
            {
                ccode = StoreCommandMOVE(client, &object, &collection, 
                                         4 == n ? tokens[3] : NULL);
            }
            break;

        case STORE_COMMAND_NOOP:
            /* NOOP */

            ccode = ConnWriteStr(client->conn, MSG1000OK);
            break;

        case STORE_COMMAND_PROPGET:
            /* PROPGET <document> [<proplist>] */
            
            int3 = 0; /* propcount */
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                (2 == n || 
                 TOKEN_OK == (ccode = ParsePropList(client, tokens[2], 
                                                    props, PROP_ARR_SZ, &int3, 0))))
            {
                ccode = StoreCommandPROPGET(client, &object, props, int3);
            }
            break;

        case STORE_COMMAND_PROPSET:
            /* PROPSET <document> <propname> <length> */
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 4, 4)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                TOKEN_OK == (ccode = ParsePropName(client, tokens[2], &prop)) &&
                TOKEN_OK == (ccode = ParseInt(client, tokens[3], &int1)))
                
            {
                prop.valueLen = int1;
                ccode = StoreCommandPROPSET(client, &object, &prop, int1);
            }
            break;

        case STORE_COMMAND_QUIT:
            /* QUIT */

            ccode = StoreCommandQUIT(client);
            break;

        case STORE_COMMAND_READ:
            /* READ <document> [<start> [<length>]] */
            // FIXME: need to get signedness etc. right on these args
            int1 = -1;
            int2 = -1;
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 4)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object)) &&
                (n < 3 || TOKEN_OK == (ccode = ParseInt(client, tokens[2], &int1))) &&
                (n < 4 || TOKEN_OK == (ccode = ParseInt(client, tokens[3], &int2))))
            {
                ccode = StoreCommandREAD(client, &object, int1, int2);
            }
            break;

        case STORE_COMMAND_REINDEX:
            /* REINDEX [<document>] */
            
            guid = 0;
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 1, 2)) &&
                (n < 2 || 
                 TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &object))))
            {
                ccode = StoreCommandREINDEX(client, &object);
            }
            break;

        case STORE_COMMAND_RENAME:
            /* RENAME <collection> <new name> */
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseCollection(client, tokens[1], &object)))
            {
                ccode = StoreCommandRENAME(client, &object, tokens[2]);
            }
            break;

        case STORE_COMMAND_REMOVE:
            /* REMOVE <collection> */
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)) &&
                TOKEN_OK == (ccode = ParseCollection(client, tokens[1], &object)))
            {
                ccode = StoreCommandREMOVE(client, &object);
            }
            break;


        case STORE_COMMAND_REPLACE:
            /* REPLACE <document> <length> [R<xx-xx>] [V<version>] 
                       [L<link document>]*/

            int2 = 0; /* range start */
            int3 = 0; /* range end */
            int4 = -1; /* version */
            guid2 = 0;
            
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 3, 6)) ||
                TOKEN_OK != (ccode = ParseDocument(client, tokens[1], &object)) ||
                TOKEN_OK != (ccode = ParseStreamLength(client, tokens[2], &int1)))
            {
                break;
            }

            for (i = 3; TOKEN_OK == ccode && i < n; i++) {
                if ('R' == *tokens[i] && -1 == int2) {
                    ccode = ParseRange(client, tokens[i] + 1 , &int2, &int3);
                } else if ('V' == *tokens[i] && -1 == int4) {
                    ccode = ParseInt(client, tokens[i] + 1, &int4);
                //} else if ('L' == *tokens[i] && !guid2) {
                //    ccode = ParseDocument(client, 1 + tokens[i], &guid2);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
            }
            if (TOKEN_OK == ccode) {
            	ccode = StoreCommandREPLACE(client, &object, int1, int2, int3, int4);
            }
            break;
            
        case STORE_COMMAND_RESET:
            /* RESET */

            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 1, 1)))
            {
                ccode = StoreCommandRESET(client); 
            }
            break;

        case STORE_COMMAND_SEARCH:
            /* SEARCH <document or collection> TEXT <query>
               SEARCH <document or collection> BODY <query>
               SEARCH <document or collection> HEADER <header> <query>
               SEARCH <document or collection> HEADERS <query>
            */

            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 4, 5)) ||
                TOKEN_OK != (ccode = ParseDocument(client, tokens[1], &object)))
            {
                break;
            }
            
            {
                StoreSearchInfo search;
                
                if (0 == XplStrCaseCmp(tokens[2], "TEXT")) {
                    search.type = STORE_SEARCH_TEXT;
                    search.query = tokens[3];
                } else if (0 == XplStrCaseCmp(tokens[2], "BODY")) {
                    search.type = STORE_SEARCH_BODY;
                    search.query = tokens[3];
                } else if (0 == XplStrCaseCmp(tokens[2], "HEADER")) {
                    if (TOKEN_OK != (ccode = CheckTokC(client, n, 5, 5))) {
                        break;
                    }
                    search.type = STORE_SEARCH_HEADER;
                    search.header = tokens[3];
                    search.query = tokens[4];
                } else if (0 == XplStrCaseCmp(tokens[2], "HEADERS")) {
                    search.type = STORE_SEARCH_HEADERS;
                    search.query = tokens[3];
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3000UNKNOWN);
                    break;
                }

                ccode = StoreCommandSEARCH(client, object.guid, &search);
            }
            break;

        case STORE_COMMAND_SHUTDOWN:
            /* SHUTDOWN */
            exit(0);
            break;
            
        case STORE_COMMAND_STORE:
            /* STORE [<storename>] */

            if (TOKEN_OK != (ccode = CheckTokC(client, n, 1, 2)) ||
                TOKEN_OK != (ccode = RequireIdentity(client)))
            {
                break;
            }
             
            if (2 == n) {
                if (TOKEN_OK != (ccode = ParseStoreName(client, tokens[1]))) {
                    break;
                }
            } else {
                tokens[1] = NULL;
            }

            ccode = StoreCommandSTORE(client, tokens[1]);
            break;

        case STORE_COMMAND_TIMEOUT:
            /* TIMEOUT [<timeoutms>] */
            
            int1 = -1;

            if (TOKEN_OK == (ccode = CheckTokC(client, n, 1, 2)) &&
                (1 == n || TOKEN_OK == (ccode = ParseInt(client, tokens[1], &int1))))
            {
                ccode = StoreCommandTIMEOUT(client, int1);
            }
            break;
            
        case STORE_COMMAND_TOKEN:
            /* TOKEN <token> */

            if (TOKEN_OK == (ccode = CheckTokC(client, n, 2, 2)) &&
                TOKEN_OK == (ccode = RequireIdentity(client)))
            {
                ccode = StoreCommandTOKEN(client, tokens[1]);
            }
            break;
            
        case STORE_COMMAND_TOKENS:
            /* TOKENS */

            if (TOKEN_OK == (ccode = CheckTokC(client, n, 1, 1)) &&
                TOKEN_OK == (ccode = RequireIdentity(client)))
            {
                ccode = StoreCommandTOKENS(client);
            }
            break;
            
        case STORE_COMMAND_UNLINK:
            /* UNLINK <document> <document> */
            
            if (TOKEN_OK == (ccode = RequireStore(client)) &&
                TOKEN_OK == (ccode = CheckTokC(client, n, 3, 3)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[1], &collection)) &&
                TOKEN_OK == (ccode = ParseDocument(client, tokens[2], &object)))
            {
                ccode = StoreCommandUNLINK(client, &collection, &object);
            }
            break;

        case STORE_COMMAND_USER:
            /* USER [<username>] */

            if (TOKEN_OK != (ccode = RequireManager(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 1, 2))) 
            {
                break;
            }
            if (2 == n) {
                if (TOKEN_OK != (ccode = ParseUserName(client, tokens[1]))) {
                    break;
                }
            } else {
                tokens[1] = NULL;
            }

            ccode = StoreCommandUSER(client, tokens[1], NULL, 0);
            break;

        case STORE_COMMAND_WATCH:
            /* WATCH <collection> [DELETED] [FLAGS] [MODIFIED] [NEW] */

            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 2, 6)) ||
                TOKEN_OK != (ccode = ParseCollection(client, tokens[1], &object)))
            {
                break;
            }
                
            int1 = 0;
            for (i = 2; i < n; i++) {
                if (!XplStrCaseCmp("FLAGS", tokens[i])) {
                    int1 |= STORE_WATCH_EVENT_FLAGS;
                } else if (!XplStrCaseCmp("MODIFIED", tokens[i])) {
                    int1 |= STORE_WATCH_EVENT_MODIFIED;
                } else if (!XplStrCaseCmp("NEW", tokens[i])) {
                    int1 |= STORE_WATCH_EVENT_NEW;
                } else if (!XplStrCaseCmp("DELETED", tokens[i])) {
                    int1 |= STORE_WATCH_EVENT_DELETED;
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                    break;
                }
            }
            
            if (TOKEN_OK == ccode) {
                ccode = StoreCommandWATCH(client, &object, int1);
            }
            break;

        case STORE_COMMAND_WRITE:
            /* WRITE <collection> <type> <length> 
                     [F<filename>] [G<guid>] [T<timeCreated>] 
                     [Z<flags>] [L<link guid>] [N]
             */
            
            if (TOKEN_OK != (ccode = RequireStore(client)) ||
                TOKEN_OK != (ccode = CheckTokC(client, n, 4, 9)) ||
                TOKEN_OK != (ccode = ParseCollection(client, tokens[1], &object)) ||
                TOKEN_OK != (ccode = ParseDocType(client, tokens[2], &doctype)) ||
                TOKEN_OK != (ccode = ParseStreamLength(client, tokens[3], &int1)))
            {
                break;
            }

            filename = NULL;
            timestamp = 0;
            ulong = 0; /* addflags */
            guid = 0;
            guid2 = 0; /* link guid */
            int2 = 0; /* whether or not to process the document */

            for (i = 4; i < n; i++) {
                if ('G' == *tokens[i] && !guid) {
                    ccode = ParseGUID(client, 1 + tokens[i], &guid);
                //} else if ('L' == *tokens[i] && !guid2) {
                //    ccode = ParseDocument(client, 1 + tokens[i], &guid2);
                } else if ('F' == *tokens[i] && tokens[i][1] != '\0') {
                    filename = tokens[i] + 1;
                } else if ('N' == *tokens[i] && tokens[i][1] == '\0') {
                    int2 = 1;
                } else if ('T' == *tokens[i] && !timestamp) {
                    ccode = ParseDateTimeToUint64(client, 1 + tokens[i], &timestamp); 
                } else if ('Z' == *tokens[i] && !ulong) {
                    ccode = ParseUnsignedLong(client, tokens[i] + 1, &ulong);
                } else {
                    ccode = ConnWriteStr(client->conn, MSG3022BADSYNTAX);
                }
                if (TOKEN_OK != ccode) {
                    break;
                }
            }
            if (TOKEN_OK != ccode) {
                break;
            }

            ccode = StoreCommandWRITE(client, &object, doctype, int1,
                                      (uint32_t) ulong, guid, filename, 
                                      timestamp, guid2, int2);
            break;

        default:
            break;
        }

        if (client->watchLock) {
            StoreReleaseCollectionLock(client, &(client->watchLock));
            client->watchLock = NULL;
        }

        if (ccode >= 0) {
            ccode = ConnFlush(client->conn);
        }
    }
    
    return ccode;
}

static CCode
ShowDocumentBody(StoreClient *client, StoreObject *document,
                 int64_t requestStart, uint64_t requestLength)
{
    int ccode = 0;
    char path[XPL_MAX_PATH + 1];
    FILE *fh = NULL;
    uint64_t start;
    uint64_t length;

    if (STORE_IS_FOLDER(document->type)) {
        return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
    } else {
        FindPathToDocument(client, document->collection_guid, document->guid, path, sizeof(path));
        fh = fopen(path, "rb");
        if (!fh)
            return ConnWriteStr(client->conn, MSG4224CANTREAD);

        if (requestStart < 0) {
            start = 0;
            length = document->size;
        } else {
            if ((uint64_t)requestStart < document->size) {
                start = requestStart;
                length = document->size - start;
                if ((requestLength < length) && (requestLength != 0)) {
                    length = requestLength; 
                }
            } else {
                start = document->size;
                length = 0;
            }
        }

        ccode = ConnWriteF(client->conn, "2001 nmap.document %ld\r\n", (long) length);
        if (-1 == ccode) goto finish;
        
        if (0 != XplFSeek64(fh, start, SEEK_SET)) {
            ccode = ConnWriteStr(client->conn, MSG4224CANTREAD);
            goto finish;
        }
        ccode = ConnWriteFromFile(client->conn, fh, length);
        if (-1 == ccode) {
            goto finish;
        }
        ccode = ConnWriteStr(client->conn, "\r\n");
    }
    
finish:
    if (fh) {
        fclose(fh);
    }

    return ccode;
}

CCode
StoreCommandALARMS(StoreClient *client, uint64_t start, uint64_t end)
{
	/* FIXME - redo this completely
	 * 
    AlarmInfoStmt *stmt;
    AlarmInfo info;
    CCode ccode = 0;

    stmt = AlarmDBListAlarms(client->system.alarmdb, start, end);
    if (!stmt) {
        return ConnWriteStr(client->conn, MSG5005DBLIBERR);
    }
    do {
        switch (AlarmInfoStmtStep(client->system.alarmdb, stmt, &info)) {
        case 1:
            ccode = ConnWriteF(client->conn, 
                               "2001 " GUID_FMT " %lld\r\n"
                               "2001 nmap.alarm.summary %d\r\n%s\r\n",
                               info.guid, BongoCalTimeUtcAsUint64(info.time),
                               strlen(info.summary), info.summary);
            if (-1 == ccode) {
                break;
            }

            if (info.email) {
                ccode = ConnWriteF(client->conn, 
                                   "2001 nmap.alarm.email %d\r\n%s\r\n",
                                   strlen(info.email), info.email);
            } else {
                ccode = ConnWriteF(client->conn, MSG3245NOPROP, "nmap.alarm.email");
            }
            if (-1 == ccode) {
                break;
            }

            if (info.sms) {
                ccode = ConnWriteF(client->conn, 
                                   "2001 nmap.alarm.sms %d\r\n%s\r\n",
                                   strlen(info.email), info.email);
            } else {
                ccode = ConnWriteF(client->conn, MSG3245NOPROP, "nmap.alarm.sms");
            }
            break;
        case 0:
            ccode = ConnWriteStr(client->conn, MSG1000OK);
            goto finish;
        case -1:
        default:
            ccode = ConnWriteStr(client->conn, MSG5005DBLIBERR);
            goto finish;
        }
    } while (ccode >= 0);

finish:
    AlarmStmtEnd(client->system.alarmdb, stmt);
	*/
	return ConnWriteStr(client->conn, MSG1000OK);
}

CCode
StoreCommandCALENDARS(StoreClient *client, unsigned long mask,
                      StorePropInfo *props, int propcount)
{
	StoreObject calendar_collection;
	CCode ccode;

	StoreObjectFind(client, STORE_CALENDARS_GUID, &calendar_collection);
	ccode = StoreObjectCheckAuthorization(client, &calendar_collection, STORE_PRIV_LIST);
	if (ccode) return ccode;
	
	return StoreObjectIterCollectionContents(client, &calendar_collection, -1, 
	    -1, 0, 0, props, propcount, "= nmap.type 6", NULL, FALSE);
}

// list the collections who are subcollections of container
CCode
StoreCommandCOLLECTIONS(StoreClient *client, StoreObject *container)
{
	CCode ccode;
	
	ccode = StoreObjectCheckAuthorization(client, container, STORE_PRIV_LIST);
	if (ccode) return ConnWriteStr(client->conn, MSG4240NOPERMISSION);

	return StoreObjectIterSubcollections(client, container);
}

CCode
StoreCommandCONVERSATION(StoreClient *client, StoreObject *conversation,
                         StorePropInfo *props, int propcount)
{
	CCode ccode = 0;
	StoreObject conversation_collection;

	StoreObjectFind(client, STORE_CONVERSATIONS_GUID, &conversation_collection);
	ccode = StoreObjectCheckAuthorization(client, &conversation_collection, STORE_PRIV_READ);
	if (ccode) return ConnWriteStr(client->conn, MSG4240NOPERMISSION);
	
	return StoreObjectIterConversationMails(client, conversation, props, propcount);
}

// FIXME: Need to implement support for 'center' properly.
// The headers options can take a running jump, though.
CCode
StoreCommandCONVERSATIONS(StoreClient *client, const char *source, const char *query, 
                          int start, int end, StoreObject *center, 
                          uint32_t flagsmask, uint32_t flags,
                          int displayTotal,
                          StoreHeaderInfo *headers, int headercount, 
                          StorePropInfo *props, int propcount)
{
	int ccode;
	StoreObject conversation_collection;
	BOOL show_total;
	
	StoreObjectFind(client, STORE_CONVERSATIONS_GUID, &conversation_collection);
	
	// FIXME: which priv should we really be checking? either/or/both?
	ccode = StoreObjectCheckAuthorization(client, &conversation_collection, STORE_PRIV_LIST);
	if (ccode) return ConnWriteStr(client->conn, MSG4240NOPERMISSION);
	ccode = StoreObjectCheckAuthorization(client, &conversation_collection, STORE_PRIV_READ);
	if (ccode) return ConnWriteStr(client->conn, MSG4240NOPERMISSION);
	
	show_total = (displayTotal == 0)? FALSE : TRUE;
	
	return StoreObjectIterCollectionContents(client, &conversation_collection, start, 
	    end, flagsmask, flags, props, propcount, NULL, query, show_total);
}

CCode 
StoreCommandCOPY(StoreClient *client, StoreObject *object, StoreObject *collection)
{
    CCode ccode;
    FILE *src = NULL;
    char srcpath[XPL_MAX_PATH + 1];
    char dstpath[XPL_MAX_PATH + 1];
    StoreObject newobject;
     
    // check the parameters are ok and the document exists   
    if (STORE_IS_FOLDER(object->type)) return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);

	// check authorization on the parent collection
	ccode = StoreObjectCheckAuthorization(client, collection, STORE_PRIV_BIND | STORE_PRIV_READ);
    if (ccode) return ccode;

	// copy the files on disk
    FindPathToDocument(client, object->collection_guid, object->guid, srcpath, sizeof(srcpath));
    src = fopen(srcpath, "r");
    if (!src) {
        ccode = ConnWriteStr(client->conn, MSG4224CANTREADMBOX);
        goto finish;
    }
    fclose(src);

    MaildirTempDocument(client, collection->guid, dstpath, sizeof(dstpath));
    
    if (link(srcpath, dstpath) != 0) {
        ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
        goto finish;
    }
    // TODO: how to detect MSG5220QUOTAERR ?
    
    // make a copy of the storeobject
	memcpy(&newobject, object, sizeof(StoreObject));
	newobject.guid = 0;
	newobject.filename[0] = '\0';
		
	if (StoreObjectCreate(client, &newobject))
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
    
    StoreObjectCopyInfo(client, object, &newobject);
    
    // swap dest to src, and find new dest - move temp file into correct position
    srcpath[0] = '\0';
    strncpy(srcpath, dstpath, sizeof(srcpath));
    FindPathToDocument(client, collection->guid, newobject.guid, dstpath, sizeof(dstpath));
    if (link(srcpath, dstpath) != 0) {
    	// StoreObjectCreate saved the new object; if we can't create
    	// the data, we should remove that object
    	StoreObjectRemove(client, &newobject);
        ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
        goto finish;
    }
    unlink(srcpath);
    
    // update metadata
    newobject.collection_guid = collection->guid;
    newobject.time_created = newobject.time_modified = time(NULL);
    
    StoreObjectFixUpFilename(collection, &newobject);
    
    StoreObjectSave(client, &newobject);
    
    // slight race here without store-level locking
    StoreObjectUpdateImapUID(client, &newobject);
    
    ++client->stats.insertions;
    StoreWatcherEvent(client, &newobject, STORE_WATCH_EVENT_NEW);
    
    ccode = ConnWriteF(client->conn, "1000 " GUID_FMT " %d\r\n",
                       newobject.guid, newobject.version);

finish:
	return ccode;
}

// FIXME: doesn't detect all error conditions
CCode
StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid)
{
	StoreObject object;
	StoreObject container;
	int ret;
	char container_path[MAX_FILE_NAME+1];
	char *container_final_sep = NULL;
	char *ptr;
	
	// look for existing name/guid first?
	if (name != NULL) {
		if (StoreObjectFindByFilename(client, name, &object) == 0) {
			return ConnWriteStr(client->conn, MSG4226MBOXEXISTS);
		}
	}
	if (guid != 0) {
		if (StoreObjectFind(client, guid, &object) == 0) {
			return ConnWriteStr(client->conn, MSG4226GUIDINUSE);
		}
	}
	
	// first, find the name of the containing collection from the
	// name given. This will end in a /, but we will want to remove
	// that final / later unless the path is literally '/', so we
	// save the pointer for later...
	strncpy(container_path, name, MAX_FILE_NAME);
	ptr = strrchr(container_path, '/');
	if (ptr == NULL) {
		return ConnWriteStr(client->conn, MSG3019BADCOLLNAME);
	} 
	container_final_sep = ptr++;
	*ptr = '\0';
	
	// Create the object first to allocate our wanted guid
	memset(&object, 0, sizeof(StoreObject));
	object.type = STORE_DOCTYPE_FOLDER;
	object.guid = guid;
	strncpy(object.filename, name, MAX_FILE_NAME);
	
	ret = StoreObjectCreate(client, &object);
	switch (ret) {
		case 0:
			// if it's successful, that's ok.
			break;
		case -1:
			return ConnWriteF(client->conn, "4226 Guid " GUID_FMT " already exists\r\n", 
				object.guid);
			break;
		default:
			return ConnWriteStr(client->conn, MSG5005DBLIBERR);
			break;
	}
	/* FIXME - detect following errors?
	return ConnWriteStr(client->conn, MSG4240NOPERMISSION);
	return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
	*/
	// now try to find the parent collection, creating any collections
	// as needed.
	if (container_path[1] != '\0') {
		uint64_t parent_guid = 0;
		
		ptr = container_path + 1;
		while (*ptr != '\0') {
			if (*ptr == '/') {
				*ptr = '\0';
				if (StoreObjectFindByFilename(client, container_path, &container)) {
					// need to create this path
					memset(&container, 0, sizeof(StoreObject));
					container.type = STORE_DOCTYPE_FOLDER;
					container.collection_guid = parent_guid;
					strncpy(container.filename, container_path, MAX_FILE_NAME);
					ret = StoreObjectCreate(client, &container);
					if (ret != 0) {
						StoreObjectRemove(client, &container);
						return ConnWriteStr(client->conn, MSG5005DBLIBERR);
					}
					// save the guid for later.
					parent_guid = container.guid;
				} else {
					if (container.type != 4096) {
						// this isn't a collection - erk!
						return ConnWriteStr(client->conn, MSG3019BADCOLLNAME);
					}
				}
				*ptr = '/';
			}
			ptr++;
		}
	}
	
	// remove any trailing / from the path unless it's literally /
	if (container_final_sep != container_path) {
		*container_final_sep = '\0';
	}
	// now find the containing collection
	if (StoreObjectFindByFilename(client, container_path, &container)) {
		// no such collection - but it should have been created. Must be DB error
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	}
	
	// check we can save this new object and if not, error out
	object.collection_guid = container.guid;
	StoreObjectFixUpFilename(&container, &object);
	
	if (StoreObjectSave(client, &object)) {
		StoreObjectRemove(client, &object);
		return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
	}
	
	// announce creation on parent container
	++client->stats.insertions;
	StoreWatcherEvent(client, &container, STORE_WATCH_EVENT_NEW);
	
	return ConnWriteF(client->conn, "1000 " GUID_FMT " %d\r\n", 
		object.guid, object.version);
}


CCode
StoreCommandDELETE(StoreClient *client, StoreObject *object)
{
	StoreObject collection;
	char path[XPL_MAX_PATH]; 
	int ccode;
		
	if (STORE_IS_FOLDER(object->type))
		return ConnWriteStr(client->conn, MSG4231USEREMOVE);
	
	// find the containing collection
	ccode = StoreObjectFind(client, object->collection_guid, &collection);
	if (ccode != 0) return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	
	// check we have the required permissions.
	ccode = StoreObjectCheckAuthorization(client, &collection, STORE_PRIV_UNBIND);
	if (ccode) return ConnWriteStr(client->conn, MSG5005DBLIBERR); // TODO : less generic errors
	
	switch(object->type) {
	case STORE_DOCTYPE_MAIL:
		// check to see if we can remove this document from a conversation
		break;
	case STORE_DOCTYPE_CALENDAR:
		// do we want to remove its events also?
		break;
	default:
		break;
	}
	
	// TODO: check to see if we can unlink other documents from this document
	
	// Remove the document from the store
	StoreObjectRemove(client, object);
	
	// Remove document from the search index
	// IndexRemoveDocument(index, guid);
	
	// Remove the file content from the filesystem
	FindPathToDocument(client, object->collection_guid, object->guid, path, sizeof(path));
	if (unlink(path) != 0) {
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	
	// update stats and fire any event
	++client->stats.deletions;
	StoreWatcherEvent(client, object, STORE_WATCH_EVENT_DELETED);
	
	ccode = ConnWriteStr(client->conn, MSG1000OK);
	
finish:
	
	return ccode;
}


// one of filename and bytes must be set 
// if DELIVER FILE - it's filename
// if DELIVER STREAM - it's bytes on a network socket

CCode
StoreCommandDELIVER(StoreClient *client, char *sender, char *authSender, 
                    char *filename, size_t bytes)
{
    CCode ccode;
    FILE *msgfile = NULL;
    char tmpFile[XPL_MAX_PATH+1];

    tmpFile[0] = 0;

    if (!filename) {
        if (bytes <= 0) {
            return ConnWriteStr(client->conn, MSG3017INTARGRANGE);
        }
        msgfile = XplOpenTemp(MsgGetDir(MSGAPI_DIR_WORK, NULL, 0), "w+b", tmpFile);
        if (!msgfile) {
            return ConnWriteStr(client->conn, MSG5202TMPWRITEERR);
        }
        ccode = ConnReadToFile(client->conn, msgfile, bytes);
        if (-1 == ccode) {
            unlink(tmpFile);
            fclose (msgfile);
            return ConnWriteStr(client->conn, MSG5202TMPWRITEERR);
        }
    } else {
        struct stat sb;
        char fullPath[XPL_MAX_PATH + 1];

        snprintf(fullPath, sizeof(fullPath), "%s/%s", StoreAgent.store.spoolDir, filename);


        if (stat(fullPath, &sb)) {
            return ConnWriteStr(client->conn, MSG4201FILENOTFOUND);
        }
        bytes = sb.st_size;

        msgfile = fopen(fullPath, "r");
        if (!msgfile) {
            return ConnWriteStr(client->conn, MSG5201FILEREADERR);
        }
    }

    if (-1 == (ccode = ConnWriteStr(client->conn, MSG2053SENDRECIP)) ||
        -1 == (ccode = ConnFlush(client->conn))) 
    {
        goto cleanup;
    }

    while (ccode >= 0) {
        char *args[3];
        char *recipient;
        char *mbox;
        unsigned long flags = 0;
        int n;
        int status;
        char buffer[CONN_BUFSIZE];

        ccode = ConnReadAnswer(client->conn, buffer, sizeof(buffer));
        if (-1 == ccode) {
            goto cleanup;
        }
        if (!buffer[0]) {
            ccode = ConnWriteStr(client->conn, MSG1000OK);
            break;
        }

        /* <recipient> <mailbox> <flags> */

        n = BongoStringSplit(buffer, ' ', args, 3);
        if (TOKEN_OK != (ccode = CheckTokC(client, n, 3, 3)) ||
            TOKEN_OK != (ccode = ParseUserName(client, args[0])) ||
            TOKEN_OK != (ccode = ParseUnsignedLong(client, args[2], &flags)))
        {
            if (-1 == (ccode = ConnFlush(client->conn))) {
                goto cleanup;
            }
            continue;
        }
        recipient = args[0];
        mbox = args[1];

        if (fseek (msgfile, 0, SEEK_SET)) {
            ccode = ConnWriteStr(client->conn, MSG5201FILEREADERR);
            goto cleanup;
        }
        SelectStore(client, recipient);
        status = DeliverMessageToMailbox(client, sender, authSender, recipient, mbox,
                                         msgfile, 0, bytes, flags);
        UnselectStore(client);
        switch (status) {
        case DELIVER_SUCCESS:
            ccode = ConnWriteStr(client->conn, MSG1000DELIVERYOK);
            break;
        default:
            ccode = ConnWriteF(client->conn, MSG5260DELIVERYFAILED, status);
            break;
        }
        if (-1 == ccode) {
            goto cleanup;
        }
        if (-1 == (ccode = ConnFlush(client->conn))) {
            goto cleanup;
        }
    }

cleanup:    
    if (msgfile) {
        fclose (msgfile);
    }
    if (tmpFile[0]) {
        unlink(tmpFile);
    }

    return ccode;
}

CCode 
StoreCommandEVENTS(StoreClient *client, 
                   char *startUTC, char *endUTC, 
                   StoreObject *calendar, 
                   unsigned int mask,
                   char *uid,
                   const char *query,
                   int start, int end,
                   StorePropInfo *props, int propcount)
{
	// pretend we have no events for now.
	return ConnWriteStr(client->conn, MSG1000OK);
/*
    CCode ccode = 0;

    // FIXME - need to handle STORE_PRIV_READ_BUSY (bug 174023) 

    if (calendar) {
        ccode = StoreCheckAuthorization(client, calendar, 
                                        STORE_PRIV_LIST);
        if (ccode) {
            return ccode;
        }
    }

    if (query) {
        IndexHandle *index;
        uint64_t *guids = NULL;
        uint64_t *guidsptr = NULL;
        char newQuery[CONN_BUFSIZE + 64];
        int total;
        int i;
        DStoreDocInfo *info;
        int checkauth = !calendar && !IsOwnStoreSelected(client);
        
        snprintf(newQuery, sizeof(newQuery) - 1, "(nmap.type:event) AND (%s)", query);

        index = IndexOpen(client);
        if (!index) {
            return ConnWriteStr(client->conn, MSG4120INDEXLOCKED);
        }
        guids = IndexSearch(index, newQuery, TRUE, TRUE, 
                            -1, -1, NULL, &total, INDEX_SORT_DATE_DESC);
        IndexClose(index);

        if (!guids) {
            return ConnWriteStr(client->conn, MSG5007INDEXLIBERR);
        }
        
        // FIXME: This is a big allocation 
        info = BONGO_MEMSTACK_ALLOC(&client->memstack, total * sizeof(DStoreDocInfo));

        for (guidsptr = guids, i = 0; *guidsptr; ++guidsptr) {
            switch (DStoreGetDocInfoGuid(client->handle, *guidsptr, &info[i])) {
            case 1:
                if (checkauth && 
                    StoreCheckAuthorizationQuiet(client, info, STORE_PRIV_READ)) 
                {
                    --total;
                    continue;
                }
                i++;
                break;
            case 0:
                --total;
                continue;
            case -1:
                MemFree(guids);
                return ConnWriteStr(client->conn, MSG5005DBLIBERR);
            }
        }

        MemFree(guids);

        if (-1 == start) {
            start = 0;
            end = total;
        }

        for (i = start; i < end && i < total && -1 != ccode; i++) {
            ccode = ShowDocumentInfo(client, &info[i], props, propcount);
        }
        
        if (-1 != ccode) {
            ccode = ConnWriteStr(client->conn, MSG1000OK);
        }
    } else {
        DStoreStmt *stmt;
        AuthFilterStruct f;

        stmt = DStoreFindEvents(client->handle, 
                                startUTC, endUTC,
                                calendar ? calendar->guid : 0,
                                mask, uid);
        
        if (!stmt) {
            return ConnWriteStr(client->conn, MSG5005DBLIBERR);
        }
        
        if (!calendar && !IsOwnStoreSelected(client)) {
            f.client = client;
            f.neededPrivileges = STORE_PRIV_READ;
            DStoreInfoStmtAddFilter(stmt, AuthFilter, &f);
        }

        ccode = ShowStmtDocuments(client, stmt, props, propcount);

        DStoreStmtEnd(client->handle, stmt);
    }

    return 0;
    */
}

CCode
StoreCommandFLAG(StoreClient *client, StoreObject *object, uint32_t change, int mode)
{
	uint32_t old_flags;
	int ccode;
	
	if (mode == STORE_FLAG_SHOW) {
		ccode = StoreObjectCheckAuthorization(client, object, STORE_PRIV_READ_PROPS);
		if (ccode) return ccode;
		return ConnWriteF(client->conn, "1000 %u %u\r\n", object->flags, object->flags);
	}
	
	// just handle ADD / REMOVE / REPLACE now
	old_flags = object->flags;
	if (mode == STORE_FLAG_ADD) 	object->flags |= change;
	if (mode == STORE_FLAG_REMOVE)	object->flags &= ~change;
	if (mode == STORE_FLAG_REPLACE)	object->flags = change;
	
	ccode = StoreObjectCheckAuthorization(client, object, STORE_PRIV_WRITE_PROPS);
	if (ccode) return ccode;
	
	// FIXME: need to propogate changes on mail documents to conversations?
	if (object->type == STORE_DOCTYPE_MAIL) {
		
	}
	
	// save the changes
	if (StoreObjectSave(client, object) != 0) {
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	}
	
	++client->stats.updates;
	StoreWatcherEvent(client, object, STORE_WATCH_EVENT_FLAGS);

	return ConnWriteF(client->conn, "1000 %u %u\r\n", old_flags, object->flags);
}

CCode
StoreCommandINFO(StoreClient *client, StoreObject *object,
                 StorePropInfo *props, int propcount)
{
	return StoreObjectIterDocinfo(client, object, props, propcount, FALSE);
}


CCode
StoreCommandISEARCH(StoreClient *client, const char *query, int start, int end,
                    StorePropInfo *props, int propcount)
{
	return ConnWriteStr(client->conn, MSG1000OK);
	/*
    char buf[200];
    int ccode = 0;
    IndexHandle *index;
    uint64_t *guids;
    int displayed;
    int total;

    if (end - start > STORE_GUID_LIST_MAX) {
        return ConnWriteStr(client->conn, MSG3021BADRANGESIZE);
    }
    
    index = IndexOpen(client);
    if (!index) {
        return ConnWriteStr(client->conn, MSG4120INDEXLOCKED);
    }

    guids = IndexSearch(index, query, FALSE, FALSE, start, end, &displayed, &total, 
                        INDEX_SORT_RELEVANCE);
    if (!guids) {
        IndexClose(index);
        return ConnWriteStr(client->conn, MSG5007INDEXLIBERR);
    }

    snprintf(buf, sizeof(buf), "1000 %d\r\n", total);

    ccode = ShowGuidsDocumentsWithMessage(client, guids, NULL, NULL, 
                                          props, propcount, buf);

    IndexClose(index);

    MemFree(guids);

    return ccode;
    */
}

CCode
StoreCommandLIST(StoreClient *client, 
                 StoreObject *collection, 
                 int start, int end, 
                 uint32_t flagsmask, uint32_t flags,
                 StorePropInfo *props, int propcount)
{
	int ccode;
	
	ccode = StoreObjectCheckAuthorization(client, collection, STORE_PRIV_LIST);
	if (ccode) return ccode;
	
	return StoreObjectIterCollectionContents(client, collection, start, 
	    end, flagsmask, flags, props, propcount, NULL, NULL, FALSE);
}

CCode
StoreCommandLINK(StoreClient *client, StoreObject *document, StoreObject *related)
{
	if (StoreObjectLink(client, document, related) == 0) {
		return ConnWriteStr(client->conn, MSG1000OK);
	} else {
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	}
}

CCode
StoreCommandLINKS(StoreClient *client, BOOL reverse, StoreObject *document)
{
	int ccode;
	
	// FIXME: does this auth check make sense?
	ccode = StoreObjectCheckAuthorization(client, document, STORE_PRIV_READ);
	if (ccode) return ccode;
	
	return StoreObjectIterLinks(client, document, reverse);
}

CCode
StoreCommandUNLINK(StoreClient *client, StoreObject *document, StoreObject *related)
{
	if (StoreObjectUnlink(client, document, related) == 0) {
		return ConnWriteStr(client->conn, MSG1000OK);
	} else {
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	}
}

CCode
StoreCommandMAILINGLISTS(StoreClient *client, char *source)
{
	// TODO
	return ConnWriteStr(client->conn, MSG3000UNKNOWN);
}

CCode
StoreCommandMESSAGES(StoreClient *client, const char *source, const char *query, 
                     int start, int end, StoreObject *center, 
                     uint32_t flagsmask, uint32_t flags,
                     int displayTotal,
                     StoreHeaderInfo *headers, int headercount, 
                     StorePropInfo *props, int propcount)
{
	// FIXME: obsolete? This looks very similar to LIST; just added
	// source and query. Might as well make it one command...
	return ConnWriteStr(client->conn, MSG3000UNKNOWN);
}

CCode
StoreCommandMFLAG(StoreClient *client, uint32_t change, int mode)
{
	// FIXME: obsolete.
	return ConnWriteStr(client->conn, MSG3000UNKNOWN);
}


CCode
StoreCommandMIME(StoreClient *client, StoreObject *document)
{
	return -1;
#if 0
	StoreObject object;
    int ccode = 0;
    DStoreDocInfo info;
    MimeReport *report = NULL;
    NLockStruct *lock = NULL;

    ccode = StoreObjectFind(client, guid, &object);
    if (ccode == -1) return ConnWriteStr(client->conn, MSG4220NOGUID);
    if (ccode != 0) return ConnWriteStr(client->conn, MSG5005DBLIBERR);
    
	ccode = StoreObjectCheckAuthorization(client, &object, STORE_PRIV_READ);
	if (ccode) return ccode;
	
    if (STORE_IS_FOLDER(object.type)) return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
 
    ccode = StoreGetSharedFairLockQuiet(client, &lock, object.collection_guid, 3000);
    if (ccode) return ccode;
    
    // FIXME: need to re-do the MimeGetInfo() API for store objects
    switch (MimeGetInfo(client, &info, &report)) {
    case 1:
        ccode = MimeReportSend(client, report);
        MimeReportFree(report);
        if (-1 != ccode) {
            ccode = ConnWriteStr(client->conn, MSG1000OK);
        }
        break;
    case -1:
        ccode = ConnWriteStr(client->conn, MSG5005DBLIBERR);
        break;
    case -2:
        ccode = ConnWriteStr(client->conn, MSG4120DBLOCKED);
        break;
    case -3:
        ccode = ConnWriteF(client->conn, MSG4220NOGUID);
        break;
    case -4:
        ccode = ConnWriteStr(client->conn, MSG4224CANTREAD);
        break;
    case -5:
    case 0:
    default:
        ccode = ConnWriteStr(client->conn, MSG5004INTERNALERR);
        break;
    }
            
    if (lock) {
        StoreReleaseSharedFairLockQuiet(&lock);
    }
        
    return ccode;
#endif
}

// FIXME: needs to move any store properties etc. related to this document

CCode 
StoreCommandMOVE(StoreClient *client, StoreObject *object, 
                 StoreObject *destination_collection, const char *newfilename)
{
	int ccode = 0;
	char src_path[XPL_MAX_PATH];
	char dst_path[XPL_MAX_PATH];
	StoreObject source_collection;
	StoreObject original;
	
	if (STORE_IS_FOLDER(object->type)) return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
	if (!STORE_IS_FOLDER(destination_collection->type)) 
		return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
	
	ccode = StoreObjectFind(client, object->collection_guid, &source_collection);
	if (ccode != 0) return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	
	// check we have the rights to do this move
	ccode = StoreObjectCheckAuthorization(client, &source_collection, STORE_PRIV_UNBIND);
	if (ccode) goto finish;
	ccode = StoreObjectCheckAuthorization(client, destination_collection, STORE_PRIV_BIND);
	if (ccode) goto finish;
	
	// copy the original object so we can fire events on it later
	memcpy(&original, object, sizeof(StoreObject));
	
	// move the file to a temporary location
	FindPathToDocument(client, object->collection_guid, object->guid, src_path, sizeof(src_path));  
	MaildirTempDocument(client, destination_collection->guid, dst_path, sizeof(dst_path)); 
	
	if (link(src_path, dst_path) != 0) {
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}

	if (unlink(src_path) != 0) {
		// FIXME: try to remove the new link we just created?
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	
	// now change the information on this object and fix up the filename
	object->collection_guid = destination_collection->guid;
	if (newfilename != NULL)
		strncpy(object->filename, newfilename, MAX_FILE_NAME);
	StoreObjectFixUpFilename(destination_collection, object);
	StoreObjectUpdateImapUID(client, object);
	
	if (STORE_DOCTYPE_MAIL == object->type) {
		// FIXME: do something with the conversations ??
	}
	
	// move the temp doc we created to the new location
	src_path[0] = '\0';
	strncpy(src_path, dst_path, sizeof(src_path));
	FindPathToDocument(client, object->collection_guid, object->guid, dst_path, sizeof(dst_path));
	
	if (link(src_path, dst_path) != 0) {
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	if (unlink(src_path) != 0) {
		// FIXME: try to remove the new link we just created?
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	
	// save our changes
	StoreObjectSave(client, object);

	// fire off any events
	StoreWatcherEvent(client, &original, STORE_WATCH_EVENT_DELETED);
	StoreWatcherEvent(client, object, STORE_WATCH_EVENT_NEW);

    ccode = ConnWriteStr(client->conn, MSG1000OK);
    
finish:
    
    return ccode;
}

CCode
StoreCommandPROPGET(StoreClient *client, 
                    StoreObject *object, 
                    StorePropInfo *props, int propcount)
{
	if (propcount == 0) {
		return StoreObjectIterProperties(client, object);
	} else {
		// output requested props for each document
		return StoreObjectIterDocinfo(client, object, props, propcount, TRUE);
	}
}

CCode
StoreCommandPROPSET(StoreClient *client, 
                    StoreObject *object,
                    StorePropInfo *prop,
                    int size)
{
	int ccode;
	
	prop->value = BONGO_MEMSTACK_ALLOC(&client->memstack, prop->valueLen + 1);
	if (!prop->value) {
		return ConnWriteStr(client->conn, MSG5001NOMEMORY);
	}
	if (-1 == (ccode = ConnWriteStr(client->conn, MSG2002SENDVALUE)) ||
	    -1 == (ccode = ConnFlush(client->conn)) ||
	    -1 == (ccode = ConnReadCount(client->conn, prop->value, prop->valueLen)))
	{
		return ccode;
	}
	prop->value[prop->valueLen] = 0;
	
	ccode = StoreObjectCheckAuthorization(client, object, STORE_PRIV_WRITE_PROPS);
	if (ccode) return ccode;
	switch (prop->type) {
		case STORE_PROP_EVENT_ALARM:
			// FIXME: this did cache alarms in a separate DB.
			return ConnWriteStr(client->conn, MSG5005DBLIBERR);
			break;
		case STORE_PROP_ACCESS_CONTROL:
			// FIXME: this is how we used to set ACLs.
			return ConnWriteStr(client->conn, MSG5005DBLIBERR);
			break;
		default:
			// this is a plain external property
			ccode = StoreObjectSetProperty(client, object, prop);
			if (ccode < -1) {
				return ConnWriteStr(client->conn, MSG5005DBLIBERR);
			} else if (ccode == -1) {
				// failed, but already returned a notice to the client
				return -1;
			} else {
				return ConnWriteStr(client->conn, MSG1000OK);
			}
			break;
	}
	
	++client->stats.updates;
	StoreWatcherEvent(client, object, STORE_WATCH_EVENT_MODIFIED);
	ccode = ConnWriteStr(client->conn, MSG1000OK);
	
	return ccode;
}


/* returns: -1, causing storecommandloop to exit */
CCode
StoreCommandQUIT(StoreClient *client)
{
    if (-1 != ConnWriteStr(client->conn, MSG1000BYE)) {
        ConnFlush(client->conn);
    }
    return -1;
}

CCode
StoreCommandREAD(StoreClient *client, StoreObject *object,
                 int64_t requestStart, uint64_t requestLength)
{
	int ccode = 0;
	
	// check that we're allowed to read
	ccode = StoreObjectCheckAuthorization(client, object, STORE_PRIV_READ);
	if (ccode) goto finish;
	
	if (STORE_IS_FOLDER(object->type)) {
		// 2001 <guid> <version>
		ccode = ConnWriteF(client->conn, "2001 " GUID_FMT " %d\r\n", object->guid, object->version);
		ccode = ConnWriteStr(client->conn, MSG1000OK);
	} else if (STORE_IS_DBONLY(object->type)) {
		ccode = ConnWriteStr(client->conn, MSG3016DOCTYPEUNREADABLE);
	} else {
		ccode = ShowDocumentBody(client, object, requestStart, requestLength);
	}

finish:
    return ccode;
}


CCode 
StoreCommandREINDEX(StoreClient *client, StoreObject *document)
{
	// want to re-do this completely.
	return ConnWriteStr(client->conn, MSG5005DBLIBERR);
}

// FIXME: doesn't handle following errors
// MSG3019BADCOLLNAME
// MSG4240NOPERMISSION
// MSG4228CANTWRITEMBOX
// FIXME: doesn't create collections as needed to create destination tree
CCode
StoreCommandRENAME(StoreClient *client, StoreObject *collection,
                   char *newfilename)
{
	int ccode = 0;
	
	ccode = StoreObjectCheckAuthorization(client, collection, STORE_PRIV_UNBIND);
	if (ccode) return ccode;
	
	// locking: want to acquire first a lock on the source tree, 
	// then take a lock on the non-existant destination tree
	
	if (StoreObjectRenameSubobjects(client, collection, newfilename))
		goto dberror;
	
	strncpy(collection->filename, newfilename, MAX_FILE_NAME);
	if (StoreObjectSave(client, collection)) 
		goto dberror;
	
	// FIXME - need to fire an event for each collection renamed
	StoreWatcherEvent(client, collection, STORE_WATCH_EVENT_COLL_RENAMED);
	
	return ConnWriteStr(client->conn, MSG1000OK);

dberror:
	return ConnWriteStr(client->conn, MSG5005DBLIBERR);
}

CCode
ReceiveToFile(StoreClient *client, const char *path, uint64_t *size)
{
	FILE *fh;
	CCode ccode;
	uint64_t receive_size;
	
	fh = fopen(path, "w");
	if (!fh) {
		if (ENOSPC == errno)
			return ConnWriteStr(client->conn, MSG5220QUOTAERR); // FIXME
		else
			return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
	}	
	if (-1 == (ccode = ConnWrite(client->conn, "2002 Send document.\r\n", 21)) || 
	    -1 == (ccode = ConnFlush(client->conn))) {
		return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
	}
	
	if (size >= 0) {
		ccode = ConnReadToFile(client->conn, fh, *size);
		receive_size = *size;
	} else {
		ccode = receive_size = ConnReadToFileUntilEOS(client->conn, fh);
	}
	if (-1 == ccode) {
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	*size = receive_size;
	
	ccode = fclose(fh);
	fh = NULL;
	if (ccode) {
		ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
		goto finish;
	}
	
	return 0;
	
finish:
	return ccode;
}

// FIXME: rstart/rend range processing totally broken. 
// FIXME: range processing needs to unlink and copy-on-write
// FIXME: we should probably be removing some/all document properties?
CCode
StoreCommandREPLACE(StoreClient *client, StoreObject *object, int size, uint64_t rstart, uint64_t rend, uint32_t version)
{
	CCode ccode;
	char path[XPL_MAX_PATH + 1];
	char tmppath[XPL_MAX_PATH + 1];
	uint64_t tmpsize;
	
	if (StoreAgent.flags & STORE_DISK_SPACE_LOW) {
		return ConnWriteStr(client->conn, MSG5221SPACELOW);
	}
	
	// check object type and permissions
	if (STORE_IS_FOLDER(object->type) || STORE_IS_CONVERSATION(object->type)) {
		return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
	}
	
	ccode = StoreObjectCheckAuthorization(client, object, STORE_PRIV_WRITE_CONTENT);
	if (ccode) return ccode;
	
	// write the waiting data to a temporary file
	MaildirTempDocument(client, object->collection_guid, tmppath, sizeof(tmppath));
	
	tmpsize = size;
	ccode = ReceiveToFile(client, tmppath, &tmpsize);
	if (ccode) goto finish;
	
	// update metadata
	if ((rend != 0) && (rend > object->size)) {
		// writing a range - new size must be maximum of old size and new end of range
		tmpsize = rend;
	}
	
	StoreObjectUpdateImapUID(client, object);
	StoreProcessDocument(client, object, tmppath);
	
	// update the document
	FindPathToDocument(client, object->collection_guid, object->guid, path, sizeof(path));
	if (rend != 0) {
		// we're writing a range, not replacing the whole file
		// TODO
	} else {
		if (unlink(path) != 0) {
			ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX);
			goto finish;
		}
		link(tmppath, path);
		unlink(tmppath);
	}
	
	// update the version if required
	if ((version > 0) && (version != object->version)) {
		object->version = version;
	}
	// update other metadata
	StoreObjectUpdateModifiedTime(object);
	object->size = tmpsize;
	
	if (StoreObjectSave(client, object)) {
		ccode = ConnWriteStr(client->conn, MSG5005DBLIBERR);
		goto finish;
	}
	
	// now we're done, do watcher events
	++client->stats.updates;
	StoreWatcherEvent(client, object, STORE_WATCH_EVENT_MODIFIED);
	
	ccode = ConnWriteF(client->conn, "1000 " GUID_FMT " %d\r\n", 
                       object->guid, object->version);

finish:
	return ccode;
}

CCode
StoreCommandREMOVE(StoreClient *client, StoreObject *collection)
{
    CCode ccode;
    
    if (! STORE_IS_FOLDER(collection->type)) {
        return ConnWriteStr(client->conn, MSG4231USEPURGE);
    } else {
    	ccode = StoreObjectCheckAuthorization(client, collection, STORE_PRIV_UNBIND);
    	if (ccode) return ccode;

		if (StoreObjectRemoveCollection(client, collection) == 0) {
			// finished with success!
			return ConnWriteStr(client->conn, MSG1000OK);
		} else {
			// some error
			return ConnWriteStr(client->conn, MSG5005DBLIBERR);
		}
    } 
}


/* N.B. This command does no locking.  It is for debugging purposes only. */

CCode 
StoreCommandRESET(StoreClient *client)
{
    DeleteStore(client);
    return ConnWriteStr(client->conn, MSG1000OK);
}


CCode
StoreCommandSTORE(StoreClient *client, char *user)
{
    if (!user) {
        UnselectStore(client);
        return ConnWriteStr(client->conn, MSG1000OK);
    }

    if (StoreAgent.installMode || !strncmp(user, "_system", 7)) {
        int ret = 0;
        ret = SelectStore(client, user);
        switch (ret) {
            case 0:
                return ConnWriteStr(client->conn, MSG1000OK);
                break;
            case -2:
                return ConnWriteStr(client->conn, MSG4225BADVERSION);
            case -1:
            default:
                return ConnWriteStr(client->conn, MSG4224BADSTORE);
                break;
        }
    }

    if (0 != MsgAuthFindUser(user)) {
        Log(LOG_INFO, "Couldn't find user object for %s\r\n", user);
        /* the previous nmap returned some sort of user locked message? */
        return ConnWriteStr(client->conn, MSG4100STORENOTFOUND);
    }
    
    if (SelectStore(client, user)) {
        return ConnWriteStr(client->conn, MSG4224BADSTORE);
    } else {
        return ConnWriteStr(client->conn, MSG1000OK);
    }
}

CCode
StoreCommandTIMEOUT(StoreClient *client, int lockTimeoutMs)
{
	// should be obsolete really
	return ConnWriteStr(client->conn, MSG3000UNKNOWN);
}

CCode 
StoreCommandTOKEN(StoreClient *client, char *token)
{
    StoreToken *tok;

    tok = MemMalloc(sizeof(StoreToken));

    strncpy(tok->data, token, sizeof(tok->data));
    tok->data[sizeof(tok->data)-1] = 0;

    tok->next = client->principal.tokens;
    client->principal.tokens = tok;

    return ConnWriteStr(client->conn, MSG1000OK);
}

CCode 
StoreCommandTOKENS(StoreClient *client)
{
    StoreToken *tok;
    CCode ccode = 0;

    for (tok = client->principal.tokens;
         tok && ccode >= 0;
         tok = tok->next)
    {
        ccode = ConnWriteF(client->conn, "2001 %s\r\n", tok->data);
    }
    
    if (ccode >= 0) {
        ccode = ConnWriteStr(client->conn, MSG1000OK);
    }
    return ccode;
}

CCode
StoreCommandUSER(StoreClient *client, char *user, char *password, int nouser)
{
    if (user) {
        return SelectUser(client, user, password, nouser);
    } else {
        UnselectUser(client);
        return ConnWriteStr(client->conn, MSG1000OK);
    }
}

// Set the watched flags for the specified collection.  If we were watching 
// something else before, stop watching it now.

CCode 
StoreCommandWATCH(StoreClient *client, StoreObject *collection, int flags)
{
	CCode ccode;
	
	// stop watching whatever we were watching before
	if (client->watch.collection) {
		if (StoreWatcherRemove(client, client->watch.collection))
			return ConnWriteStr(client->conn, MSG5004INTERNALERR);
		
		client->watch.collection = 0;
		client->watch.flags = 0;
	}
	
	// watch the new collection
	if (flags) {
		flags |= STORE_WATCH_EVENT_COLL_REMOVED;
		flags |= STORE_WATCH_EVENT_COLL_RENAMED;
		
		ccode = StoreObjectCheckAuthorization(client, collection, STORE_PRIV_READ);
		if (ccode) {
			return ccode;
		}
		
		if (StoreWatcherAdd(client, collection->guid))
			return ConnWriteStr(client->conn, MSG5004INTERNALERR);
		
		client->watch.collection = collection->guid;
		client->watch.flags = flags;
	}
	return ConnWriteStr(client->conn, MSG1000OK);
}

// FIXME: do something with the link document
CCode
StoreCommandWRITE(StoreClient *client, 
				  StoreObject *collection, 
				  int doctype, 
				  uint64_t size,
				  uint32_t addflags, 
				  uint64_t guid, 
				  const char *filename, 
				  uint64_t timestamp, 
				  uint64_t guid_link,
				  int no_process)
{
	CCode ccode;
	StoreObject newdocument;
	uint64_t tmpsize;
	char path[XPL_MAX_PATH+1];
	char tmppath[XPL_MAX_PATH+1];
	
	if (StoreAgent.flags & STORE_DISK_SPACE_LOW) {
		return ConnWriteStr(client->conn, MSG5221SPACELOW);
	}

	if (STORE_IS_FOLDER(doctype) || STORE_IS_CONVERSATION(doctype)) {
		return ConnWriteStr(client->conn, MSG3015BADDOCTYPE);
	}

	memset(&newdocument, 0, sizeof(StoreObject));
	
	// try to create new object
	newdocument.guid = guid;
	newdocument.type = doctype;
	
	if (StoreObjectCreate(client, &newdocument))
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	
	// read content to temporary file
	MaildirTempDocument(client, collection->guid, tmppath, sizeof(tmppath));
	
	tmpsize = size;
	ccode = ReceiveToFile(client, tmppath, &tmpsize);
	if (ccode) goto finish;
	
	// update new object metadata
	newdocument.collection_guid = collection->guid;
	newdocument.size = tmpsize;
	newdocument.time_created = timestamp;
	newdocument.time_modified = timestamp;
	newdocument.flags = addflags;
	if (filename) strncpy(newdocument.filename, filename, MAX_FILE_NAME);
	
	StoreObjectFixUpFilename(collection, &newdocument);
	StoreObjectUpdateImapUID(client, &newdocument);
	if (no_process == 0) {
		// update metadata if we haven't been asked not to
		StoreProcessDocument(client, &newdocument, tmppath);
        }
	
	// put content in place
	FindPathToDocument(client, collection->guid, newdocument.guid, path, sizeof(path));
	link(tmppath, path);
	unlink(tmppath);
	
	// save new object
	if (StoreObjectSave(client, &newdocument)) {
	        StoreObjectRemove(client, &newdocument);
		return ConnWriteStr(client->conn, MSG5005DBLIBERR);
	}
	
	// announce its creation
	++client->stats.insertions;
	StoreWatcherEvent(client, &newdocument, STORE_WATCH_EVENT_NEW);
	
	return ConnWriteF(client->conn, "1000 " GUID_FMT " %d\r\n", newdocument.guid, newdocument.version);

finish:
	return ccode;
}
