#include "SMTP.h"

NAMESPACE_UPP

static bool sSmtpTrace;

#define LLOG(x)      do { if(sSmtpTrace) RLOG(x); } while(0)

void Smtp::Trace(bool b)
{
	sSmtpTrace = b;
}

static String GetDelimiter(const char *b, const char *e, String init)
{
	static const char delimiters[] =
		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
		"ghijklmnopqrstuvxyz()+/:?0123456"
		"789abcdefghijklmnopqrstuvwxyzABC"
		"DEFGHIJKLMNOPQRSTUVWXYZ012345678";

	String out = init;
	if(b == e)
		return out;
	if(IsNull(out))
		out.Cat(delimiters[*b++ & 0x7F]);
	int l = out.GetLength();
	for(; b != e; b++)
	{
		b = (const char *)memchr(b, *out, e - b);
		if(!b || e - b < l)
			return out;
		if(!memcmp(b, out, l))
		{
			if(e - b == l)
				return out + '/';
			out.Cat(delimiters[b[l] & 0x7F]);
		}
	}
	return out;
}

static String GetDelimiter(String s, String init)
{
	return GetDelimiter(s.Begin(), s.End(), init);
}

void Smtp::CheckFail()
{
	if(msecs(start_time) > request_timeout)
		throw Exc(t_("Communication Failure: Timeout."));
	if(IsError())
		throw Exc("Connection error: " + GetErrorDesc());
}

void Smtp::Send(const String &s)
{
	LLOG("SMTP send: " << s);
	const char *p = s.Begin(), *e = s.End();
	while(p != e) {
		CheckFail();
		int amount = Put(p, int(e - p));
		p += amount;
	}
}

String Smtp::SendRecv(const String& s)
{
	Send(s);
	String reply;
	for(;;) {
		CheckFail();
		int c = Get();
		if(c >= 0) {
			if(c == '\n') {
				LLOG("Reply: " << reply);
				return reply;
			}
			reply.Cat(c);
		}
	}
}

void Smtp::SendRecvOK(const String& s)
{
	String ans = SendRecv(s);
	if(ans[0] != '2' || ans[1] != '5' || ans[2] != '0')
		throw Exc(ans);
}

//////////////////////////////////////////////////////////////////////
// Smtp::

static const char default_mime[] = "application/octet-stream";

String Smtp::Encode(const String& text)
{
	String txt = ToCharset(CHARSET_UTF8, text);
	String r = "=?Utf-8?q?";
	for(const char *s = txt; *s; s++) {
		if((byte)*s < ' ' || (byte)*s > 127 || *s == '=' || *s == '?' || *s == ' ')
			r << '=' << FormatIntHexUpper((byte)*s, 2);
		else
			r.Cat(*s);
	}
	r << "?=";
	return r;
}

Smtp& Smtp::To(const String& t, const String& name, AS a)
{
	to.Add(t);
	to_name.Add(name);
	as.Add(a);
	return *this;
}

Smtp& Smtp::Subject(const String& s)
{
	subject = s;
	return *this;
}

Smtp& Smtp::ReplyTo(const String& r, const String& name)
{
	reply_to = r;
	reply_to_name = name;
	return *this;
}

Smtp& Smtp::From(const String& f, const String& name)
{
	from = f;
	from_name = name;
	return *this;
}

Smtp& Smtp::AttachFile(const char *filename, const char *mime)
{
	Attachment& attach = attachments.Add();
	attach.name = GetFileNamePos(filename);
	attach.mime = (mime ? mime : default_mime);
	attach.file = filename;
	return *this;
}

Smtp& Smtp::Attach(const char *name, const String& data, const char *mime)
{
	Attachment& attach = attachments.Add();
	attach.name = name;
	attach.mime = (mime ? mime : default_mime);
	attach.data = data;
	return *this;
}

String Smtp::FormatAddr(const String& addr, const String& name)
{
	String r;
	if(name.GetCount())
		r << "\"" << Encode(name) << "\" ";
	r << '<' << addr << '>';
	return r;
}

bool Smtp::Send()
{
	start_time = msecs();

	String ipaddr;

	try {
		if(IsNull(host))
			throw Exc(t_("Host not set."));

		if(to.IsEmpty())
			throw Exc(t_("Recipient not set."));

		if(!Connect(host, Nvl(port, ssl ? 465 : 25)))
			throw Exc(Format("Cannot open socket %s:%d: %s", host, port, GetErrorDesc()));

		String ans;
		
		if(ssl)
			if(!StartSSL())
				throw Exc("Unable to start SSL");

		// receive initial message & send hello
		SendRecv(Null);
		String org;
		int pos = from.Find('@');
		if(pos >= 0) {
			int start = ++pos, len = from.GetLength();
			while(pos < len && from[pos] != '>')
				pos++;
			org = from.Mid(start, pos - start);
		}
		else
			org << TcpSocket::GetHostName();

		SendRecvOK("HELO " + org + "\r\n");
		if(!IsNull(auth_user)) {
			String ans = SendRecv("AUTH LOGIN\r\n");
			while(ans[0] != '2')
				if(ans[0] == '3' && ans[1] == '3' && ans[2] == '4' && ans[3] == ' ') {
					String param = Base64Decode(ans.GetIter(4), ans.End());
					if(param == "Username:")
						ans = SendRecv(Base64Encode(auth_user) + "\r\n");
					else if(param == "Password:")
						ans = SendRecv(Base64Encode(auth_pwd) + "\r\n");
					else
						throw Exc(ans);
				}
				else
					throw Exc(ans);
		}
		SendRecvOK("MAIL FROM:<" + from + ">\r\n");
		for(int i = 0; i < to.GetCount(); i++)
			SendRecv("RCPT TO:<" + to[i] + ">\r\n");
		ans = SendRecv("DATA\r\n");

		if(memcmp(ans, "354", 3))
			throw Exc(ans);

		String delimiter = "?";
		for(int i = 0; i < body.GetCount(); i++)
			delimiter = GetDelimiter(body[i], delimiter);
		bool alter = body.GetCount() > 1;
		bool multi = !attachments.IsEmpty();

		{ // format message
			String msg;
			if(!no_header) { // generate message header
				msg << "From: " << FormatAddr(from, from_name) << "\r\n";
				static const AS as_list[] = { TO, CC, BCC };
				static const char *as_name[] = { "To", "CC", "BCC" };
				for(int a = 0; a < __countof(as_list); a++)
				{
					int pos = 0;
					for(int i = 0; i < as.GetCount(); i++)
						if(as[i] == as_list[a])
						{
							if(pos && pos + to[i].GetLength() >= 70)
							{
								msg << "\r\n     ";
								pos = 5;
							}
							else if(pos)
							{
								msg << ", ";
								pos += 2;
							}
							else
							{
								msg << as_name[a] << ": ";
								pos = (int)strlen(as_name[a]) + 2;
							}
							msg << FormatAddr(to[i], to_name[i]);
						}
					if(pos)
						msg << "\r\n";
				}
				if(!IsNull(subject))
					msg << "Subject: " << Encode(subject) << "\r\n";
				if(!IsNull(reply_to))
					msg << "Reply-To: " << FormatAddr(reply_to, reply_to_name) << "\r\n";
				if(!IsNull(time_sent)) {
					static const char *dayofweek[] =
					{ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
					static const char *month[] =
					{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
					msg << "Date: "
						<< dayofweek[DayOfWeek(time_sent)] << ", "
						<< (int)time_sent.day << ' ' << month[time_sent.month - 1] << ' ' << (int)time_sent.year
						<< ' ' << Sprintf("%2d:%02d:%02d " + GetTimeZoneText(),
						                  time_sent.hour, time_sent.minute, time_sent.second)
						<< "\r\n";
				}
				if(multi || alter)
					msg << "Content-Type: Multipart/" << (alter ? "alternative" : "mixed")
						<< "; boundary=\"" << delimiter << "\"\r\n"
						"\r\n";
				msg << add_header;
			}

			for(int i = 0; i < body.GetCount(); i++) {
				String t = body[i], m = mime[i];
				if(!no_header) {
					if(multi || alter)
						msg << "--" << delimiter << "\r\n";
					if(IsNull(m))
						m << "text/plain; charset=\"" << MIMECharsetName(CHARSET_DEFAULT) << "\"";
					msg << "Content-Type: " << m << "\r\n"
					"Content-Transfer-Encoding: quoted-printable\r\n";
				}
				if(!no_header_sep)
					msg << "\r\n";
				bool begin = true;
				for(const char *p = t.Begin(), *e = t.End(); p != e; p++)
					if(*p >= 33 && *p <= 126 && *p != '=' && (*p != '.' || !begin)) {
						msg.Cat(*p);
						begin = false;
					}
					else if(*p == '.' && begin) {
						msg.Cat("..");
						begin = false;
					}
					else if(*p == ' ' && p + 1 != e && p[1] != '\r' && p[1] != '\n') {
						msg.Cat(' ');
						begin = false;
					}
					else if(*p == '\r')
						;
					else if(*p == '\n') {
						msg.Cat("\r\n");
						begin = true;
					}
					else {
						static const char hex[] = "0123456789ABCDEF";
						msg.Cat('=');
						msg.Cat(hex[(*p >> 4) & 15]);
						msg.Cat(hex[*p & 15]);
					}

				if(!begin)
					msg.Cat("\r\n");
			}
			for(int i = 0; i < attachments.GetCount(); i++) {
				const Attachment& a = attachments[i];
				One<Stream> source;
				if(a.file.GetCount()) {
					FileIn& in = source.Create<FileIn>();
					if(!in.Open(a.file))
						throw Exc("Unable to open attachment file " + a.file);
				}
				else
					source.Create<StringStream>().Open(a.data);
				msg << "--" << delimiter << "\r\n"
					"Content-Type: " << a.mime << "; name=\"" << a.name << "\"\r\n"
					"Content-Transfer-Encoding: base64\r\n"
					"Content-Disposition: attachment; filename=\"" << a.name << "\"\r\n"
					"\r\n";

				char buffer[54];
				for(int c; (c = source -> Get(buffer, sizeof(buffer))) != 0;)
				{
					msg.Cat(Base64Encode(buffer, buffer + c));
					msg.Cat('\r');
					msg.Cat('\n');
					if(msg.GetLength() >= 65536) {
						Send(msg);
						msg = Null;
					}
				}
			}
			if(multi || alter)
				msg << "--" << delimiter << "--\r\n";
			msg.Cat(".\r\n");
			LLOG("Msg:");
			LLOG(msg);
			SendRecvOK(msg);
		}

		SendRecv("QUIT\r\n");
		return true;
	}
	catch(Exc e) {
		error = e;
		return false;
	}
}

Smtp& Smtp::New() {
	to.Clear();
	to_name.Clear();
	as.Clear();
	body.Clear();
	mime.Clear();
	error.Clear();
	add_header.Clear();
	return *this;
}

Smtp::Smtp()
{
	port = Null;
	no_header = no_header_sep = false;
	time_sent = GetSysTime();
	request_timeout = 120000;
}

END_UPP_NAMESPACE
