/*
 *
 * SASL authentication mechanisms for qpopper, using the Cyrus-SASL library
 *
 * Copyright (c) 2011 QUALCOMM Incorporated.  All rights reserved.
 * The file License.txt specifies the terms for use, modification,
 * and redistribution.
 *
 * File:      pop_sasl.c
 */

#include <stdio.h>
#include <sys/types.h>
#include <ctype.h>

#include "config.h"

#include <string.h>
#include <stdlib.h>
#include <stdarg.h>

#if HAVE_STRINGS_H
#  include <strings.h>
#endif

#include "genpath.h"
#include "popper.h"
#include "pop_sasl.h"

#ifdef GSSAPI_KUSEROK
#  include <krb5.h>
#  include <com_err.h>
#endif /* GSSAPI_KUSEROK */

/*
 * The rest is only compiled if we're using Cyrus-SASL
 */

#ifdef CYRUS_SASL

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

/*
 * Callback and internal functions we use later
 */

static int popper_authorize ( sasl_conn_t *, void *, const char *, unsigned,
                              const char *, unsigned, const char *, unsigned,
                              struct propctx * );

static int pop_sasl_dialog ( POP * );
static int pop_sasl_ok ( POP * );
static int pop_sasl_log ( void *, int, const char * );
static void pop_sasl_log_login ( POP * );

/*
 * These are our server callbacks
 */

sasl_callback_t server_callbacks[] = {
    { SASL_CB_PROXY_POLICY, popper_authorize, NULL },
    { SASL_CB_LOG, pop_sasl_log, NULL },
    { SASL_CB_LIST_END, NULL, NULL },
};

/*
 * We use this to keep track of our login time
 */

static time_t my_timer;

/*
 * Other miscellany
 */

extern char *ERRMSG_AUTH;
#define MAX_PROT_BUFSIZE          8192     /* Our maximum output bufsize */

/*
 * This is where we initialize our sasl library
 */

void *
pop_sasl_init ( POP *p )
{
    int result, salen, i;
    sasl_conn_t *conn;
    sasl_security_properties_t secprops;
    struct sockaddr_in sin;


    memset ( &secprops, 0L, sizeof(secprops) );
    secprops.maxbufsize = MAXLINELEN;

    for ( i = 0; server_callbacks[i].id != SASL_CB_LIST_END; i++ )
        server_callbacks[i].context = p;

    /*
     * First, initialize the global SASL library
     */

    result = sasl_server_init ( server_callbacks, "qpopper" );

    if ( result != SASL_OK)  {
        pop_log ( p, POP_PRIORITY, HERE, "SASL server initialization failed: %s",
                  sasl_errstring ( result, NULL, NULL ) );
        return NULL;
    }

    /*
     * Next initialize the connection.  Note that we hardcode the service
     * name here to be "pop" ... since this is required by the RFC and
     * there's no reason for anyone to change it, we'll leave it as-is
     * for now.
     *
     * A note on the security flags (second-to-last argument).  The SASL v2
     * API has the flag "SASL_SUCCESS_DATA" that can be set here, if the
     * server supports data on success.  Research indicates that the POP
     * SASL profile predates this ability of SASL, and since Cyrus does
     * _not_ set that flag for POP, we don't set it here (after all, they
     * should know, right?)
     */

    result = sasl_server_new ( "pop", p->myhost, NULL, NULL, NULL, NULL, 0,
                               &conn);

    if ( result != SASL_OK ) {
        pop_log ( p, POP_PRIORITY, HERE, "SASL server connection initialization "
                  "failed: %s", sasl_errstring ( result, NULL, NULL ) );
        return NULL;
    }

#if 0
    /*
     * Set up the local and remote IP addresses (which are only used for
     * Kerberos 4, but do them just in case)
     */
#endif

    /*
     * Here is where we set our security flags based on the information in
     * the qpopper configuration file.
     */

    if ( p->sasl.sasl_no_plaintext )
        secprops.security_flags |= SASL_SEC_NOPLAINTEXT;
    if ( p->sasl.sasl_no_active )
        secprops.security_flags |= SASL_SEC_NOACTIVE;
    if ( p->sasl.sasl_no_dictionary )
        secprops.security_flags |= SASL_SEC_NODICTIONARY;
    if ( p->sasl.sasl_forward_secrecy )
        secprops.security_flags |= SASL_SEC_FORWARD_SECRECY;
    if ( p->sasl.sasl_no_anonymous )
        secprops.security_flags |= SASL_SEC_NOANONYMOUS;
    secprops.min_ssf = p->sasl.sasl_min_ssf;
    secprops.max_ssf = p->sasl.sasl_max_ssf;

    result = sasl_setprop ( conn, SASL_SEC_PROPS, &secprops );

    if ( result != SASL_OK ) {
        pop_log ( p, POP_PRIORITY, HERE, "SASL security property initialization "
                  "failed: %s", sasl_errdetail(conn) );
        sasl_dispose ( &conn );
        return NULL;
    }

    return (void *) conn;
}

/*
 * Our main entry for the AUTH command.  This is where we do all of our
 * processing
 */

pop_result
pop_auth ( POP *p )
{
    int result;
    const char *mechs, *out;
    char buf[MAXLINELEN];
    unsigned int mechlen, buflen, outlen;

    if ( p->bDo_timing )
        my_timer = time(0);

    /*
     * The "auth" command with no arguments shows the list of supported
     * mechanisms
     */

    if ( p->parm_count == 0 ) {
        pop_msg ( p, POP_SUCCESS, HERE, "Following are the supported "
                  "SASL mechanisms:" );
        result = sasl_listmech ( p->sasl.sasl_conn, NULL, NULL, "\r\n", "\r\n", &mechs,
                                 &mechlen, NULL );
        if ( result == SASL_OK ) {
            pop_write ( p, (char *) mechs, mechlen );
        } else {
            pop_log ( p, POP_PRIORITY, HERE, "SASL mechanism list failed: %s",
                      sasl_errdetail(p->sasl.sasl_conn) );
        }
        pop_write_line ( p, "." );
        pop_write_flush ( p );
        return POP_FAILURE;
    }

    /*
     * We either got one argument (which indicates a chosen mechanism)
     * or two arguments (which means that we got a mechanism plus initial
     * token).  Set up the initial token if we've been given two arguments.
     */

    if ( p->parm_count == 2)  {
        result = sasl_decode64 ( p->pop_parm[2], p->pop_parmlen[2],
                                buf, sizeof(buf), &buflen );
        
        if ( result != SASL_OK )
            return ( pop_msg ( p, POP_FAILURE, HERE, "Unable to decode base64 "
                               "data: %s", sasl_errstring(result, NULL, NULL ) ) );
    }

    /*
     * Start the SASL dialog (giving the function our optional first token)
     */

    result = sasl_server_start ( p->sasl.sasl_conn, p->pop_parm[1],
                                 p->parm_count == 2 ? buf : NULL,
                                 p->parm_count == 2 ? buflen : 0,
                                 &out, &outlen );

    if ( result != SASL_OK && result != SASL_CONTINUE )
        return ( pop_msg ( p, POP_FAILURE, HERE, "SASL initial authentication "
                           "failed: %s", sasl_errdetail(p->sasl.sasl_conn)) );

    p->sasl.sasl_mechname = strdup ( p->pop_parm[1] );

    if ( ! p->sasl.sasl_mechname )
        return ( pop_msg ( p, POP_FAILURE, HERE, "Unable to allocate memory "
                                                 "for SASL mechanism name" ) );

    /*
     * If we got SASL_OK, then we're done and can move on to the final phase.
     * Otherwise, use Push to put an input handler onto the stack so we
     * can process the authentication dialog (after we output any data
     * we were supposed to).
     */

    if ( result == SASL_CONTINUE ) {
        if ( out ) {
            result = sasl_encode64(out, outlen, buf, sizeof(buf), NULL);

            if (result != SASL_OK)
                return (pop_msg(p, POP_FAILURE, HERE, "SASL base64 encoding "
                                " error: %s",
                                sasl_errstring(result, NULL, NULL)));
        } else
            buf[0] = '\0';
        
        pop_write_fmt(p, "+ %s\r\n", buf);
        pop_write_flush(p);
        Push(&(p->InProcess), (FP) pop_sasl_dialog);
        return POP_SUCCESS;
    } else
        return ( pop_sasl_ok ( p ) );
}

/*
 * This is where we list our mechanisms in response to "CAPA"
 */

void
pop_sasl_mechs ( POP *p )
{
    sasl_conn_t *conn = (sasl_conn_t *) p->sasl.sasl_conn;
    int result, nmechs;
    const char *str;
    unsigned int len;

    result = sasl_listmech ( conn, NULL, "SASL ", " ", "\r\n", &str,
                             &len, &nmechs) ;

    if ( result == SASL_OK && nmechs > 0 )
         pop_write ( p, (char *) str, (int) len );
}

/*
 * This is the main processing loop for implementing the SASL authentication
 * dialog.  This is implemented here as a input processor we push onto
 * the qpopper input stack.  Every time we get called, it's with another
 * blob of input data.  Process each blob and return the appropriate
 * response.
 */ 

static pop_result
pop_sasl_dialog ( POP *p )
{
    char *out, *msg, buf[MAXLINELEN];
    unsigned int buflen, outlen;
    int i, result;

    /*
     * First off, strip off any trailing CR/LF's (stolen from the
     * SCRAM code)
     */

    msg = p->inp_buffer;
    if ( ( i = strlen(msg)) && (msg[i - 1] == '\n' ) ) {
        if ( --i && (msg[i - 1] == '\r' )) i--;
        msg[i] = '\0';
    }

    /*
     * Did we get the abort code?
     */

    if ( i == 1 && msg[0] == '*' ) {
        pop_msg ( p, POP_FAILURE, HERE, "SASL authentication abort received "
                  "from client." );
        p->CurrentState = halt;
        (void) Pop(&(p->InProcess));
        return POP_FAILURE;
    }

    /*
     * Note that for sasl_decode64() (and for encode64 as well) we don't
     * use errdetail.  Sigh.
     */

    result = sasl_decode64(msg, strlen(msg), buf, sizeof(buf), &buflen);

    if ( result != SASL_OK ) {
        pop_msg ( p, POP_FAILURE, HERE, "SASL base64 decode failed: %s",
                sasl_errstring(result, NULL, NULL));
        p->CurrentState = halt;
        (void) Pop(&(p->InProcess));
        return POP_FAILURE;
    }

    /*
     * Now that our buffer is now into binary format, feed it into
     * sasl_client_step()
     */

    result = sasl_server_step ( p->sasl.sasl_conn, buf, buflen,
                                (const char **) &out, &outlen );

    if ( result != SASL_OK && result != SASL_CONTINUE ) {
        pop_msg ( p, POP_FAILURE, HERE, "SASL authentication failed: %s",
                  sasl_errdetail(p->sasl.sasl_conn) );
        p->CurrentState = halt;
        (void) Pop(&(p->InProcess));
        return POP_FAILURE;
    }

    /*
     * If we get SASL_OK here, then we're done; proceed to the final step
     */

    if ( result == SASL_OK ) {
        (void) Pop(&(p->InProcess));
        return ( pop_sasl_ok(p) );
    }

    /*
     * Otherwise, output the response, and wait for the next one ...
     */

    if (out) {
        result = sasl_encode64 ( out, outlen, buf, sizeof(buf), NULL );
        if ( result != SASL_OK ) {
            pop_msg ( p, POP_FAILURE, HERE, "SASL base64 encoding failed: %s",
                      sasl_errstring(result, NULL, NULL) );
            p->CurrentState = halt;
            (void) Pop(&(p->InProcess));
            return POP_FAILURE;
        }
    } else
        buf[0] = '\0';

    pop_write_fmt ( p, "+ %s\r\n", buf );
    pop_write_flush ( p );

    return POP_SUCCESS;
}

/*
 * This is the final step.  Here we perform the final bit of authorization,
 * and setup the mailbox for real POP work.
 */

static pop_result
pop_sasl_ok(POP *p)
{
    const char *user;
    int result, *ssf;
    unsigned int *maxout;
    struct passwd *pw = NULL;
#ifdef CHECK_SHELL
    char *getusershell();
    void endusershell();
    char *shell;
    char *cp;
    int shellvalid;
#endif /* CHECK_SHELL */

    /*
     * First, we need to get out the authenticated username from SASL
     */

    result = sasl_getprop(p->sasl.sasl_conn, SASL_USERNAME, (const void **) &user);

    if (result != SASL_OK)
        return (pop_msg(p, POP_FAILURE, HERE, "Could not retrieve SASL "
                        "username: %s", sasl_errdetail(p->sasl.sasl_conn)));

    strlcpy(p->user, user, sizeof(p->user));

    if (p->bDowncase_user)
        downcase_uname(p, p->user);

    /*
     * Get the SSF (Security strength factor, aka number of bits used to do
     * encryption)
     */

    result = sasl_getprop(p->sasl.sasl_conn, SASL_SSF, (const void **) &ssf);

    if (result != SASL_OK)
        pop_log(p, POP_WARNING, HERE, "Unable to retrieve the SASL SSF: %s",
                sasl_errdetail(p->sasl.sasl_conn));
    else
        p->sasl.sasl_ssf = *ssf;

    /*
     * Get our output buffer size
     */

    result = sasl_getprop(p->sasl.sasl_conn, SASL_MAXOUTBUF,
                          (const void **) &maxout);

    if (result != SASL_OK)
        pop_log(p, POP_WARNING, HERE, "Unable to retrieve the SASL "
                "MAXOUTBUF: %s", sasl_errdetail(p->sasl.sasl_conn));
    else
        p->sasl.sasl_maxbuf = *maxout;

    if (p->sasl.sasl_maxbuf == 0 || p->sasl.sasl_maxbuf > MAX_PROT_BUFSIZE)
        /*
         * maxbuf == 0 means unlimited; this is to prevent busted SASL
         * negotiation from screwing us ip.
         */
        p->sasl.sasl_maxbuf = MAX_PROT_BUFSIZE;
    /*
     * Now perform various POP authorization checks
     */

    if (p->nonauthfile != NULL && checknonauthfile(p) != 0) {
        return (pop_msg(p, POP_FAILURE, HERE, ERRMSG_AUTH, p->user));
    }

    if (p->authfile != NULL && checkauthfile(p) != 0) {
        return (pop_msg(p, POP_FAILURE, HERE, ERRMSG_AUTH, p->user));
    }

    /*
     * Get (and cache) our password file information
     */

    pw = getpwnam(p->user);

    if (pw == NULL) {
        DEBUG_LOG1(p, "User %.128s not known by system", p->user);
        return (pop_msg(p, POP_FAILURE, HERE, "[AUTH] Userid %.128s not known "
                        "by system", p->user));
    }

    p->pw = *pw;
    p->pw.pw_dir = strdup(pw->pw_dir);

#ifdef BLOCK_UID
    if (pw->pw_uid <= BLOCK_UID) {
        return (pop_msg(p, POP_FAILURE, HERE, "[AUTH Access is blocked for "
                        "UIDs below %d", BLOCK_UID));
    }
#endif /* BLOCK_UID */

#ifdef CHECK_SHELL
    /*  
     * Disallow anyone who does not have a standard shell as returned by
     * getusershell(), unless the sys admin has included the wildcard
     * shell in /etc/shells.  (default wildcard - /POPPER/ANY/SHELL)
     */
    shell = pw->pw_shell;

    if (shell == NULL || *shell == 0) {
        return (pop_msg(p, POP_FAILURE, HERE, "[AUTH] No user shell defined"));
    }

    for (shellvalid = 0; shellvalid == 0 && ( cp = getusershell() ) != NULL; )
        if (!strcmp ( cp, WILDCARD_SHELL ) || !strcmp ( cp, shell ))
            shellvalid = 1;
    endusershell();

    if (shellvalid == 0) {
        return (pop_msg(p, POP_FAILURE, HERE, "[AUTH] \"%s\": shell not "
                        "found.", p->user));
    }
#endif /* CHECK_SHELL */

    /*
     * Check if server mode should be set or reset based on group membership.
     */

    if (p->grp_serv_mode != NULL && check_group(p, &p->pw, p->grp_serv_mode)) {
        p->server_mode = TRUE;
        DEBUG_LOG2(p, "Set server mode for user %s; member of "
                   "\"%.128s\" group", p->user, p->grp_serv_mode);
    }

    if (p->grp_no_serv_mode != NULL && check_group(p, &p->pw,
                                                   p->grp_no_serv_mode ) ) {
        p->server_mode = FALSE;
        DEBUG_LOG2(p, "Set server mode OFF for user %s; member of "
                   "\"%.128s\" group", p->user, p->grp_no_serv_mode);
    }

    /*
     * Do option-file processing
     */

    check_config_files(p, &p->pw);

    if (p->bDo_timing)
        p->login_time = time(0) - my_timer;

    /*
     * Copy the mailbox and set the group and user id.
     */

    if (pop_dropcopy(p, &p->pw) != POP_SUCCESS) {
        return (POP_FAILURE);
    }

    /*
     * Initialize the last-message-accessed number
     */

    p->last_msg = 0;
    p->AuthState = sasl;        /* We did it! */
    p->CurrentState = trans;    /* Move into transaction status */

    /*
     * Everything is now done.  If we have a SASL logging string, then
     * log that; if we don't, check to see if we have a regular log string
     * to use instead.
     */

    if (p->sasl.sasl_log_login != NULL)
        pop_sasl_log_login(p);
    else if (p->pLog_login != NULL)
        do_log_login(p);

#ifdef   DRAC_AUTH
    drac_it(p);
#endif /* DRAC_AUTH */

    pop_msg(p, POP_SUCCESS, HERE,
            "%s has %d visible message%s (%d hidden) in %ld octets.",
            p->user, p->visible_msg_count,
            p->visible_msg_count == 1 ? "" : "s",
            (p->msg_count - p->visible_msg_count), p->drop_size);

    /*
     * Note that we set the flag here _after_ we send the okay message,
     * because the +OK message isn't supposed to be encrypted (but
     * everything after it is
     */

    p->sasl_complete = 1;

    return POP_SUCCESS;
}

/*
 * This is where we do our authorization check.  Note that if the user has
 * defined it, we will call krb5_kuserok() (but just for the GSSAPI mechanism)
 */

static int
popper_authorize(sasl_conn_t *conn, void *context, const char *req_user,
                 unsigned int reqlen, const char *auth_id, unsigned int authlen,
                 const char *def_realm, unsigned int realmlen,
                 struct propctx *propctx)
{
    char *user, *auth;
    POP *p = (POP *) context;
    int result;
#ifdef GSSAPI_KUSEROK
    krb5_context kcontext;
    krb5_principal client;
    krb5_error_code code;
#endif /* GSSAPI_KUSEROK */

    /*
     * It's not clear if these are supposed to be null terminated or
     * not, but we'll play it safe.
     */

    if (!(user = (char *) malloc(reqlen + 1))) {
        return SASL_NOMEM;
    }

    if (!(auth = (char *) malloc(authlen + 1))) {
        free(user);
        return SASL_NOMEM;
    }

    strncpy(user, req_user, reqlen);
    user[reqlen] = '\0';
    strncpy(auth, auth_id, authlen);
    auth[authlen] = '\0';
    p->sasl.sasl_authname = strdup(auth);

#ifdef GSSAPI_KUSEROK
    if (strcmp(p->sasl.sasl_mechname, "GSSAPI") == 0) {
        /*
         * We're using the GSSAPI mechanism.  Call krb5_kuserok(),
         * which means we need to invoke all of the Kerberos goo.  Yes,
         * this pisses me off that all of this crap is necessary even
         * though the GSSAPI has already done all of the Kerberos
         * initialization.
         *
         * We need both a user and a auth_id, but the GSSAPI mechanism
         * makes sure we have both.
         */
        
        if ((code = krb5_init_context(&kcontext))) {
            sasl_seterror(conn, 0, "krb5_init_context failed: %s",
                          error_message(code));
            free(user);
            free(auth);
            return SASL_FAIL;
        }

        if ((code = krb5_parse_name(kcontext, auth, &client))) {
            sasl_seterror(conn, 0, "krb5_parse_name failed: %s",
                          error_message(code));
            free(user);
            free(auth);
            krb5_free_context(kcontext);
            return SASL_FAIL;
        }

        result = krb5_kuserok(kcontext, client, user);

        krb5_free_principal(kcontext, client);
        krb5_free_context(kcontext);

        if (!result) {
            sasl_seterror(conn, 0, "Kerberos user \"%s\" not authorized to "
                          "authenticate as \"%s\"", auth, user);
            free(user);
            free(auth);
            return SASL_BADAUTH;
        }

        free(user);
        free(auth);

        /*
         * Looks like the user passes muster after all
         */

        return SASL_OK;
    }
#endif /* GSSAPI_KUSEROK */

    /*
     * Perform the basic checks that the default authorization plug-in
     * does.  Salt to taste if you want to do something funky.
     */

    if (!req_user || *req_user == '\0') {
        free(user);
        free(auth);
        return SASL_OK;
    }

    if (!auth_id || !req_user || reqlen != authlen ||
        (memcmp(auth_id, req_user, reqlen) != 0)) {
        sasl_seterror(conn, 0, "Requested identity (%s) not authenticated "
                      "identity (%s)", user, auth);
        free(user);
        free(auth);
        return SASL_BADAUTH;
    }

    free(user);
    free(auth);

    return SASL_OK;
}

/*
 * This is our callback to the SASL internal logging
 */

static int
pop_sasl_log(void *context, int level, const char *message)
{
    POP *p = (POP *) context;

    switch (level) {
    case SASL_LOG_ERR:
    case SASL_LOG_FAIL:
        pop_log(p, POP_PRIORITY, HERE, "SASL: %s", message);
        break;
    case SASL_LOG_WARN:
        pop_log(p, POP_WARNING, HERE, "SASL: %s", message);
        break;
    case SASL_LOG_NOTE:
        pop_log(p, POP_NOTICE, HERE, "SASL: %s", message);
        break;
    case SASL_LOG_DEBUG:
    case SASL_LOG_TRACE:
    case SASL_LOG_PASS:
        pop_log(p, POP_DEBUG, HERE, "SASL: %s", message);
        break;
    default:
        break;
    }

    return SASL_OK;
}

/*
 * Log user logins; because of the extra information SASL provides, we
 * have extra stuff that could be logged.  A lot of this was taken from
 * do_log_login().  % escapes we support:
 *
 * %0 - POP version
 * %1 - POP user
 * %2 - Hostname of client
 * %3 - IP address of client
 * %4 - Authenticated identity
 * %5 - Authentication mechanism
 * %6 - # of encryption bits (SSF)
 */

static void
pop_sasl_log_login(POP *p)
{
    char *str = NULL, *q, *ptrs[7], bits[25];
    BOOL bFmt = FALSE;
    int inx = 0;

    str = strdup(p->sasl.sasl_log_login);
    if (str == NULL)
        return;

    for (q = str; *q != '\0'; q++) {
        if (*q == '%')
            bFmt = TRUE;
        else if (bFmt) {
            switch (*q) {
            case '0':
                ptrs[inx++] = VERSION;
                break;
            case '1':
                ptrs[inx++] = p->user;
                break;
            case '2':
                ptrs[inx++] = p->client;
                break;
            case '3':
                ptrs[inx++] = p->ipaddr;
                break;
            case '4':
                ptrs[inx++] = p->sasl.sasl_authname;
                break;
            case '5':
                ptrs[inx++] = p->sasl.sasl_mechname;
                break;
            case '6':
                Qsnprintf(bits, sizeof(bits), "%d", p->sasl.sasl_ssf);
                ptrs[inx++] = bits;
                break;
            default:
                pop_log(p, POP_NOTICE, HERE, "\"%%%c\" is invalid in a "
                        "sasl-log-login string: \"%.128s\"", *q,
                        p->sasl.sasl_log_login);
                free(str);
                return;
            }

            bFmt = FALSE;
            *q = 's';
            if (inx > 7) {
                pop_log(p, POP_NOTICE, HERE, "Sasl-log-login string contains "
                        "too many substitutions: \"%.128s\"",
                        p->sasl.sasl_log_login);
                free(str);
                return;
            }
        } /* bFmt && substitution loop */
    } /* for loop */

    pop_log(p, POP_PRIORITY, HERE, str, ptrs[0], ptrs[1], ptrs[2], ptrs[3],
            ptrs[4], ptrs[5], ptrs[6]);
    free(str);
}

/*
 * Handle encrypted reads from our peer
 */

int
pop_sasl_read(POP *p, char *buffer, int len)
{
    char buf[MAXLINELEN];
    int result, n;
    const char *outbuf;
    unsigned int outlen;

    /*
     * If we're not doing encryption, then just call read directly
     */

    if (p->sasl.sasl_ssf == 0)
        return read(p->input_fd, buffer, len);

    /*
     * If we have any data stored in our buffer, return that (we're
     * allowed to do a short read)
     */

    if (p->sasl.sasl_cnt > 0) {
        if (p->sasl.sasl_cnt > len) {
            memcpy(buffer, p->sasl.sasl_ptr, len);
            p->sasl.sasl_cnt -= len;
            p->sasl.sasl_ptr += len;
            return len;
        } else {
            memcpy(buffer, p->sasl.sasl_ptr, p->sasl.sasl_cnt);
            p->sasl.sasl_ptr = p->sasl.sasl_buffer;
            len = p->sasl.sasl_cnt;
            p->sasl.sasl_cnt = 0;
            return len;
        }
    }

    /*
     * Read some blobs from the network, until we get some data
     */

    for (;;) {

        n = read(p->input_fd, buf, sizeof(buf));

        if (n <= 0)
            return n;
        
        /*
         * Note that although sasl_decode() may return SASL_OK, it might
         * not necessarily have decoded anything (this happens if we get
         * a short read that doesn't cover one PDU)
         */

        result = sasl_decode(p->sasl.sasl_conn, buf, (unsigned) n, &outbuf,
                             &outlen);

        if (result != SASL_OK) {
            pop_log(p, POP_PRIORITY, HERE, "sasl_decode() failed: %s",
                    sasl_errdetail(p->sasl.sasl_conn));
            return -1;
        }

        if (outlen > 0 && outlen <= len) {
            /*
             * We've got some data shorter (or equal) than what they asked for;
             * Give it all to them.
             */
        
            memcpy(buffer, outbuf, outlen);
            return outlen;
        }

        if (outlen > 0) {
            /*
             * We have some data that's larger than what we need.  Copy the
             * first part of the data over to the destination buffer, but
             * save the rest for later
             */
        
            memcpy(buffer, outbuf, len);

            if (outlen - len > p->sasl.sasl_buf_size) {
                p->sasl.sasl_buffer = realloc(p->sasl.sasl_buffer, outlen - len);
                if (! p->sasl.sasl_buffer) {
                    pop_log(p, POP_PRIORITY, HERE, "Unable to allocate %d "
                            "bytes for sasl buffer", outlen - len);
                    return -1;
                }
            }

            memcpy(p->sasl.sasl_buffer, outbuf + len, outlen - len);
            p->sasl.sasl_ptr = p->sasl.sasl_buffer;
            p->sasl.sasl_cnt = outlen - len;

            return len;
        }
    }
}

/*
 * Handle encrypted writes.  This one is easier because qpopper already
 * does buffering for us.
 */

int
pop_sasl_write(POP *p, char *buffer, int len)
{
    const char *outbuf;
    unsigned int outlen;
    int result, cnt = len, n;

    if (p->sasl.sasl_ssf == 0)
        return write(fileno(p->output), buffer, len);

    /*
     * Handle the case where our output buffer is too big
     */

    while (cnt > p->sasl.sasl_maxbuf) {
        result = sasl_encode(p->sasl.sasl_conn, buffer, p->sasl.sasl_maxbuf,
                             &outbuf, &outlen);
        
        if (result != SASL_OK) {
            pop_log(p, POP_PRIORITY, HERE, "sasl_encode() failed: %s",
                    sasl_errdetail(p->sasl.sasl_conn));
            return -1;
        }

        n = write(fileno(p->output), outbuf, outlen);

        if (n != outlen) {
            if (n >= 0)
                pop_log(p, POP_PRIORITY, HERE, "Short write of SASL data "
                        "(%d != %d)", n, outlen);
            else
                pop_log(p, POP_PRIORITY, HERE, "Error when writing SASL "
                        "data: %s (%d)", STRERROR(errno), errno);
            return -1;
        }

        cnt -= p->sasl.sasl_maxbuf;
        buffer += p->sasl.sasl_maxbuf;
    }

    result = sasl_encode(p->sasl.sasl_conn, buffer, cnt, &outbuf, &outlen);

    if (result != SASL_OK) {
        pop_log(p, POP_PRIORITY, HERE, "sasl_encode() failed: %s",
                sasl_errdetail(p->sasl.sasl_conn));
        return -1;
    }

    n = write(fileno(p->output), outbuf, outlen);

    if (n != outlen) {
        if (n >= 0)
            pop_log(p, POP_PRIORITY, HERE, "Short write of SASL data "
                    "(%d != %d)", n, outlen);
        else
            pop_log(p, POP_PRIORITY, HERE, "Error when writing SASL "
                    "data: %s (%d)", STRERROR(errno), errno);
        return -1;
    }

    return len;
}

/*
 * Since currently we're not using stdio, this is just a no-op.  But leave
 * it in here if we change our mind.
 */

int
pop_sasl_flush(POP *p)
{
    (void) p;

    return 0;
}

/*
 * This function gets called during program exit, to completely clean up
 * our SASL state.
 */

void
pop_sasl_done(POP *p)
{
    if (p->sasl.sasl_buffer)
        free(p->sasl.sasl_buffer);

    if (p->sasl.sasl_mechname)
        free(p->sasl.sasl_mechname);

    if (p->sasl.sasl_authname)
        free(p->sasl.sasl_authname);

    if (p->sasl.sasl_conn)
        sasl_dispose((sasl_conn_t **) &p->sasl.sasl_conn);

    sasl_done();
}
#else /* ! CYRUS_SASL */

/*
 * We need to create stubs that error out for the sasl I/O functions,
 * and a stub pop_sasl_done() and pop_sasl_mechs()
 */

int
pop_sasl_read(POP *p, char *buffer, int len)
{
    (void) p;
    (void) buffer;
    (void) len;

    return -1;
}

int
pop_sasl_write(POP *p, char *buffer, int len)
{
    (void) p;
    (void) buffer;
    (void) len;

    return -1;
}

int pop_sasl_flush(POP *p)
{
    (void) p;

    return 0;
}

void pop_sasl_done(POP *p)
{
    (void) p;

    return;
}

void     pop_sasl_mechs(POP *p)
{
    (void) p;
    return;
}
#endif /* CYRUS_SASL */
