/***************************************************************************
 *   Copyright (C) 2004 by Matthew Wlazlo <mwlazlo@gmail.com>              *
 *   Copyright (C) 2007 by Raphael Geissert <atomo64@gmail.com>            *
 *                                                                         *
 *   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 2 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.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

// define this symbol if you want to write the fetched data to disc
#undef DUMP_PAGES
#define DUMP_DIR "/tmp/"

#include "gmail.h"
#include "gmailparser.h"
#include "gmailwalletmanager.h"
#include "prefs.h"
#include "gmail_constants.h"

#include <kio/job.h>
#include <kio/global.h>
#include <klocale.h>
#include <kdebug.h>

#include <qmutex.h>
#include <qregexp.h>
#include <qtimer.h>

#include <kapp.h>
#include <dcopclient.h>
#include <kcharsets.h>

#ifdef DUMP_PAGES
#include <qfile.h>
#endif

#define MILLISECS(x) (x * 1000)


GMail::GMail() : QObject(0, "GMailNetwork")
{
	mInterval = Prefs::interval();
	mCheckLock = new QMutex();
	mLoginLock = new QMutex();
	mLoginParamsChanged = false;
	isGAP4D = false;
	
	//Any % should be replaced with @ due to a problem with QString not looking for escaped %
	gGMailLoginURL = "https://www.google.com/accounts/ServiceLoginAuth?service=mail";
	gGMailLoginPOSTFormat = "Email=%1&Passwd=%2&signIn=Sign+in&service=mail"
			"&continue=http@3A@2F@2Fmail.google.com@2Fmail@3F"
			"&ltmpl=default&ltmplcache=2&rm=false&rmShown=1";
	gGMailCheckURL = "%1://mail.google.com/mail/?search=query"
			"&q=%2&as_subset=unread&view=tl&start=0";
	gGMailLogOut = "https://mail.google.com/mail/?logout";
	
	gGAP4DLoginURL = "https://www.google.com/a/%1/LoginAction";
	gGAP4DLoginPOSTFormat = "userName=%1&password=%2&at=null&service=mail"
			"&continue=http@3A@2F@2Fmail.google.com@2Fa@2F%3";
	gGAP4DCheckURL = "%1://mail.google.com/a/%2/?search=query"
			"&q=%3&as_subset=unread&view=tl&start=0";
	gGAP4DLogOut = "https://mail.google.com/a/%1/?logout";
	
	mTimer = new QTimer(this);
	connect(mTimer, SIGNAL(timeout()), this, SLOT(slotTimeout()));
	connect(this, SIGNAL(sessionChanged()), 
		this, SLOT(slotSessionChanged()));
}

GMail::~GMail()
{
	delete mCheckLock;
	delete mLoginLock;
}

void GMail::slotSetWalletPassword(bool)
{
	kdDebug() << k_funcinfo << "now, check login params." << endl;
	checkLoginParams();
}

void GMail::checkLoginParams()
{
	QString username = Prefs::gmailUsername();
	const QString& password = GMailWalletManager::instance()->getHash();
	
	if(mUsername == username && mPasswordHash == password
		  || username.length() == 0)
		return;
	
	mUsername = username;
	mPasswordHash = password;
	sessionCookie = QString::null;
	
	useUsername = mUsername;
	useDomain = "";
	isGAP4D = false;
	
	if( mUsername.find("@") != -1 ) {
		
		if ( QString::compare(mUsername.section("@",1),"gmail.com") != 0 &&
			QString::compare(mUsername.section("@",1),"googlemail.com") != 0) {
			kdDebug() << k_funcinfo << mUsername << " seems to be a GAP4D account" << endl;
			isGAP4D = true;
			useDomain = mUsername.section("@",1);
		}
		useUsername = mUsername.section("@",0,0);
	}
	
	kdDebug() << k_funcinfo << "Using " << useUsername << " as username and " << useDomain << " as domain" << endl;
	
	if(!mLoginLock->locked()) {
		
		//Try to log out if a session already exists (because it might be from an other address)
		if(isLoggedIn(false)) {
			kdDebug() << k_funcinfo << "A gmail session was already open, logging out from it" << endl;
			logOut(true);
		}
		
		mLoginFromTimer = false;
		login();
	} else {
		kdDebug() << k_funcinfo << "Login in process. "
			<< "scheduling login for next timeout." << endl;
		mLoginParamsChanged = true;
	}
}

///////////////////////////////////////////////////////////////////////////
// Initial login exchange methods
///////////////////////////////////////////////////////////////////////////
void GMail::login()
{
	if(mLoginLock->tryLock()) {
		emit loginStart();
		
		kdDebug() << k_funcinfo << "Waiting for wallet..." << endl;
		// this will call back to gotWalletPassword().
		// we will continue the process from there.
		GMailWalletManager::instance()->get();
	}
}

void GMail::slotGetWalletPassword(const QString& pass)
{
	QString str, LoginPOSTFormat, LoginURL;
	
	if(isGAP4D) {
		LoginPOSTFormat = QString(gGAP4DLoginPOSTFormat).replace("%3",useDomain);
		LoginURL = QString(gGAP4DLoginURL).arg(useDomain).replace('@','%');
	} else {
		LoginPOSTFormat = gGMailLoginPOSTFormat;
		LoginURL = gGMailLoginURL;
	}
	
	str = QString(LoginPOSTFormat).arg(
			KURL::encode_string(useUsername),
			KURL::encode_string(pass)
					  ).replace('@','%');
	kdDebug() << k_funcinfo << "Requesting login URL" << endl;

	loginRedirection = "";
	
	QCString b(str.utf8());
	QByteArray postData(b);

	// get rid of terminating 0x0
	postData.truncate(b.length());
	
	KIO::TransferJob *job = KIO::http_post(
			LoginURL,
			postData,
   			false);
	job->addMetaData("content-type", "Content-Type: application/x-www-form-urlencoded");
	job->addMetaData("cookies", "auto");
	job->addMetaData("cache", "reload");
	
	connect(job, SIGNAL(result(KIO::Job*)),
		SLOT(slotLoginResult(KIO::Job*)));
	
	connect(job, SIGNAL(data(KIO::Job*, const QByteArray&)),
		SLOT(slotLoginData(KIO::Job*, const QByteArray&)));
	
	connect(job, SIGNAL(redirection(KIO::Job*, const KURL&)),
		SLOT(slotLoginRedirection(KIO::Job*, const KURL&)));
}

void GMail::slotLoginData(KIO::Job *job, const QByteArray &data)
{
	kdDebug() << k_funcinfo << endl;

	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;
	} else {
		QCString str(data, data.size() + 1);
		mLoginBuffer.append(str);
	}
}

void GMail::slotLoginRedirection(KIO::Job *job, const KURL &url)
{
	kdDebug() << k_funcinfo << url.url() << endl;

	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;
	} else {
		loginRedirection = url;
	}
}

void GMail::slotLoginResult(KIO::Job *job)
{	
	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;
		mLoginLock->unlock();
		emit loginDone(false, mLoginFromTimer, job->errorString());
	} else {
		
		QString redirection;
		
		redirection = getRedirectURL(mLoginBuffer);
		dump2File("gmail_login.html", mLoginBuffer);
		
		if( redirection == QString::null ) {
			
			if(!isLoggedIn(false)) {
				if(mLoginBuffer.find("onload") != -1 && (
						mLoginBuffer.find("FixForm") != -1 ||
						mLoginBuffer.find("start_time") != -1)) {
				
					mLoginLock->unlock();
					emit loginDone(false, mLoginFromTimer, i18n("Invalid username or password"));
					return;
				} else {
					kdWarning() << k_funcinfo << " Redirection couldn't be found!" << endl;
					
					mLoginLock->unlock();
					emit loginDone(false, mLoginFromTimer, i18n("GMail's login procedure has changed, check for new version"));
					return;
				}
			} else if (mLoginBuffer.find("?ui=html") != -1 && (
				   		mLoginBuffer.find("nocheckbrowser") != -1 || 
						mLoginBuffer.find("noscript") != -1 )) { 
				kdDebug() << k_funcinfo << "Google is performing dirty JS check, bypassing it" << endl;
				
				if (loginRedirection.isEmpty()) {
					kdWarning() << k_funcinfo << "loginRedirection is empty!" << endl;
					mLoginLock->unlock();
					emit loginDone(false, mLoginFromTimer, i18n("GMail's login procedure has changed, check for new version"));
					return;
				}
				
				mLoginBuffer = "";
				
				KURL _url;
				
				_url = loginRedirection;
				_url.setQuery("ui=html&zy=n");
				
				if (!_url.isValid()) {
					kdWarning() << k_funcinfo << "New _url is invalid!:" << _url.url() << endl;
					mLoginLock->unlock();
					// let's show a nice error message to the user instead
					emit loginDone(false, mLoginFromTimer, i18n("GMail's login procedure has changed, check for new version"));
					return;
				}
				
				postLogin(_url.url());
				
				
			} else {
				//no more redirections?
				kdWarning() << k_funcinfo << "No redirection was found, but seems like we are logged in!" << endl;
				
				mLoginBuffer = "";
				slotPostLoginResult(job);
				return;
			}
		} else {
			mLoginBuffer = "";
			postLogin(redirection);
			// NOTE: LoginLock is still locked()
			return;
		}
	}
}

///////////////////////////////////////////////////////////////////////////
// Post login procedure: Gather cookies
///////////////////////////////////////////////////////////////////////////
void GMail::postLogin(QString url)
{
	// this is expected to be locked.
	if(mLoginLock->locked()) {
		
		QRegExp rx("^(http[s]?://)(.*)$");
		int found;
		
		if(!rx.isValid()) {
			kdWarning() << k_funcinfo << "Invalid RX!\n"
					<< rx.errorString() << endl;
		}
		
		found = rx.search(url);
		
		if(found == -1) {
			kdWarning() <<  "This can't be a valid url!: " << url << endl;
			if (!KURL(url).isValid()) {
				kdError() <<  "This is absolutely a non-valid URL!: " << url << endl;
			}
		}
		
		if(rx.cap(1).compare("https://") != 0) {
			url = (Prefs::useHTTPS()? "https" : "http" );
			url.append("://");
			url.append(rx.cap(2));
		}
		
		mPostLoginBuffer = "";
		
		kdDebug() << k_funcinfo << "Starting job to " << url << endl;

		KIO::TransferJob *job = KIO::get(url, true, false);
		job->addMetaData("cookies", "auto");
		job->addMetaData("cache", "reload");

		connect(job, SIGNAL(result(KIO::Job*)),
			SLOT(slotPostLoginResult(KIO::Job*)));

		connect(job, SIGNAL(data(KIO::Job*, const QByteArray&)),
			SLOT(slotPostLoginData(KIO::Job*, const QByteArray&)));
	} else {
		kdWarning() << k_funcinfo << "mLoginLock is not locked!" << endl;
	}
}

void GMail::slotPostLoginData(KIO::Job *job, const QByteArray &data)
{
	kdDebug() << k_funcinfo << endl;

	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;
	} else {
		QCString str(data, data.size() + 1);
		mPostLoginBuffer.append(str);
	}
}

void GMail::slotPostLoginResult(KIO::Job *job)
{
	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;

		mLoginLock->unlock();
		emit loginDone(false, mLoginFromTimer, job->errorString());
	} else {
		
		mLoginLock->unlock();
		
		if(isLoggedIn()) {
			
			mPostLoginBuffer = "";
			emit loginDone(true, mLoginFromTimer);
			checkGMail();
			
		} else {
			
			QString url = getRedirectURL(mPostLoginBuffer);
			
			if(url == QString::null) {
				dump2File("gmail_postlogin.html", mPostLoginBuffer);
				mPostLoginBuffer = "";
				
				emit loginDone(false, mLoginFromTimer, 
				       i18n("Unknown error retrieving cookies"));
			} else {
				kdDebug() << k_funcinfo << "Found an other redirect!: " << url << endl;
				mLoginLock->tryLock();
				postLogin(url);
			}
			
		}
	}
}

///////////////////////////////////////////////////////////////////////////
// Email checking methods
///////////////////////////////////////////////////////////////////////////
void GMail::checkGMail()
{
	if(isLoggedIn() && mCheckLock->tryLock()) {
		kdDebug() << k_funcinfo << "Starting check..." << endl;
		// stop timer. start again when we have some sort of result.
		mTimer->stop();
		emit checkStart();

		QString url;

		if(!isGAP4D)
			url = QString(gGMailCheckURL).arg(
				(Prefs::useHTTPS()
					? "https" 
					: "http" ),
				KURL::encode_string(Prefs::searchFor())
							 ).replace('@','%');
		else
			url = QString(gGAP4DCheckURL).arg(
				(Prefs::useHTTPS()
					? "https" 
					: "http" ),
				useDomain,
				KURL::encode_string(Prefs::searchFor())
							 ).replace('@','%');

		kdDebug() << k_funcinfo << "GET: " << url << endl;

		KIO::TransferJob *job = KIO::get(url, true, false);
		job->addMetaData("cookies", "auto");
		job->addMetaData("cache", "reload");

		connect(job, SIGNAL(result(KIO::Job*)),
			SLOT(slotCheckResult(KIO::Job*)));

		connect(job, SIGNAL(data(KIO::Job*, const QByteArray&)),
			SLOT(slotCheckData(KIO::Job*, const QByteArray&)));
	}
}

void GMail::slotCheckData(KIO::Job *job, const QByteArray &data)
{
	if(job->error() != 0) {
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;
	} else {
		QCString str(data, data.size() + 1);
		mPageBuffer.append(str);
	}
}

void GMail::slotCheckResult(KIO::Job *job)
{
	if(job->error() != 0) 
		kdWarning() << k_funcinfo << "error: " << job->errorString() << endl;

	kdDebug() << k_funcinfo << "Check finished." << endl;

	dump2File("gmail_data.html", mPageBuffer);
	
	static QRegExp rx("top\\.location=[\"\']http[s]?://www\\.google\\.com/accounts/ServiceLogin");
	static QRegExp rx2("gmail_error=[0-9]*;");
	int found;
	
	if(!rx.isValid()) {
		kdWarning() << k_funcinfo << "Invalid RX!\n"
				<< rx.errorString() << endl;
	}
	
	if(!rx2.isValid()) {
		kdWarning() << k_funcinfo << "Invalid RX2!\n"
				<< rx2.errorString() << endl;
	}
	
	found = rx.search(mPageBuffer);
			
	if( found != -1 || !isLoggedIn() ) {
		kdWarning() << k_funcinfo << "User is not logged in!" << endl;
		
		mPageBuffer = "";
		mCheckLock->unlock();
		
		//Clearing values will force login
		mUsername = "";
		mPasswordHash = "";
		checkLoginParams();
		
		return;
	} 
			
	found = rx2.search(mPageBuffer);
	
	if( found != -1 ) {
		kdWarning() << k_funcinfo << "Gmail is unavailable because of server-side errors!" << endl;
		
		mPageBuffer = "";
		mCheckLock->unlock();
		
		// let's try again in 60 seconds
		setInterval(60, true);
		return;
	}
	
	setInterval(mInterval, true);
	emit checkDone(mPageBuffer);
	mPageBuffer = "";
	
	mCheckLock->unlock();
}

///////////////////////////////////////////////////////////////////////////
// Other methods...
///////////////////////////////////////////////////////////////////////////
void GMail::slotTimeout()
{
	if(!isLoggedIn() || mLoginParamsChanged) {
		mLoginFromTimer = true;
		login();
	} else {
		// do the check
		mCheckFromTimer = true;
		checkGMail();
	}
}

void GMail::setInterval(unsigned int i, bool forceStart)
{
	bool running;
	
	if(i > Prefs::self()->intervalItem()->minValue().toUInt()) {
		mInterval = i;
		
		if (forceStart)
			running = true;
		else
			running = mTimer->isActive();
		
		mTimer->changeInterval(MILLISECS(mInterval));
		
		//Prevent starting the timer when it wasn't needed to
		if(!running)
			mTimer->stop();
	}
}

void GMail::setInterval(unsigned int i)
{
	setInterval(i, false);
}

bool GMail::isLoggedIn(bool lockCheck)
{
	bool ret = false;
	
	if( !lockCheck || ( lockCheck && !mLoginLock->locked() ) ) {
		if(cookieExists(ACTION_TOKEN_COOKIE))
			ret = true;
		else kdDebug() << k_funcinfo << ACTION_TOKEN_COOKIE << " wasn't found!" << endl;
	} else kdDebug() << "mLoginLock is locked" << endl;

	return ret;
}

bool GMail::isLoggedIn()
{
	return isLoggedIn(true);
}

void GMail::slotCheckGmail()
{
	mCheckFromTimer = false;
	if(!isLoggedIn()) {
		login();
	} else {
		checkGMail();
	}
}

QString GMail::getURLPart()
{
	QString part;
	
	if(isGAP4D) {
		part = QString("a/%1").arg(useDomain);
	} else {
		part = "mail";
	}
	
	return part;
}

void GMail::logOut(bool force)
{
	if(!isLoggedIn() && !force)
		return;
	
	emit logingOut();
	
	sessionCookie = QString::null;
	
	QString logoutUrl = (!isGAP4D)? gGMailLogOut : QString(gGAP4DLogOut).arg(useDomain);
	
	KIO::TransferJob *job = KIO::get(logoutUrl, true, false);
	job->addMetaData("cookies", "auto");
	job->addMetaData("cache", "reload");
	kdDebug() << "Loging out! " << logoutUrl << endl;
	sleep(1);
}

void GMail::logOut()
{
	logOut(false);
}

void GMail::slotLogOut()
{
	logOut(true);
}

void GMail::dump2File(const QString filename, const QString data)
{
#ifdef DUMP_PAGES
	QString dump_dir = DUMP_DIR;
		
	dump_dir += filename;
	
	kdDebug() << k_funcinfo << "Dumping data to file " << dump_dir << endl;
		
	QFile f(dump_dir);
		
	f.open( IO_WriteOnly );
		
	QTextStream stream(&f);
	stream << data;

	f.close();
#endif
}

//From kcookiejartest.cpp
QString GMail::findCookies(QString url)
{
	QCString replyType;
	QByteArray params, reply;
	QDataStream stream(params, IO_WriteOnly);
	stream << url;
	if (!kapp->dcopClient()->call("kcookiejar", "kcookiejar",
	     "findCookies(QString)", params, replyType, reply))
	{
		kdWarning() << k_funcinfo << "There was some error using DCOP!" << endl;
		return QString::null;
	}

	QDataStream stream2(reply, IO_ReadOnly);
	if(replyType != "QString")
	{
		kdWarning() << k_funcinfo << "DCOP function findCookies(...) return " << replyType.data() << ", expected QString" << endl;
		return QString::null;
	}

	QString result;
	stream2 >> result;
	return result;
}

bool GMail::cookieExists(QString cookieName,QString url)
{
	kdDebug() << k_funcinfo << "Searching for cookie " << cookieName << " at " << url << endl;;
	
	QString cookies;
	int found;
	bool ret = false;
	
	cookies = findCookies(url);
	
	if(cookies.length() == 0)
		return false;
	
	cookies += ";";
	
	QRegExp search(" (" + QRegExp::escape(cookieName) + ")=([^;]*)");
	
	if(!search.isValid()) {
		kdWarning() << k_funcinfo << "Invalid RX!\n"
				<< search.errorString() << endl;
	}
	
	 
	found = search.search(cookies);
	ret = ( found != -1 );
	
	if(ret && cookieName.compare(ACTION_TOKEN_COOKIE) == 0) {
		if(sessionCookie.compare(search.cap(2)) != 0) {
			QString oldSessionCookie;
			
			oldSessionCookie = sessionCookie;
			sessionCookie = search.cap(2);
			
			if(oldSessionCookie.length() != 0)
				emit sessionChanged();
		}
	}
	
	kdDebug() << cookieName << " was " << (ret? "FOUND":"NOT FOUND") << endl;
	
	return ret;
}

bool GMail::cookieExists(QString cookieName)
{
	return cookieExists(cookieName, QString("https://mail.google.com/%1/").arg(getURLPart()));
}

void GMail::slotSessionChanged()
{
	logOut();
	
	if(mCheckLock->locked())
		mCheckLock->unlock();
	
	//Clearing values will force login
	mUsername = "";
	mPasswordHash = "";
	checkLoginParams();
}

QString GMail::getRedirectURL(QString buffer)
{	
	static QRegExp metaRX("<meta[ ]+.*url='(http[s]?://[^']+)'.*>");
	static QRegExp jsRX  ("location\\.replace[ ]*\\([ ]*['\"](http[s]?://[^'\"]+)['\"][ ]*\\)");
	int found;
	QString url, jsurl;
	
	kdDebug() << k_funcinfo << endl;

	if(!metaRX.isValid()) {
		kdWarning() << k_funcinfo << "Invalid metaRX!\n"
				<< metaRX.errorString() << endl;
	}

	if(!jsRX.isValid()) {
		kdWarning() << k_funcinfo << "Invalid jsRX!\n"
				<< jsRX.errorString() << endl;
	}
	
	found = metaRX.search(buffer);
			
	if( found == -1 ) {
		return QString::null;
	}
	
	url = KCharsets::resolveEntities(metaRX.cap(1));
	
	// now let's check if there's a JS redirection (location.replace)
	found = jsRX.search(buffer);
	
	if( found == -1 ) {
		return url;
	}
	
	jsurl = GMailParser::cleanUpData(jsRX.cap(1));
	
	// if both match it's ok
	if (url.compare(jsurl) == 0) {
		kdDebug() << k_funcinfo << "Found redirection to " << url << endl;
		return url;
	} else {
		// otherwise use JS redirection
		kdDebug() << k_funcinfo << "META and JS redirections do not match! META: " << url << "JS: " << jsurl << endl;
		return jsurl;
	}
	
}

#include "gmail.moc"


syntax highlighted by Code2HTML, v. 0.9.1