#include "SMTP.h" namespace Upp { namespace Ini { INI_BOOL(Smtp_Trace, false, "Activates HTTP requests tracing") INI_BOOL(Smtp_TraceBody, false, "Activates HTTP requests body tracing") INI_BOOL(Smtp_CompressLog, false, "Activates log compression (removes long hex/encode64-like data)") }; #define LLOG(x) do { if(Ini::Smtp_Trace) { if(Ini::Smtp_CompressLog) RLOG(CompressLog(String().Cat() << x)); else RLOG(x); } } while(0) #define LLOGB(x) do { if(Ini::Smtp_TraceBody) { if(Ini::Smtp_CompressLog) RLOG(CompressLog(String().Cat() << x)); else RLOG(x); } } while(0) void Smtp::Trace(bool b) { Ini::Smtp_Trace = b; } void Smtp::TraceBody(bool b) { Ini::Smtp_TraceBody = 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); } String Smtp::GetDomainName() { String org; auto pos = sender.Find('@'); if(pos >= 0) { auto start = ++pos, len = sender.GetLength(); while(pos < len && sender[pos] != '>') pos++; org = sender.Mid(start, pos - start); } else org << TcpSocket::GetHostName(); return org; } ValueMap Smtp::GetExtensions() { ValueMap features; if(!smtp_msg.IsEmpty()) { StringStream ss(smtp_msg); ss.GetLine(); while(!ss.IsEof()) { // Get smtp service extensions. auto e = NormalizeSpaces(TrimBoth(ss.GetLine().Mid(4))); Vector v = Split(e, ' '); features.GetAdd(ToLower(v[0])); for(auto i = 1; i < v.GetCount(); i++) features(v[0]) << v[i]; } } return pick(features); } int Smtp::GetSmtpCode(const String& s) { if(s.IsVoid() || s.GetLength() < 3 || !IsDigit(s[0]) || !IsDigit(s[1]) || !IsDigit(s[2])) return -1; return StrInt(s.Mid(0, 3)); } void Smtp::SetSender() { // Specify the e-mail address of the sender. SendRecvOK("MAIL FROM:<" + sender + ">"); } void Smtp::SetRecipients() { // A single e-mail can be sent to multiple recipients. for(auto& rcp : to) SendRecv("RCPT TO:<" + rcp + ">"); } void Smtp::SendRecv(const String& s) { // We need to check the control connection. if(!IsOpen()) throw Exc("Socket is not open."); // Send request. if(!s.IsEmpty()) SendData(s + "\r\n"); // Receive response. // Response can be "multiline". smtp_msg = GetLine(); smtp_code = GetSmtpCode(smtp_msg); if(smtp_code == -1) throw Exc("Recv failed. " << GetErrorDesc()); LLOG("<< " << smtp_msg); smtp_msg.Cat('\n'); if(smtp_msg[3] && smtp_msg[3] == '-') { for(;;) { auto line = GetLine(); if(line.IsVoid()) { throw Exc("Recv failed: " << GetErrorDesc()); } auto end_code = GetSmtpCode(line); LLOG("<< " << line); smtp_msg.Cat(line); smtp_msg.Cat('\n'); if(smtp_code == end_code && line[3] && line[3] == ' ') break; } } } void Smtp::SendRecvOK(const String& s) { SendRecv(s); if(!ReplyIsSuccess()) throw Exc(smtp_msg); } bool Smtp::SendHello() { auto org = GetDomainName(); // Try EHLO command first. // EHLO command may return available service extensions. SendRecv("EHLO " << org); if(!ReplyIsSuccess()) { // Fall back to original client greeting if EHLO fails. SendRecvOK("HELO " << org); return false; } return true; } void Smtp::StartTls() { SendRecv("STARTTLS"); if(!ReplyIsSuccess() || !StartSSL()) throw Exc("Unable to init TLS session."); LLOG("++ STARTTLS successful."); } void Smtp::Authenticate() { if(!IsNull(auth_user)) { SendRecv("AUTH LOGIN"); while(ReplyIsPending()){ auto param = Base64Decode(smtp_msg.GetIter(4), smtp_msg.End()); if(param == "Username:") SendRecv(Base64Encode(auth_user)); else if(param == "Password:") SendRecv(Base64Encode(auth_pwd)); } if(!ReplyIsSuccess()) throw Exc(smtp_msg); } } void Smtp::Quit() { SendRecv("QUIT"); } void Smtp::SendMail(const String& msg_) { SetSender(); SetRecipients(); // Now send the actual e-mail. SendRecv("DATA"); if(ReplyIsPending()) { String msg = msg_; if(msg.IsEmpty()) msg = GetMessage(true); SendRecv(msg + "."); if(ReplyIsSuccess()) return; } throw Exc(smtp_msg); } void Smtp::SendData(const String &s) { if(Ini::Smtp_TraceBody) LLOG(">> " << s); else LLOG(">> [Smtp send body: " << s.GetCharCount() << " bytes]"); if(!PutAll(s)) throw Exc("Send failed. " << GetErrorDesc()); } ////////////////////////////////////////////////////////////////////// // 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, const String& s) { from = f; from_name = name; sender = Nvl(s, f); 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; } String Smtp::GetMessageID() { int q = sender.Find('@'); return message_id + (q >= 0 ? sender.Mid(q) : "@unknown_host.org"); } bool Smtp::NoAdd(const char *id) { return !(add_header.StartsWith(id + String(":")) || add_header.Find(String("\n") + id + ":") >= 0); } String Smtp::GetMessage(bool chunks) { String delimiter = "?"; for(int i = 0; i < body.GetCount(); i++) delimiter = GetDelimiter(body[i], delimiter); bool alter = body.GetCount() > 1; bool multi = !attachments.IsEmpty(); String msg; if(!no_header) { // generate message header if (sender != from) msg << "Sender: " << sender << "\r\n"; 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) && NoAdd("Subject")) msg << "Subject: " << Encode(subject) << "\r\n"; if(!IsNull(reply_to) && NoAdd("Reply-To")) msg << "Reply-To: " << FormatAddr(reply_to, reply_to_name) << "\r\n"; if(NoAdd("Message-ID")) msg << "Message-ID: <" << GetMessageID() << ">\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) && NoAdd("Content-Type")) 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]); begin = false; } if(!begin) msg.Cat("\r\n"); } for(int i = 0; i < attachments.GetCount(); i++) { const Attachment& a = attachments[i]; One source; if(a.file.GetCount()) { FileIn& in = source.Create(); if(!in.Open(a.file)) throw Exc("Unable to open attachment file " + a.file); } else source.Create().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 && chunks) { SendData(msg); msg = Null; } } } if(multi || alter) msg << "--" << delimiter << "--\r\n"; LLOGB("Msg:"); LLOGB(msg); return msg; } bool Smtp::Send(const String& msg_) { smtp_code = 0; smtp_msg.Clear(); try { if(IsNull(host)) throw Exc(t_("Host not set.")); if(to.IsEmpty()) throw Exc(t_("Recipient not set.")); if(!Connect(host, Nvl(port, starttls ? 587 : (ssl ? 465 : 25)))) throw Exc(Format("Cannot open socket %s:%d: %s", host, port, GetErrorDesc())); GlobalTimeout(request_timeout); if(ssl) if(!StartSSL()) throw Exc("Unable to start SSL"); // Receive initial message. SendRecv(Null); // Send HELO/EHLO command and query smtp service extensions. if(SendHello()) { auto ext = GetExtensions(); if(!ext.IsEmpty()) { // Check services. if(starttls) { if(ext.Find("starttls") < 0) throw Exc("STARTTLS is not supported by the server."); StartTls(); SendHello(); } } } // Everything is fine. Let us login now. Authenticate(); // Send mail. SendMail(msg_); // Close connection. Quit(); return true; } catch(Exc e) { error = e; LLOG("-- " << error); return false; } } Smtp& Smtp::New() { to.Clear(); to_name.Clear(); as.Clear(); body.Clear(); mime.Clear(); error.Clear(); add_header.Clear(); from.Clear(); sender.Clear(); message_id = AsString(Uuid::Create()); return *this; } Smtp::Smtp() { port = Null; no_header = no_header_sep = false; time_sent = GetSysTime(); request_timeout = 120000; smtp_code = 0; ssl = false; starttls = false; New(); } }