/* global.c -- Configuration routines
 *
 * Copyright (c) 1994-2008 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 legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@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.
 *
 * $Id: global.c,v 1.33 2009/10/13 15:10:36 murch Exp $
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/stat.h>

#if HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <errno.h>

#include "acl.h"
#include "cyrusdb.h"
#include "exitcodes.h"
#include "gmtoff.h"
#include "hash.h"
#include "imap_err.h"
#include "global.h"
#include "libconfig.h"
#include "libcyr_cfg.h"
#include "mboxlist.h"
#include "mupdate_err.h"
#include "mutex.h"
#include "prot.h" /* for PROT_BUFSIZE */
#include "util.h"
#include "wildmat.h"
#include "xmalloc.h"
#include "xstrlcpy.h"
#include "xstrlcat.h"

static enum {
    NOT_RUNNING = 0,
    RUNNING = 1,
    DONE = 2
} cyrus_init_run = NOT_RUNNING;

static int cyrus_init_nodb = 0;

int config_fulldirhash;				/* 0 */
int config_implicitrights;			/* "lkxa" */
unsigned long config_metapartition_files;	/* 0 */
struct cyrusdb_backend *config_mboxlist_db;
struct cyrusdb_backend *config_quota_db;
struct cyrusdb_backend *config_subscription_db;
struct cyrusdb_backend *config_annotation_db;
struct cyrusdb_backend *config_seenstate_db;
struct cyrusdb_backend *config_mboxkey_db;
struct cyrusdb_backend *config_duplicate_db;
struct cyrusdb_backend *config_tlscache_db;
struct cyrusdb_backend *config_ptscache_db;
struct cyrusdb_backend *config_statuscache_db;
struct cyrusdb_backend *config_userdeny_db;

/* Called before a cyrus application starts (but after command line parameters
 * are read) */
int cyrus_init(const char *alt_config, const char *ident, unsigned flags)
{
    char *p;
    const char *val;
    const char *prefix;
    int umaskval = 0;

    if(cyrus_init_run != NOT_RUNNING) {
	fatal("cyrus_init called twice!", EC_CONFIG);
    } else {
	cyrus_init_run = RUNNING;
    }

    cyrus_init_nodb = (flags & CYRUSINIT_NODB);

    initialize_imap_error_table();
    initialize_mupd_error_table();

    if(!ident)
	fatal("service name was not specified to cyrus_init", EC_CONFIG);

    config_ident = ident;
    
    /* xxx we lose here since we can't have the prefix until we load the
     * config file */
    openlog(config_ident, LOG_PID, SYSLOG_FACILITY);

    /* Load configuration file.  This will set config_dir when it finds it */
    config_read(alt_config);

    prefix = config_getstring(IMAPOPT_SYSLOG_PREFIX);

    /* Reopen the log with the new prefix, if needed  */    
    if(prefix) {
	int size = strlen(prefix) + 1 + strlen(ident) + 1;
	char *ident_buf = xmalloc(size);
	
	strlcpy(ident_buf, prefix, size);
	strlcat(ident_buf, "/", size);
	strlcat(ident_buf, ident, size);

	closelog();
	openlog(ident_buf, LOG_PID, SYSLOG_FACILITY);

	/* don't free the openlog() string! */
    }

    /* Look up default partition */
    config_defpartition = config_getstring(IMAPOPT_DEFAULTPARTITION);
    for (p = (char *)config_defpartition; p && *p; p++) {
	if (!Uisalnum(*p))
	  fatal("defaultpartition option contains non-alphanumeric character",
		EC_CONFIG);
	if (Uisupper(*p)) *p = tolower((unsigned char) *p);
    }

    /* Look up umask */
    val = config_getstring(IMAPOPT_UMASK);
    while (*val) {
	if (*val >= '0' && *val <= '7') umaskval = umaskval*8 + *val - '0';
	val++;
    }
    umask(umaskval);

    config_fulldirhash = config_getswitch(IMAPOPT_FULLDIRHASH);

    /* look up and canonify the implicit rights of mailbox owners */
    config_implicitrights =
	cyrus_acl_strtomask(config_getstring(IMAPOPT_IMPLICIT_OWNER_RIGHTS));

    config_metapartition_files = config_getbitfield(IMAPOPT_METAPARTITION_FILES);

    if (!cyrus_init_nodb) {
	/* lookup the database backends */
	config_mboxlist_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_MBOXLIST_DB));
	config_quota_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_QUOTA_DB));
	config_subscription_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_SUBSCRIPTION_DB));
	config_annotation_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_ANNOTATION_DB));
	config_seenstate_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_SEENSTATE_DB));
	config_mboxkey_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_MBOXKEY_DB));
	config_duplicate_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_DUPLICATE_DB));
	config_tlscache_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_TLSCACHE_DB));
	config_ptscache_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_PTSCACHE_DB));
	config_statuscache_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_STATUSCACHE_DB));
	config_userdeny_db =
	    cyrusdb_fromname(config_getstring(IMAPOPT_USERDENY_DB));

	/* configure libcyrus as needed */
	libcyrus_config_setstring(CYRUSOPT_CONFIG_DIR, config_dir);
	libcyrus_config_setswitch(CYRUSOPT_AUTH_UNIX_GROUP_ENABLE,
				  config_getswitch(IMAPOPT_UNIX_GROUP_ENABLE));
	libcyrus_config_setswitch(CYRUSOPT_USERNAME_TOLOWER,
				  config_getswitch(IMAPOPT_USERNAME_TOLOWER));
	libcyrus_config_setswitch(CYRUSOPT_SKIPLIST_UNSAFE,
				  config_getswitch(IMAPOPT_SKIPLIST_UNSAFE));
	libcyrus_config_setstring(CYRUSOPT_TEMP_PATH,
				  config_getstring(IMAPOPT_TEMP_PATH));
	libcyrus_config_setint(CYRUSOPT_PTS_CACHE_TIMEOUT,
			       config_getint(IMAPOPT_PTSCACHE_TIMEOUT));
	libcyrus_config_setswitch(CYRUSOPT_FULLDIRHASH,
				  config_getswitch(IMAPOPT_FULLDIRHASH));
	libcyrus_config_setstring(CYRUSOPT_PTSCACHE_DB,
				  config_getstring(IMAPOPT_PTSCACHE_DB));
	libcyrus_config_setstring(CYRUSOPT_PTLOADER_SOCK,
				  config_getstring(IMAPOPT_PTLOADER_SOCK));
	libcyrus_config_setswitch(CYRUSOPT_VIRTDOMAINS,
				  config_getenum(IMAPOPT_VIRTDOMAINS));
	libcyrus_config_setint(CYRUSOPT_BERKELEY_CACHESIZE,
			       config_getint(IMAPOPT_BERKELEY_CACHESIZE));
	libcyrus_config_setstring(CYRUSOPT_AUTH_MECH,
				  config_getstring(IMAPOPT_AUTH_MECH));
	libcyrus_config_setint(CYRUSOPT_BERKELEY_LOCKS_MAX,
			       config_getint(IMAPOPT_BERKELEY_LOCKS_MAX));
	libcyrus_config_setint(CYRUSOPT_BERKELEY_TXNS_MAX,
			       config_getint(IMAPOPT_BERKELEY_TXNS_MAX));
	libcyrus_config_setstring(CYRUSOPT_DELETERIGHT,
				  config_getstring(IMAPOPT_DELETERIGHT));
	libcyrus_config_setstring(CYRUSOPT_SQL_DATABASE,
				  config_getstring(IMAPOPT_SQL_DATABASE));
	libcyrus_config_setstring(CYRUSOPT_SQL_ENGINE,
				  config_getstring(IMAPOPT_SQL_ENGINE));
	libcyrus_config_setstring(CYRUSOPT_SQL_HOSTNAMES,
				  config_getstring(IMAPOPT_SQL_HOSTNAMES));
	libcyrus_config_setstring(CYRUSOPT_SQL_USER,
				  config_getstring(IMAPOPT_SQL_USER));
	libcyrus_config_setstring(CYRUSOPT_SQL_PASSWD,
				  config_getstring(IMAPOPT_SQL_PASSWD));
	libcyrus_config_setswitch(CYRUSOPT_SQL_USESSL,
				  config_getswitch(IMAPOPT_SQL_USESSL));

	/* Not until all configuration parameters are set! */
	libcyrus_init();
    }
    
    return 0;
}

void global_sasl_init(int client, int server, const sasl_callback_t *callbacks)
{
    static int called_already = 0;
    
    assert(client || server);
    assert(!called_already);
    
    called_already = 1;

    /* 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 *) &cyrus_mutex_alloc,
                   (sasl_mutex_lock_t *) &cyrus_mutex_lock,
                   (sasl_mutex_unlock_t *) &cyrus_mutex_unlock,
                   (sasl_mutex_free_t *) &cyrus_mutex_free);

    if(client && sasl_client_init(callbacks)) {
	fatal("could not init sasl (client)", EC_SOFTWARE);
    }

    if(server && sasl_server_init(callbacks, "Cyrus")) {
	fatal("could not init sasl (server)", EC_SOFTWARE);
    }
}

/* this is a wrapper to call the cyrus configuration from SASL */
int mysasl_config(void *context __attribute__((unused)), 
		  const char *plugin_name,
		  const char *option,
		  const char **result,
		  unsigned *len)
{
    char opt[1024];

    if (!strcmp(option, "srvtab")) { 
	/* we don't transform srvtab! */
	*result = config_getstring(IMAPOPT_SRVTAB);
    } else {
	*result = NULL;

	if (plugin_name) {
	    /* first try it with the plugin name */
	    strlcpy(opt, "sasl_", sizeof(opt));
	    strlcat(opt, plugin_name, sizeof(opt));
	    strlcat(opt, "_", sizeof(opt));
	    strlcat(opt, option, sizeof(opt));
	    *result = config_getoverflowstring(opt, NULL);
	}

	if (*result == NULL) {
	    /* try without the plugin name */
	    strlcpy(opt, "sasl_", sizeof(opt));
	    strlcat(opt, option, sizeof(opt));
	    *result = config_getoverflowstring(opt, NULL);
	}
    }

    if (*result != NULL) {
	if (len) { *len = strlen(*result); }
	return SASL_OK;
    }
   
    return SASL_FAIL;
}

/* This creates a structure that defines the allowable
 *   security properties 
 */
sasl_security_properties_t *mysasl_secprops(int flags)
{
    static sasl_security_properties_t ret;

    ret.maxbufsize = PROT_BUFSIZE;
    ret.min_ssf = config_getint(IMAPOPT_SASL_MINIMUM_LAYER);	
				/* minimum allowable security strength */
    ret.max_ssf = config_getint(IMAPOPT_SASL_MAXIMUM_LAYER);
				/* maximum allowable security strength */

    ret.security_flags = flags;
    if (!config_getswitch(IMAPOPT_ALLOWPLAINTEXT)) {
	ret.security_flags |= SASL_SEC_NOPLAINTEXT;
    }
    if (!config_getswitch(IMAPOPT_ALLOWANONYMOUSLOGIN)) {
	ret.security_flags |= SASL_SEC_NOANONYMOUS;
    }
    ret.property_names = NULL;
    ret.property_values = NULL;

    return &ret;
}

/* true if 'authstate' is in 'opt' */
int global_authisa(struct auth_state *authstate, enum imapopt opt)
{
    char buf[1024];
    const char *val = config_getstring(opt);
    size_t len;

    /* Is the option defined? */
    if(!val) return 0;

    while (*val) {
	char *p;
	
	for (p = (char *) val; *p && !Uisspace(*p); p++);
	len = p-val;
	if(len >= sizeof(buf))
	    len = sizeof(buf) - 1;
	memcpy(buf, val, len);
	buf[len] = '\0';

	if (auth_memberof(authstate, buf)) {
	    return 1;
	}
	val = p;
	while (*val && Uisspace(*val)) val++;
    }

    return 0;
}

/* Note: This function is not idempotent! Only call it once for a given ID
 * or you will be unhappy (think IP hosting). */
char *canonify_userid(char *user, char *loginid, int *domain_from_ip)
{
    char *domain = NULL;
    int len = strlen(user);
    char buf[81];

    /* check for domain */
    if (config_virtdomains &&
	((domain = strrchr(user, '@')) || (domain = strrchr(user, '%')))) {
	*domain = '@';
	len = domain - user;
    }

    /* check for global identifiers */
    if (is_userid_anonymous(user)) {
	return "anonymous";
    }
    else if ((len == 7 && strncasecmp(user, "anybody", len) == 0) ||
	     (len == 6 && strncasecmp(user, "anyone", len) == 0)) {
	return "anyone";
    }

    if (config_virtdomains) {
	if (domain) {
	    if (config_defdomain && !strcasecmp(config_defdomain, domain+1)) {
		*domain = '\0'; /* trim the default domain */
	    }
	}
	else if (loginid) { /* used for LISTRIGHTS */
	    if ((domain = strrchr(loginid, '@'))) {
		/* append the domain from the login id */
		snprintf(buf, sizeof(buf), "%s@%s", user, domain+1);
		user = buf;
	    }
	}
	else if (config_virtdomains != IMAP_ENUM_VIRTDOMAINS_USERID) {
	    socklen_t salen;
	    int error;
	    struct sockaddr_storage localaddr;
	    char hbuf[NI_MAXHOST];
	    
	    salen = sizeof(localaddr);
	    if (getsockname(0, (struct sockaddr *)&localaddr, &salen) == 0) {
		error = getnameinfo((struct sockaddr *)&localaddr, salen,
				    hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD);
		if (error == 0 && (domain = strchr(hbuf, '.')) &&
		    !(config_defdomain && !strcasecmp(config_defdomain, domain+1))) {
		    /* append the domain from our IP */
		    snprintf(buf, sizeof(buf), "%s@%s", user, domain+1);
		    user = buf;
		    
		    if (domain_from_ip) *domain_from_ip = 1;
		}
	    }
	}
    }

    return auth_canonifyid(user, 0);
}

int mysasl_canon_user(sasl_conn_t *conn,
		      void *context,
		      const char *user, unsigned ulen,
		      unsigned flags __attribute__((unused)),
		      const char *user_realm __attribute__((unused)),
		      char *out,
		      unsigned out_max, unsigned *out_ulen)
{
    char *canonuser = NULL;

    if (ulen+1 > out_max) {
	sasl_seterror(conn, 0, "buffer overflow while canonicalizing");
	return SASL_BUFOVER;
    }
    memcpy(out, user, ulen);
    out[ulen] = '\0';

    canonuser = canonify_userid(out, NULL, (int*) context);
    if (!canonuser) {
	sasl_seterror(conn, 0, "bad userid authenticated");
	return SASL_BADAUTH;
    }
    *out_ulen = strlen(canonuser);
    if (*out_ulen >= out_max) {
	sasl_seterror(conn, 0, "buffer overflow while canonicalizing");
	return SASL_BUFOVER;
    }
    
    strcpy(out, canonuser);

    return SASL_OK;
}

int is_userid_anonymous(const char *user) 
{
    int len = strlen(user);
    const char *domain;

    assert(user);

    /* check for domain */
    if (config_virtdomains &&
	((domain = strrchr(user, '@')) || (domain = strrchr(user, '%')))) {
	len = domain - user;
    }

    /* check if we are anonymous */
    if (len == 9 && strncasecmp(user, "anonymous", len) == 0) {
	return 1;
    } else {
	return 0;
    }
}

/*
 * acl_ok() checks to see if the the inbox for 'user' grants the 'a'
 * right to 'authstate'. Returns 1 if so, 0 if not.
 */
/* Note that we do not determine if the mailbox is remote or not */
static int acl_ok(const char *user, struct auth_state *authstate)
{
    struct namespace namespace;
    char *acl;
    char bufuser[MAX_MAILBOX_BUFFER], inboxname[MAX_MAILBOX_BUFFER];
    int r;

    /* Set namespace */
    if ((r = mboxname_init_namespace(&namespace, 0)) != 0) {
	syslog(LOG_ERR, error_message(r));
	fatal(error_message(r), EC_CONFIG);
    }
    
    strlcpy(bufuser, user, sizeof(bufuser));

    /* Translate any separators in userid */
    mboxname_hiersep_tointernal(&namespace, bufuser,
				config_virtdomains ?
				strcspn(bufuser, "@") : 0);

    if (!r)
	r = (*namespace.mboxname_tointernal)(&namespace, "INBOX",
					     bufuser, inboxname);

    if (r || !authstate ||
	mboxlist_lookup(inboxname, &acl, NULL)) {
	r = 0;  /* Failed so assume no proxy access */
    }
    else {
	r = (cyrus_acl_myrights(authstate, acl) & ACL_ADMIN) != 0;
    }
    return r;
}

#define DENYDB config_userdeny_db
#define FNAME_USERDENYDB "/user_deny.db"
#define USERDENY_VERSION 2

/*
 * access_ok() checks to see if 'user' is allowed access to 'service'
 * Returns 1 if so, 0 if not.
 */
int access_ok(const char *user, const char *service, char *msgbuf, int size)
{
    static char *fname = NULL;
    struct db *db = NULL;
    int r, ret = 1;  /* access always granted by default */

    if (!fname) {
	/* create path to database */
	fname = xmalloc(strlen(config_dir) + sizeof(FNAME_USERDENYDB) + 1);
	strcpy(fname, config_dir);
	strcat(fname, FNAME_USERDENYDB);
    }

    /* try to open database */
    r = DENYDB->open(fname, 0, &db);
    if (r) {
	/* ignore non-existent DB, report all other errors */
	if (errno != ENOENT) {
	    syslog(LOG_WARNING, "DENYDB_ERROR: error opening '%s': %s",
		   fname, cyrusdb_strerror(r));
	}

    } else {
	/* fetch entry for user */
	const char *data = NULL;
	int datalen;

	syslog(LOG_DEBUG, "fetching user_deny.db entry for '%s'", user);
	do {
	    r = DENYDB->fetch(db, user, strlen(user), &data, &datalen, NULL);
	} while (r == CYRUSDB_AGAIN);

	if (r || !data || !datalen) {
	    /* ignore non-existent/empty entry, report all other errors */
	    if (r != CYRUSDB_NOTFOUND) {
		syslog(LOG_WARNING,
		       "DENYDB_ERROR: error reading entry '%s': %s",
		       user, cyrusdb_strerror(r));
	    }
	} else {
	    /* parse the data */
	    char *buf, *wild;
	    unsigned long version;

	    buf = xstrndup(data, datalen);  /* use a working copy */

	    /* check version */
	    if (((version = strtoul(buf, &wild, 10)) < 1) ||
		(version > USERDENY_VERSION)) {
		syslog(LOG_WARNING,
		       "DENYDB_ERROR: invalid version for entry '%s': %lu",
		       user, version);
	    } else if (*wild++ != '\t') {
		syslog(LOG_WARNING,
		       "DENYDB_ERROR: missing wildmat for entry '%s'", user);
	    } else {
		char *pat, *msg = "Access to this service has been blocked";
		int not;

		/* check if we have a deny message */
		switch (version) {
		case USERDENY_VERSION:
		    if ((msg = strchr(wild, '\t'))) *msg++ = '\0';
		    break;
		}

		/* scan wildmat right to left for a match against our service */
		syslog(LOG_DEBUG, "wild: '%s'   service: '%s'", wild, service);
		do {
		    /* isolate next pattern */
		    if ((pat = strrchr(wild, ','))) {
			*pat++ = '\0';
		    } else {
			pat = wild;
		    }

		    /* XXX  trim leading & trailing whitespace? */

		    /* is it a negated pattern? */
		    not = (*pat == '!');
		    if (not) ++pat;

		    syslog(LOG_DEBUG, "pat %d:'%s'", not, pat);

		    /* see if pattern matches our service */
		    if (wildmat(service, pat)) {
			/* match ==> we're done */
			ret = not;
			if (msgbuf) strlcpy(msgbuf, msg, size);
			break;
		    }

		    /* continue until we reach head of wildmat */
		} while (pat != wild);
	    }

	    free(buf);
	}


	r = DENYDB->close(db);
	if (r) {
	    syslog(LOG_WARNING, "DENYDB_ERROR: error closing: %s",
		   cyrusdb_strerror(r));
	}
    }

    return ret;
}

/* should we allow users to proxy?  return SASL_OK if yes,
   SASL_BADAUTH otherwise */
int mysasl_proxy_policy(sasl_conn_t *conn,
			void *context,
			const char *requested_user, unsigned rlen,
			const char *auth_identity, unsigned alen,
			const char *def_realm __attribute__((unused)),
			unsigned urlen __attribute__((unused)),
			struct propctx *propctx __attribute__((unused)))
{
    struct proxy_context *ctx = (struct proxy_context *) context;
    const char *val = config_getstring(IMAPOPT_LOGINREALMS);
    struct auth_state *authstate;
    int userisadmin = 0;
    char *realm;

    /* check if remote realm */
    if ((!config_virtdomains || *val) &&
	(realm = strchr(auth_identity, '@'))!=NULL) {
	realm++;
	while (*val) {
	    if (!strncasecmp(val, realm, strlen(realm)) &&
		(!val[strlen(realm)] || Uisspace(val[strlen(realm)]))) {
		break;
	    }
	    /* not this realm, try next one */
	    while (*val && !Uisspace(*val)) val++;
	    while (*val && Uisspace(*val)) val++;
	}
	if (!*val) {
	    sasl_seterror(conn, 0, "cross-realm login %s denied",
			  auth_identity);
	    return SASL_BADAUTH;
	}
    }

    authstate = auth_newstate(auth_identity);

    /* ok, is auth_identity an admin? */
    userisadmin = global_authisa(authstate, IMAPOPT_ADMINS);

    if (!ctx) {
	/* for now only admins are allowed */
	auth_freestate(authstate);
    
	if (!userisadmin) {
	    syslog(LOG_ERR, "%s is not an admin", auth_identity);
	    sasl_seterror(conn, SASL_NOLOG, "only admins may authenticate");
	    return SASL_BADAUTH;
	}

	return SASL_OK;
    }

    /* is requested_user denied access?  authenticated admins are exempt */
    if (!userisadmin && !access_ok(requested_user, config_ident, NULL, 0)) {
	syslog(LOG_ERR, "user '%s' denied access to service '%s'",
	       requested_user, config_ident);
	sasl_seterror(conn, SASL_NOLOG,
		      "user '%s' is denied access to service '%s'",
		      requested_user, config_ident);

	auth_freestate(authstate);

	return SASL_NOAUTHZ;
    }

    if (alen != rlen || strncmp(auth_identity, requested_user, alen)) {
	/* we want to authenticate as a different user; we'll allow this
	   if we're an admin or if we've allowed ACL proxy logins */
	int use_acl = ctx->use_acl && config_getswitch(IMAPOPT_LOGINUSEACL);

	if (userisadmin ||
	    (use_acl && acl_ok(requested_user, authstate)) ||
	    (ctx->proxy_servers &&
	     global_authisa(authstate, IMAPOPT_PROXYSERVERS))) {
	    /* proxy ok! */

	    userisadmin = 0;	/* no longer admin */
	    auth_freestate(authstate);
	    
	    authstate = auth_newstate(requested_user);

	    /* are we a proxy admin? */
	    if (ctx->userisproxyadmin)
		*(ctx->userisproxyadmin) =
		    global_authisa(authstate, IMAPOPT_ADMINS);
	} else {
	    sasl_seterror(conn, 0, "user %s is not allowed to proxy",
			  auth_identity);

	    auth_freestate(authstate);

	    return SASL_BADAUTH;
	}
    }

    if (ctx->authstate)
	*(ctx->authstate) = authstate;
    else 
	auth_freestate(authstate);
    if (ctx->userisadmin) *(ctx->userisadmin) = userisadmin;

    return SASL_OK;
}

/* covert a time_t date to an IMAP-style date
 * datebuf needs to be >= 30 bytes */
void cyrus_ctime(time_t date, char *datebuf) 
{
    struct tm *tm = localtime(&date);
    long gmtoff = gmtoff_of(tm, date);
    int gmtnegative = 0;
    static const char *monthname[] = {
	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };

    if (date == 0 || tm->tm_year < 69) {
	abort();
    }

    if (gmtoff < 0) {
	gmtoff = -gmtoff;
	gmtnegative = 1;
    }
    gmtoff /= 60;
    sprintf(datebuf,
	    "%2u-%s-%u %.2u:%.2u:%.2u %c%.2lu%.2lu",
	    tm->tm_mday, monthname[tm->tm_mon], tm->tm_year+1900,
	    tm->tm_hour, tm->tm_min, tm->tm_sec,
	    gmtnegative ? '-' : '+', gmtoff/60, gmtoff%60);
}

/* call before a cyrus application exits */
void cyrus_done() 
{
    if(cyrus_init_run != RUNNING) return;
    cyrus_init_run = DONE;
    
    if (!cyrus_init_nodb) libcyrus_done();
}

/*
 * Returns 1 if we have a shutdown file, with the first line in buf.
 * Otherwise returns 0, and the contents of buf is undefined.
 */
int shutdown_file(char *buf, int size)
{
    FILE *f;
    static char shutdownfilename[1024] = "";
    char *p;
    
    if (!shutdownfilename[0])
	snprintf(shutdownfilename, sizeof(shutdownfilename), 
		 "%s/msg/shutdown", config_dir);
    if ((f = fopen(shutdownfilename, "r")) == NULL) return 0;

    fgets(buf, size, f);
    if ((p = strchr(buf, '\r')) != NULL) *p = 0;
    if ((p = strchr(buf, '\n')) != NULL) *p = 0;

    syslog(LOG_DEBUG, "Shutdown file: %s, closing connection", buf);

    return 1;
}

struct part_stats {
    char name[MAX_PARTITION_LEN+1]; /* name of part with most space */
    unsigned long avail;	/* 1k free blocks on freeest part */
    unsigned long tavail;	/* total 1k free blocks on server */
    unsigned long fsid[512];	/* array of file system IDs */
    unsigned nfsid;		/* number of file system IDs */
};

/*
 * config_foreachoverflowstring() callback which finds spool partition
 * with the most available space and totals the space available on
 * all partitions.
 */
static void get_part_stats(const char *key, const char *val, void *rock)
{
    struct part_stats *stats = (struct part_stats *) rock;
    struct statvfs s;
    unsigned long avail;
    unsigned i;

    /* not a partition-* config option */
    if (strncmp("partition-", key, 10)) return;

    /* can't stat the given path */
    if (statvfs(val, &s)) return;

    /* eliminate duplicate filesystems */
    for (i = 0; i < stats->nfsid; i++) {
	if (s.f_fsid == stats->fsid[i]) return;
    }
    stats->fsid[stats->nfsid++] = s.f_fsid;

    /* calculate avail space in 1k blocks */
    avail = (unsigned long) (s.f_bavail * (s.f_frsize / 1024.0));

    /* add to total */
    stats->tavail += avail;

    if (avail > stats->avail) {
	/* this part has the most avail space */
	stats->avail = avail;
	strlcpy(stats->name, key+10, MAX_PARTITION_LEN);
    }
}

/*
 * Returns the name of the spool partition with the most available space.
 * Optionally returns the total amount of available space on the server
 * (all partitions) in 1k blocks.
 */
char *find_free_partition(unsigned long *tavail)
{
    static struct part_stats stats;

    memset(&stats, 0, sizeof(struct part_stats));
    config_foreachoverflowstring(get_part_stats, &stats);

    if (tavail) *tavail = stats.tavail;
    return stats.name;
}
