#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>

#include "imapfilter.h"
#include "session.h"

#ifndef NO_SSLTLS
#include <openssl/ssl.h>
#include <openssl/err.h>
#endif


/*
 * Connect to mail server.
 */
int
open_connection(session *ssn, const char *server, const char *port,
    const char *protocol)
{
	struct addrinfo hints, *res, *ressave;
	int n, sockfd;

#ifdef NO_SSLTLS
	if (protocol) {
		error("SSL not supported by this build\n");
		return -1;
	}
#endif

	memset(&hints, 0, sizeof(struct addrinfo));

	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;

	n = getaddrinfo(server, port, &hints, &res);

	if (n < 0) {
		error("gettaddrinfo; %s\n", gai_strerror(n));
		return -1;
	}

	ressave = res;

	sockfd = -1;

	while (res) {
		sockfd = socket(res->ai_family, res->ai_socktype,
		    res->ai_protocol);

		if (sockfd >= 0) {
			if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
				break;

			sockfd = -1;
		}
		res = res->ai_next;
	}

	if (ressave)
		freeaddrinfo(ressave);

	if (sockfd == -1) {
		error("error while initiating connection to %s at port %s\n",
		    server, port);
		return -1;
	}

	ssn->socket = sockfd;

#ifndef NO_SSLTLS
	if (protocol) {
		if (open_secure_connection(ssn, server, port, protocol) == -1) {
			close_connection(ssn);
			return -1;
		}
	}
#endif

	return ssn->socket;
}


#ifndef NO_SSLTLS
/*
 * Initialize SSL/TLS connection.
 */
int
open_secure_connection(session *ssn, const char *server, const char *port,
    const char *protocol)
{
	int r, e;
	SSL_CTX *ctx;
	SSL_METHOD *method;

	method = NULL;

	if (!strncasecmp(protocol, "tls1", 4))
		method = TLSv1_client_method();
	else if (!strncasecmp(protocol, "ssl3", 4) ||
	    !strncasecmp(protocol, "ssl2", 4))
		method = SSLv23_client_method();

	if (!(ctx = SSL_CTX_new(method)))
		goto fail;

	if (!(ssn->ssl = SSL_new(ctx)))
		goto fail;

	SSL_set_fd(ssn->ssl, ssn->socket);

	for (;;) {
		if ((r = SSL_connect(ssn->ssl)) > 0)
			break;

		switch (SSL_get_error(ssn->ssl, r)) {
		case SSL_ERROR_ZERO_RETURN:
			error("initiating SSL connection to %s; the "
			    "connection has been closed cleanly\n", server);
			goto fail;
		case SSL_ERROR_WANT_CONNECT:
		case SSL_ERROR_WANT_ACCEPT:
		case SSL_ERROR_WANT_X509_LOOKUP:
		case SSL_ERROR_WANT_READ:
		case SSL_ERROR_WANT_WRITE:
			break;
		case SSL_ERROR_SYSCALL:
			e = ERR_get_error();
			if (e == 0)
				error("initiating SSL connection to %s; EOF "
				    "in violation of the protocol\n", server);
			else if (e == -1)
				error("initiating SSL connection to %s; %s\n",
				    server, strerror(errno));
			goto fail;
		case SSL_ERROR_SSL:
			error("initiating SSL connection to %s; %s\n", server,
			    ERR_error_string(ERR_get_error(), NULL));
			goto fail;
		default:
			goto fail;
		}
	}
	if (get_option_boolean("certificates") && get_cert(ssn) == -1)
		goto fail;

	SSL_CTX_free(ctx);

	return 0;

fail:
	ssn->ssl = NULL;
	SSL_CTX_free(ctx);

	return -1;
}
#endif				/* NO_SSLTLS */


/*
 * Disconnect from mail server.
 */
int
close_connection(session *ssn)
{
	int r;

	r = 0;

#ifndef NO_SSLTLS
	close_secure_connection(ssn);
#endif

	if (ssn->socket != -1) {
		r = close(ssn->socket);
		ssn->socket = -1;

		if (r == -1)
			error("closing socket; %s\n", strerror(errno));
	}
	return r;
}


#ifndef NO_SSLTLS
/*
 * Shutdown SSL/TLS connection.
 */
int
close_secure_connection(session *ssn)
{

	if (ssn->ssl) {
		SSL_shutdown(ssn->ssl);
		SSL_free(ssn->ssl);
		ssn->ssl = NULL;
	}

	return 0;
}
#endif


/*
 * Read data from socket.
 */
ssize_t
socket_read(session *ssn, char *buf, size_t len, long timeout, int timeoutfail)
{
	int s;
	ssize_t r;
	fd_set fds;

	struct timeval tv;

	struct timeval *tvp;

	r = 0;
	s = 1;
	tvp = NULL;

	memset(buf, 0, len + 1);

	if (timeout > 0) {
		tv.tv_sec = timeout;
		tv.tv_usec = 0;
		tvp = &tv;
	}

	FD_ZERO(&fds);
	FD_SET(ssn->socket, &fds);
 
#ifndef NO_SSLTLS
	if (ssn->ssl) {
		if (SSL_pending(ssn->ssl) > 0 ||
		    ((s = select(ssn->socket + 1, &fds, NULL, NULL, tvp)) > 0 &&
		    FD_ISSET(ssn->socket, &fds))) {
			r = socket_secure_read(ssn, buf, len);

			if (r <= 0)
				goto fail;
		}
	} else
#endif
	{
		if ((s = select(ssn->socket + 1, &fds, NULL, NULL, tvp)) > 0 &&
		    FD_ISSET(ssn->socket, &fds)) {
			r = read(ssn->socket, buf, len);

			if (r == -1) {
				error("reading data; %s\n", strerror(errno));
				goto fail;
			} else if (r == 0) {
				goto fail;
			}
		}
	}

	if (s == -1) {
		error("waiting to read from socket; %s\n", strerror(errno));
		goto fail;
	} else if (s == 0 && timeoutfail) {
		error("timeout period expired while waiting to read data\n");
		goto fail;
	}

	return r;
fail:
	close_connection(ssn);

	return -1;

}


#ifndef NO_SSLTLS
/*
 * Read data from a TLS/SSL connection.
 */
ssize_t
socket_secure_read(session *ssn, char *buf, size_t len)
{
	int r, e;

	for (;;) {
		r = (ssize_t) SSL_read(ssn->ssl, buf, len);

		if (r > 0)
			break;

		switch (SSL_get_error(ssn->ssl, r)) {
		case SSL_ERROR_ZERO_RETURN:
			error("reading data; the connection has been closed "
			    "cleanly\n");
			return -1;
		case SSL_ERROR_WANT_READ:
		case SSL_ERROR_WANT_WRITE:
		case SSL_ERROR_WANT_CONNECT:
		case SSL_ERROR_WANT_ACCEPT:
		case SSL_ERROR_WANT_X509_LOOKUP:
			break;
		case SSL_ERROR_SYSCALL:
			e = ERR_get_error();
			if (e == 0)
				error("reading data; EOF in violation of the "
				    "protocol\n");
			else if (e == -1)
				error("reading data; %s\n", strerror(errno));
			return -1;
		case SSL_ERROR_SSL:
			error("reading data; %s\n",
			    ERR_error_string(ERR_get_error(), NULL));
			return -1;
		default:
			return -1;
		}
	}

	return r;
}
#endif


/*
 * Write data to socket.
 */
ssize_t
socket_write(session *ssn, const char *buf, size_t len)
{
	int s;
	ssize_t w, wt;
	fd_set fds;

	w = wt = 0;
	s = 1;

	FD_ZERO(&fds);
	FD_SET(ssn->socket, &fds);

	while (len) {
		if ((s = select(ssn->socket + 1, NULL, &fds, NULL, NULL) > 0 &&
		    FD_ISSET(ssn->socket, &fds))) {
#ifndef NO_SSLTLS
			if (ssn->ssl) {
				w = socket_secure_write(ssn, buf, len);

				if (w <= 0)
					goto fail;
			} else
#endif
			{
				w = write(ssn->socket, buf, len);

				if (w == -1) {
					error("writing data; %s\n",
					    strerror(errno));
					goto fail;
				} else if (w == 0) {
					goto fail;
				}
			}

			if (w > 0) {
				len -= w;
				buf += w;
				wt += w;
			}
		}
	}

	if (s == -1) {
		error("waiting to write to socket; %s\n", strerror(errno));
		goto fail;
	} else if (s == 0) {
		error("timeout period expired while waiting to write data\n");
		goto fail;
	}

	return wt;
fail:
	close_connection(ssn);

	return -1;
}


#ifndef NO_SSLTLS
/*
 * Write data to a TLS/SSL connection.
 */
ssize_t
socket_secure_write(session *ssn, const char *buf, size_t len)
{
	int w, e;

	for (;;) {
		w = (ssize_t) SSL_write(ssn->ssl, buf, len);

		if (w > 0)
			break;

		switch (SSL_get_error(ssn->ssl, w)) {
		case SSL_ERROR_ZERO_RETURN:
			error("writing data; the connection has been closed "
			    "cleanly\n");
			return -1;
		case SSL_ERROR_WANT_READ:
		case SSL_ERROR_WANT_WRITE:
		case SSL_ERROR_WANT_CONNECT:
		case SSL_ERROR_WANT_ACCEPT:
		case SSL_ERROR_WANT_X509_LOOKUP:
			break;
		case SSL_ERROR_SYSCALL:
			e = ERR_get_error();
			if (e == 0)
				error("writing data; EOF in violation of the "
				    "protocol\n");
			else if (e == -1)
				error("writing data; %s\n", strerror(errno));
			return -1;
		case SSL_ERROR_SSL:
			error("writing data; %s\n",
			    ERR_error_string(ERR_get_error(), NULL));
			return -1;
		default:
			return -1;
		}
	}

	return w;
}
#endif
