/*
** Copyright 2002-2004, Double Precision Inc.
**
** See COPYING for distribution information.
*/
#include "libmail_config.h"
#include "mbox.H"
#include "misc.H"
#include "mboxlock.H"
#include "mboxopen.H"
#include "mboxexpunge.H"
#include "mboxmagictag.H"
#include "mboxadd.H"
#include "file.H"
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include "unicode/unicode.h"

#if HAVE_DIRENT_H
# include <dirent.h>
# define NAMLEN(dirent) strlen((dirent)->d_name)
#else
# define dirent direct
# define NAMLEN(dirent) (dirent)->d_namlen
# if HAVE_SYS_NDIR_H
#  include <sys/ndir.h>
# endif
# if HAVE_SYS_DIR_H
#  include <sys/dir.h>
# endif
# if HAVE_NDIR_H
#  include <ndir.h>
# endif
#endif

#include <vector>

#undef feof

using namespace std;

mail::mbox::folder::folder(string pathArg,
			   mail::mbox &mboxArg)
	: mail::folder(&mboxArg), path(pathArg),
	  name(defaultName(pathArg)), mboxAccount(mboxArg),
	  saveHasMessages(false),
	  saveHasFolders(false)
{
}

mail::mbox::folder::folder(const mail::mbox::folder &f)
	: mail::folder(&f.mboxAccount), path(f.path), name(f.name),
	  mboxAccount(f.mboxAccount),
	  saveHasMessages(false),
	  saveHasFolders(false)
{
}

mail::mbox::folder::~folder()
{
}

string mail::mbox::folder::defaultName(string path)
{
	string::iterator b, e, c;

	b=path.begin();
	e=path.end();
	c=b;

	while (b != e)
		if (*b++ == '/')
			c=b;

	path=string(c, e);

	char *p=libmail_u_convert_tobuf(path.c_str(),
					unicode_x_imap_modutf7,
					unicode_default_chset(),
					NULL);

	if (p)
	{
		try {
			path=p;
			free(p);
		} catch (...) {
			free(p);
			LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
		}
	}

	return path;
}

void mail::mbox::folder::sameServerAsHelperFunc() const
{
}

string mail::mbox::folder::getName() const
{
	return name;
}

string mail::mbox::folder::getPath() const
{
	return path;
}

bool mail::mbox::folder::hasMessages() const
{
	struct stat stat_buf;

	return saveHasMessages || path == "INBOX" ||
		(stat(path.c_str(), &stat_buf) == 0
		 && !S_ISDIR(stat_buf.st_mode));
}

bool mail::mbox::folder::hasSubFolders() const
{
	return saveHasFolders || !hasMessages();
}

bool mail::mbox::folder::isParentOf(string pathArg) const
{
	return pathArg.size() > path.size() &&
		pathArg.substr(0, path.size() + 1 ) == (path + "/");
}

void mail::mbox::folder::hasMessages(bool arg)
{
	saveHasMessages=arg;
}

void mail::mbox::folder::hasSubFolders(bool arg)
{
	saveHasFolders=arg;
}

///////////////////////////////////////////////////////////////////////////
//

class mail::mbox::StatusTask : public mail::mbox::TimedTask {

	string folder;

	mail::callback::folderInfo &info;

	bool count(int fd);

public:
	StatusTask(string folderArg, mail::mbox &mboxAccount,
		   mail::callback &callbackArg,
		   mail::callback::folderInfo &infoArg);
	~StatusTask();

	bool doit();
};

mail::mbox::StatusTask::StatusTask(string folderArg, mail::mbox &mboxAccount,
				   mail::callback &callback,
				   mail::callback::folderInfo &infoArg)
	: TimedTask(mboxAccount, callback), folder(folderArg), info(infoArg)
{
}

mail::mbox::StatusTask::~StatusTask()
{
}

bool mail::mbox::StatusTask::doit()
{
	if (folder == mboxAccount.currentFolder)
	{
		info.messageCount=mboxAccount.index.size();

		size_t i;

		for (i=0; i<info.messageCount; i++)
			if (mboxAccount.index[i].unread)
				info.unreadCount++;
		info.success();
		callback.success("OK");
		done();
		return true;
	}

	if (info.fastInfo)  // mboxes are expensive
	{
		callback.success("OK");
		done();
		return true;
	}

	if (folder == "INBOX")
	{
		int fd=::open(mboxAccount.inboxSpoolPath.c_str(),
			      O_RDWR|O_CREAT, 0600);

		if (fd >= 0)
			close(fd);

		fd=::open(mboxAccount.inboxMailboxPath.c_str(), O_RDWR|O_CREAT, 0600);

		if (fd >= 0)
			close(fd);

		mail::mbox::lock inbox_lock(mboxAccount.inboxSpoolPath);
		mail::mbox::lock mailbox_lock(mboxAccount.inboxMailboxPath);

		if (!inbox_lock(true) || !count(inbox_lock.getfd()))
		{
			if (errno != EAGAIN && errno != EEXIST
			    && errno != ENOENT)
			{
				fail(string("INBOX: ") + strerror(errno));
				return true;
			}
		}

		if (!mailbox_lock(true) || !count(mailbox_lock.getfd()))
		{
			if (errno != EAGAIN && errno != EEXIST
			    && errno != ENOENT)
			{
				fail(string("INBOX: ") + strerror(errno));
				return true;
			}
		}
	}
	else
	{
		mail::mbox::lock mailbox_lock(folder);

		if (!mailbox_lock(true) || !count(mailbox_lock.getfd()))
		{
			if (errno != EAGAIN && errno != EEXIST)
			{
				fail(folder + ": " + strerror(errno));
				return true;
			}

			return false;
		}
	}

	info.success();
	callback.success("OK");
	done();
	return true;
}

bool mail::mbox::StatusTask::count(int fd)
{
	mail::file fp(fd, "r");

	if (!fp)
		return false;

	bool firstLine=true;

	while (!feof(fp))
	{
		string l=fp.getline();

		if (l.size() == 0)
			continue;

		if (strncmp(l.c_str(), "From ", 5) == 0)
		{
			firstLine=false;
			info.messageCount++;
			info.unreadCount++;

			mail::mboxMagicTag tag(fp.getline(),
					       mboxAccount.keywordHashtable);

			if (!tag.good())
				continue;

			mail::messageInfo msginfo=tag.getMessageInfo();

			if (!msginfo.unread)
				--info.unreadCount;
		}

		if (firstLine)
			break; // Not an mboxAccount file.
	}
	return true;
}

void mail::mbox::folder::getParentFolder(callback::folderList &callback1,
					 callback &callback2) const
{
	if (isDestroyed(callback2))
		return;

	size_t n;

	if (path == "INBOX" || path == mboxAccount.rootPath ||
	    (n=path.rfind('/')) == std::string::npos)
	{
		mail::mbox::folder dummy("", mboxAccount);

		vector<const mail::folder *> array;

		array.push_back(&dummy);
		callback1.success(array);
		callback2.success("OK");
		return;
	}

	mboxAccount.findFolder(path.substr(0, n), callback1, callback2);
}

void mail::mbox::folder::readFolderInfo( mail::callback::folderInfo
					 &callback1,
					 mail::callback &callback2) const
{
	if (isDestroyed(callback2))
		return;

	callback1.messageCount=0;
	callback1.unreadCount=0;

	if (path.size() == 0)
	{
		callback1.success();
		callback2.success("OK");
		return;
	}

	mboxAccount.installTask(new mail::mbox::StatusTask(path, mboxAccount, callback2,
						    callback1));
}

/////////////////////////////////////////////////////////////////////////////

void mail::mbox::folder::readSubFolders( mail::callback::folderList &callback1,
					 mail::callback &callback2) const
{
	if (isDestroyed(callback2))
		return;

	if (path.size() == 0)
	{
		mboxAccount.readTopLevelFolders(callback1, callback2);
		return;
	}

	vector<folder *> folderList;

	vector<folder *>::iterator b, e;

	DIR *dirp=opendir(path.c_str());

	try {
		struct dirent *de;

		while (dirp && (de=readdir(dirp)) != NULL)
		{
			char *p;

			if (strcmp(de->d_name, "..") == 0 ||
			    strcmp(de->d_name, ".") == 0 ||
			    ((p=strrchr(de->d_name, '~')) && p[1] == 0))
				continue;

			for (p=de->d_name; *p; p++)
				if (*p == '.')
					if (strcmp(p, ".lock") == 0 ||
					    strncmp(p, ".lock.", 6) == 0)
						break;

			if (*p)
				continue;

			string name=de->d_name;

			string fpath=path + "/" + name;

			folder *f=new folder(fpath, mboxAccount);

			if (!f)
				LIBMAIL_THROW("Out of memory.");

			try {
				folderList.push_back(f);
			} catch (...) {
				delete f;
				LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
			}
		}

		vector<const mail::folder *> folders;

		b=folderList.begin();
		e=folderList.end();

		while (b != e)
			folders.push_back(*b++);

		callback1.success(folders);

		b=folderList.begin();
		e=folderList.end();

		while (b != e)
			delete *b++;

		if (dirp)
			closedir(dirp);
	} catch (...) {
		b=folderList.begin();
		e=folderList.end();

		while (b != e)
			delete *b++;

		if (dirp)
			closedir(dirp);
		LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
	}

	callback2.success("OK");
}

mail::addMessage *mail::mbox::folder::addMessage(mail::callback &callback)
	const
{
	mail::mbox::folder::add *f
		=new mail::mbox::folder::add(mboxAccount, path, callback);

	if (!f)
	{
		callback.fail(path + ": " + strerror(errno));
		return NULL;
	}

	return f;
}

void mail::mbox::folder::createSubFolder(string name,
					 bool isDirectory,
					 mail::callback::folderList &callback1,
					 mail::callback &callback2) const
{
	string fpath;

	char *p=libmail_u_convert_tobuf(name.c_str(), unicode_default_chset(),
					unicode_x_imap_modutf7 " ./~:", NULL);

	if (!p)
		LIBMAIL_THROW("Out of memory.");

	try {
		fpath=path + "/" + p;
		free(p);
	} catch (...) {
		free(p);
		LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
	}

	if (isDirectory) {
		if (mkdir(fpath.c_str(), 0700) < 0)
		{
			callback2.fail(fpath + ": " + strerror(errno));
			return;
		}
	} else {
		int fd= ::open(fpath.c_str(), O_RDWR|O_CREAT|O_EXCL, 0600);

		if (fd < 0)
		{
			callback2.fail(fpath + ": " + strerror(errno));
			return;
		}
		close(fd);
	}

	folder newFolder(fpath, mboxAccount);

	vector<const mail::folder *> folderList;

	folderList.push_back(&newFolder);
	callback1.success(folderList);
	callback2.success("OK");
}

void mail::mbox::folder::create(bool isDirectory,
				mail::callback &callback) const
{
	if (isDirectory) {
		if (mkdir(path.c_str(), 0700) < 0)
		{
			callback.fail(path + ": " + strerror(errno));
			return;
		}
	} else {
		int fd= ::open(path.c_str(), O_RDWR|O_CREAT|O_EXCL, 0600);

		if (fd < 0)
		{
			callback.fail(path + ": " + strerror(errno));
			return;
		}
		close(fd);
	}

	callback.success("OK");
}

void mail::mbox::folder::destroy(mail::callback &callback, bool destroyDir)
	const
{
	if (destroyDir) {

		// Clean up garbage first.

		DIR *dirp=opendir(path.c_str());

		try {
			struct dirent *de;

			while (dirp && (de=readdir(dirp)) != NULL)
			{
				const char *p=strrchr(de->d_name, '~');

				if (p && p[1] == 0) // Temp file?
				{
					string s=path + "/" +
						string((const char *)
						       de->d_name, p);

					struct stat stat_buf;

					if (stat(s.c_str(), &stat_buf) < 0)
					{
						s += "~";
						unlink(s.c_str());
					}
					continue;
				}

				for (p=de->d_name; *p; p++)
				{
					if (strncmp(p, ".lock.", 6) == 0)
					{
						string s=path + "/" +
							de->d_name;

						struct stat stat_buf;

						if (stat(s.c_str(), &stat_buf)
						    == 0 &&
						    stat_buf.st_mtime + 120
						    < time(NULL))
							break;
					}

					if (strcmp(p, ".lock") == 0)
					{
						string s=path + "/"
							+ string((const char *)
								 de->d_name,
								 p);
						struct stat stat_buf;

						if (stat(s.c_str(), &stat_buf)
						    < 0)
							break;
					}
				}

				if (*p)
				{
					string s=path + "/" + de->d_name;

					unlink(s.c_str());
				}
			}

			if (dirp)
				closedir(dirp);
		} catch (...) {
			if (dirp)
				closedir(dirp);
			LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
		}

		if (rmdir(path.c_str()) < 0)
		{
			callback.fail(path + ": " + strerror(errno));
			return;
		}
	} else {
		if (unlink(path.c_str()) < 0)
		{
			callback.fail(path + ": " + strerror(errno));
			return;
		}
	}

	callback.success("OK");
}

class mail::mbox::RenameTask : public mail::mbox::LockTask {

	string filename;
	string newpath;
	string newname;
	mail::callback::folderList &callback1;

public:
	RenameTask(mbox &mboxAccount,
		   mail::callback::folderList &callback1Arg,
		   mail::callback &callbackArg,
		   string filenameArg,
		   string newpathArg,
		   string newnameArg);
	~RenameTask();

	bool locked(mail::mbox::lock &mlock, std::string path);
	bool locked(mail::file &);
};

mail::mbox::RenameTask::RenameTask(mbox &mboxAccount,
				   mail::callback::folderList &callback1Arg,
				   mail::callback &callbackArg,
				   string filenameArg,
				   string newpathArg,
				   string newnameArg)
	: LockTask(mboxAccount, callbackArg, filenameArg),
	  filename(filenameArg),
	  newpath(newpathArg),
	  newname(newnameArg),
	  callback1(callback1Arg)
{
}

mail::mbox::RenameTask::~RenameTask()
{
}

bool mail::mbox::RenameTask::locked(mail::mbox::lock &mlock, std::string path)
{
	mail::callback *volatile cb= &callback;
	string msg="Folder renamed.";

	if (link(filename.c_str(), newpath.c_str()) < 0)
	{
		fail(strerror(errno));
		return true;
	}
	unlink(filename.c_str());

	mail::mbox::folder newFolder(newpath, mboxAccount);

	vector<const mail::folder *> folders;

	folders.push_back(&newFolder);
	callback1.success( folders );

	try {
		done();
	} catch (...) {
	}
	cb->success(msg);
	return true;
}

bool mail::mbox::RenameTask::locked(mail::file &)
{
	return true;
}

void mail::mbox::folder::renameFolder(const mail::folder *newParent,
				      std::string newName,
				      mail::callback::folderList &callback1,
				      mail::callback &callback) const
{
	if (isDestroyed(callback))
		return;

	if (path == "INBOX")
	{
		callback.fail("Not implemented.");
		return;
	}

	if (mboxAccount.currentFolder.size() > 0)
	{
		if (mboxAccount.currentFolder == path ||
		    (path + "/") ==
		    mboxAccount.currentFolder.substr(0, path.size()+1))
		{
			callback.fail("Cannot RENAME currently open folder.");
			return;
		}
	}

	// The name of the folder is translated from the local charset
	// to modified UTF-7 (Courier-IMAP compatibility), with the following
	// blacklisted characters:

	string nameutf7=mail::iconvert::convert(newName,
						unicode_default_chset(),
						unicode_x_imap_modutf7 " ./~:");

	string newpath=(newParent ? newParent->getPath() + "/":
			string("")) + nameutf7;

	if (newpath.size() == 0 || newpath[0] != '/')
		newpath=mboxAccount.rootPath + "/" + newpath;

	struct stat stat_buf;

	if (stat(path.c_str(), &stat_buf) == 0 &&
	    S_ISDIR(stat_buf.st_mode))
	{
		if (rename(path.c_str(), newpath.c_str()) < 0)
		{
			callback.fail(strerror(errno));
			return;
		}

		mail::mbox::folder newFolder(newpath, mboxAccount);

		vector<const mail::folder *> folders;

		folders.push_back(&newFolder);
		callback1.success( folders );
		callback.success("Mail folder renamed.");
		return;
	}

	mboxAccount.installTask(new mail::mbox::RenameTask(mboxAccount,
							   callback1,
							   callback,
							   path, newpath,
							   newName));
}

mail::folder *mail::mbox::folder::clone() const
{
	return new mail::mbox::folder(path, mboxAccount);
}

string mail::mbox::folder::toString() const
{
	string s="";
	string::const_iterator b=name.begin(), e=name.end();

	while (b != e)
	{
		if (*b == ':' || *b == '\\')
			s += "\\";

		s += *b++;
	}

	// If path is in the home directory, drop $HOME

	string p=path;

	if (!isDestroyed())
	{
		string h=mboxAccount.rootPath;

		if (p == h)
			p="";
		else
		{
			h += "/";
			if (p.size() > h.size() && p.substr(0, h.size()) == h)
				p=p.substr(h.size());
		}
	}

	return s + ":" + p;
}

void mail::mbox::folder::open(mail::callback &openCallback,
			      snapshot *restoreSnapshot,
			      mail::callback::folder &folderCallback) const
{
	if (mboxAccount.currentFolder.size() == 0 &&
	    mboxAccount.folderDirty)
	{
		closeCallback *cc=new closeCallback(&mboxAccount,
						    openCallback,
						    folderCallback,
						    path);

		if (!cc)
		{
			openCallback.fail("Out of memory.");
			return;
		}

		try {
			mboxAccount
				.installTask(new mail::mbox
					     ::ExpungeTask(mboxAccount, *cc,
							   false, NULL));
		} catch (...) {
			delete cc;
			LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
		}
		return;
	}

	mboxAccount.installTask(new mail::mbox::OpenTask(mboxAccount, path, openCallback,
						  &folderCallback));
}

mail::mbox::folder::closeCallback::closeCallback(mail::mbox *origmboxArg,
						 mail::callback
						 &origCallbackArg,
						 mail::callback::folder
						 &origFolderCallbackarg,
						 string origPathArg)
	: origmbox(origmboxArg), origCallback(origCallbackArg),
	  origFolderCallback(origFolderCallbackarg),
	  origPath(origPathArg)
{
}

mail::mbox::folder::closeCallback::~closeCallback()
{
}

void mail::mbox::folder::closeCallback::success(string msg)
{
	if (origmbox.isDestroyed())
	{
		origCallback.success(msg);
		delete this;
	}

	try {
		origmbox->installTask(new OpenTask(*origmbox,
						   origPath,
						   origCallback,
						   &origFolderCallback));
		delete this;
	} catch (...) {
		delete this;
		LIBMAIL_THROW(LIBMAIL_THROW_EMPTY);
	}
}

void mail::mbox::folder::closeCallback::fail(string msg)
{
	origCallback.fail(msg);
	delete this;
}
