/*
 *  Copyright (C) 2007-2008 CMM Sigma Andrzej Chmielowiec 
<cmmsigma@cmmsigma.eu>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  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.
 */

// QtPop3.cpp: implementation of the QtPop3 class.
//
//////////////////////////////////////////////////////////////////////

#include "QtPop3.h"
#include <QDir>

bool QtPop3::ValNameList::containsName(const QString name)
{
	for(int i=0; i<size(); i++)
	{
		ValName vn = at(i);
		if(equal(vn.name, name))
			return true;
	}

	return false;
}

bool QtPop3::ValNameList::getValue(const QString name, QString &value)
{
	for(int i=0; i<size(); i++)
	{
		ValName vn = at(i);
		if(equal(vn.name, name))
		{
			value = vn.value;
			return true;
		}
	}

	value.clear();
	return false;
}

void QtPop3::ValName::clear()
{
	name.clear();
	value.clear();
}

void QtPop3::ValName::trim()
{
	name = name.trimmed();
	value = value.trimmed();
}

bool QtPop3::equal(const QString &s1, const QString &s2)
{
	return s1.compare(s2, Qt::CaseInsensitive) == 0;
}

bool QtPop3::contains(const QString &text, const QString token)
{
	return text.indexOf(token, 0, Qt::CaseInsensitive) != -1;
}

QtPop3::QtPop3(QObject *parent) : QObject(parent)
{
	mTelnet = new QtTelnet(this);
	QObject::connect(mTelnet, SIGNAL(message(const QStringList &)), this, SLOT(telnetMessage(const QStringList &)));
	QObject::connect(mTelnet, SIGNAL(connectionError(const QString &)), this, SLOT(telnetConnectionError(const QString &)));
	QObject::connect(mTelnet, SIGNAL(connected()), this, SLOT(telnetConnected()));
	mState = DISCONNECTED;
	mLeaveOnServer = false;
	mBytesLimit = -1;
	mWriteLocal = true;
}


QtPop3::~QtPop3()
{
	delete mTelnet;
}

//////////////////////////////////////////////////////////////////////
// telnet slots
//////////////////////////////////////////////////////////////////////

void QtPop3::telnetConnected()
{
	//tr::trace("telnetConnected()");
	mState = CONNECTED;
}


void QtPop3::telnetMessage(const QStringList &lines)
{
	processReceiving(lines);
}


void QtPop3::telnetConnectionError(const QString &error)
{
	//tr::trace("telnetConnectionError()");
	mState = DISCONNECTED;
	emit errorOccured(error);
}

//////////////////////////////////////////////////////////////////////
// smtp setup
//////////////////////////////////////////////////////////////////////

void QtPop3::setServer(const QString host)
{
	mHost = host;
}


void QtPop3::setUserPassword(const QString user, const QString password)
{
	mUser = user;
	mPassword = password;
}


void QtPop3::receiveAll()
{
	if(mHost.isEmpty())
	{
		emit errorOccured(tr("Please specify POP3 server name."));
	}
	else if(mUser.isEmpty())
	{
		emit errorOccured(tr("Please specify user account name."));
	}
	else if(mPassword.isEmpty())
	{
		emit errorOccured(tr("Please specify user account password."));
	}
	else if(mWriteLocal && mDownloadPath.isEmpty())
	{
		emit errorOccured(tr("Please provide path for received mails."));
	}
	else
	{
		readStoredIDs();
		mState = DISCONNECTED;
		mReceivedUIDList.clear();
		mTelnet->connectToHost(mHost, 110);
	}
}


void QtPop3::processReceiving(const QStringList &lines)
{
	int value;
	QList<int> numbers;

	switch(mState)
	{
		case DISCONNECTED:
			break;
		case CONNECTED:
			if(checkResponse(lines, value))
			{
				//tr::trace("case CONNECTED -> USER");

				mState = USER;
				send("USER " + mUser);
			}
			break;
		case USER:
			if(checkResponse(lines, value))
			{
				//tr::trace("case USER -> PASSWORD");

				mState = PASSWORD;
				send("PASS " + mPassword);
			}
			break;
		case PASSWORD:
			if(checkResponse(lines, value))
			{
				//tr::trace("case PASSWORD -> STAT");

				mState = STAT;
				send("STAT");
			}
			break;
		case STAT:
			if(checkResponse(lines, value))
			{
				//tr::trace("case STAT -> LIST");

				if(value != -1)
				{
					if(value <= 0)
					{
						mState = NOMESSAGES;
						send("QUIT");
					}
					else
					{
						mState = LIST;
						send("LIST", true);
					}
				}
			}
			break;
		case LIST:
			if(checkResponse(lines, value))
			{
				//tr::trace("case LIST -> receiving");

				if(collectResponse(lines))
				{
					//tr::trace("case LIST -> UIDL");
	
					mToReceive.clear();
					index2UID.clear();

					// dodawanie z filtrowaniem listy do sciagniecia
					for(int i=1; i<mResponse.size(); i++)
					{
						QStringList values = mResponse[i].trimmed().split(' ', QString::SkipEmptyParts);
						if(values.size() >= 2)
						{
							if((mBytesLimit != -1 && mBytesLimit > toInt(values[1])) || mBytesLimit == -1)
							{
								//tr::trace("mToReceive.enqueue(values[0]); #%s", qPrintable(values[0]));
								mToReceive.enqueue(values[0]);
							}
						}
					}

					serverUIDL.clear(); // czyszczenie listy UID na serwerze

					mState = UIDL;
					send("UIDL", true);
				}
			}
			break;
		case UIDL:
			if(checkResponse(lines, value))
			{
				//tr::trace("case UIDL -> receiving");

				if(collectResponse(lines))
				{
					//tr::trace("case UIDL -> RETR");
	
					// odrzucenie przeczytanych wiadomosci
					for(int i=1; i<mResponse.size(); i++)
					{
						QStringList opt = mResponse[i].trimmed().split(' ', QString::SkipEmptyParts);
						if(opt.size() >= 2)
						{
							QString index = opt[0];
							QString uid = opt[1];

							index2UID.insert(index, uid);

							//tr::trace("index: %s, uid: %s", qPrintable(index), qPrintable(uid));

							serverUIDL.append(uid);

							if(mToReceive.contains(index) && receivedUIDL.contains(uid))
								mToReceive.removeAll(index); // mail juz byl sciagany z serwera
						}
					}

					//tr::trace("bedzie getNextMessage");
					
					emit newMessagesCount(mToReceive.size());
					
					if(mToReceive.isEmpty())
					{
						mState = NOMESSAGES;
						send("QUIT");
					}
					else
					{
						mState = RETR;
						getNextMessage();
					}
				}
			}
			break;
		case RETR:
			if(checkResponse(lines, value))
			{
				//tr::trace("case RETR -> receiving");

				if(collectResponse(lines))
				{
					//tr::trace("case RETR -> message completed!");

					saveMessage();
					getNextMessage();
				}
			}
			break;
		case NOMESSAGES:
			if(checkResponse(lines, value))
			{
				//tr::trace("case NOMESSAGES -> DISCONNECTED");

				mState = DISCONNECTED;
				mTelnet->close();
				updateStoredList();
				emit noNewMessages();
			}
			break;
		case COMPLETED:
			if(checkResponse(lines, value))
			{
				//tr::trace("case COMPLETED -> DISCONNECTED");

				mState = DISCONNECTED;
				mTelnet->close();
				updateStoredList();
				emit newMessages(mReceivedUIDList);
			}
			break;
	}
}


void QtPop3::getNextMessage()
{
	//tr::trace("getNextMessage()");

	if(mToReceive.isEmpty())
	{
		//tr::trace("nothing to receive");

		mState = COMPLETED;
		mTelnet->sendData("QUIT");
	}
	else
	{
		send("RETR " + mToReceive.first(), true); // get next message
	}
}


bool QtPop3::checkResponse(const QStringList &lines, int &value)
{
	bool result = false;

	if(lines.size() > 0)
	{
		if(mMultiPartResponse && mNextMultiPartResponse)
		{
			return true;
		}

		QString response = QString(lines[0]).remove("\r\n");

		QStringList opt = response.split(' ', QString::SkipEmptyParts);
		if(opt.size() > 0)
		{
			if(opt[0].toUpper() == "+OK")
				result = true;
		}
		if(opt.size() > 1)
		{
			bool ok;
			value = opt[1].toInt(&ok);
			if(!ok)
				value = -1;
		}

		if(false == result)
		{
			mState = DISCONNECTED;
			mTelnet->sendData("QUIT");
			mTelnet->close();
			emit errorOccured(response);
		}

		mNextMultiPartResponse = true;
	}

	return result;
}

void QtPop3::setLeaveOnServer(bool leaveOnServer)
{
	mLeaveOnServer = leaveOnServer;
}

void QtPop3::setDownloadPath(QString path)
{
	if(!path.endsWith(QDir::QDir::separator()))
		path += QDir::separator();

	mDownloadPath = path;
}

bool QtPop3::saveMessage()
{
	bool result = true;
	QString index = mToReceive.dequeue();
	QString uid = index2UID.value(index);
	
	//tr::trace("saveMessage() - %s%s", qPrintable(mDownloadPath), qPrintable(uid));

	if(mWriteLocal)
	{
		QString body;
		for(int i=1; i<mResponse.size() - 1; i++)
		{
			body += mResponse[i];
		}

		QFile file(mDownloadPath + uid + ".eml");
		if(false == file.open(QIODevice::WriteOnly))
		{
			//tr::trace("Cannot open message file for writing.");
			emit errorOccured(tr("Error saving message on disk."));
			result = false;
		}
		else
		{
			if(file.write(qPrintable(body), body.length()) == -1)
			{
				//tr::trace("Error writing message to disk.");
				emit errorOccured(tr("Error writing message to disk."));
				result = false;
			}

			file.close();
		}
	}
	else
	{
		MessageItem msg;
		parseMessage(msg);
		emit messageReceived(msg);
	}

	if(result)
	{
		receivedUIDL.append(uid);
		mReceivedUIDList.append(uid);
	}

	return result;
}

void QtPop3::updateStoredList()
{
	QStringList toRemove;
	QString item;

	// usun element uid z listy odebranych, jak nie ma ich juz na serwerze
	foreach(item, receivedUIDL)
	{
		if(false == serverUIDL.contains(item))
		{
			toRemove.append(item);
		}
	}
	foreach(item, toRemove)
	{
		receivedUIDL.removeAll(item);
	}

	writeStoredIDs();
}

void QtPop3::readStoredIDs()
{
	QFile file(mDownloadPath + "received_list");

	if(file.open(QIODevice::ReadOnly))
	{
		receivedUIDL.clear();

		QByteArray line;
		do
		{
			line = file.readLine().trimmed();
			if(line.length() > 0)
				receivedUIDL.append(line);
		} while(line.size() > 0);
		
		file.close();
	}
}

void QtPop3::writeStoredIDs()
{
	QFile file(mDownloadPath + "received_list");

	if(file.open(QIODevice::WriteOnly))
	{
		foreach(QString line, receivedUIDL)
		{
			line += "\r\n";
			file.write(line.toUtf8(), line.length());
		}
		
		file.close();
	}
}


void QtPop3::setMessageLimit(int bytes)
{
	mBytesLimit = bytes;
}

void QtPop3::setLocalWriteMode(bool local)
{
	mWriteLocal = local;
}

bool QtPop3::collectResponse(const QStringList &lines)
{
	foreach(QString line, lines)
	{
		mResponse.append(line);
	}
	
	int size = lines.size();
	if(size > 2 && lines[size - 1] == ".\r\n" && lines[size - 2].endsWith("\r\n"))
	{
		//tr::trace("* response completed");
		return true;
	}
	else
	{
		//tr::trace("* response not completed");
		return false;
	}
}

void QtPop3::send(QString data, bool multiPart)
{
	mMultiPartResponse = multiPart;
	mNextMultiPartResponse = false;

	mResponse.clear();
	mTelnet->sendData(data);
}


QString QtPop3::toString(int value)
{
	QString num;
	return num.sprintf("%d", value);
}


int QtPop3::toInt(QString text)
{
	bool ok;
	return text.toInt(&ok);
}


void QtPop3::getValues(const QStringList &lines, int &index, QString &name, QStringList &values, ValNameList &sub)
{
	ValName vn;
	enum { NAME, VAL, SUB, SUBVAL } state = NAME;

	name.clear();
	values.clear();
	sub.clear();

	bool cont = false;
	bool comment = false;
	QString value;
	int i = index;

	for(i=index; i<lines.size(); i++)
	{
		QString line = lines[i].trimmed();

		if(state == SUB && !line.contains("=\""))
		{
			break; // not a value line
		}
		
		cont = line.endsWith(",");
		int last = line.size() - 1;

		for(int j=0; j<line.size(); j++)
		{
			QChar c = line[j];
			if(c == '\"')
				comment =! comment;

			if(state == NAME)
			{
				if(c == ':')
					state = VAL;
				else
					name += c;
			}
			else if(state == VAL)
			{
				if(!comment && c == ';')
					state = SUB;
				else if(c == ',' && j == last)
				{

				}
				else
					value += c;
			}
			else if(state == SUB)
			{
				if(c == '=')
					state = SUBVAL;
				else
					vn.name += c;
			}
			else if(state == SUBVAL)
			{
				if(!comment && c == ';')
				{
					state = SUB;
					vn.trim();
					sub.append(vn);
					vn.clear();
				}
				else if(c == ',')
				{
					//state = NEXT;
					break;
				}
				else if(c != '\"')
					vn.value += c;
			}
		}

		if(!vn.name.isEmpty())
		{
			vn.trim();
			sub.append(vn);
			vn.clear();
		}

		name = name.trimmed();
		value = value.trimmed();

		values.append(value);
		value.clear();

		if(!cont && (state == VAL || state == NAME || state == SUBVAL))
			break;
	}

	index = i;
}


bool QtPop3::decodeMultipart(QStringList &lines, int &index, MessageItem &msg, QString boundary, QString parentBoundary)
{
	int numLines = lines.size();
	bool partFound = false;
	bool headerCompleted = false;
	bool partCompleted = false;
	bool hasParent = !parentBoundary.isEmpty();
	bool endBoundary = false;

	QString nextBoundary;
	QString name;
	QString value;
	QStringList values;
	ValNameList sub;
	int i, j, k;

	for(i=index; i<numLines; i++)
	{
		QString line = lines[i].trimmed();

		if(!partFound)
		{
			if(contains(line, boundary))
				partFound = true;
			else if(hasParent && contains(line, parentBoundary))
				break;
		}
		else
		{
			getValues(lines, i, name, values, sub);
			value = values[0];

			if(equal(name, "content-type"))
			{
				if(equal(value, "multipart/alternative"))
				{
					QString subBoundary;
					if(!sub.getValue("boundary", subBoundary))
					{
						emit errorOccured(tr("Parse error - Multipart message does not contains boundary tag."));
						return false;
					}
					else
						subBoundary.insert(0, "--");

					i ++;

					MessageItem subMsg;
					if(decodeMultipart(lines, i, subMsg, subBoundary, boundary) == false)
						return false;

					msg.subMessages.append(subMsg);
				}
				else if(equal(value, "text/plain"))
				{
					AttachmentItem att;
					headerCompleted = false;

					i ++;
					for(j=i; j<numLines; j++)
					{
						if(!headerCompleted)
						{
							getValues(lines, j, name, values, sub);

							if(sub.containsName("filename"))
								sub.getValue("filename", att.file);
	
							if(name.isEmpty())
								headerCompleted = true;
						}
						else
						{
							line = lines[j];
							if(contains(line, boundary))
							{
								if(!att.file.isEmpty())
									msg.atts.append(att);
								break;
							}
							else if(hasParent && contains(line, parentBoundary))
							{
								endBoundary = true;
								break;
							}
							else
							{
								msg.body += line;
								if(!att.file.isEmpty())
									att.body += line;
							}
						}
					}
					
					i = j;
				}
				else if(equal(value, "application/octet-stream") || equal(value, "text/html"))
				{
					partCompleted = false;
					headerCompleted = false;
					bool base64 = false;
					bool printable = false;
					AttachmentItem att;

					i ++;
					for(j=i; j<numLines; j++)
					{
						if(!headerCompleted)
						{
							getValues(lines, j, name, values, sub);
							value = values[0];

							if(equal(name, "content-transfer-encoding"))
							{
								if(equal(value, "base64"))
									base64 = true;
								else if(equal(value, "quoted-printable"))
									printable = true;
							}
							else if(equal(name, "content-disposition") && equal(value, "attachment"))
							{
								if(sub.size() > 0 && equal(sub[0].name, "filename"))
								{
									att.file = sub[0].value;	
								}
							}
							else if(name.isEmpty())
								headerCompleted = true;
						}
						else
						{
							if(base64 || printable)
							{
								for(k=j; k<numLines; k++)
								{
									line = lines[k].trimmed();

									if(contains(line, boundary))
									{
										partCompleted = true;
										partFound = false;
										break;
									}
									else if(hasParent && contains(line, parentBoundary))
									{
										endBoundary = true;
										break;
									}
									else
									{
										if(printable)
										{
											QStringList parts = line.split('=', QString::SkipEmptyParts);
											if(parts.size() > 0)
											{
												att.body += parts[0];
											
												for(int m=1; m<parts.size(); m++)
												{
													att.body += QByteArray::fromHex(parts[m].toAscii());
												}
											}
										}
										else
											att.body += line; // do zmiany
									}
								}

								if(base64)
								{
									att.body = QByteArray::fromBase64(att.body);
								}

								j = k;
							}
							else
							{
								// unsupported coding
								emit errorOccured(tr("Unsupported coding of attachment."));
								break;
							}
						}

						if(partCompleted || endBoundary)
							break;
					}

					if(partCompleted)
					{
						msg.atts.append(att);
					}
					
					i = j;
				}
			}
		}

		if(endBoundary)
			break;
	}

	index = i;

	return true;
}


bool QtPop3::parseMessage(MessageItem &msg, QStringList *lines)
{
	if(lines == NULL)
		lines = &mResponse;

	QString name;
	QString boundary;
	QStringList values;
	ValNameList sub;

	msg.clear();

	bool multipart = false;
	bool headerCompleted = false;
	int numLines = lines->size();
	int j;

	for(int i=0; i<numLines; i++)
	{
		if(!headerCompleted)
		{
			getValues(*lines, i, name, values, sub);

			if(equal(name, "message-id"))
				msg.msgId = values[0];
			else if(equal(name, "from"))
				msg.from = values[0];
			else if(equal(name, "to"))
			{
				for(int i=0; i<values.size(); i++)
				{
					msg.to.append(values[i]);
				}
			}
			else if(equal(name, "cc"))
			{
				for(int i=0; i<values.size(); i++)
				{
					msg.cc.append(values[i]);
				}
			}
			else if(equal(name, "subject"))
				msg.subject = values[0];
			if(equal(name, "date"))
				msg.date = values[0];
			else if(equal(name, "content-type"))
			{
				headerCompleted = true;

				QString val = values[0];

				multipart = equal(val, "multipart/mixed");
				if(multipart)
				{
					if(!sub.getValue("boundary", boundary))
					{
						emit errorOccured(tr("Parse error - Multipart message does not contains boundary tag."));
						return false;
					}
					else
						boundary.insert(0, "--");
				}
			}
		}
		else
		{
			if(!multipart)
			{
				bool body = false;
				for(j=i; j<numLines; j++)
				{
					QString line = lines->at(j);
					if(!body && line.isEmpty())
						body = true;
					else if(body)
					{
						msg.body += lines->at(j);
					}
				}
				i = j;
			}
			else
			{
				if(decodeMultipart(*lines, i, msg, boundary, "") == false)
					return false;
			}
		}
	}

	return true;
}
