/* $Id: Pop3Client.c,v 1.23 2004/12/12 00:01:53 bluehal Exp $ */
/* Author : Scott Holden ( scotth@thezone.net )
   Modified : Yong-iL Joh ( tolkien@mizi.com )
   Modified : Jorge García ( Jorge.Garcia@uv.es )
   Modified ; Mark Hurley ( debian4tux@telocity.com )
   Modified : Neil Spring ( nspring@cs.washington.edu )
 *
 * Pop3 Email checker.
 *
 * Last Updated : Tue Nov 13 13:45:23 PST 2001
 *
 */
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "Client.h"
#include "charutil.h"
#include "regulo.h"
#include "MessageList.h"
#include <strings.h>
#include "tlsComm.h"
#include "passwordMgr.h"

#ifdef USE_DMALLOC
#include <dmalloc.h>
#endif

extern int Relax;
/* temp */
static void ask_user_for_password( /*@notnull@ */ Pop3 pc, int bFlushCache);

#define	PCU	(pc->u).pop_imap
#define POP_DM(pc, lvl, args...) DM(pc, lvl, "pop3: " args)

#ifdef HAVE_GCRYPT_H
static struct connection_state *authenticate_md5( /*@notnull@ */ Pop3 pc, struct connection_state * scs,
							  char *unused);
static struct connection_state *authenticate_apop( /*@notnull@ */ Pop3 pc, struct connection_state * scs,
							   char *apop_str);
#endif
static struct connection_state *authenticate_plaintext( /*@notnull@ */ Pop3 pc, struct connection_state * scs,
									char *unused);

void pop3_cacheHeaders( /*@notnull@ */ Pop3 pc);

extern void imap_releaseHeaders(Pop3 pc
								__attribute__ ((unused)),
								struct msglst *h);

extern struct connection_state *state_for_pcu(Pop3 pc);

static struct authentication_method {
	const char *name;
	/* callback returns the connection state pointer if successful,
	   NULL if failed */
	struct connection_state  *(*auth_callback) (Pop3 pc, struct connection_state * scs, char *apop_str);
} auth_methods[] = {
	{
#ifdef HAVE_GCRYPT_H
	"cram-md5", authenticate_md5}, {
	"apop", authenticate_apop}, {
#endif
	"plaintext", authenticate_plaintext}, {
	NULL, NULL}
};

/*@null@*/
struct connection_state *pop3Login(Pop3 pc)
{
	int fd;
	char buf[BUF_SIZE];
	char apop_str[BUF_SIZE];
	char *ptr1, *ptr2;
	struct authentication_method *a;
	struct connection_state *scs;
	char *connection_name;


	apop_str[0] = '\0';			/* if defined, server supports apop */

	if ((fd = sock_connect(PCU.serverName, PCU.serverPort)) == -1) {
		POP_DM(pc, DEBUG_ERROR, "Not Connected To Server '%s:%d'\n",
			   PCU.serverName, PCU.serverPort);
		return NULL;
	}

	connection_name = malloc(strlen(PCU.serverName) + 20);
	sprintf(connection_name, "%s:%d", PCU.serverName, PCU.serverPort);

	if (PCU.dossl != 0) {
		scs = initialize_gnutls(fd, connection_name, pc, PCU.serverName);
		if (scs == NULL) {
			POP_DM(pc, DEBUG_ERROR, "Failed to initialize TLS\n");
			return NULL;
		}
	} else {
		scs = initialize_unencrypted(fd, connection_name, pc);
	}

    tlscomm_gets(buf, BUF_SIZE, scs);
	POP_DM(pc, DEBUG_INFO, "%s", buf);

	/* Detect APOP, copy challenge into apop_str */
	for (ptr1 = buf + strlen(buf), ptr2 = NULL; ptr1 > buf; --ptr1) {
		if (*ptr1 == '>') {
			ptr2 = ptr1;
		} else if (*ptr1 == '<') {
			if (ptr2) {
				*(ptr2 + 1) = 0;
				strncpy(apop_str, ptr1, BUF_SIZE);
			}
			break;
		}
	}


	/* try each authentication method in turn. */
	for (a = auth_methods; a->name != NULL; a++) {
		/* was it specified or did the user leave it up to us? */
		if (PCU.authList[0] == '\0' || strstr(PCU.authList, a->name))
			/* did it work? */
			if ((a->auth_callback(pc, scs, apop_str)) != NULL)
				return (scs);
	}

	/* if authentication worked, we won't get here */
	POP_DM(pc, DEBUG_ERROR,
		   "All Pop3 authentication methods failed for '%s@%s:%d'\n",
		   PCU.userName, PCU.serverName, PCU.serverPort);
    tlscomm_printf(scs, "QUIT\r\n");
    tlscomm_close(scs);

	return NULL;
}

int pop3CheckMail( /*@notnull@ */ Pop3 pc)
{
	struct connection_state *scs;
	int read;
	char buf[BUF_SIZE];

	scs = pop3Login(pc);
	if (scs == NULL)
		return -1;

	tlscomm_printf(scs, "STAT\r\n");
	if( ! tlscomm_expect(scs, "+", buf, BUF_SIZE) ) {
		POP_DM(pc, DEBUG_ERROR,
			   "Error Receiving Stats '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		POP_DM(pc, DEBUG_INFO, "It said: %s\n", buf);
		return -1;
	} else {
		sscanf(buf, "+OK %d", &(pc->TotalMsgs));
	}

	/*  - Updated - Mark Hurley - debian4tux@telocity.com
	 *  In compliance with RFC 1725
	 *  which removed the LAST command, any servers
	 *  which follow this spec will return:
	 *      -ERR unimplimented
	 *  We will leave it here for those servers which haven't
	 *  caught up with the spec.
	 */
    tlscomm_printf(scs, "LAST\r\n");
	tlscomm_gets(buf, BUF_SIZE, scs);
	if (buf[0] != '+') {
		/* it is not an error to receive this according to RFC 1725 */
		/* no error should be returned */
		pc->UnreadMsgs = pc->TotalMsgs;
        // there's also a LIST command... not sure how to make use of it. */
	} else {
		sscanf(buf, "+OK %d", &read);
		pc->UnreadMsgs = pc->TotalMsgs - read;
	}

	tlscomm_printf(scs, "QUIT\r\n");
	tlscomm_close(scs);

	return 0;
}


struct msglst *pop_getHeaders( /*@notnull@ */ Pop3 pc)
{
	if (pc->headerCache == NULL)
		pop3_cacheHeaders(pc);
	if (pc->headerCache != NULL)
		pc->headerCache->in_use = 1;
	return pc->headerCache;
}



int pop3Create(Pop3 pc, const char *str)
{
	/* POP3 format: pop3:user:password@server[:port] */
	/* new POP3 format: pop3:user password server [port] */
	/* If 'str' line is badly formatted, wmbiff won't display the mailbox. */
	int i;
	int matchedchars;
	/* ([^: ]+) user
	   ([^@]+) or ([^ ]+) password
	   ([^: ]+) server
	   ([: ][0-9]+)? optional port
	   ' *' gobbles trailing whitespace before authentication types.
	   use separate regexes for old and new types to permit
	   use of '@' in passwords
	 */
	const char *regexes[] = {
		"pop3s?:([^: ]{1,32}):([^@]{0,32})@([A-Za-z1-9][-A-Za-z0-9_.]+)(:[0-9]+)?(  *([CcAaPp][-A-Za-z5 ]*))?$",
		"pop3s?:([^: ]{1,32}) ([^ ]{1,32}) ([A-Za-z1-9][-A-Za-z0-9_.]+)( [0-9]+)?(  *([CcAaPp][-A-Za-z5 ]*))?$",
		//      "pop3:([^: ]{1,32}) ([^ ]{1,32}) ([^: ]+)( [0-9]+)? *",
		// "pop3:([^: ]{1,32}):([^@]{0,32})@([^: ]+)(:[0-9]+)? *",
		NULL
	};
	struct regulo regulos[] = {
		{1, PCU.userName, regulo_strcpy},
		{2, PCU.password, regulo_strcpy},
		{3, PCU.serverName, regulo_strcpy},
		{4, &PCU.serverPort, regulo_atoi},
		{6, PCU.authList, regulo_strcpy_tolower},
		{0, NULL, NULL}
	};

	if (Relax) {
		regexes[0] =
			"pop3:([^: ]{1,32}):([^@]{0,32})@([^/: ]+)(:[0-9]+)?(  *(.*))?$";
		regexes[1] =
			"pop3:([^: ]{1,32}) ([^ ]{1,32}) ([^/: ]+)( [0-9]+)?(  *(.*))?$";
	}

	if (strncmp("pop3s:", str, 6) == 0) {
#ifdef HAVE_GNUTLS_GNUTLS_H
		PCU.dossl = 1;
#else
		printf("This copy of wmbiff was not compiled with gnutls;\n"
			   "imaps is unavailable.  Exiting to protect your\n"
			   "passwords and privacy.\n");
		exit(EXIT_FAILURE);
#endif
	} else {
		PCU.dossl = 0;
	}


	/* defaults */
	PCU.serverPort = (PCU.dossl != 0) ? 995 : 110;
	PCU.authList[0] = '\0';

	for (matchedchars = 0, i = 0;
		 regexes[i] != NULL && matchedchars <= 0; i++) {
		matchedchars = regulo_match(regexes[i], str, regulos);
	}

	/* failed to match either regex */
	if (matchedchars <= 0) {
		pc->label[0] = '\0';
		POP_DM(pc, DEBUG_ERROR, "Couldn't parse line %s (%d)\n"
			   "  If this used to work, run wmbiff with the -relax option, and\n "
			   "  send mail to "PACKAGE_BUGREPORT" with the hostname\n"
			   "  of your mail server.\n", str, matchedchars);
		return -1;
	}
	// grab_authList(str + matchedchars, PCU.authList);

	PCU.password_len = strlen(PCU.password);
	if (PCU.password[0] == '\0') {
		PCU.interactive_password = 1;
	} else {
      // ENFROB(PCU.password);
	}

	POP_DM(pc, DEBUG_INFO, "userName= '%s'\n", PCU.userName);
	POP_DM(pc, DEBUG_INFO, "password is %ld chars long\n",
		   strlen(PCU.password));
	POP_DM(pc, DEBUG_INFO, "serverName= '%s'\n", PCU.serverName);
	POP_DM(pc, DEBUG_INFO, "serverPort= '%d'\n", PCU.serverPort);
	POP_DM(pc, DEBUG_INFO, "authList= '%s'\n", PCU.authList);

	pc->checkMail = pop3CheckMail;
	pc->getHeaders = pop_getHeaders;
	pc->TotalMsgs = 0;
	pc->UnreadMsgs = 0;
	pc->OldMsgs = -1;
	pc->OldUnreadMsgs = -1;

	return 0;
}


#ifdef HAVE_GCRYPT_H
static struct connection_state *authenticate_md5(Pop3 pc, struct connection_state * scs, char *apop_str
							  __attribute__ ((unused)))
{
	char buf[BUF_SIZE];
	char buf2[BUF_SIZE];
	unsigned char *md5;
	gcry_md_hd_t gmh;
	gcry_error_t rc;

	/* See if MD5 is supported */
	tlscomm_printf(scs, "AUTH CRAM-MD5\r\n");
	tlscomm_gets(buf, BUF_SIZE, scs);
	POP_DM(pc, DEBUG_INFO, "%s", buf);

	if (buf[0] != '+' || buf[1] != ' ') {
		/* nope, not supported. */
		return NULL;
	}

	Decode_Base64(buf + 2, buf2);
	POP_DM(pc, DEBUG_INFO, "CRAM-MD5 challenge: %s\n", buf2);

	strcpy(buf, PCU.userName);
	strcat(buf, " ");


	rc = gcry_md_open(&gmh, GCRY_MD_MD5, GCRY_MD_FLAG_HMAC);
	if (rc != 0) {
		POP_DM(pc, DEBUG_ERROR, "unable to initialize gcrypt md5.\n");
		return NULL;
	}
	gcry_md_setkey(gmh, PCU.password, strlen(PCU.password));
	gcry_md_write(gmh, (unsigned char *) buf2, strlen(buf2));
	gcry_md_final(gmh);
	md5 = gcry_md_read(gmh, 0);
	/* hmac_md5(buf2, strlen(buf2), PCU.password,
	   strlen(PCU.password), md5); */
	Bin2Hex(md5, 16, buf2);
	gcry_md_close(gmh);

	strcat(buf, buf2);
	POP_DM(pc, DEBUG_INFO, "CRAM-MD5 response: %s\n", buf);
	Encode_Base64(buf, buf2);

	tlscomm_printf(scs, "%s\r\n", buf2);
	tlscomm_gets(buf, BUF_SIZE, scs);

	if (!strncmp(buf, "+OK", 3))
		return scs;				/* AUTH successful */
	else {
		POP_DM(pc, DEBUG_ERROR,
			   "CRAM-MD5 AUTH failed for user '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		fprintf(stderr, "It said %s", buf);
		return NULL;
	}
}

static struct connection_state *authenticate_apop(Pop3 pc, struct connection_state * scs, char *apop_str)
{
	gcry_md_hd_t gmh;
	gcry_error_t rc;
	char buf[BUF_SIZE];
	unsigned char *md5;


	if (apop_str[0] == '\0') {
		/* server doesn't support apop. */
		return (NULL);
	}
	POP_DM(pc, DEBUG_INFO, "APOP challenge: %s\n", apop_str);
	strcat(apop_str, PCU.password);

	rc = gcry_md_open(&gmh, GCRY_MD_MD5, 0);
	if (rc != 0) {
		POP_DM(pc, DEBUG_ERROR, "unable to initialize gcrypt md5.\n");
		return NULL;
	}
	gcry_md_write(gmh, (unsigned char *) apop_str, strlen(apop_str));
	gcry_md_final(gmh);
	md5 = gcry_md_read(gmh, 0);
	Bin2Hex(md5, 16, buf);
	gcry_md_close(gmh);

	POP_DM(pc, DEBUG_INFO, "APOP response: %s %s\n", PCU.userName, buf);
	tlscomm_printf(scs, "APOP %s %s\r\n", PCU.userName, buf);
	tlscomm_gets(buf, BUF_SIZE, scs);

	if (!strncmp(buf, "+OK", 3))
		return scs;				/* AUTH successful */
	else {
		POP_DM(pc, DEBUG_ERROR,
			   "APOP AUTH failed for user '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		POP_DM(pc, DEBUG_INFO, "It said %s", buf);
		return NULL;
	}
}
#endif							/* HAVE_GCRYPT_H */

/*@null@*/
static struct connection_state *authenticate_plaintext( /*@notnull@ */ Pop3 pc,
									struct connection_state * scs, char *apop_str
									__attribute__ ((unused)))
{
	char buf[BUF_SIZE];

	tlscomm_printf(scs, "USER %s\r\n", PCU.userName);
	if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) {
		POP_DM(pc, DEBUG_ERROR,
			   "Error reading from server authenticating '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		return NULL;
	}
	if (buf[0] != '+') {
		POP_DM(pc, DEBUG_ERROR,
			   "Failed user name when authenticating '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		/* deb #128863 might be easier if we printed: */
		POP_DM(pc, DEBUG_ERROR, "The server's error message was: %s\n",
			   buf);
		return NULL;
	};


	tlscomm_printf(scs, "PASS %s\r\n", PCU.password);
	if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) {
		POP_DM(pc, DEBUG_ERROR,
			   "Error reading from server (2) authenticating '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		return NULL;
	}
    if (strncmp(buf, "-ERR [AUTH] Password required", 20) == 0) {
      if (PCU.interactive_password) {
        PCU.password[0] = '\0';
        ask_user_for_password(pc, 1);	/* 1=overwrite the cache */
        tlscomm_printf(scs, "PASS %s\r\n", PCU.password);
        if (tlscomm_gets(buf, BUF_SIZE, scs) == 0) {
          POP_DM(pc, DEBUG_ERROR,
                 "Error reading from server (2) authenticating '%s@%s:%d'\n",
                 PCU.userName, PCU.serverName, PCU.serverPort);
          return NULL;
        }
      }
    }
	if (buf[0] != '+') {
		POP_DM(pc, DEBUG_ERROR,
			   "Failed password when authenticating '%s@%s:%d'\n",
			   PCU.userName, PCU.serverName, PCU.serverPort);
		POP_DM(pc, DEBUG_ERROR, "The server's error message was: %s\n",
			   buf);
		return NULL;
	};

	return scs;
}

void pop3_cacheHeaders( /*@notnull@ */ Pop3 pc)
{
	char buf[BUF_SIZE];
	struct connection_state *scs;
	int i;

	if (pc->headerCache != NULL) {
		/* decrement the reference count, and free our version */
		imap_releaseHeaders(pc, pc->headerCache);
		pc->headerCache = NULL;
	}

	POP_DM(pc, DEBUG_INFO, "working headers\n");
	/* login the server */
	scs = pop3Login(pc);
	if (scs == NULL)
		return;
	/* pc->UnreadMsgs = pc->TotalMsgs - read; */
	pc->headerCache = NULL;
	for (i = pc->TotalMsgs - pc->UnreadMsgs + 1; i <= pc->TotalMsgs; ++i) {
		struct msglst *m;
		m = malloc(sizeof(struct msglst));

		m->subj[0] = '\0';
		m->from[0] = '\0';
		POP_DM(pc, DEBUG_INFO, "search: %s", buf);

		tlscomm_printf(scs, "TOP %i 0\r\n", i);
		while (tlscomm_gets(buf, 256, scs) && buf[0] != '.') {
			if (!strncasecmp(buf, "From: ", 6)) {
				/* manage the from in heads */
				strncpy(m->from, buf + 6, FROM_LEN - 1);
				m->from[FROM_LEN - 1] = '\0';
			} else if (!strncasecmp(buf, "Subject: ", 9)) {
				/* manage subject */
				strncpy(m->subj, buf + 9, SUBJ_LEN - 1);
				m->subj[SUBJ_LEN - 1] = '\0';
			}
			if (!m->subj[0]) {
				strncpy(m->subj, "[NO SUBJECT]", 14);
			}
			if (!m->from[0]) {
				strncpy(m->from, "[ANONYMOUS]", 14);
			}
		}
		m->next = pc->headerCache;
		pc->headerCache = m;
		pc->headerCache->in_use = 0;
	}
	tlscomm_printf(scs, "QUIT\r\n");
	tlscomm_close(scs);
}

/* vim:set ts=4: */
static void ask_user_for_password( /*@notnull@ */ Pop3 pc, int bFlushCache)
{
	/* see if we already have a password, as provided in the config file, or
	   already requested from the user. */
	if (PCU.interactive_password) {
		if (strlen(PCU.password) == 0) {
			/* we need to grab the password from the user. */
			char *password;
			POP_DM(pc, DEBUG_INFO, "asking for password %d\n",
					bFlushCache);
			password =
				passwordFor(PCU.userName, PCU.serverName, pc, bFlushCache);
			if (password != NULL) {
				if (strlen(password) + 1 > BUF_SMALL) {
					DMA(DEBUG_ERROR, "Password is too long.\n");
					memset(PCU.password, 0, BUF_SMALL - 1);
				} else {
					strncpy(PCU.password, password, BUF_SMALL - 1);
					PCU.password_len = strlen(PCU.password);
				}
				free(password);
				// ENFROB(PCU.password);
			}
		}
	}
}

