/*
** Copyright 1998 - 2016 S. Varshavchik.
** See COPYING for distribution information.
*/

#if	HAVE_CONFIG_H
#include	"config.h"
#endif
#include	<stdio.h>
#include	<stdlib.h>
#include	<string.h>
#include	<errno.h>
#include	<ctype.h>
#include	<fcntl.h>
#include	<time.h>
#if	HAVE_UNISTD_H
#include	<unistd.h>
#endif
#if	HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#include	<sys/socket.h>
#include	<netinet/in.h>
#include	<arpa/inet.h>
#include	<netdb.h>
#if HAVE_SYS_SELECT_H
#include	<sys/select.h>
#endif
#include	<sys/time.h>

#include	"imaptoken.h"
#include	"imapwrite.h"
#include	"proxy.h"

#include	<courierauth.h>
#include	<courierauthdebug.h>
#include	"tcpd/spipe.h"
#include	"numlib/numlib.h"
#include	"tcpd/tlsclient.h"
#include	"imapd.h"

#include <functional>

FILE *debugfile=0;
extern void initcapability();
void mainloop();
extern void imapcapability();
extern int have_starttls();
extern int tlsrequired();
int authenticate(const char *, char *, int);
unsigned long header_count=0, body_count=0;	/* Dummies */
int enabled_utf8=0;

extern unsigned long bytes_received_count; /* counter for received bytes (imaptoken.c) */
extern unsigned long bytes_sent_count; /* counter for sent bytes (imapwrite.c) */

int main_argc;
char **main_argv;
extern time_t start_time;

static const char *imapd;
static const char *defaultmaildir;

void cmdfail(const char *, const char *);
void cmdsuccess(const char *, const char *);

static int	starttls(const char *tag)
{
	char *argvec[4];

	char localfd_buf[NUMBUFSIZE+40];
	char buf2[NUMBUFSIZE];
	struct couriertls_info cinfo;
	int pipefd[2];

	if (libmail_streampipe(pipefd))
	{
		cmdfail(tag, "libmail_streampipe() failed.\r\n");
		return (-1);
	}

	couriertls_init(&cinfo);
	fcntl(pipefd[0], F_SETFD, FD_CLOEXEC);

	strcat(strcpy(localfd_buf, "-localfd="),
	       libmail_str_size_t(pipefd[1], buf2));

	char opt1[]="-tcpd";
	char opt2[]="-server";
	argvec[0]=localfd_buf;
	argvec[1]=opt1;
	argvec[2]=opt2;
	argvec[3]=NULL;

	cmdsuccess(tag, "Begin SSL/TLS negotiation now.\r\n");
	writeflush();
	cinfo.username=MAILUSER;

	if (couriertls_start(argvec, &cinfo))
	{
		close(pipefd[0]);
		close(pipefd[1]);
		cmdfail(tag, "STARTTLS failed: ");
		writes(cinfo.errmsg);
		writes("\r\n");
		couriertls_destroy(&cinfo);
		return (-1);
	}

	couriertls_export_subject_environment(&cinfo);
	couriertls_destroy(&cinfo);

	close(pipefd[1]);
	close(0);
	close(1);
	if (dup(pipefd[0]) != 0 || dup(pipefd[0]) != 1)
	{
		perror("dup");
		exit(1);
	}
	close(pipefd[0]);

	/* We use select() with a timeout, so use non-blocking filedescs */

	if (fcntl(0, F_SETFL, O_NONBLOCK) ||
	    fcntl(1, F_SETFL, O_NONBLOCK))
	{
		perror("fcntl");
		exit(1);
	}
	fflush(stdin);
	return (0);
}

struct imapproxyinfo {
	const char *tag;
	const char *uid;
	const char *pwd;
};

static int login_imap(int fd, const std::string &hostname, imapproxyinfo *ipi);

static const char *safe_getenv(const char *p)
{
	p=getenv(p);

	if (!p) p="";
	return p;
}

extern "C" int login_callback(struct authinfo *ainfo, void *dummy)
{
	int rc;
	const char *tag=(const char *)dummy;
	char *p;

	p=getenv("IMAP_PROXY");

	if (p && atoi(p))
	{
		if (ainfo->options == NULL ||
		    (p=auth_getoption(ainfo->options,
				      "mailhost")) == NULL)
		{
			fprintf(stderr, "WARN: proxy enabled, but no proxy"
				 " host defined for %s\n",
				 ainfo->address);

			/* Fallthru to account login */

		}
		else if (ainfo->clearpasswd == NULL)
		{
			free(p);
			fprintf(stderr, "WARN: proxy enabled, but no password"
				 " for %s\n", ainfo->address);
			return -1;
		}
		else
		{
			struct proxyinfo pi;
			struct imapproxyinfo ipi;
			struct servent *se;
			int fd;

			se=getservbyname("imap", NULL);

			pi.host=p;
			pi.port=se ? ntohs(se->s_port):143;

			ipi.uid=ainfo->address;
			ipi.pwd=ainfo->clearpasswd;
			ipi.tag=tag;

			pi.connected_func=
				[&]
				(int fd, const std::string &hostname)
				{
					return login_imap(fd, hostname, &ipi);
				};

			if ((fd=connect_proxy(&pi)) < 0)
			{
				free(p);
				return -1;
			}
			free(p);
			if (fd > 0)
			{
				alarm(0);

				fprintf(stderr, "INFO: LOGIN, user=%s, ip=[%s], port=[%s], protocol=%s%s\n",
					ainfo->address, safe_getenv("TCPREMOTEIP"),
					safe_getenv("TCPREMOTEPORT"),
					safe_getenv("PROTOCOL"),
					(p=getenv("IMAP_TLS")) != 0 && atoi(p) ? ", starttls=1" : "");

				proxyloop(fd);
				exit(0);
			}

			/* FALLTHRU */
		}
	}

	rc=auth_callback_default_autocreate(ainfo);

	if (rc == 0)
	{
		setenv("OPTIONS", ainfo->options ? ainfo->options:"", 1);
		setenv("IMAPLOGINTAG", tag, 1);
		setenv("AUTHENTICATED", ainfo->address, 1);
		alarm(0);
		execl(imapd, imapd,
		      ainfo->maildir ?
		      ainfo->maildir:defaultmaildir,
		      NULL);
		fprintf(stderr, "ERR: exec(%s) failed!!\n", imapd);
	}

	return(rc);
}

extern "C" int do_imap_command(const char *tag, int *flushflag)
{
	imaptoken curtoken=nexttoken();
	char authservice[40];

#if SMAP
	if (strcmp(tag, "\\SMAP1") == 0)
	{
		const char *p=getenv("SMAP_CAPABILITY");

		if (p && *p)
			setenv("PROTOCOL", "SMAP1", 1);
		else
			return -1;
	}
#endif

	courier_authdebug_login( 1, "command=%s", curtoken->tokenbuf.c_str() );

	if (curtoken->tokenbuf == "LOGOUT")
	{
		if (nexttoken()->tokentype != IT_EOL)   return (-1);
		writes("* BYE Courier-IMAP server shutting down\r\n");
		cmdsuccess(tag, "LOGOUT completed\r\n");
		writeflush();
		fprintf(stderr, "INFO: LOGOUT, ip=[%s], port=[%s], rcvd=%lu, sent=%lu\n",
			getenv("TCPREMOTEIP"), getenv("TCPREMOTEPORT"),
			bytes_received_count, bytes_sent_count);
		exit(0);
	}
	if (curtoken->tokenbuf == "NOOP")
	{
		if (nexttoken()->tokentype != IT_EOL)	return (-1);
		cmdsuccess(tag, "NOOP completed\r\n");
		return (0);
	}
	if (curtoken->tokenbuf == "CAPABILITY")
	{
		if (nexttoken()->tokentype != IT_EOL)	return (-1);

		writes("* CAPABILITY ");
		imapcapability();
		writes("\r\n");
		cmdsuccess(tag, "CAPABILITY completed\r\n");
		return (0);
	}

	if (curtoken->tokenbuf == "STARTTLS")
	{
		if (!have_starttls())	return (-1);
		if (starttls(tag))		return (-2);
		setenv("IMAP_STARTTLS", "NO", 1);
		setenv("IMAP_TLS_REQUIRED", "0", 1);
		setenv("IMAP_TLS", "1", 1);
		*flushflag=1;
		return (0);
	}

	if (curtoken->tokenbuf == "LOGIN")
	{
		imaptoken tok=nexttoken_nouc();
		std::string userid, passwd;
		const char *p;
		int	rc;

		if (have_starttls() && tlsrequired())	/* Not yet */
		{
			cmdfail(tag, "STARTTLS required\r\n");
			return (0);
		}

		switch (tok->tokentype)	{
		case IT_ATOM:
		case IT_NUMBER:
		case IT_QUOTED_STRING:
			break;
		default:
			return (-1);
		}

		userid=tok->tokenbuf;
		tok=nexttoken_nouc_okbracket();
		switch (tok->tokentype)	{
		case IT_ATOM:
		case IT_NUMBER:
		case IT_QUOTED_STRING:
			break;
		default:
			return (-1);
		}

		passwd=tok->tokenbuf;

		if (nexttoken()->tokentype != IT_EOL)
		{
			return (-1);
		}

		strcat(strcpy(authservice, "AUTHSERVICE"),
		       getenv("TCPLOCALPORT"));

		p=getenv(authservice);

		if (!p || !*p)
			p="imap";

		rc=auth_login_meta(NULL, p, userid.c_str(), passwd.c_str(),
				   login_callback, (void *)tag);
		courier_safe_printf("INFO: LOGIN FAILED, user=%s, ip=[%s], port=[%s]",
				    userid.c_str(), getenv("TCPREMOTEIP"),
				    getenv("TCPREMOTEPORT"));
		if (rc > 0)
		{
			perror("ERR: authentication error");
			writes("* BYE Temporary problem, please try again later\r\n");
			writeflush();
			exit(1);
		}
		sleep(5);
		cmdfail(tag, "Login failed.\r\n");
		return (0);
	}

	if (curtoken->tokenbuf == "AUTHENTICATE")
	{
	char	method[32];
	int	rc;

		if (have_starttls() && tlsrequired())	/* Not yet */
		{
			cmdfail(tag, "STARTTLS required\r\n");
			return (0);
		}
		rc=authenticate(tag, method, sizeof(method));
		courier_safe_printf("INFO: LOGIN FAILED, method=%s, ip=[%s], port=[%s]",
				    method, getenv("TCPREMOTEIP"),
				    getenv("TCPREMOTEPORT"));
		if (rc > 0)
		{
			perror("ERR: authentication error");
			writes("* BYE Temporary problem, please try again later\r\n");
			writeflush();
			exit(1);
		}
		sleep(5);
		cmdfail(tag, "Login failed.\r\n");
		writeflush();
		return (-2);
	}

	return (-1);
}

extern void ignorepunct();

int main(int argc, char **argv)
{
	const char	*ip;
	const char	*port;

#ifdef HAVE_SETVBUF_IOLBF
	setvbuf(stderr, NULL, _IOLBF, BUFSIZ);
#endif

	if (argc != 3)
	{
		printf("* BYE imaplogin expected exactly two arguments.\r\n");
		fflush(stdout);
		exit(1);
	}

	alarm(60);
	imapd=argv[1];
	defaultmaildir=argv[2];
	initcapability();

	ip=getenv("TCPREMOTEIP");
	if (!ip)
		setenv("TCPREMOTEIP", "127.0.0.1", 1);
	ip=getenv("TCPREMOTEIP");

	port=getenv("TCPREMOTEPORT");
	if (!port)
		setenv("TCPREMOTEPORT", "N/A", 1);
	port=getenv("TCPREMOTEPORT");

	if (!getenv("TCPLOCALPORT"))
		setenv("TCPLOCALPORT", "143", 1);

	time(&start_time);

#if	IMAP_CLIENT_BUGS

	ignorepunct();

#endif

	courier_authdebug_login_init();

	/* We use select() with a timeout, so use non-blocking filedescs */

	if (fcntl(0, F_SETFL, O_NONBLOCK) ||
	    fcntl(1, F_SETFL, O_NONBLOCK))
	{
		perror("fcntl");
		exit(1);
	}

	writes("* OK [CAPABILITY ");
	imapcapability();
	writes("] Courier-IMAP ready. "
	       "Copyright 1998-2022 S. Varshavchik.  "
	       "See COPYING for distribution information.\r\n");
	fprintf(stderr, "DEBUG: Connection, ip=[%s], port=[%s]\n", ip, port);
	writeflush();
	main_argc=argc;
	main_argv=argv;

	setenv("PROTOCOL", "IMAP", 1);

	mainloop();
	return (0);
}

void bye()
{
	exit(0);
}

static void imap_write_str(const char *c,
			   const std::function<void (const char *, size_t)> &cb)
{
	if (c == NULL)
	{
		cb("NIL", 3);
	}

	cb("\"", 1);

	while (*c)
	{
		size_t n;

		for (n=0; c[n]; n++)
			if (c[n] == '"' || c[n] == '\\')
				break;

		if (n)
		{
			cb(c, n);

			c += n;
		}

		if (*c)
		{
			cb("\\", 1);
			cb(c, 1);
			++c;
		}
	}
	cb("\"", 1);
}

static void imap_login_cmd(struct imapproxyinfo *ipi,
			   const std::function<void (const char *, size_t)> &cb)
{
	cb(ipi->tag, strlen(ipi->tag));
	cb(" LOGIN ", 7);
	imap_write_str(ipi->uid, cb);
	cb(" ", 1);
	imap_write_str(ipi->pwd, cb);
	cb("\r\n", 2);
}

static void imap_capability_cmd(
	struct imapproxyinfo *ipi,
	const std::function<void (const char *, size_t)> &cb)
{
	cb(ipi->tag, strlen(ipi->tag));
	cb(" CAPABILITY\r\n", 13);
}

static std::vector<char> get_imap_cmd(
	struct imapproxyinfo *ipi,
	void (*cmd)(struct imapproxyinfo *ipi,
		    const std::function<void (const char *, size_t)> &cb
	))
{
	std::vector<char> buf;

	buf.reserve(100);

	cmd(ipi,
	    [&]
	    (const char *p, size_t l)
	    {
		    buf.insert(buf.end(), p, p+l);
	    });

	return buf;
}

static int login_imap(int fd, const std::string &hostname, imapproxyinfo *ipi)
{
	struct proxybuf pb;
	char linebuf[256];
	const char *p;

	DPRINTF("Proxy connected to %s", hostname.c_str());

	if (proxy_readline(fd, &pb, linebuf, sizeof(linebuf), 1) < 0)
		return -1;

	DPRINTF("%s: %s", hostname.c_str(), linebuf);

	if ((p=strtok(linebuf, " \t")) == NULL ||
	    strcmp(p, "*") ||
	    (p=strtok(NULL, " \t")) == NULL ||
	    strcasecmp(p, "OK"))
	{
		fprintf(stderr, "WARN: Did not receive greeting from %s\n",
			hostname.c_str());
		return -1;
	}

	auto cmd=get_imap_cmd(ipi, imap_login_cmd);

	if (proxy_write(fd, hostname.c_str(), cmd.data(), cmd.size()))
	{
		return -1;
	}

#if SMAP
	if (strcmp(ipi->tag, "\\SMAP1") == 0)
	{
		do
		{
			if (proxy_readline(fd, &pb, linebuf, sizeof(linebuf),
					   0) < 0)
				return -1;

			DPRINTF("%s: %s", hostname.c_str(), linebuf);

		} while (linebuf[0] != '+' && linebuf[0] != '-');


		if (linebuf[0] != '+')
		{
			fprintf(stderr, "WARN: Login to %s failed\n",
				hostname.c_str());
			return -1;
		}

		if (fcntl(1, F_SETFL, 0) < 0 ||
		    (printf("+OK connected to proxy server.\r\n"),
		     fflush(stdout)) < 0)
			return -1;
		return (0);
	}
#endif


	for (;;)
	{
		if (proxy_readline(fd, &pb, linebuf, sizeof(linebuf), 1) < 0)
			return -1;

		DPRINTF("%s: %s", hostname.c_str(), linebuf);

		if ((p=strtok(linebuf, " \t")) == NULL ||
		    strcmp(p, ipi->tag) ||
		    (p=strtok(NULL, " \t")) == NULL)
			continue;

		if (strcasecmp(p, "OK"))
		{
			fprintf(stderr, "WARN: Login to %s failed\n",
				hostname.c_str());
			return -1;
		}
		break;
	}

	p=getenv("IMAP_PROXY_FOREIGN");

	if (p && atoi(p))
	{
		cmd=get_imap_cmd(ipi, imap_capability_cmd);

		if (proxy_write(fd, hostname, cmd.data(), cmd.size()))
		{
			return -1;
		}
	}
	else
	{
		if (fcntl(1, F_SETFL, 0) < 0 ||
		    (printf("%s OK connected to proxy server.\r\n",
			    ipi->tag), fflush(stdout)) < 0)
			return -1;
	}

	return 0;
}
