/* popsbr.c -- POP client subroutines
 *
 * This code is Copyright (c) 2002, by the authors of nmh.  See the
 * COPYRIGHT file in the root directory of the nmh distribution for
 * complete copyright information.
 */

#include <h/mh.h>
#include <h/utils.h>
#include <h/oauth.h>
#include <h/netsec.h>

#include <h/popsbr.h>
#include <h/signals.h>

#define	TRM	"."

static int poprint = 0;

char response[BUFSIZ];
static netsec_context *nsc = NULL;

/*
 * static prototypes
 */

static int command(const char *, ...);
static int multiline(void);

static int traverse (int (*)(char *), const char *, ...);
static int vcommand(const char *, va_list);
static int pop_getline (char *, int, netsec_context *);
static int pop_sasl_callback(enum sasl_message_type, unsigned const char *,
			     unsigned int, unsigned char **, unsigned int *,
			     char **);

static int
check_mech(char *server_mechs, size_t server_mechs_size)
{
  int status, sasl_capability = 0;

    /*
     * First off, we're going to send the CAPA command to see if we can
     * even support the AUTH command, and if we do, then we'll get a
     * list of mechanisms the server supports.  If we don't support
     * the CAPA command, then it's unlikely that we will support
     * SASL
     */

    if (command("CAPA") == NOTOK) {
	snprintf(response, sizeof(response),
		 "The POP CAPA command failed; POP server does not "
		 "support SASL");
	return NOTOK;
    }

    while ((status = multiline()) != DONE)
	switch (status) {
	case NOTOK:
	    return NOTOK;
	    break;
	case DONE:	/* Shouldn't be possible, but just in case */
	    break;
	case OK:
	    if (strncasecmp(response, "SASL ", 5) == 0) {
		/*
		 * We've seen the SASL capability.  Grab the mech list
		 */
		sasl_capability++;
		strncpy(server_mechs, response + 5, server_mechs_size);
	    }
	    break;
	}

    if (!sasl_capability) {
	snprintf(response, sizeof(response), "POP server does not support "
		 "SASL");
	return NOTOK;
    }

    return OK;
}

/*
 * Split string containing proxy command into an array of arguments
 * suitable for passing to exec. Returned array must be freed. Shouldn't
 * be possible to call this with host set to NULL.
 */
char **
parse_proxy(char *proxy, char *host)
{
    char **pargv, **p;
    int pargc = 2;
    int hlen = strlen(host);
    int plen = 1;
    unsigned char *cur, *pro;
    char *c;
    
    /* skip any initial space */
    for (pro = (unsigned char *) proxy; isspace(*pro); pro++)
        continue;
    
    /* calculate required size for argument array */
    for (cur = pro; *cur; cur++) {
        if (isspace(*cur) && cur[1] && !isspace(cur[1]))
	    plen++, pargc++;
	else if (*cur == '%' && cur[1] == 'h') {
	    plen += hlen;
            cur++;
	} else if (!isspace(*cur))
	    plen++;
    }

   /* put together list of arguments */
    p = pargv = mh_xmalloc(pargc * sizeof(char *));
    c = *pargv = mh_xmalloc(plen * sizeof(char));
    for (cur = pro; *cur; cur++) {
        if (isspace(*cur) && cur[1] && !isspace(cur[1])) {
	    *c++ = '\0';
	    *++p = c;
	} else if (*cur == '%' && cur[1] == 'h') {
	    strcpy (c, host);
	    c += hlen;
	    cur++;
	} else if (!isspace(*cur))
	    *c++ = *cur;
    }
    *c = '\0';
    *++p = NULL;
    return pargv;
}

int
pop_init (char *host, char *port, char *user, char *proxy, int snoop,
	  int sasl, char *mech, int tls, const char *oauth_svc)
{
    int fd1, fd2;
    char buffer[BUFSIZ];
    char *errstr;

    nsc = netsec_init();  

    if (user)
	netsec_set_userid(nsc, user);

    netsec_set_hostname(nsc, host);

    if (oauth_svc != NULL) {
	if (netsec_set_oauth_service(nsc, oauth_svc) != OK) {
	    snprintf(response, sizeof(response), "OAuth2 not supported");
	    return NOTOK;
	}
    }

    if (proxy && *proxy) {
       int pid;
       int inpipe[2];	  /* for reading from the server */
       int outpipe[2];    /* for sending to the server */

       if (pipe(inpipe) < 0) {
	   adios ("inpipe", "pipe");
       }
       if (pipe(outpipe) < 0) {
	   adios ("outpipe", "pipe");
       }

       pid=fork();
       if (pid==0) {
	   char **argv;
	   
	   /* in child */
	   close(0);  
	   close(1);
	   dup2(outpipe[0],0);  /* connect read end of connection */
	   dup2(inpipe[1], 1);  /* connect write end of connection */
	   if(inpipe[0]>1) close(inpipe[0]);
	   if(inpipe[1]>1) close(inpipe[1]);
	   if(outpipe[0]>1) close(outpipe[0]);
	   if(outpipe[1]>1) close(outpipe[1]);

	   /* run the proxy command */
	   argv=parse_proxy(proxy, host);
	   execvp(argv[0],argv);

	   perror(argv[0]);
	   close(0);
	   close(1);
	   free(*argv);
	   free(argv);
	   exit(10);
       }

       /* okay in the parent we do some stuff */
       close(inpipe[1]);  /* child uses this */
       close(outpipe[0]); /* child uses this */

       /* we read on fd1 */
       fd1=inpipe[0];
       /* and write on fd2 */
       fd2=outpipe[1];

    } else {
	if ((fd1 = client (host, port ? port : "pop3", response,
			   sizeof(response), snoop)) == NOTOK) {
	    return NOTOK;
	}
	fd2 = fd1;
    }

    SIGNAL (SIGPIPE, SIG_IGN);

    netsec_set_fd(nsc, fd1, fd2);
    netsec_set_snoop(nsc, snoop);

    if (tls & P_INITTLS) {
	if (netsec_set_tls(nsc, 1, tls & P_NOVERIFY, &errstr) != OK) {
	    snprintf(response, sizeof(response), "%s", errstr);
	    free(errstr);
	    return NOTOK;
	}

	if (netsec_negotiate_tls(nsc, &errstr) != OK) {
	    snprintf(response, sizeof(response), "%s", errstr);
	    free(errstr);
	    return NOTOK;
	}
    }

    if (sasl) {
	if (netsec_set_sasl_params(nsc, "pop", mech, pop_sasl_callback,
				   &errstr) != OK) {
	    snprintf(response, sizeof(response), "%s", errstr);
	    free(errstr);
	    return NOTOK;
	}
    }

    switch (pop_getline (response, sizeof response, nsc)) {
	case OK: 
	    if (poprint)
		fprintf (stderr, "<--- %s\n", response);
	    if (*response == '+') {
                nmh_creds_t creds;

		if (sasl) {
		    char server_mechs[256];
		    if (check_mech(server_mechs, sizeof(server_mechs)) != OK)
			return NOTOK;
		    if (netsec_negotiate_sasl(nsc, server_mechs,
		    			      &errstr) != OK) {
			strncpy(response, errstr, sizeof(response));
			response[sizeof(response) - 1] = '\0';
			free(errstr);
			return NOTOK;
		    }
		    return OK;
		}

                if (!(creds = nmh_get_credentials(host, user)))
                    return NOTOK;
                if (command ("USER %s", nmh_cred_get_user(creds))
                                                            != NOTOK) {
                    if (command("PASS %s", nmh_cred_get_password(creds))
                                                            != NOTOK) {
                        nmh_credentials_free(creds);
                        return OK;
                    }
                }
                nmh_credentials_free(creds);
	    }
	    strncpy (buffer, response, sizeof(buffer));
	    command ("QUIT");
	    strncpy (response, buffer, sizeof(response));
	    /* FALLTHRU */

	case NOTOK: 
	case DONE: 
	    if (poprint)	    
		fprintf (stderr, "%s\n", response);
	    netsec_shutdown(nsc);
	    nsc = NULL;
	    return NOTOK;
    }

    return NOTOK;	/* NOTREACHED */
}


/*
 * Our SASL callback; we are given SASL tokens and then have to format
 * them according to the protocol requirements, and then process incoming
 * messages and feed them back into the SASL library.
 */

static int
pop_sasl_callback(enum sasl_message_type mtype, unsigned const char *indata,
		  unsigned int indatalen, unsigned char **outdata,
		  unsigned int *outdatalen, char **errstr)
{
    int rc, snoopoffset;
    char *mech, *line;
    size_t len, b64len;

    switch (mtype) {
    case NETSEC_SASL_START:
	/*
	 * Generate our AUTH message, but there is a wrinkle.
	 *
	 * Technically, according to RFC 5034, if your command INCLUDING
	 * an initial response exceeds 255 octets (including CRLF), you
	 * can't issue this all in one go, but have to just issue the
	 * AUTH command, wait for a blank initial response, and then
	 * send your data.
	 */

	mech = netsec_get_sasl_mechanism(nsc);

	if (indatalen) {
	    char *b64data;
	    b64data = mh_xmalloc(BASE64SIZE(indatalen));
	    writeBase64raw(indata, indatalen, (unsigned char *) b64data);
	    b64len = strlen(b64data);

	    /* Formula here is AUTH + SP + mech + SP + out + CR + LF */
	    len = b64len + 8 + strlen(mech);
	    if (len > 255) {
		rc = netsec_printf(nsc, errstr, "AUTH %s\r\n", mech);
		if (rc)
		    return NOTOK;
		if (netsec_flush(nsc, errstr) != OK)
		    return NOTOK;
		line = netsec_readline(nsc, &len, errstr);
		if (! line)
		    return NOTOK;
		/*
		 * If the protocol is being followed correctly, should just
		 * be a "+ ", nothing else.
		 */
		if (len != 2 || strcmp(line, "+ ") != 0) {
		    netsec_err(errstr, "Did not get expected blank response "
			       "for initial challenge response");
		    return NOTOK;
		}
		netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
		rc = netsec_printf(nsc, errstr, "%s\r\n", b64data);
		netsec_set_snoop_callback(nsc, NULL, NULL);
		free(b64data);
		if (rc != OK)
		    return NOTOK;
		if (netsec_flush(nsc, errstr) != OK)
		    return NOTOK;
	    } else {
		netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder,
					  &snoopoffset);
		snoopoffset = 6 + strlen(mech);
	        rc = netsec_printf(nsc, errstr, "AUTH %s %s\r\n", mech,
				   b64data);
		free(b64data);
		netsec_set_snoop_callback(nsc, NULL, NULL);
		if (rc != OK)
		    return NOTOK;
		if (netsec_flush(nsc, errstr) != OK)
		    return NOTOK;
	    }
	} else {
	    if (netsec_printf(nsc, errstr, "AUTH %s\r\n", mech) != OK)
		return NOTOK;
	    if (netsec_flush(nsc, errstr) != OK)
		return NOTOK;
	}

	break;

	/*
	 * We should get one line back, with our base64 data.  Decode that
	 * and feed it back into the SASL library.
	 */
    case NETSEC_SASL_READ:
	netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, &snoopoffset);
	snoopoffset = 2;
	line = netsec_readline(nsc, &len, errstr);
	netsec_set_snoop_callback(nsc, NULL, NULL);

	if (line == NULL)
	    return NOTOK;
	if (len < 2 || (len == 2 && strcmp(line, "+ ") != 0)) {
	    netsec_err(errstr, "Invalid format for SASL response");
	    return NOTOK;
	}

	if (len == 2) {
	    *outdata = NULL;
	    *outdatalen = 0;
	} else {
	    rc = decodeBase64(line + 2, outdata, &len, 0, NULL);
	    *outdatalen = len;
	    if (rc != OK) {
		netsec_err(errstr, "Unable to decode base64 response");
		return NOTOK;
	    }
	}
	break;

    /*
     * Our encoding is pretty simple, so this is easy.
     */

    case NETSEC_SASL_WRITE:
	if (indatalen == 0) {
	    rc = netsec_printf(nsc, errstr, "\r\n");
	} else {
	    unsigned char *b64data;
	    b64data = mh_xmalloc(BASE64SIZE(indatalen));
	    writeBase64raw(indata, indatalen, b64data);
	    netsec_set_snoop_callback(nsc, netsec_b64_snoop_decoder, NULL);
	    rc = netsec_printf(nsc, errstr, "%s\r\n", b64data);
	    netsec_set_snoop_callback(nsc, NULL, NULL);
	    free(b64data);
	}

	if (rc != OK)
	    return NOTOK;

	if (netsec_flush(nsc, errstr) != OK)
	    return NOTOK;
	break;

    /*
     * Finish the protocol; we're looking for an +OK
     */

    case NETSEC_SASL_FINISH:
	line = netsec_readline(nsc, &len, errstr);
	if (line == NULL)
	    return NOTOK;

	if (!has_prefix(line, "+OK")) {
	    netsec_err(errstr, "Authentication failed: %s", line);
	    return NOTOK;
	}
	break;

    /*
     * Cancel the SASL exchange in the middle of the commands; for
     * POP, that's a single "*".
     *
     * It's unclear to me if I should be returning errors up; I finally
     * decided the answer should be "yes", and if the upper layer wants to
     * ignore them that's their choice.
     */

    case NETSEC_SASL_CANCEL:
	rc = netsec_printf(nsc, errstr, "*\r\n");
	if (rc == OK)
	    rc = netsec_flush(nsc, errstr);
	if (rc != OK)
	    return NOTOK;
	break;
    }

    return OK;
}

/*
 * Find out number of messages available
 * and their total size.
 */

int
pop_stat (int *nmsgs, int *nbytes)
{

    if (command ("STAT") == NOTOK)
	return NOTOK;

    *nmsgs = *nbytes = 0;
    sscanf (response, "+OK %d %d", nmsgs, nbytes);

    return OK;
}


int
pop_retr (int msgno, int (*action)(char *))
{
    return traverse (action, "RETR %d", msgno);
}


static int
traverse (int (*action)(char *), const char *fmt, ...)
{
    int result, snoopstate;
    va_list ap;
    char buffer[sizeof(response)];

    va_start(ap, fmt);
    result = vcommand (fmt, ap);
    va_end(ap);

    if (result == NOTOK)
	return NOTOK;
    strncpy (buffer, response, sizeof(buffer));

    if ((snoopstate = netsec_get_snoop(nsc)))
	netsec_set_snoop(nsc, 0);

    for (;;)
	switch (multiline ()) {
	    case NOTOK: 
		netsec_set_snoop(nsc, snoopstate);
		return NOTOK;

	    case DONE: 
		strncpy (response, buffer, sizeof(response));
		netsec_set_snoop(nsc, snoopstate);
		return OK;

	    case OK: 
		(*action) (response);
		break;
	}
}


int
pop_dele (int msgno)
{
    return command ("DELE %d", msgno);
}


int
pop_quit (void)
{
    int i;

    i = command ("QUIT");
    pop_done ();

    return i;
}


int
pop_done (void)
{
    if (nsc)
	netsec_shutdown(nsc);

    return OK;
}


int
command(const char *fmt, ...)
{
    va_list ap;
    int result;

    va_start(ap, fmt);
    result = vcommand(fmt, ap);
    va_end(ap);

    return result;
}


static int
vcommand (const char *fmt, va_list ap)
{
    /* char *cp; */
    char *errstr;

    if (netsec_vprintf(nsc, &errstr, fmt, ap) != OK) {
	strncpy(response, errstr, sizeof(response));
	response[sizeof(response) - 1] = '\0';
	free(errstr);
	return NOTOK;
    }

    if (netsec_printf(nsc, &errstr, "\r\n") != OK) {
	strncpy(response, errstr, sizeof(response));
	response[sizeof(response) - 1] = '\0';
	free(errstr);
	return NOTOK;
    }

    if (netsec_flush(nsc, &errstr) != OK) {
	strncpy(response, errstr, sizeof(response));
	response[sizeof(response) - 1] = '\0';
	free(errstr);
	return NOTOK;
    }

    switch (pop_getline (response, sizeof response, nsc)) {
	case OK: 
	    if (poprint)
		fprintf (stderr, "<--- %s\n", response);
	    return (*response == '+' ? OK : NOTOK);

	case NOTOK: 
	case DONE: 
	    if (poprint)	    
		fprintf (stderr, "%s\n", response);
	    return NOTOK;
    }

    return NOTOK;	/* NOTREACHED */
}


int
multiline (void)
{
    char buffer[BUFSIZ + LEN(TRM)];

    if (pop_getline (buffer, sizeof buffer, nsc) != OK)
	return NOTOK;
    if (has_prefix(buffer, TRM)) {
	if (buffer[LEN(TRM)] == 0)
	    return DONE;
        strncpy (response, buffer + LEN(TRM), sizeof(response));
    }
    else
	strncpy (response, buffer, sizeof(response));

    return OK;
}

/*
 * This is now just a thin wrapper around netsec_readline().
 */

static int
pop_getline (char *s, int n, netsec_context *ns)
{
    /* int c = -2; */
    char *p;
    size_t len, destlen;
    /* int rc; */
    char *errstr;

    p = netsec_readline(ns, &len, &errstr);

    if (p == NULL) {
	strncpy(response, errstr, sizeof(response));
	response[sizeof(response) - 1] = '\0';
	free(errstr);
	return NOTOK;
    }

    /*
     * If we had an error, it should have been returned already.  Since
     * netsec_readline() strips off the CR-LF ending, just copy the existing
     * buffer into response now.
     *
     * We get a length back from netsec_readline, but the rest of the POP
     * code doesn't handle it; the assumptions are that everything from
     * the network can be represented as C strings.  That should get fixed
     * someday.
     */

    destlen = min(len, (size_t)(n - 1));

    memcpy(s, p, destlen);
    s[destlen] = '\0';
    
    return OK;
}
