/* mupdate.c -- cyrus murder database master 
 *
 * $Id: mupdate.c,v 1.73 2003/04/01 19:34:37 rjs3 Exp $
 * Copyright (c) 1998-2003 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer. 
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any other legal
 *    details, please contact  
 *      Office of Technology Transfer
 *      Carnegie Mellon University
 *      5000 Forbes Avenue
 *      Pittsburgh, PA  15213-3890
 *      (412) 268-4387, fax: (412) 268-7395
 *      tech-transfer@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <sys/time.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <assert.h>
#include <syslog.h>
#include <errno.h>

#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <pthread.h>
#include <sasl/sasl.h>
#include <sasl/saslutil.h>

#include "mupdate.h"
#include "mupdate-client.h"
#include "xmalloc.h"
#include "iptostring.h"
#include "mailbox.h"
#include "mboxlist.h"
#include "exitcodes.h"
#include "prot.h"
#include "imapconf.h"
#include "imap_err.h"
#include "version.h"
#include "mpool.h"
#include "util.h"

static int masterp = 0;

enum {
    poll_interval = 1,
    update_wait = 5
};

struct pending {
    struct pending *next;

    char mailbox[MAX_MAILBOX_NAME+1];
};

struct conn {
    int fd;
    struct protstream *pin;
    struct protstream *pout;
    sasl_conn_t *saslconn;
    char *userid;

    const char *clienthost;

    struct 
    {
	char *ipremoteport;
	char *iplocalport;
    } saslprops;

    /* pending changes to send, in reverse order */
    const char *streaming; /* tag */
    pthread_mutex_t m;
    pthread_cond_t cond;

    struct pending *plist;
    struct conn *updatelist_next;
    struct prot_waitevent *ev; /* invoked every 'update_wait' seconds
				  to send out updates */

    /* Prefix for list commands */
    const char *list_prefix;
    size_t list_prefix_len;

    struct conn *next;
};

int ready_for_connections = 0;
pthread_cond_t ready_for_connections_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t ready_for_connections_mutex = PTHREAD_MUTEX_INITIALIZER;

int synced = 0;
pthread_cond_t synced_cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t synced_mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t connlist_mutex = PTHREAD_MUTEX_INITIALIZER;
struct conn *connlist = NULL;

/* ---- database access ---- */
pthread_mutex_t mailboxes_mutex = PTHREAD_MUTEX_INITIALIZER;
struct conn *updatelist = NULL;

/* --- prototypes --- */
void cmd_authenticate(struct conn *C,
		      const char *tag, const char *mech,
		      const char *clientstart);
void cmd_set(struct conn *C, 
	     const char *tag, const char *mailbox,
	     const char *server, const char *acl, enum settype t);
void cmd_find(struct conn *C, const char *tag, const char *mailbox,
	      int dook, int send_delete);
void cmd_list(struct conn *C, const char *tag, const char *host_prefix);
void cmd_startupdate(struct conn *C, const char *tag);
void shut_down(int code);
static int reset_saslconn(struct conn *c);
void database_init();
void sendupdates(struct conn *C, int flushnow);

extern int saslserver(sasl_conn_t *conn, const char *mech,
		      const char *init_resp, const char *continuation,
		      struct protstream *pin, struct protstream *pout,
		      int *sasl_result,
		      char **success_data);

/* --- prototypes in mupdate-slave.c */
void *mupdate_client_start(void *rock);
void *mupdate_placebo_kick_start(void *rock);

/* --- mutex wrapper functions for SASL */
void *my_mutex_new(void)
{
    pthread_mutex_t *ret = (pthread_mutex_t *)xmalloc(sizeof(pthread_mutex_t));

    pthread_mutex_init(ret, NULL);

    return ret;
}

int my_mutex_destroy(pthread_mutex_t *m)
{
    if(!m) return SASL_BADPARAM;
    
    if(pthread_mutex_destroy(m)) return SASL_FAIL;

    free(m);

    return SASL_OK;
}
/* end mutex wrapper functions */

static struct conn *conn_new(int fd)
{
    struct conn *C = xzmalloc(sizeof(struct conn));
    
    C->fd = fd;
    pthread_mutex_lock(&connlist_mutex); /* LOCK */
    C->next = connlist;
    connlist = C;
    pthread_mutex_unlock(&connlist_mutex); /* UNLOCK */

    return C;
}

static void conn_free(struct conn *C)
{
    if (C->streaming) {		/* remove from updatelist */
	struct conn *upc;

	pthread_mutex_lock(&mailboxes_mutex);

	if (C == updatelist) {
	    /* first thing in updatelist */
	    updatelist = C->updatelist_next;
	} else {
	    /* find in update list */
	    for (upc = updatelist; upc->updatelist_next != NULL; 
		 upc = upc->updatelist_next) {
		if (upc->updatelist_next == C) break;
	    }
	    /* must find it ! */
	    assert(upc->updatelist_next == C);

	    upc->updatelist_next = C->updatelist_next;
	}

	pthread_mutex_unlock(&mailboxes_mutex);
    }

    pthread_mutex_lock(&connlist_mutex); /* LOCK */
    /* remove from connlist */
    if (C == connlist) {
	connlist = connlist->next;
    } else {
	struct conn *t;

	for (t = connlist; t->next != NULL; t = t->next) {
	    if (t->next == C) break;
	}
	assert(t != NULL);
	t->next = C->next;
    }

    pthread_mutex_unlock(&connlist_mutex); /* UNLOCK */

    if (C->ev) prot_removewaitevent(C->pin, C->ev);
    if (C->pin) prot_free(C->pin);
    if (C->pout) prot_free(C->pout);
    cyrus_close_sock(C->fd);
    if (C->saslconn) sasl_dispose(&C->saslconn);

    /* free update list */

    free(C);
}

/* the sasl proxy policy context */
static struct proxy_context mupdate_proxyctx = {
    "mupdate", 1, 0, 0, NULL, NULL, NULL
};

static struct sasl_callback mysasl_cb[] = {
    { SASL_CB_GETOPT, &mysasl_config, NULL },
    { SASL_CB_PROXY_POLICY, &mysasl_proxy_policy, (void*) &mupdate_proxyctx },
    { SASL_CB_LIST_END, NULL, NULL }
};

/*
 * run once when process is forked;
 * MUST NOT exit directly; must return with non-zero error code
 */
int service_init(int argc, char **argv,
		 char **envp __attribute__((unused)))
{
    int r;
    int opt;

    config_changeident("mupdate");
    
    if (geteuid() == 0) fatal("must run as the Cyrus user", EC_USAGE);

    /* set signal handlers */
    signals_set_shutdown(&shut_down);
    signals_add_handlers();
    signal(SIGPIPE, SIG_IGN);

    /* set the SASL allocation functions */
    sasl_set_alloc((sasl_malloc_t *) &xmalloc, 
		   (sasl_calloc_t *) &calloc, 
		   (sasl_realloc_t *) &xrealloc, 
		   (sasl_free_t *) &free);

    /* set the SASL mutex functions */
    sasl_set_mutex((sasl_mutex_alloc_t *) &my_mutex_new,
                   (sasl_mutex_lock_t *) &pthread_mutex_lock,
                   (sasl_mutex_unlock_t *) &pthread_mutex_unlock,
                   (sasl_mutex_free_t *) &my_mutex_destroy);

    /* load the SASL plugins */
    if ((r = sasl_server_init(mysasl_cb, "Cyrus")) != SASL_OK) {
	syslog(LOG_ERR, "SASL failed initializing: sasl_server_init(): %s", 
	       sasl_errstring(r, NULL, NULL));
	return EC_SOFTWARE;
    }

    if ((r = sasl_client_init(NULL)) != SASL_OK) {
	syslog(LOG_ERR, "SASL failed initializing: sasl_client_init(): %s", 
	       sasl_errstring(r, NULL, NULL));
	return EC_SOFTWARE;
    }

    /* see if we're the master or a slave */
    while ((opt = getopt(argc, argv, "m")) != EOF) {
	switch (opt) {
	case 'm':
	    masterp = 1;
	    break;
	default:
	    break;
	}
    }

    /* Slave Skiplist Fast Resync? */
    if(!masterp && config_getswitch("mupdate_slave_fast_resync",0)) {
	char fname[4096];
	
	if(snprintf(fname,sizeof(fname),"%s%s",
		    config_dir,FNAME_MBOXLIST) == -1)
	    fatal("mboxlist database filename too large",EC_TEMPFAIL);

	putenv("CYRUS_SKIPLIST_UNSAFE=1");
 	unlink(fname);
    }

    database_init();

    if (!masterp) {
	pthread_t t;
	
	r = pthread_create(&t, NULL, &mupdate_client_start, NULL);
	if(r == 0) {
	    pthread_detach(t);
	} else {
	    syslog(LOG_ERR, "could not start client thread");
	    return EC_SOFTWARE;
	}

	/* Wait until they sync the database */
	pthread_mutex_lock(&synced_mutex);
	if(!synced)
	    pthread_cond_wait(&synced_cond, &synced_mutex);
	pthread_mutex_unlock(&synced_mutex);
    } else {
	pthread_t t;
	
	r = pthread_create(&t, NULL, &mupdate_placebo_kick_start, NULL);
	if(r == 0) {
	    pthread_detach(t);
	} else {
	    syslog(LOG_ERR, "could not start placebo kick thread");
	    return EC_SOFTWARE;
	}

	mupdate_ready();
    }

    return 0;
}

/* called if 'service_init()' was called but not 'service_main()' */
void service_abort(int error)
{
    exit(error);
}

void fatal(const char *s, int code)
{
    syslog(LOG_ERR, "%s", s);
    exit(code);
}

#define CHECKNEWLINE(c, ch) do { if ((ch) == '\r') (ch)=prot_getc((c)->pin); \
                       		 if ((ch) != '\n') goto extraargs; } while (0)

void cmdloop(struct conn *c)
{
    struct buf tag, cmd, arg1, arg2, arg3;
    const char *mechs;
    int ret;
    unsigned int mechcount;
    char slavebuf[4096];

    syslog(LOG_DEBUG, "starting cmdloop() on fd %d", c->fd);
        
    /* zero out struct bufs */
    /* Note that since they live on the stack for the duration of the
     * connection we don't need to fill up the struct conn with them */
    memset(&tag, 0, sizeof(struct buf));
    memset(&cmd, 0, sizeof(struct buf));
    memset(&arg1, 0, sizeof(struct buf));
    memset(&arg2, 0, sizeof(struct buf));
    memset(&arg3, 0, sizeof(struct buf));

    ret=sasl_listmech(c->saslconn, NULL, "* AUTH \"", "\" \"", "\"",
		      &mechs, NULL, &mechcount);

    /* AUTH banner is mandatory */
    if(!masterp) {
	const char *mupdate_server = config_getstring("mupdate_server", NULL);
	if(!mupdate_server)
	    fatal("mupdate server was not specified for slave", EC_TEMPFAIL);
	snprintf(slavebuf, sizeof(slavebuf), "mupdate://%s", mupdate_server);
    }
		
    prot_printf(c->pout,
	       "%s\r\n* OK MUPDATE \"%s\" \"Cyrus Murder\" \"%s\" \"%s\"\r\n",
    	       (ret == SASL_OK && mechcount > 0) ? mechs : "* AUTH",
	       config_servername,
	       CYRUS_VERSION, masterp ? "(master)" : slavebuf);
    for (;;) {
	int ch;
	char *p;

	signals_poll();
	
	ch = getword(c->pin, &tag);
	if (ch == EOF && errno == EAGAIN) {
	    /* streaming and no input from client */
	    continue;
	}
	if (ch == EOF) {
	    const char *err;
	    
	    if ((err = prot_error(c->pin)) != NULL) {
		syslog(LOG_WARNING, "%s, closing connection", err);
		prot_printf(c->pout, "* BYE \"%s\"\r\n", err);
	    }
	    goto done;
	}

	/* are we no longer ready ? */
	/* xxx this might be a race with streaming connections, but since
	 * the waitevent time is much shorter than the typical time between
	 * resyncs it is probably not a big concern.  For non-streaming
	 * connections, this is fine because they will only get hung up on
	 * if they ask for data before we are ready again. */
	if(!ready_for_connections) {
	    prot_printf(c->pout,
			"* BYE \"Not ready for connections\"\r\n");
	    goto done;
	}

	/* send out any updates we have pending */
	if (c->streaming) {
	    sendupdates(c, 0); /* don't flush pout though */
	}

	if (ch != ' ') {
	    prot_printf(c->pout, "%s BAD \"Need command\"\r\n", tag.s);
	    eatline(c->pin, ch);
	    continue;
	}

	/* parse command name */
	ch = getword(c->pin, &cmd);
	if (!cmd.s[0]) {
	    prot_printf(c->pout, "%s BAD \"Null command\"\r\n", tag.s);
	    eatline(c->pin, ch);
	    continue;
	}

	if (islower((unsigned char) cmd.s[0])) {
	    cmd.s[0] = toupper((unsigned char) cmd.s[0]);
	}
	for (p = &cmd.s[1]; *p; p++) {
	    if (isupper((unsigned char) *p)) *p = tolower((unsigned char) *p);
	}
	
	switch (cmd.s[0]) {
	case 'A':
	    if (!strcmp(cmd.s, "Authenticate")) {
		int opt = 0;
		
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		if (ch == ' ') {
		    ch = getstring(c->pin, c->pout, &arg2);
		    opt = 1;
		}
		CHECKNEWLINE(c, ch);

		if (c->userid) {
		    prot_printf(c->pout,
				"%s BAD \"already authenticated\"\r\n",
				tag.s);
		    continue;
		}

		cmd_authenticate(c, tag.s, arg1.s, opt ? arg2.s : NULL);
	    }
	    else if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Activate")) {
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg2);
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg3);
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;
		if (!masterp) goto masteronly;

		cmd_set(c, tag.s, arg1.s, arg2.s, arg3.s, SET_ACTIVE);
	    }
	    else goto badcmd;
	    break;

	case 'D':
	    if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Deactivate")) {
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg2);
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;
		if (!masterp) goto masteronly;
		
		cmd_set(c, tag.s, arg1.s, arg2.s, NULL, SET_DEACTIVATE);
	    }
	    else if (!strcmp(cmd.s, "Delete")) {
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;
		if (!masterp) goto masteronly;

		cmd_set(c, tag.s, arg1.s, NULL, NULL, SET_DELETE);
	    }
	    else goto badcmd;
	    break;

	case 'F':
	    if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Find")) {
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;
		
		cmd_find(c, tag.s, arg1.s, 1, 0);
	    }
	    else goto badcmd;
	    break;

	case 'L':
	    if (!strcmp(cmd.s, "Logout")) {
		CHECKNEWLINE(c, ch);

		prot_printf(c->pout, "%s OK \"bye-bye\"\r\n", tag.s);
		goto done;
	    }
	    else if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "List")) {
		int opt = 0;

		if (ch == ' ') {
		    /* Optional partition/host prefix parameter */
		    ch = getstring(c->pin, c->pout, &arg1);
		    opt = 1;
		}
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;

		cmd_list(c, tag.s, opt ? arg1.s : NULL);
		
		prot_printf(c->pout, "%s OK \"list complete\"\r\n", tag.s);
	    }
	    else goto badcmd;
	    break;

	case 'N':
	    if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Noop")) {
		CHECKNEWLINE(c, ch);

		prot_printf(c->pout, "%s OK \"Noop done\"\r\n", tag.s);
	    }
	    else goto badcmd;
	    break;

	case 'R':
	    if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Reserve")) {
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg1);
		if (ch != ' ') goto missingargs;
		ch = getstring(c->pin, c->pout, &arg2);
		CHECKNEWLINE(c, ch);

		if (c->streaming) goto notwhenstreaming;
		if (!masterp) goto masteronly;
		
		cmd_set(c, tag.s, arg1.s, arg2.s, NULL, SET_RESERVE);
	    }
	    else goto badcmd;
	    break;

	case 'U':
	    if (!c->userid) goto nologin;
	    else if (!strcmp(cmd.s, "Update")) {
		CHECKNEWLINE(c, ch);
		if (c->streaming) goto notwhenstreaming;
		
		cmd_startupdate(c, tag.s);
	    }
	    else goto badcmd;
	    break;

	default:
	badcmd:
	    prot_printf(c->pout, "%s BAD \"Unrecognized command\"\r\n", tag.s);
	    eatline(c->pin, ch);
	    continue;

	extraargs:
	    prot_printf(c->pout, "%s BAD \"Extra arguments\"\r\n", tag.s);
	    eatline(c->pin, ch);
	    continue;
	    
	missingargs:
	    prot_printf(c->pout, "%s BAD \"Missing arguments\"\r\n", tag.s);
	    eatline(c->pin, ch);
	    continue;

	notwhenstreaming:
	    prot_printf(c->pout, "%s BAD \"not legal when streaming\"\r\n",
			tag.s);
	    continue;

	masteronly:
	    prot_printf(c->pout,
			"%s BAD \"read-only session\"\r\n",
			tag.s);
	    continue;
	}

	continue;

    nologin:
	prot_printf(c->pout, "%s BAD Please login first\r\n", tag.s);
	eatline(c->pin, ch);
	continue;
    }

 done:
    prot_flush(c->pout);

    /* free struct bufs */
    freebuf(&tag);
    freebuf(&cmd);
    freebuf(&arg1);
    freebuf(&arg2);
    freebuf(&arg3);

    syslog(LOG_DEBUG, "ending cmdloop() on fd %d", c->fd);
}

void *start(void *rock)
{
    struct conn *c = (struct conn *) rock;
    struct sockaddr_in localaddr, remoteaddr;
    int haveaddr = 0;
    int salen;
    int secflags, plaintext_result;
    sasl_security_properties_t *secprops = NULL;
    char localip[60], remoteip[60];
    char clienthost[250];
    struct hostent *hp;

    pthread_mutex_lock(&ready_for_connections_mutex);
    /* are we ready to take connections? */
    while(!ready_for_connections) {
	pthread_cond_wait(&ready_for_connections_cond,
			  &ready_for_connections_mutex);
    }
    pthread_mutex_unlock(&ready_for_connections_mutex);

    c->pin = prot_new(c->fd, 0);
    c->pout = prot_new(c->fd, 1);
    c->clienthost = clienthost;

    prot_setflushonread(c->pin, c->pout);
    prot_settimeout(c->pin, 180*60);

    /* Find out name of client host */
    salen = sizeof(remoteaddr);
    if (getpeername(c->fd, (struct sockaddr *)&remoteaddr, &salen) == 0 &&
	remoteaddr.sin_family == AF_INET) {
	hp = gethostbyaddr((char *)&remoteaddr.sin_addr,
			   sizeof(remoteaddr.sin_addr), AF_INET);
	if (hp != NULL) {
	    strncpy(clienthost, hp->h_name, sizeof(clienthost)-30);
	    clienthost[sizeof(clienthost)-30] = '\0';
	} else {
	    clienthost[0] = '\0';
	}
	strlcat(clienthost, "[", sizeof(clienthost));
	strlcat(clienthost, inet_ntoa(remoteaddr.sin_addr),
		sizeof(clienthost));
	strlcat(clienthost, "]", sizeof(clienthost));
	salen = sizeof(localaddr);
	if (getsockname(c->fd, (struct sockaddr *)&localaddr, &salen) == 0
	    && iptostring((struct sockaddr *)&remoteaddr,
			  sizeof(struct sockaddr_in), remoteip, 60) == 0
	    && iptostring((struct sockaddr *)&localaddr,
			  sizeof(struct sockaddr_in), localip, 60) == 0) {
	    haveaddr = 1;
	}
    }

    if(haveaddr) {
	c->saslprops.ipremoteport = remoteip;
	c->saslprops.iplocalport = localip;
    }

    /* create sasl connection */
    if (sasl_server_new("mupdate",
			config_servername, NULL,
			(haveaddr ? localip : NULL),
			(haveaddr ? remoteip : NULL),
			NULL, 0, 
			&c->saslconn) != SASL_OK) {
	fatal("SASL failed initializing: sasl_server_new()", EC_TEMPFAIL);
    }

    /* set my allowable security properties */
    secflags = SASL_SEC_NOANONYMOUS;
    plaintext_result = config_getswitch("allowplaintext",1);
    if (!config_getswitch("mupdate_allowplaintext", plaintext_result)) {
	secflags |= SASL_SEC_NOPLAINTEXT;
    }
    secprops = mysasl_secprops(secflags);
    sasl_setprop(c->saslconn, SASL_SEC_PROPS, secprops);

    cmdloop(c);

    /* This will close c->fd also */
    conn_free(c);

    return NULL;
}

/*
 * run for each accepted connection
 */
int service_main_fd(int fd,
		    int argc __attribute__((unused)),
		    char **argv __attribute__((unused)),
		    char **envp __attribute__((unused)))
{
    /* spawn off a thread to handle this connection */
    pthread_t t;
    struct conn *c = conn_new(fd);
    int r;

    r = pthread_create(&t, NULL, &start, c);
    if (r == 0) {
	pthread_detach(t);
    }

    return 0;
}

/* read from disk database must be unlocked. */
void database_init()
{
    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */

    mboxlist_init(0);
    mboxlist_open(NULL);

    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */
}

/* log change to database. database must be locked. */
void database_log(const struct mbent *mb)
{
    switch (mb->t) {
    case SET_ACTIVE:
	mboxlist_insertremote(mb->mailbox, 0, mb->server, mb->acl, NULL);
	break;

    case SET_RESERVE:
	mboxlist_insertremote(mb->mailbox, MBTYPE_RESERVE, mb->server,
			      "", NULL);
	break;

    case SET_DELETE:
	mboxlist_deletemailbox(mb->mailbox, 1, "", NULL, 0, 0, 0);
	break;

    case SET_DEACTIVATE:
	/* SET_DEACTIVATE is not a real value that an actual
	   mailbox can have! */
	abort();
    }
}

/* lookup in database. database must be locked */
/* This could probabally be more efficient and avoid some copies */
/* passing in a NULL pool implies that we should use regular xmalloc,
 * a non-null pool implies we should use the mpool functionality */
struct mbent *database_lookup(const char *name, struct mpool *pool) 
{
    char *path, *acl;
    int type;
    struct mbent *out;
    
    if(!name) return NULL;
    
    if(mboxlist_detail(name, &type, &path, NULL, &acl, NULL))
	return NULL;

    if(type & MBTYPE_RESERVE) {
	if(!pool) out = xmalloc(sizeof(struct mbent) + 1);
	else out = mpool_malloc(pool, sizeof(struct mbent) + 1);
	out->t = SET_RESERVE;
	out->acl[0] = '\0';
    } else {
	if(!pool) out = xmalloc(sizeof(struct mbent) + strlen(acl));
	else out = mpool_malloc(pool, sizeof(struct mbent) + strlen(acl));
	out->t = SET_ACTIVE;
	strcpy(out->acl, acl);
    }

    out->mailbox = (pool) ? mpool_strdup(pool, name) : xstrdup(name);
    out->server = (pool) ? mpool_strdup(pool, path) : xstrdup(path);

    return out;
}

void cmd_authenticate(struct conn *C,
		      const char *tag, const char *mech,
		      const char *clientstart)
{
    int r, sasl_result;

    r = saslserver(C->saslconn, mech, clientstart, "", C->pin, C->pout,
		   &sasl_result, NULL);

    if (r) {
	const char *errorstring = NULL;

	switch (r) {
	case IMAP_SASL_CANCEL:
	    prot_printf(C->pout,
			"%s NO Client canceled authentication\r\n", tag);
	    break;
	case IMAP_SASL_PROTERR:
	    errorstring = prot_error(C->pin);

	    prot_printf(C->pout,
			"%s NO Error reading client response: %s\r\n",
			tag, errorstring ? errorstring : "");
	    break;
	default:
	    sleep(3);
	
	    syslog(LOG_ERR, "badlogin: %s %s %s",
		   C->clienthost,
		   mech, sasl_errdetail(C->saslconn));

	    prot_printf(C->pout, "%s NO \"%s\"\r\n", tag,
			sasl_errstring((r == SASL_NOUSER ? SASL_BADAUTH : r),
				       NULL, NULL));
	}

	reset_saslconn(C);
	return;
    }

    /* Successful Authentication */
    r = sasl_getprop(C->saslconn, SASL_USERNAME, (const void **)&C->userid);
    if(r != SASL_OK) {
	prot_printf(C->pout, "%s NO \"SASL Error\"\r\n", tag);
	reset_saslconn(C);
	return;
    }

    syslog(LOG_NOTICE, "login: %s from %s",
	   C->userid, C->clienthost);

    prot_printf(C->pout, "%s OK \"Authenticated\"\r\n", tag);

    prot_setsasl(C->pin, C->saslconn);
    prot_setsasl(C->pout, C->saslconn);

    return;
}

void cmd_set(struct conn *C, 
	     const char *tag, const char *mailbox,
	     const char *server, const char *acl, enum settype t)
{
    struct mbent *m;
    struct conn *upc;

    syslog(LOG_DEBUG, "cmd_set(fd:%d, %s)", C->fd, mailbox);

    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */

    m = database_lookup(mailbox, NULL);
    if (m && t == SET_RESERVE) {
	/* failed; mailbox already exists */
	prot_printf(C->pout, "%s NO \"mailbox already exists\"\r\n", tag);
	goto done;
    }

    if ((!m || m->t != SET_ACTIVE) && t == SET_DEACTIVATE) {
	/* failed; mailbox not currently active */
	prot_printf(C->pout, "%s NO \"mailbox not currently active\"\r\n",
		    tag);
	goto done;
    } else if (t == SET_DEACTIVATE) {
	t = SET_RESERVE;
    }
    
    if (t == SET_DELETE) {
	if (!m) {
	    /* failed; mailbox doesn't exist */
            prot_printf(C->pout, "%s NO \"mailbox doesn't exist\"\r\n", tag);
	    goto done;
	}

	/* do the deletion */
	m->t = SET_DELETE;
    } else {
	if (m && (!acl || strlen(acl) < strlen(m->acl))) {
	    /* change what's already there */
	    free(m->server);
	    m->server = xstrdup(server);
	    if (acl) strcpy(m->acl, acl);
	    else m->acl[0] = '\0';

	    m->t = t;
	} else {
	    struct mbent *newm;
	    
	    /* allocate new mailbox */
	    if (acl) {
		newm = xrealloc(m, sizeof(struct mbent) + strlen(acl));
	    } else {
		newm = xrealloc(m, sizeof(struct mbent) + 1);
	    }
	    newm->mailbox = xstrdup(mailbox);
	    newm->server = xstrdup(server);
	    if (acl) {
		strcpy(newm->acl, acl);
	    } else {
		newm->acl[0] = '\0';
	    }

	    newm->t = t;

	    /* re-scope */
	    m = newm;
	}
    }

    /* write to disk */
    database_log(m);

    /* post pending changes */
    for (upc = updatelist; upc != NULL; upc = upc->updatelist_next) {
	/* for each connection, add to pending list */
	struct pending *p = (struct pending *) xmalloc(sizeof(struct pending));
	strlcpy(p->mailbox, mailbox, sizeof(p->mailbox));
	
	pthread_mutex_lock(&upc->m);
	p->next = upc->plist;
	upc->plist = p;

	pthread_cond_signal(&upc->cond);
	pthread_mutex_unlock(&upc->m);
    }

    prot_printf(C->pout, "%s OK \"done\"\r\n", tag);
 done:
    free_mbent(m);
    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */
}

void cmd_find(struct conn *C, const char *tag, const char *mailbox, int dook,
              int send_delete)
{
    struct mbent *m;

    syslog(LOG_DEBUG, "cmd_find(fd:%d, %s)", C->fd, mailbox);

    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */
    m = database_lookup(mailbox, NULL);

    if (m && m->t == SET_ACTIVE) {
	prot_printf(C->pout, "%s MAILBOX {%d+}\r\n%s {%d+}\r\n%s {%d+}\r\n%s\r\n",
		    tag,
		    strlen(m->mailbox), m->mailbox,
		    strlen(m->server), m->server,
		    strlen(m->acl), m->acl);
    } else if (m && m->t == SET_RESERVE) {
	prot_printf(C->pout, "%s RESERVE {%d+}\r\n%s {%d+}\r\n%s\r\n",
		    tag,
		    strlen(m->mailbox), m->mailbox,
		    strlen(m->server), m->server);
    } else if (send_delete) {
	/* not found, if needed, send a delete */
	prot_printf(C->pout, "%s DELETE {%d+}\r\n%s\r\n",
		    tag, strlen(mailbox), mailbox);
    }

    free_mbent(m);
    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */

    if (dook) {
	prot_printf(C->pout, "%s OK \"Search completed\"\r\n", tag);
    }
}

/* Callback for cmd_startupdate to be passed to mboxlist_findall. */
/* Requires that C->streaming be set to the tag to respond with */
static int sendupdate(char *name,
		      int matchlen __attribute__((unused)),
		      int maycreate __attribute__((unused)),
		      void *rock)
{
    struct conn *C = (struct conn *)rock;
    struct mbent *m;
    
    if(!C) return -1;
    
    m = database_lookup(name, NULL);
    if(!m) return -1;

    if(!C->list_prefix ||
       !strncmp(m->server, C->list_prefix, C->list_prefix_len)) {
	/* Either there is not a prefix to test, or we matched it */
    
	switch (m->t) {
	case SET_ACTIVE:
	    prot_printf(C->pout,
			"%s MAILBOX {%d+}\r\n%s {%d+}\r\n%s {%d+}\r\n%s\r\n",
			C->streaming,
			strlen(m->mailbox), m->mailbox,
			strlen(m->server), m->server,
			strlen(m->acl), m->acl);
	    break;
	case SET_RESERVE:
	    prot_printf(C->pout, "%s RESERVE {%d+}\r\n%s {%d+}\r\n%s\r\n",
			C->streaming,
			strlen(m->mailbox), m->mailbox,
			strlen(m->server), m->server);
	    break;

	case SET_DELETE:
	    /* deleted item in the list !?! */
	case SET_DEACTIVATE:
	    /* SET_DEACTIVATE is not a real value! */
	    abort();
	}
    }
    
    free_mbent(m);
    return 0;
}

void cmd_list(struct conn *C, const char *tag, const char *host_prefix) 
{
    char pattern[2] = {'*','\0'};

    /* indicate interest in updates */
    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */

    /* since this isn't valid when streaming, just use the same callback */
    C->streaming = tag;
    C->list_prefix = host_prefix;
    if(C->list_prefix) C->list_prefix_len = strlen(C->list_prefix);
    else C->list_prefix_len = 0;
    
    mboxlist_findall(NULL, pattern, 1, NULL,
		     NULL, sendupdate, (void*)C);

    C->streaming = NULL;

    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */
}


/* 
 * we've registered this connection for streaming, and every X seconds
 * this will be invoked.  note that we always send out updates as soon
 * as we get a noop: that resets this counter back */
struct prot_waitevent *sendupdates_evt(struct protstream *s __attribute__((unused)), 
				       struct prot_waitevent *ev,
				       void *rock)
{
    struct conn *C = (struct conn *) rock;

    sendupdates(C, 1);

    /* 'sendupdates()' will update when we next trigger */
    return ev;
}

void cmd_startupdate(struct conn *C, const char *tag)
{
    char pattern[2] = {'*','\0'};

    /* initialize my condition variable */
    pthread_cond_init(&C->cond, NULL);

    /* indicate interest in updates */
    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */

    C->updatelist_next = updatelist;
    updatelist = C;
    C->streaming = xstrdup(tag);

    /* dump initial list */
    mboxlist_findall(NULL, pattern, 1, NULL,
		     NULL, sendupdate, (void*)C);

    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */

    prot_printf(C->pout, "%s OK \"streaming starts\"\r\n", tag);

    /* schedule our first update */
    C->ev = prot_addwaitevent(C->pin, time(NULL) + update_wait, 
			      sendupdates_evt, C);
}

/* send out any pending updates.
   if 'flushnow' is set, flush the output buffer */
void sendupdates(struct conn *C, int flushnow)
{
    struct pending *p, *q;

    pthread_mutex_lock(&C->m);

    /* just grab the update list and release the lock */
    p = C->plist;
    C->plist = NULL;
    pthread_mutex_unlock(&C->m);

    while (p != NULL) {
	/* send update */
	q = p;
	p = p->next;

	/* notify just like a FIND - except enable sending of DELETE
	 * notifications */
	cmd_find(C, C->streaming, q->mailbox, 0, 1);

	free(q);
    }

    /* reschedule event for 'update_wait' seconds */
    C->ev->mark = time(NULL) + update_wait;

    if (flushnow) {
	prot_flush(C->pout);
    }
}

void shut_down(int code) __attribute__((noreturn));
void shut_down(int code)
{
    exit(code);
}

/* Reset the given sasl_conn_t to a sane state */
static int reset_saslconn(struct conn *c)
{
    int ret, secflags, plaintext_result;
    sasl_security_properties_t *secprops = NULL;

    sasl_dispose(&c->saslconn);
    /* do initialization typical of service_main */
    ret = sasl_server_new("mupdate", config_servername,
                         NULL, NULL, NULL,
                         NULL, 0, &c->saslconn);
    if(ret != SASL_OK) return ret;

    if(c->saslprops.ipremoteport)
       ret = sasl_setprop(c->saslconn, SASL_IPREMOTEPORT,
                          c->saslprops.ipremoteport);
    if(ret != SASL_OK) return ret;
    
    if(c->saslprops.iplocalport)
       ret = sasl_setprop(c->saslconn, SASL_IPLOCALPORT,
                          c->saslprops.iplocalport);
    if(ret != SASL_OK) return ret;
    
    secflags = SASL_SEC_NOANONYMOUS;
    plaintext_result = config_getswitch("allowplaintext",1);
    if (!config_getswitch("mupdate_allowplaintext", plaintext_result)) {
	secflags |= SASL_SEC_NOPLAINTEXT;
    }
    secprops = mysasl_secprops(secflags);
    ret = sasl_setprop(c->saslconn, SASL_SEC_PROPS, secprops);
    if(ret != SASL_OK) return ret;
    /* end of service_main initialization excepting SSF */

    return SASL_OK;
}

int cmd_change(struct mupdate_mailboxdata *mdata,
	       const char *rock, void *context __attribute__((unused)))
{
    struct mbent *m = NULL;
    struct conn *upc = NULL;
    enum settype t = -1;
    int ret = 0;

    if(!mdata || !rock || !mdata->mailbox) return 1;

    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */

    if(!strncmp(rock, "DELETE", 6)) {
	m = database_lookup(mdata->mailbox, NULL);

	if(!m) {
	    syslog(LOG_DEBUG, "attempt to delete unknown mailbox %s",
		   mdata->mailbox);
	    /* Mailbox doesn't exist - this isn't as fatal as you might
	     * think. */
            /* ret = -1; */
	    goto done;
	}
	m->t = t = SET_DELETE;
    } else {
	m = database_lookup(mdata->mailbox, NULL);
	
	if (m && (!mdata->acl || strlen(mdata->acl) < strlen(m->acl))) {
	    /* change what's already there */
	    free(m->server);
	    m->server = xstrdup(mdata->server);

	    if (mdata->acl) strcpy(m->acl, mdata->acl);
	    else m->acl[0] = '\0';

	    if(!strncmp(rock, "MAILBOX", 6)) {
		m->t = t = SET_ACTIVE;
	    } else if(!strncmp(rock, "RESERVE", 7)) {
		m->t = t = SET_RESERVE;
	    } else {
		syslog(LOG_DEBUG,
		       "bad mupdate command in cmd_change: %s", rock);
		ret = 1;
		goto done;
	    }
	} else {
	    struct mbent *newm;

	    if(m) {
		free(m->mailbox);
		free(m->server);
	    }

	    /* allocate new mailbox */
	    if (mdata->acl) {
		newm = xrealloc(m, sizeof(struct mbent) + strlen(mdata->acl));
	    } else {
		newm = xrealloc(m, sizeof(struct mbent) + 1);
	    }

	    newm->mailbox = xstrdup(mdata->mailbox);
	    newm->server = xstrdup(mdata->server);

	    if (mdata->acl) {
		strcpy(newm->acl, mdata->acl);
	    } else {
		newm->acl[0] = '\0';
	    }

	    if(!strncmp(rock, "MAILBOX", 6)) {
		newm->t = t = SET_ACTIVE;
	    } else if(!strncmp(rock, "RESERVE", 7)) {
		newm->t = t = SET_RESERVE;
	    } else {
		syslog(LOG_DEBUG,
		       "bad mupdate command in cmd_change: %s", rock);
		ret = 1;
		goto done;
	    }
	    
	    /* Bring it back into scope */
	    m = newm;
	
	}
    }

    /* write to disk */
    database_log(m);
    
    /* post pending changes to anyone we are talking to */
    for (upc = updatelist; upc != NULL; upc = upc->updatelist_next) {
	/* for each connection, add to pending list */

	struct pending *p = (struct pending *) xmalloc(sizeof(struct pending));
	strlcpy(p->mailbox, mdata->mailbox, sizeof(p->mailbox));
	
	pthread_mutex_lock(&upc->m);
	p->next = upc->plist;
	upc->plist = p;

	pthread_cond_signal(&upc->cond);
	pthread_mutex_unlock(&upc->m);
    }

 done:
    free_mbent(m);
    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */

    return ret;
}

struct sync_rock 
{
    struct mpool *pool;
    struct mbent_queue *boxes;
};

/* Read a series of MAILBOX and RESERVE commands and tack them onto a
 * queue */
int cmd_resync(struct mupdate_mailboxdata *mdata,
	       const char *rock, void *context)
{
    struct sync_rock *r = (struct sync_rock *)context;
    struct mbent_queue *remote_boxes = r->boxes;
    struct mbent *newm = NULL;

    if(!mdata || !rock || !mdata->mailbox || !remote_boxes) return 1;

    /* allocate new mailbox */
    if (mdata->acl) {
	newm = mpool_malloc(r->pool,sizeof(struct mbent) + strlen(mdata->acl));
    } else {
	newm = mpool_malloc(r->pool,sizeof(struct mbent) + 1);
    }

    newm->mailbox = mpool_strdup(r->pool, mdata->mailbox);
    newm->server = mpool_strdup(r->pool, mdata->server);

    if (mdata->acl) {
	strcpy(newm->acl, mdata->acl);
    } else {
	newm->acl[0] = '\0';
    }
	
    if(!strncmp(rock, "MAILBOX", 6)) {
	newm->t = SET_ACTIVE;
    } else if(!strncmp(rock, "RESERVE", 7)) {
	newm->t = SET_RESERVE;
    } else {
	syslog(LOG_NOTICE,
	       "bad mupdate command in cmd_resync: %s", rock);
	return 1;
    }

    /* Insert onto queue */
    newm->next = NULL;
    *(remote_boxes->tail) = newm;
    remote_boxes->tail = &(newm->next);
    
    return 0;
}

/* Callback for mupdate_synchronize to be passed to mboxlist_findall. */
static int sync_findall_cb(char *name,
			   int matchlen __attribute__((unused)),
			   int maycreate __attribute__((unused)),
			   void *rock)
{
    struct sync_rock *r = (struct sync_rock *)rock;
    struct mbent_queue *local_boxes = (struct mbent_queue *)r->boxes;
    struct mbent *m;

    if(!local_boxes) return 1;

    m = database_lookup(name, r->pool);
    /* If it doesn't exist, fine... */
    if(!m) return 0;
    
    m->next = NULL;
    *(local_boxes->tail) = m;
    local_boxes->tail = &(m->next);

    return 0;
}

int mupdate_synchronize(mupdate_handle *handle) 
{
    struct mbent_queue local_boxes;
    struct mbent_queue remote_boxes;
    struct mbent *l,*r;
    struct mpool *pool;
    struct sync_rock rock;
    char pattern[] = { '*', '\0' };

    if(!handle || !handle->saslcompleted) return 1;

    pool = new_mpool(131072); /* Arbitrary, but large (128k) */
    rock.pool = pool;
    
    /* ask for updates and set nonblocking */
    prot_printf(handle->pout, "U01 UPDATE\r\n");

    /* Note that this prevents other people from running an UPDATE against
     * us for the duration.  this is a GOOD THING */
    pthread_mutex_lock(&mailboxes_mutex); /* LOCK */
    
    syslog(LOG_NOTICE, 
	   "synchronizing mailbox list with master mupdate server");

    local_boxes.head = NULL;
    local_boxes.tail = &(local_boxes.head);
    remote_boxes.head = NULL;
    remote_boxes.tail = &(remote_boxes.head);

    rock.boxes = &remote_boxes;

    /* If there is a fatal error, die, other errors ignore */
    if (mupdate_scarf(handle, cmd_resync, &rock, 1, NULL) != 0) {
	struct mbent *p=remote_boxes.head, *p_next=NULL;
	while(p) {
	    p_next = p->next;
	    p = p_next;
	}
	
	pthread_mutex_unlock(&mailboxes_mutex);
	free_mpool(pool);
	return 1;
    }

    /* Make socket nonblocking now */
    prot_NONBLOCK(handle->pin);

    rock.boxes = &local_boxes;

    mboxlist_findall(NULL, pattern, 1, NULL,
		     NULL, sync_findall_cb, (void*)&rock);

    /* Traverse both lists, compare the names */
    /* If they match, ensure that server and acl are correct, if so,
       move on, if not, fix them */
    /* If the local is before the next remote, delete it */
    /* If the next remote is before theis local, insert it and try again */
    for(l = local_boxes.head, r = remote_boxes.head; l && r;
	l = local_boxes.head, r = remote_boxes.head) 
    {
	int ret = strcmp(l->mailbox, r->mailbox);
	if(!ret) {
	    /* Match */
	    if(l->t != r->t ||
	       strcmp(l->server, r->server) ||
	       strcmp(l->acl,r->acl)) {
		/* Something didn't match, replace it */
		mboxlist_insertremote(r->mailbox, 
				     (r->t == SET_RESERVE ?
				        MBTYPE_RESERVE : 0),
				      r->server, r->acl, NULL);
	    }
	    /* Okay, dump these two */
	    local_boxes.head = l->next;
	    remote_boxes.head = r->next;
	} else if (ret < 0) {
	    /* Local without corresponding remote, delete it */
	    mboxlist_deletemailbox(l->mailbox, 1, "", NULL, 0, 0, 0);
	    local_boxes.head = l->next;
	} else /* (ret > 0) */ {
	    /* Remote without corresponding local, insert it */
	    mboxlist_insertremote(r->mailbox, 
				  (r->t == SET_RESERVE ?
				   MBTYPE_RESERVE : 0),
				  r->server, r->acl, NULL);
	    remote_boxes.head = r->next;
	}
    }

    if(l && !r) {
	/* we have more deletes to do */
	while(l) {
	    mboxlist_deletemailbox(l->mailbox, 1, "", NULL, 0, 0, 0);
	    local_boxes.head = l->next;
	    l = local_boxes.head;
	}
    } else if (r && !l) {
	/* we have more inserts to do */
	while(r) {
	    mboxlist_insertremote(r->mailbox, 
				  (r->t == SET_RESERVE ?
				   MBTYPE_RESERVE : 0),
				  r->server, r->acl, NULL);
	    remote_boxes.head = r->next;
	    r = remote_boxes.head;
	}
    }

    /* All up to date! */
    syslog(LOG_NOTICE, "mailbox list synchronization complete");
    pthread_mutex_unlock(&mailboxes_mutex); /* UNLOCK */
    free_mpool(pool);
    return 0;
}

void mupdate_signal_db_synced(void) 
{
    pthread_mutex_lock(&synced_mutex);
    synced = 1;
    pthread_cond_broadcast(&synced_cond);
    pthread_mutex_unlock(&synced_mutex);
}

void mupdate_ready(void) 
{
    if(ready_for_connections) {
	syslog(LOG_CRIT, "mupdate_ready called when already ready");
	fatal("mupdate_ready called when already ready", EC_TEMPFAIL);
    }

    pthread_mutex_lock(&ready_for_connections_mutex);
    ready_for_connections = 1;
    pthread_cond_broadcast(&ready_for_connections_cond);
    pthread_mutex_unlock(&ready_for_connections_mutex);
}

void mupdate_unready(void)
{
    pthread_mutex_lock(&ready_for_connections_mutex);

    syslog(LOG_NOTICE, "unready for connections");

    /* signal our lack of readyness */
    ready_for_connections = 0;

    pthread_mutex_unlock(&ready_for_connections_mutex);
}

/* Used to free malloc'd mbent's */
void free_mbent(struct mbent *p) 
{
    if(!p) return;
    free(p->server);
    free(p->mailbox);
    free(p);
}
