//
// 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/IMAPUtils.hpp"
#include "vmime/net/imap/IMAPStore.hpp"

#include "vmime/net/message.hpp"
#include "vmime/net/folder.hpp"

#include <sstream>
#include <iterator>
#include <algorithm>


namespace vmime {
namespace net {
namespace imap {


static const char* IMAP_MONTH_NAMES[12] = {
	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};


// static
const string IMAPUtils::quoteString(const string& text) {

	//
	// ATOM_CHAR       ::= <any CHAR except atom_specials>
	//
	// atom_specials   ::= "(" / ")" / "{" / SPACE / CTL /
	//                     list_wildcards / quoted_specials
	//
	// list_wildcards  ::= "%" / "*"
	//
	// quoted_specials ::= <"> / "\"
	//
	// CHAR            ::= <any 7-bit US-ASCII character except NUL,
	//                      0x01 - 0x7f>
	//
	// CTL             ::= <any ASCII control character and DEL,
	//                      0x00 - 0x1f, 0x7f>
	//

	bool needQuoting = text.empty();

	for (string::const_iterator it = text.begin() ;
	     !needQuoting && it != text.end() ; ++it) {

		const unsigned char c = *it;

		switch (c) {

			case '(':
			case ')':
			case '{':
			case 0x20:   // SPACE
			case '%':
			case '*':
			case '"':
			case '\\':

				needQuoting = true;
				break;

			default:

				if (c <= 0x1f || c >= 0x7f) {
					needQuoting = true;
				}
			}
	}

	if (needQuoting) {

		string quoted;
		quoted.reserve((text.length() * 3) / 2 + 2);

		quoted += '"';

		for (string::const_iterator it = text.begin() ; it != text.end() ; ++it) {

			const unsigned char c = *it;

			if (c == '\\' || c == '"') {
				quoted += '\\';
			}

			quoted += c;
		}

		quoted += '"';

		return quoted;

	} else {

		return text;
	}
}


const string IMAPUtils::pathToString(
	const char hierarchySeparator,
	const folder::path& path
) {

	string result;

	for (size_t i = 0 ; i < path.getSize() ; ++i) {

		if (i > 0) result += hierarchySeparator;
		result += toModifiedUTF7(hierarchySeparator, path[i]);
	}

	return (result);
}


const folder::path IMAPUtils::stringToPath(
	const char hierarchySeparator,
	const string& str
) {

	folder::path result;
	string::const_iterator begin = str.begin();

	for (string::const_iterator it = str.begin() ; it != str.end() ; ++it) {

		if (*it == hierarchySeparator) {
			result /= fromModifiedUTF7(string(begin, it));
			begin = it + 1;
		}
	}

	if (begin != str.end()) {
		result /= fromModifiedUTF7(string(begin, str.end()));
	}

	return result;
}


const string IMAPUtils::toModifiedUTF7(
	const char hierarchySeparator,
	const folder::path::component& text
) {

	// We will replace the hierarchy separator with an equivalent
	// UTF-7 sequence, so we compute it here...
	const char base64alphabet[] =
		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,=";

	const unsigned int hs = static_cast <unsigned int>(static_cast <unsigned char>(hierarchySeparator));

	string hsUTF7;
	hsUTF7.resize(3);

	hsUTF7[0] = base64alphabet[0];
	hsUTF7[1] = base64alphabet[(hs & 0xF0) >> 4];
	hsUTF7[2] = base64alphabet[(hs & 0x0F) << 2];

	// iconv() is buggy with UTF-8 to UTF-7 conversion, so we do it "by hand".
	// This code is largely inspired from "imap/utf7.c", in mutt 1.4.
	// Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org>

	// WARNING: This may throw "exceptions::charset_conv_error"
	const string cvt = text.getConvertedText(charset(charsets::UTF_8));

	// In the worst case we convert 2 chars to 7 chars.
	// For example: "\x10&\x10&..." -> "&ABA-&-&ABA-&-...".
	string out;
	out.reserve((cvt.length() / 2) * 7 + 6);

	int b = 0, k = 0;
	bool base64 = false;

	size_t remaining = cvt.length();

	for (size_t i = 0, len = cvt.length() ; i < len ; ) {

		const unsigned char c = cvt[i];

		// Replace hierarchy separator with an equivalent UTF-7 Base64 sequence
		if (!base64 && c == hierarchySeparator) {

			out += "&" + hsUTF7 + "-";

			++i;
			--remaining;
			continue;
		}

		size_t n = 0;
		int ch = 0;

		if (c < 0x80) {
			ch = c, n = 0;
		} else if (c < 0xc2) {
			return "";
		} else if (c < 0xe0) {
			ch = c & 0x1f, n = 1;
		} else if (c < 0xf0) {
			ch = c & 0x0f, n = 2;
		} else if (c < 0xf8) {
			ch = c & 0x07, n = 3;
		} else if (c < 0xfc) {
			ch = c & 0x03, n = 4;
		} else if (c < 0xfe) {
			ch = c & 0x01, n = 5;
		} else {
			return "";
		}

		if (n > remaining) {
			return "";  // error
		}

		++i;
		--remaining;

		for (size_t j = 0 ; j < n ; j++) {

			if ((cvt[i + j] & 0xc0) != 0x80) {
				return "";  // error
			}

			ch = (ch << 6) | (cvt[i + j] & 0x3f);
		}

		if (n > 1 && !(ch >> (n * 5 + 1))) {
			return "";  // error
		}

		i += n;
		remaining -= n;

		if (ch < 0x20 || ch >= 0x7f) {

			if (!base64) {
				out += '&';
				base64 = true;
				b = 0;
				k = 10;
			}

			if (ch & ~0xffff) {
				ch = 0xfffe;
			}

			out += base64alphabet[b | ch >> k];

			k -= 6;

			for ( ; k >= 0 ; k -= 6) {
				out += base64alphabet[(ch >> k) & 0x3f];
			}

			b = (ch << (-k)) & 0x3f;
			k += 16;

		} else {

			if (base64) {

				if (k > 10) {
					out += base64alphabet[b];
				}

				out += '-';
				base64 = false;
			}

			out += static_cast <char>(ch);

			if (ch == '&') {
				out += '-';
			}
		}
	}

	if (base64) {

		if (k > 10) {
			out += base64alphabet[b];
		}

		out += '-';
	}

	return (out);
}


const folder::path::component IMAPUtils::fromModifiedUTF7(const string& text) {

	// Transcode from modified UTF-7 (RFC-2060).
	string out;
	out.reserve(text.length());

	bool inB64sequence = false;
	bool plusOutput = false;
	unsigned char prev = 0;

	for (string::const_iterator it = text.begin() ; it != text.end() ; ++it) {

		const unsigned char c = *it;

		switch (c) {

			// Start of Base64 sequence
			case '&': {

				if (!inB64sequence) {
					inB64sequence = true;
					plusOutput = false;
				} else {
					out += '&';
				}

				break;
			}
			// End of Base64 sequence (or "&-" --> "&")
			case '-': {

				if (inB64sequence && prev == '&') {  // special case "&-" --> "&"
					out += '&';
				} else {
					out += '-';
				}

				inB64sequence = false;
				break;
			}
			// ',' is used instead of '/' in modified Base64
			case ',': {

				if (inB64sequence && !plusOutput) {
					out += '+';
					plusOutput = true;
				}

				out += (inB64sequence ? '/' : ',');
				break;
			}
			default: {

				if (inB64sequence && !plusOutput) {
					out += '+';
					plusOutput = true;
				}

				out += c;
				break;
			}
		}

		prev = c;
	}

	// Store it as UTF-8 by default
	string cvt;
	charset::convert(out, cvt, charset(charsets::UTF_7), charset(charsets::UTF_8));

	return folder::path::component(cvt, charset(charsets::UTF_8));
}


// static
void IMAPUtils::mailboxFlagsToFolderAttributes(
	const shared_ptr <const IMAPConnection>& cnt,
	const folder::path &path,
	const IMAPParser::mailbox_flag_list& list,
	folderAttributes& attribs
) {

	int specialUse = folderAttributes::SPECIALUSE_NONE;
	int type = folderAttributes::TYPE_CONTAINS_MESSAGES | folderAttributes::TYPE_CONTAINS_FOLDERS;
	int flags = 0;

	// If CHILDREN extension (RFC-3348) is not supported, assume folder has children
	// as we have no hint about it
	if (!cnt->hasCapability("CHILDREN")) {
		flags |= folderAttributes::FLAG_HAS_CHILDREN;
	}

	for (auto it = list.flags.begin() ; it != list.flags.end() ; ++it) {

		switch ((*it)->type) {

			case IMAPParser::mailbox_flag::NOSELECT:

				type &= ~folderAttributes::TYPE_CONTAINS_MESSAGES;
				flags |= folderAttributes::FLAG_NO_OPEN;
				break;

			case IMAPParser::mailbox_flag::NOINFERIORS:
			case IMAPParser::mailbox_flag::HASNOCHILDREN:

				flags &= ~folderAttributes::FLAG_HAS_CHILDREN;
				break;

			case IMAPParser::mailbox_flag::HASCHILDREN:

				flags |= folderAttributes::FLAG_HAS_CHILDREN;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_ALL:

				specialUse = folderAttributes::SPECIALUSE_ALL;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_ARCHIVE:

				specialUse = folderAttributes::SPECIALUSE_ARCHIVE;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_DRAFTS:

				specialUse = folderAttributes::SPECIALUSE_DRAFTS;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_FLAGGED:

				specialUse = folderAttributes::SPECIALUSE_FLAGGED;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_JUNK:

				specialUse = folderAttributes::SPECIALUSE_JUNK;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_SENT:

				specialUse = folderAttributes::SPECIALUSE_SENT;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_TRASH:

				specialUse = folderAttributes::SPECIALUSE_TRASH;
				break;

			case IMAPParser::mailbox_flag::SPECIALUSE_IMPORTANT:

				specialUse = folderAttributes::SPECIALUSE_IMPORTANT;
				break;

			default:

				break;
		}
	}

	if (path.getSize() == 1 && path.getLastComponent().getBuffer() == "INBOX") {
		specialUse = folderAttributes::SPECIALUSE_INBOX;
	}

	attribs.setSpecialUse(specialUse);
	attribs.setType(type);
	attribs.setFlags(flags);
}


int IMAPUtils::messageFlagsFromFlags(const IMAPParser::flag_list& list) {

	int flags = 0;

	for (auto &flag : list.flags) {

		switch (flag->type) {

			case IMAPParser::flag::ANSWERED:

				flags |= message::FLAG_REPLIED;
				break;

			case IMAPParser::flag::FLAGGED:

				flags |= message::FLAG_MARKED;
				break;

			case IMAPParser::flag::DELETED:

				flags |= message::FLAG_DELETED;
				break;

			case IMAPParser::flag::SEEN:

				flags |= message::FLAG_SEEN;
				break;

			case IMAPParser::flag::DRAFT:

				flags |= message::FLAG_DRAFT;
				break;

			default:
			//case IMAPParser::flag::UNKNOWN:

				break;
		}
	}

	return flags;
}


// static
const std::vector <string> IMAPUtils::messageFlagList(const int flags) {

	std::vector <string> flagList;

	if (flags == -1) {
		return flagList;  // default flags
	}

	if (flags & message::FLAG_REPLIED) flagList.push_back("\\Answered");
	if (flags & message::FLAG_MARKED) flagList.push_back("\\Flagged");
	if (flags & message::FLAG_DELETED) flagList.push_back("\\Deleted");
	if (flags & message::FLAG_SEEN) flagList.push_back("\\Seen");
	if (flags & message::FLAG_DRAFT) flagList.push_back("\\Draft");

	return flagList;
}


// static
const string IMAPUtils::searchDate(const vmime::datetime& date) {

	std::ostringstream res;
	res.imbue(std::locale::classic());

	res << date.getDay();
	res << '-';
	res << IMAP_MONTH_NAMES[std::min(std::max(date.getMonth() - 1, 0), 11)];
	res << '-';
	res << date.getYear();

	return res.str();
}


// static
const string IMAPUtils::dateTime(const vmime::datetime& date) {

	std::ostringstream res;
	res.imbue(std::locale::classic());

	// date_time ::= <"> date_day_fixed "-" date_month "-" date_year
	//               SPACE time SPACE zone <">
	//
	// time      ::= 2digit ":" 2digit ":" 2digit
	//               ;; Hours minutes seconds
	// zone      ::= ("+" / "-") 4digit
	//               ;; Signed four-digit value of hhmm representing
	//               ;; hours and minutes west of Greenwich
	res << '"';

	// Date
	if (date.getDay() < 10) res << ' ';
	res << date.getDay();

	res << '-';

	res << IMAP_MONTH_NAMES[std::min(std::max(date.getMonth() - 1, 0), 11)];

	res << '-';

	if (date.getYear() < 10) res << '0';
	if (date.getYear() < 100) res << '0';
	if (date.getYear() < 1000) res << '0';
	res << date.getYear();

	res << ' ';

	// Time
	if (date.getHour() < 10) res << '0';
	res << date.getHour() << ':';

	if (date.getMinute() < 10) res << '0';
	res << date.getMinute() << ':';

	if (date.getSecond() < 10) res << '0';
	res << date.getSecond();

	res << ' ';

	// Zone
	const int zs = (date.getZone() < 0 ? -1 : 1);
	const int zh = (date.getZone() * zs) / 60;
	const int zm = (date.getZone() * zs) % 60;

	res << (zs < 0 ? '-' : '+');

	if (zh < 10) res << '0';
	res << zh;

	if (zm < 10) res << '0';
	res << zm;

	res << '"';

	return res.str();
}


// static
shared_ptr <IMAPCommand> IMAPUtils::buildFetchCommand(
	const shared_ptr <IMAPConnection>& cnt,
	const messageSet& msgs,
	const fetchAttributes& options
) {

	// Example:
	//   C: A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])
	//   S: * 2 FETCH ....
	//   S: * 3 FETCH ....
	//   S: * 4 FETCH ....
	//   S: A654 OK FETCH completed

	std::vector <string> items;

	if (options.has(fetchAttributes::SIZE)) {
		items.push_back("RFC822.SIZE");
	}

	if (options.has(fetchAttributes::FLAGS)) {
		items.push_back("FLAGS");
	}

	if (options.has(fetchAttributes::STRUCTURE)) {
		items.push_back("BODYSTRUCTURE");
	}

	if (options.has(fetchAttributes::UID)) {

		items.push_back("UID");

		// Also fetch MODSEQ if CONDSTORE is supported
		if (cnt && cnt->hasCapability("CONDSTORE") && !cnt->isMODSEQDisabled()) {
			items.push_back("MODSEQ");
		}
	}

	if (options.has(fetchAttributes::FULL_HEADER)) {

		items.push_back("RFC822.HEADER");

	} else {

		if (options.has(fetchAttributes::ENVELOPE)) {
			items.push_back("ENVELOPE");
		}

		std::vector <string> headerFields;

		if (options.has(fetchAttributes::CONTENT_INFO)) {
			headerFields.push_back("CONTENT_TYPE");
		}

		if (options.has(fetchAttributes::IMPORTANCE)) {
			headerFields.push_back("IMPORTANCE");
			headerFields.push_back("X-PRIORITY");
		}

		// Also add custom header fields to fetch, if any
		const std::vector <string> customHeaderFields = options.getHeaderFields();
		std::copy(customHeaderFields.begin(), customHeaderFields.end(), std::back_inserter(headerFields));

		if (!headerFields.empty()) {

			string list;

			for (std::vector <string>::iterator it = headerFields.begin() ;
			     it != headerFields.end() ; ++it) {

				if (it != headerFields.begin()) {
					list += " ";
				}

				list += *it;
			}

			if (options.has(fetchAttributes::PEEK)) {
				items.push_back("BODY.PEEK[HEADER.FIELDS (" + list + ")]");
			} else {
				items.push_back("BODY[HEADER.FIELDS (" + list + ")]");
			}
		}
	}

	return IMAPCommand::FETCH(msgs, items);
}


// static
void IMAPUtils::convertAddressList(
	const IMAPParser::address_list& src,
	mailboxList& dest
) {

	for (auto& addr : src.addresses) {

		text name;
		text::decodeAndUnfold(addr->addr_name->value, &name);

		string email = addr->addr_mailbox->value
			+ "@" + addr->addr_host->value;

		dest.appendMailbox(make_shared <mailbox>(name, email));
	}
}



class IMAPUIDMessageSetEnumerator : public messageSetEnumerator {

public:

	IMAPUIDMessageSetEnumerator()
		: m_first(true) {

		m_oss.imbue(std::locale::classic());
	}

	void enumerateNumberMessageRange(const vmime::net::numberMessageRange& range) {

		if (!m_first) {
			m_oss << ",";
		}

		if (range.getFirst() == range.getLast()) {
			m_oss << range.getFirst();
		} else if (range.getLast() == size_t(-1)) {
			m_oss << range.getFirst() << ":*";
		} else {
			m_oss << range.getFirst() << ":" << range.getLast();
		}

		m_first = false;
	}

	void enumerateUIDMessageRange(const vmime::net::UIDMessageRange& range) {

		if (!m_first) {
			m_oss << ",";
		}

		if (range.getFirst() == range.getLast()) {
			m_oss << range.getFirst();
		} else if (range.getLast() == size_t(-1)) {
			m_oss << range.getFirst() << ":*";
		} else {
			m_oss << range.getFirst() << ":" << range.getLast();
		}

		m_first = false;
	}

	const std::string str() const {

		return m_oss.str();
	}

private:

	std::ostringstream m_oss;
	bool m_first;
};


class IMAPMessageSetEnumerator : public messageSetEnumerator {

public:

	void enumerateNumberMessageRange(const vmime::net::numberMessageRange& range) {

		for (size_t i = range.getFirst(), last = range.getLast() ; i <= last ; ++i) {
			m_list.push_back(i);
		}
	}

	void enumerateUIDMessageRange(const vmime::net::UIDMessageRange& /* range */) {

		// Not used
	}

	const std::vector <size_t>& list() const {

		return m_list;
	}

public:

	std::vector <size_t> m_list;
};



// static
const string IMAPUtils::messageSetToSequenceSet(const messageSet& msgs) {

	IMAPUIDMessageSetEnumerator en;
	msgs.enumerate(en);

	return en.str();
}


// static
messageSet IMAPUtils::buildMessageSet(const IMAPParser::uid_set& uidSetRef) {

	messageSet set = messageSet::empty();

	const IMAPParser::uid_set* uidSet = &uidSetRef;

	for ( ; uidSet ; uidSet = uidSet->next_uid_set.get()) {

		if (uidSet->uid_range) {

			set.addRange(
				UIDMessageRange(
					message::uid(uidSet->uid_range->uniqueid1->value),
					message::uid(uidSet->uid_range->uniqueid2->value)
				)
			);

		} else {

			set.addRange(
				UIDMessageRange(
					message::uid(uidSet->uniqueid->value)
				)
			);
		}
	}

	return set;
}


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


#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_IMAP

