//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002 Vincent Richard <vincent@vmime.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 3 of
// the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library.  Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//

#include "vmime/config.hpp"


#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_IMAP


#include "vmime/net/imap/IMAPTag.hpp"
#include "vmime/net/imap/IMAPConnection.hpp"
#include "vmime/net/imap/IMAPUtils.hpp"
#include "vmime/net/imap/IMAPStore.hpp"
#include "vmime/net/imap/IMAPCommand.hpp"

#include "vmime/exception.hpp"
#include "vmime/platform.hpp"

#include "vmime/utility/stringUtils.hpp"

#include "vmime/net/defaultConnectionInfos.hpp"

#if VMIME_HAVE_SASL_SUPPORT
	#include "vmime/security/sasl/SASLContext.hpp"
#endif // VMIME_HAVE_SASL_SUPPORT

#if VMIME_HAVE_TLS_SUPPORT
	#include "vmime/net/tls/TLSSession.hpp"
	#include "vmime/net/tls/TLSSecuredConnectionInfos.hpp"
#endif // VMIME_HAVE_TLS_SUPPORT

#include <sstream>

// Helpers for service properties
#define GET_PROPERTY(type, prop) \
	(getStoreOrThrow()->getInfos().getPropertyValue <type>(getSession(), \
		dynamic_cast <const IMAPServiceInfos&>(getStoreOrThrow()->getInfos()).getProperties().prop))
#define HAS_PROPERTY(prop) \
	(getStoreOrThrow()->getInfos().hasProperty(getSession(), \
		dynamic_cast <const IMAPServiceInfos&>(getStoreOrThrow()->getInfos()).getProperties().prop))


namespace vmime {
namespace net {
namespace imap {


IMAPConnection::IMAPConnection(
	const shared_ptr <IMAPStore>& store,
	const shared_ptr <security::authenticator>& auth
)
	: m_store(store),
	  m_auth(auth),
	  m_socket(null),
	  m_parser(null),
	  m_tag(null),
	  m_hierarchySeparator('\0'),
	  m_state(STATE_NONE),
	  m_timeoutHandler(null),
	  m_secured(false),
	  m_firstTag(true),
	  m_capabilitiesFetched(false),
	  m_noModSeq(false) {

	static int connectionId = 0;

	m_tag = make_shared <IMAPTag>();

	if (store->getTracerFactory()) {
		m_tracer = store->getTracerFactory()->create(store, ++connectionId);
	}

	m_parser = make_shared <IMAPParser>();
	m_parser->setTracer(m_tracer);
}


IMAPConnection::~IMAPConnection() {

	try {

		if (isConnected()) {
			disconnect();
		} else if (m_socket) {
			internalDisconnect();
		}

	} catch (...) {

		// Don't throw in destructor
	}
}


void IMAPConnection::connect() {

	if (isConnected()) {
		throw exceptions::already_connected();
	}

	m_state = STATE_NONE;
	m_hierarchySeparator = '\0';

	const string address = GET_PROPERTY(string, PROPERTY_SERVER_ADDRESS);
	const port_t port = GET_PROPERTY(port_t, PROPERTY_SERVER_PORT);

	shared_ptr <IMAPStore> store = m_store.lock();

	if (!store) {
		throw exceptions::illegal_state("Store not found");
	}

	// Create the time-out handler
	if (store->getTimeoutHandlerFactory()) {
		m_timeoutHandler = store->getTimeoutHandlerFactory()->create();
	}

	// Create and connect the socket
	m_socket = store->getSocketFactory()->create(m_timeoutHandler);
	m_socket->setTracer(m_tracer);

#if VMIME_HAVE_TLS_SUPPORT
	if (store->isIMAPS()) {  // dedicated port/IMAPS

		shared_ptr <tls::TLSSession> tlsSession = tls::TLSSession::create
			(store->getCertificateVerifier(),
			 store->getSession()->getTLSProperties());

		shared_ptr <tls::TLSSocket> tlsSocket =
			tlsSession->getSocket(m_socket);

		m_socket = tlsSocket;

		m_secured = true;
		m_cntInfos = make_shared <tls::TLSSecuredConnectionInfos>(address, port, tlsSession, tlsSocket);

	} else
#endif // VMIME_HAVE_TLS_SUPPORT
	{
		m_cntInfos = make_shared <defaultConnectionInfos>(address, port);
	}

	m_socket->connect(address, port);


	m_parser->setSocket(m_socket);
	m_parser->setTimeoutHandler(m_timeoutHandler);


	setState(STATE_NON_AUTHENTICATED);


	// Connection greeting
	//
	// eg:  C: <connection to server>
	// ---  S: * OK mydomain.org IMAP4rev1 v12.256 server ready

	scoped_ptr <IMAPParser::greeting> greet(m_parser->readGreeting());
	bool needAuth = false;

	if (greet->resp_cond_bye) {

		internalDisconnect();
		throw exceptions::connection_greeting_error(greet->getErrorLog());

	} else if (greet->resp_cond_auth->condition != IMAPParser::resp_cond_auth::PREAUTH) {

		needAuth = true;
	}

	if (greet->resp_cond_auth->resp_text->resp_text_code &&
	    greet->resp_cond_auth->resp_text->resp_text_code->capability_data) {

		processCapabilityResponseData(greet->resp_cond_auth->resp_text->resp_text_code->capability_data.get());
	}

#if VMIME_HAVE_TLS_SUPPORT
	// Setup secured connection, if requested
	const bool tls = HAS_PROPERTY(PROPERTY_CONNECTION_TLS)
		&& GET_PROPERTY(bool, PROPERTY_CONNECTION_TLS);
	const bool tlsRequired = HAS_PROPERTY(PROPERTY_CONNECTION_TLS_REQUIRED)
		&& GET_PROPERTY(bool, PROPERTY_CONNECTION_TLS_REQUIRED);

	if (!store->isIMAPS() && tls) {  // only if not IMAPS

		try {

			startTLS();

		// Non-fatal error
		} catch (exceptions::command_error&) {

			if (tlsRequired) {
				m_state = STATE_NONE;
				throw;
			} else {
				// TLS is not required, so don't bother
			}

		// Fatal error
		} catch (...) {

			m_state = STATE_NONE;
			throw;
		}
	}
#endif // VMIME_HAVE_TLS_SUPPORT

	// Authentication
	if (needAuth) {

		try {

			authenticate();

		} catch (...) {

			m_state = STATE_NONE;
			throw;
		}
	}

	// Get the hierarchy separator character
	initHierarchySeparator();

	// Switch to state "Authenticated"
	setState(STATE_AUTHENTICATED);
}

shared_ptr <IMAPStore> IMAPConnection::getStoreOrThrow() {

	auto store = m_store.lock();

	if (!store) {
		throw exceptions::illegal_state("Store not found");
	}

	return store;
}


void IMAPConnection::authenticate() {

	getAuthenticator()->setService(m_store.lock());

#if VMIME_HAVE_SASL_SUPPORT
	// First, try SASL authentication
	if (GET_PROPERTY(bool, PROPERTY_OPTIONS_SASL)) {

		try {

			authenticateSASL();
			return;

		} catch (exceptions::authentication_error&) {

			if (!GET_PROPERTY(bool, PROPERTY_OPTIONS_SASL_FALLBACK)) {

				// Can't fallback on normal authentication
				internalDisconnect();
				throw;

			} else {

				// Ignore, will try normal authentication
			}

		} catch (exception&) {

			internalDisconnect();
			throw;
		}
	}
#endif // VMIME_HAVE_SASL_SUPPORT

	// Normal authentication
	const string username = getAuthenticator()->getUsername();
	const string password = getAuthenticator()->getPassword();

	shared_ptr <IMAPConnection> conn = dynamicCast <IMAPConnection>(shared_from_this());
	IMAPCommand::LOGIN(username, password)->send(conn);

	scoped_ptr <IMAPParser::response> resp(m_parser->readResponse(*m_tag));

	if (resp->isBad()) {

		internalDisconnect();
		throw exceptions::command_error("LOGIN", resp->getErrorLog());

	} else if (resp->response_done->response_tagged->
					resp_cond_state->status != IMAPParser::resp_cond_state::OK) {

		internalDisconnect();
		throw exceptions::authentication_error(resp->getErrorLog());
	}

	// Server capabilities may change when logged in
	if (!processCapabilityResponseData(resp.get())) {
		invalidateCapabilities();
	}
}


#if VMIME_HAVE_SASL_SUPPORT

void IMAPConnection::authenticateSASL() {

	if (!dynamicCast <security::sasl::SASLAuthenticator>(getAuthenticator())) {
		throw exceptions::authentication_error("No SASL authenticator available.");
	}

	const std::vector <string> capa = getCapabilities();
	std::vector <string> saslMechs;

	for (unsigned int i = 0 ; i < capa.size() ; ++i) {

		const string& x = capa[i];

		if (x.length() > 5 &&
		    (x[0] == 'A' || x[0] == 'a') &&
		    (x[1] == 'U' || x[1] == 'u') &&
		    (x[2] == 'T' || x[2] == 't') &&
		    (x[3] == 'H' || x[3] == 'h') &&
		    x[4] == '=') {

			saslMechs.push_back(string(x.begin() + 5, x.end()));
		}
	}

	if (saslMechs.empty()) {
		throw exceptions::authentication_error("No SASL mechanism available.");
	}

	std::vector <shared_ptr <security::sasl::SASLMechanism> > mechList;

	shared_ptr <security::sasl::SASLContext> saslContext =
		security::sasl::SASLContext::create();

	for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) {

		try {
			mechList.push_back(saslContext->createMechanism(saslMechs[i]));
		} catch (exceptions::no_such_mechanism&) {
			// Ignore mechanism
		}
	}

	if (mechList.empty()) {
		throw exceptions::authentication_error("No SASL mechanism available.");
	}

	// Try to suggest a mechanism among all those supported
	shared_ptr <security::sasl::SASLMechanism> suggestedMech =
		saslContext->suggestMechanism(mechList);

	if (!suggestedMech) {
		throw exceptions::authentication_error("Unable to suggest SASL mechanism.");
	}

	// Allow application to choose which mechanisms to use
	mechList = dynamicCast <security::sasl::SASLAuthenticator>(getAuthenticator())->
		getAcceptableMechanisms(mechList, suggestedMech);

	if (mechList.empty()) {
		throw exceptions::authentication_error("No SASL mechanism available.");
	}

	// Try each mechanism in the list in turn
	for (unsigned int i = 0 ; i < mechList.size() ; ++i) {

		shared_ptr <security::sasl::SASLMechanism> mech = mechList[i];

		shared_ptr <security::sasl::SASLSession> saslSession =
			saslContext->createSession("imap", getAuthenticator(), mech);

		saslSession->init();

		shared_ptr <IMAPCommand> authCmd;

		if (saslSession->getMechanism()->hasInitialResponse()) {

			byte_t* initialResp = 0;
			size_t initialRespLen = 0;

			saslSession->evaluateChallenge(NULL, 0, &initialResp, &initialRespLen);

			string encodedInitialResp(saslContext->encodeB64(initialResp, initialRespLen));
			delete [] initialResp;

			if (encodedInitialResp.empty()) {
				authCmd = IMAPCommand::AUTHENTICATE(mech->getName(), "=");
			} else {
				authCmd = IMAPCommand::AUTHENTICATE(mech->getName(), encodedInitialResp);
			}

		} else {

			authCmd = IMAPCommand::AUTHENTICATE(mech->getName());
		}

		authCmd->send(dynamicCast <IMAPConnection>(shared_from_this()));

		for (bool cont = true ; cont ; ) {

			scoped_ptr <IMAPParser::response> resp(m_parser->readResponse(*m_tag));

			if (resp->response_done &&
			    resp->response_done->response_tagged &&
			    resp->response_done->response_tagged->resp_cond_state->
			    	status == IMAPParser::resp_cond_state::OK) {

				m_socket = saslSession->getSecuredSocket(m_socket);
				return;

			} else {

				string response;
				bool hasResponse = false;

				for (auto &respData : resp->continue_req_or_response_data) {

					if (respData->continue_req) {

						response = respData->continue_req->resp_text->text;
						hasResponse = true;
						break;
					}
				}

				if (!hasResponse) {
					cont = false;
					continue;
				}

				byte_t* challenge = 0;
				size_t challengeLen = 0;

				byte_t* resp = 0;
				size_t respLen = 0;

				try {

					// Extract challenge
					saslContext->decodeB64(response, &challenge, &challengeLen);

					// Prepare response
					saslSession->evaluateChallenge
						(challenge, challengeLen, &resp, &respLen);

					// Send response
					const string respB64 = saslContext->encodeB64(resp, respLen) + "\r\n";
					sendRaw(utility::stringUtils::bytesFromString(respB64), respB64.length());

					if (m_tracer) {
						m_tracer->traceSendBytes(respB64.length() - 2, "SASL exchange");
					}

					// Server capabilities may change when logged in
					invalidateCapabilities();

				} catch (exceptions::sasl_exception& e) {

					if (challenge) {
						delete [] challenge;
						challenge = NULL;
					}

					if (resp) {
						delete [] resp;
						resp = NULL;
					}

					// Cancel SASL exchange
					sendRaw(utility::stringUtils::bytesFromString("*\r\n"), 3);

					if (m_tracer) {
						m_tracer->traceSend("*");
					}

				} catch (...) {

					if (challenge) {
						delete [] challenge;
					}

					if (resp) {
						delete [] resp;
					}

					throw;
				}

				if (challenge) {
					delete [] challenge;
				}

				if (resp) {
					delete [] resp;
				}
			}
		}
	}

	throw exceptions::authentication_error("Could not authenticate using SASL: all mechanisms failed.");
}

#endif // VMIME_HAVE_SASL_SUPPORT


#if VMIME_HAVE_TLS_SUPPORT

void IMAPConnection::startTLS() {

	try {

		IMAPCommand::STARTTLS()->send(dynamicCast <IMAPConnection>(shared_from_this()));

		scoped_ptr <IMAPParser::response> resp(m_parser->readResponse(*m_tag));

		if (resp->isBad() || resp->response_done->response_tagged->
			resp_cond_state->status != IMAPParser::resp_cond_state::OK) {

			throw exceptions::command_error("STARTTLS", resp->getErrorLog(), "bad response");
		}

		auto store = m_store.lock();

		if (!store) {
			throw exceptions::illegal_state("Store not found");
		}

		shared_ptr <tls::TLSSession> tlsSession = tls::TLSSession::create(
			store->getCertificateVerifier(),
			store->getSession()->getTLSProperties()
		);

		shared_ptr <tls::TLSSocket> tlsSocket = tlsSession->getSocket(m_socket);

		tlsSocket->handshake();

		m_socket = tlsSocket;
		m_parser->setSocket(m_socket);

		m_secured = true;
		m_cntInfos = make_shared <tls::TLSSecuredConnectionInfos>(
			m_cntInfos->getHost(), m_cntInfos->getPort(), tlsSession, tlsSocket
		);

		// " Once TLS has been started, the client MUST discard cached
		//   information about server capabilities and SHOULD re-issue the
		//   CAPABILITY command.  This is necessary to protect against
		//   man-in-the-middle attacks which alter the capabilities list prior
		//   to STARTTLS. " (RFC-2595)
		invalidateCapabilities();

	} catch (exceptions::command_error&) {

		// Non-fatal error
		throw;

	} catch (exception&) {

		// Fatal error
		internalDisconnect();
		throw;
	}
}

#endif // VMIME_HAVE_TLS_SUPPORT


const std::vector <string> IMAPConnection::getCapabilities() {

	if (!m_capabilitiesFetched) {
		fetchCapabilities();
	}

	return m_capabilities;
}


bool IMAPConnection::hasCapability(const string& capa) {

	if (!m_capabilitiesFetched) {
		fetchCapabilities();
	}

	const string normCapa = utility::stringUtils::toUpper(capa);

	for (size_t i = 0, n = m_capabilities.size() ; i < n ; ++i) {

		if (m_capabilities[i] == normCapa) {
			return true;
		}
	}

	return false;
}


bool IMAPConnection::hasCapability(const string& capa) const {

	const string normCapa = utility::stringUtils::toUpper(capa);

	for (size_t i = 0, n = m_capabilities.size() ; i < n ; ++i) {

		if (m_capabilities[i] == normCapa)
			return true;
	}

	return false;
}


void IMAPConnection::invalidateCapabilities() {

	m_capabilities.clear();
	m_capabilitiesFetched = false;
}


void IMAPConnection::fetchCapabilities() {

	IMAPCommand::CAPABILITY()->send(dynamicCast <IMAPConnection>(shared_from_this()));

	scoped_ptr <IMAPParser::response> resp(m_parser->readResponse(*m_tag));

	if (resp->response_done->response_tagged->
			resp_cond_state->status == IMAPParser::resp_cond_state::OK) {

		processCapabilityResponseData(resp.get());
	}
}


bool IMAPConnection::processCapabilityResponseData(const IMAPParser::response* resp) {

	for (auto &respData : resp->continue_req_or_response_data) {

		if (respData->response_data == NULL) {
			continue;
		}

		auto &capaData = respData->response_data->capability_data;

		if (!capaData) {
			continue;
		}

		processCapabilityResponseData(capaData.get());
		return true;
	}

	return false;
}


void IMAPConnection::processCapabilityResponseData(const IMAPParser::capability_data* capaData) {

	std::vector <string> res;

	for (auto &cap : capaData->capabilities) {

		if (cap->auth_type) {
			res.push_back("AUTH=" + cap->auth_type->name);
		} else {
			res.push_back(utility::stringUtils::toUpper(cap->atom->value));
		}
	}

	m_capabilities = res;
	m_capabilitiesFetched = true;
}


shared_ptr <security::authenticator> IMAPConnection::getAuthenticator() {

	return m_auth;
}


bool IMAPConnection::isConnected() const {

	return m_socket
	    && m_socket->isConnected()
	    && (m_state == STATE_AUTHENTICATED || m_state == STATE_SELECTED);
}


bool IMAPConnection::isSecuredConnection() const {

	return m_secured;
}


shared_ptr <connectionInfos> IMAPConnection::getConnectionInfos() const {

	return m_cntInfos;
}


void IMAPConnection::disconnect() {

	bool wasConnected = isConnected();
	internalDisconnect();
	if (!wasConnected) {
		throw exceptions::not_connected();
	}
}


void IMAPConnection::internalDisconnect() {

	if (isConnected()) {

		IMAPCommand::LOGOUT()->send(dynamicCast <IMAPConnection>(shared_from_this()));
	}

	if (m_socket) {
		m_socket->disconnect();
		m_socket = null;
	}

	m_timeoutHandler = null;

	m_state = STATE_LOGOUT;

	m_secured = false;
	m_cntInfos = null;
}


void IMAPConnection::initHierarchySeparator() {

	IMAPCommand::LIST("", "")->send(dynamicCast <IMAPConnection>(shared_from_this()));

	scoped_ptr <IMAPParser::response> resp(m_parser->readResponse(*m_tag));

	if (resp->isBad() || resp->response_done->response_tagged->
			resp_cond_state->status != IMAPParser::resp_cond_state::OK) {

		internalDisconnect();
		throw exceptions::command_error("LIST", resp->getErrorLog(), "bad response");
	}

	const auto& respDataList = resp->continue_req_or_response_data;

	bool found = false;

	for (unsigned int i = 0 ; !found && i < respDataList.size() ; ++i) {

		if (!respDataList[i]->response_data) {
			continue;
		}

		auto &mailboxData = respDataList[i]->response_data->mailbox_data;

		if (!mailboxData || mailboxData->type != IMAPParser::mailbox_data::LIST) {
			continue;
		}

		if (mailboxData->mailbox_list->quoted_char != '\0') {
			m_hierarchySeparator = mailboxData->mailbox_list->quoted_char;
			found = true;
		}
	}

	if (!found) {  // default
		m_hierarchySeparator = '/';
	}
}


void IMAPConnection::sendCommand(const shared_ptr <IMAPCommand>& cmd) {

	if (!m_firstTag) {
		++(*m_tag);
	}

	m_socket->send(*m_tag);
	m_socket->send(" ");
	m_socket->send(cmd->getText());
	m_socket->send("\r\n");

	m_firstTag = false;

	if (m_tracer) {

		std::ostringstream oss;
		oss << string(*m_tag) << " " << cmd->getText();

		m_tracer->traceSend(oss.str());
	}
}


void IMAPConnection::sendRaw(const byte_t* buffer, const size_t count) {

	m_socket->sendRaw(buffer, count);
}


IMAPParser::response* IMAPConnection::readResponse(IMAPParser::literalHandler* lh) {

	return m_parser->readResponse(*m_tag, lh);
}


IMAPConnection::ProtocolStates IMAPConnection::state() const {

	return m_state;
}


void IMAPConnection::setState(const ProtocolStates state) {

	m_state = state;
}


char IMAPConnection::hierarchySeparator() const {

	return m_hierarchySeparator;
}


shared_ptr <const IMAPStore> IMAPConnection::getStore() const {

	return m_store.lock();
}


shared_ptr <IMAPStore> IMAPConnection::getStore() {

	return m_store.lock();
}


shared_ptr <session> IMAPConnection::getSession() {

	auto store = m_store.lock();

	if (!store) {
		throw exceptions::illegal_state("Store not found");
	}

	return store->getSession();
}


shared_ptr <const socket> IMAPConnection::getSocket() const {

	return m_socket;
}


void IMAPConnection::setSocket(const shared_ptr <socket>& sok) {

	m_socket = sok;
	m_parser->setSocket(sok);
}


shared_ptr <tracer> IMAPConnection::getTracer() {

	return m_tracer;
}


shared_ptr <IMAPTag> IMAPConnection::getTag() {

	return m_tag;
}


bool IMAPConnection::isMODSEQDisabled() const {

	return m_noModSeq;
}


void IMAPConnection::disableMODSEQ() {

	m_noModSeq = true;
}


} // imap
} // net
} // vmime


#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_IMAP

