/*==LICENSE==* CyanWorlds.com Engine - MMOG client, server and tools Copyright (C) 2011 Cyan Worlds, Inc. 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, see . You can contact Cyan Worlds, Inc. by email legal@cyan.com or by snail mail at: Cyan Worlds, Inc. 14617 N Newport Hwy Mead, WA 99021 *==LICENSE==*/ /***************************************************************************** * * $/Plasma20/Sources/Plasma/NucleusLib/pnMail/pnMail.cpp * ***/ #include "Pch.h" #pragma hdrstop /***************************************************************************** * * Private * ***/ enum EMailStep { kMailStepConnect, // wait for 220, then send "helo" command kMailStepAuth, // wait for 250, then send "AUTH PLAIN " kMailStepSender, // wait for 235/250, then send "mail from:" command kMailStepRecipient, // wait for 250, then send "rcpt to:" command kMailStepStartLetter, // wait for 250, then send "data" command kMailStepLetter, // wait for 354, then send letter kMailStepQuit, // wait for 250, then send "quit" command kMailStepDisconnect, // wait for 221, then disconnect }; struct MailTransaction { LINK(MailTransaction) link; AsyncSocket sock; const char ** stepTable; EMailStep step; unsigned subStep; EMailError error; FMailResult callback; void * param; char * smtp; char * sender; char * recipient; char * replyTo; char * subject; char * bodyEncoding; char * body; char * auth; char buffer[1]; }; static bool MailNotifyProc ( AsyncSocket sock, EAsyncNotifySocket code, AsyncNotifySocket * notify, void ** userState ); static void __cdecl Send ( AsyncSocket sock, const char str[], ... ); /***************************************************************************** * * Private data * ***/ static bool s_shutdown; static CCritSect s_critsect; static LISTDECL(MailTransaction, link) s_mail; //=========================================================================== // Authenticated email sequence // -> "220 catherine.cyan.com ESMTP\r\n" // <- "HELO cyan.com\r\n" // -> "250 catherine.cyan.com\r\n" // <- "AUTH PLAIN XXXXXXXXX\r\n" // -> "235 go ahead\r\n" // <- "MAIL FROM:\r\n" // -> "250 ok mail from accepted\r\n" // <- "RCPT TO:\r\n" // -> "250 ok recipient accepted\r\n" // <- "DATA\r\n" // -> "354 ok, go ahead:\r\n" // -> + "\r\n.\r\n" // -> "250 message accepted for delivery\r\n" // <- "QUIT\r\n" // -> "221 catherine.cyan.com\r\n" //=========================================================================== static const char * s_waitStrAuth[] = { "220 ", // kMailStepConnect "250 ", // kMailStepAuth "235 ", // kMailStepSender "250 ", // kMailStepRecipient "250 ", // kMailStepStartLetter "354 ", // kMailStepLetter "250 ", // kMailStepQuit "221 ", // kMailStepDisconnect }; //=========================================================================== // Unauthenticated email seqeunce // -> "220 catherine.cyan.com ESMTP\r\n" // <- "HELO cyan.com\r\n" // -> "250 catherine.cyan.com\r\n" // <- "MAIL FROM:\r\n" // -> "250 ok mail from accepted\r\n" // <- "RCPT TO:\r\n" // -> "250 ok recipient accepted\r\n" // <- "DATA\r\n" // -> "354 ok, go ahead:\r\n" // -> + "\r\n.\r\n" // -> "250 message accepted for delivery\r\n" // <- "QUIT\r\n" // -> "221 catherine.cyan.com\r\n" //=========================================================================== static const char * s_waitStrNoAuth[] = { "220 ", // kMailStepConnect nil, // kMailStepAuth "250 ", // kMailStepSender "250 ", // kMailStepRecipient "250 ", // kMailStepStartLetter "354 ", // kMailStepLetter "250 ", // kMailStepQuit "221 ", // kMailStepDisconnect }; /***************************************************************************** * * Internal functions * ***/ //=========================================================================== static bool AdvanceStep ( MailTransaction * transaction, AsyncSocket sock ) { switch (transaction->step) { case kMailStepConnect: { const char * host = transaction->sender; const char * separator = StrChr(host, '@'); if (separator) host = separator + 1; Send(sock, "helo ", host, "\r\n", nil); transaction->step = kMailStepSender; if (transaction->auth[0]) transaction->step = kMailStepAuth; } break; case kMailStepAuth: Send(sock, "AUTH PLAIN ", transaction->auth, "\r\n", nil); transaction->step = kMailStepSender; break; case kMailStepSender: Send(sock, "mail from:<", transaction->sender, ">\r\n", nil); transaction->step = kMailStepRecipient; break; case kMailStepRecipient: { const char * start = transaction->recipient + transaction->subStep; while (*start == ' ') ++start; const char * term = StrChr(start, ';'); if (term) { char * buffer = ALLOCA(char, term + 1 - start); StrCopy(buffer, start, term + 1 - start); Send(sock, "rcpt to:<", buffer, ">\r\n", nil); transaction->subStep = term + 1 - transaction->recipient; } else { Send(sock, "rcpt to:<", start, ">\r\n", nil); transaction->step = kMailStepStartLetter; transaction->subStep = 0; } } break; case kMailStepStartLetter: Send(sock, "data\r\n", nil); transaction->step = kMailStepLetter; break; case kMailStepLetter: if (transaction->replyTo[0]) { Send( sock, "Reply-to: ", transaction->replyTo, "\r\n", nil ); } if (transaction->bodyEncoding) { Send( sock, "From: ", transaction->sender, "\r\nTo: ", transaction->recipient, "\r\nSubject: ", transaction->subject, "\r\nContent-Type: text/plain; charset=", transaction->bodyEncoding, "\r\n\r\n", transaction->body, "\r\n.\r\n", nil ); } else { Send( sock, "From: ", transaction->sender, "\r\nTo: ", transaction->recipient, "\r\nSubject: ", transaction->subject, "\r\n\r\n", transaction->body, "\r\n.\r\n", nil ); } transaction->step = kMailStepQuit; break; case kMailStepQuit: Send(sock, "quit\r\n", nil); transaction->step = kMailStepDisconnect; break; case kMailStepDisconnect: return false; DEFAULT_FATAL(transaction->step); } return true; } //=========================================================================== static bool NotifyRead ( AsyncSocket sock, AsyncNotifySocketRead * read, MailTransaction * transaction ) { // Parse available lines looking for an acknowledgement of last command const char * compareStr = transaction->stepTable[transaction->step]; unsigned compareLen = StrLen(compareStr); unsigned offset = 0; for (;;) { const char * source = (const char *)&read->buffer[offset]; // Search for an end-of-line marker const char * eol = StrChr(source, '\n', read->bytes - offset); if (!eol) break; offset += (eol + 1 - source); // Search for an acknowledgement if (!StrCmp(source, compareStr, compareLen)) { read->bytesProcessed = offset; return AdvanceStep(transaction, sock); } // An error was received; log it and bail LogMsg( kLogError, "Mail: step %u error '%.*s'", transaction->step, eol - source - 1, source ); transaction->error = kMailErrServerError; return false; } // We did not find an acknowledgement, so skip past fully-received lines // and wait for more data to arrive read->bytesProcessed = offset; return true; } //=========================================================================== static void DestroyTransaction (MailTransaction * transaction) { // Remove transaction from list so that it can // be safely deleted outside the critical section s_critsect.Enter(); { s_mail.Unlink(transaction); } s_critsect.Leave(); // Perform callback if requested if (transaction->callback) { transaction->callback( transaction->param, transaction->error ); } DEL(transaction); } //=========================================================================== static void MailLookupProc ( void * param, const wchar * , unsigned addrCount, const NetAddress addrs[] ) { // If no address was found, cancel the transaction MailTransaction * transaction = (MailTransaction *) param; if (!addrCount) { LogMsg(kLogError,"Mail: failed to resolve %s", transaction->smtp); transaction->error = kMailErrDnsFailed; DestroyTransaction(transaction); return; } // Initiate a connection AsyncCancelId cancelId; AsyncSocketConnect( &cancelId, addrs[0], MailNotifyProc, transaction ); } //=========================================================================== static bool MailNotifyProc ( AsyncSocket sock, EAsyncNotifySocket code, AsyncNotifySocket * notify, void ** userState ) { switch (code) { case kNotifySocketConnectFailed: { MailTransaction * transaction = (MailTransaction *)notify->param; LogMsg(kLogError, "Mail: unable to connect to %s", transaction->smtp); transaction->error = kMailErrConnectFailed; DestroyTransaction(transaction); } break; case kNotifySocketConnectSuccess: { MailTransaction * transaction = (MailTransaction *) notify->param; *userState = notify->param; transaction->sock = sock; } break; case kNotifySocketRead: { MailTransaction * transaction = (MailTransaction *) *userState; return NotifyRead(sock, (AsyncNotifySocketRead *) notify, transaction); } break; case kNotifySocketDisconnect: { MailTransaction * transaction = (MailTransaction *) *userState; if ((transaction->step != kMailStepDisconnect) && !transaction->error) { transaction->error = kMailErrDisconnected; LogMsg(kLogError, "Mail: unexpected disconnection from %s", transaction->smtp); } DestroyTransaction(transaction); AsyncSocketDelete(sock); } break; } return !s_shutdown; } //=========================================================================== static void __cdecl Send ( AsyncSocket sock, const char str[], ... ) { // Count bytes unsigned bytes = 1; { va_list argList; va_start(argList, str); for (const char * source = str; source; source = va_arg(argList, const char *)) bytes += StrLen(source); va_end(argList); } // Allocate string buffer char * packed; const unsigned kStackBufSize = 8 * 1024; if (bytes > kStackBufSize) packed = (char *) ALLOC(bytes); else packed = (char *) _alloca(bytes); // Pack the string { va_list argList; va_start(argList, str); char * dest = packed; for (const char * source = str; source; source = va_arg(argList, const char *)) dest += StrCopyLen(dest, source, packed + bytes - dest); va_end(argList); } // Send the string AsyncSocketSend(sock, packed, bytes - 1); // Free the string if (bytes > kStackBufSize) FREE(packed); } //=========================================================================== static void IMail ( const char * stepTable[], const char smtp[], const char sender[], const char recipient[], const char replyTo[], const char subject[], const char body[], const char auth[], EMailEncoding bodyEncoding, FMailResult callback, void * param ) { static const char * utf8 = "\"utf-8\""; const char * encodingStr; if (bodyEncoding == kMailEncodeUtf8) encodingStr = utf8; else encodingStr = ""; // Calculate string lengths unsigned lenSmtp = StrLen(smtp); unsigned lenSender = StrLen(sender); unsigned lenRecipient = StrLen(recipient); unsigned lenReplyTo = StrLen(replyTo); unsigned lenSubject = StrLen(subject); unsigned lenBodyEncoding = StrLen(encodingStr); unsigned lenBody = StrLen(body); unsigned lenAuth = StrLen(auth); // Calculate the required buffer size for all strings unsigned bytes = ( lenSmtp + 1 + lenSender + 1 + lenRecipient + 1 + lenReplyTo + 1 + lenSubject + 1 + lenBody + 1 + lenAuth + 1 ) * sizeof(char); if (lenBodyEncoding) bytes += (lenBodyEncoding + 1) * sizeof(char); // Create a transaction record MailTransaction * transaction = new( ALLOC(offsetof(MailTransaction, buffer) + bytes) ) MailTransaction; transaction->stepTable = stepTable; transaction->sock = nil; transaction->step = kMailStepConnect; transaction->subStep = 0; transaction->error = kMailSuccess; transaction->callback = callback; transaction->param = param; unsigned offset = 0; transaction->smtp = transaction->buffer + offset; offset += StrCopyLen(transaction->smtp, smtp, lenSmtp + 1) + 1; transaction->sender = transaction->buffer + offset; offset += StrCopyLen(transaction->sender, sender, lenSender + 1) + 1; transaction->recipient = transaction->buffer + offset; offset += StrCopyLen(transaction->recipient, recipient, lenRecipient + 1) + 1; transaction->replyTo = transaction->buffer + offset; offset += StrCopyLen(transaction->replyTo, replyTo, lenReplyTo + 1) + 1; transaction->subject = transaction->buffer + offset; offset += StrCopyLen(transaction->subject, subject, lenSubject + 1) + 1; if (lenBodyEncoding) { transaction->bodyEncoding = transaction->buffer + offset; offset += StrCopyLen(transaction->bodyEncoding, encodingStr, lenBodyEncoding + 1) + 1; } else { transaction->bodyEncoding = nil; } transaction->body = transaction->buffer + offset; offset += StrCopyLen(transaction->body, body, lenBody + 1) + 1; transaction->auth = transaction->buffer + offset; offset += StrCopyLen(transaction->auth, auth, lenAuth + 1) + 1; ASSERT(offset == bytes); // Start the transaction with a dns lookup const unsigned kSmtpPort = 25; wchar smtpName[256]; StrToUnicode(smtpName, smtp, arrsize(smtpName)); // Add transaction to global list bool shutdown; s_critsect.Enter(); { shutdown = s_shutdown; s_mail.Link(transaction); } s_critsect.Leave(); if (shutdown) { DestroyTransaction(transaction); } else { NetAddress addr; if (NetAddressFromString(&addr, smtpName, kSmtpPort)) { AsyncCancelId cancelId; AsyncSocketConnect( &cancelId, addr, MailNotifyProc, transaction ); } else { AsyncCancelId cancelId; AsyncAddressLookupName( &cancelId, MailLookupProc, smtpName, kSmtpPort, transaction ); } } } /**************************************************************************** * * Exported functions * ***/ //=========================================================================== void MailEncodePassword ( const char username[], const char password[], ARRAY(char) * emailAuth ) { // base64_encode("\0#{user}\0#{secret}") emailAuth->Reserve(512); emailAuth->Push(0); emailAuth->Add(username, StrBytes(username)); emailAuth->Add(password, StrBytes(password)); unsigned srcChars = emailAuth->Bytes(); // Allocate space for encoded data unsigned dstChars = Base64EncodeSize(srcChars); char * dstData = emailAuth->New(dstChars); // Encode data and move it back to the front of the array dstChars = Base64Encode( srcChars, (const byte *) emailAuth->Ptr(), dstChars, dstData ); emailAuth->Move( 0, srcChars, dstChars ); emailAuth->SetCountFewer(dstChars); } //=========================================================================== void Mail ( const char smtp[], const char sender[], const char recipient[], // multiple recipients separated by semicolons const char subject[], const char body[], const char username[], const char password[], const char replyTo[], EMailEncoding bodyEncoding, FMailResult callback, void * param ) { s_shutdown = false; // Get email authorization const char * auth; const char ** stepTable; ARRAY(char) authBuffer; if (!password || !*password) { // No password is specified, use non-authenticated email auth = ""; stepTable = s_waitStrNoAuth; } else if (!username || !*username) { // No username specified, user is providing the base64 encoded secret auth = password; stepTable = s_waitStrAuth; } else { MailEncodePassword(username, password, &authBuffer); auth = authBuffer.Ptr(); stepTable = s_waitStrAuth; } IMail( stepTable, smtp, sender, recipient, replyTo ? replyTo : "", subject, body, auth, bodyEncoding, callback, param ); } //=========================================================================== void MailStop () { s_critsect.Enter(); { s_shutdown = true; MailTransaction * transaction = s_mail.Head(); for (; transaction; transaction = s_mail.Next(transaction)) { if (transaction->sock) AsyncSocketDisconnect(transaction->sock, true); transaction->error = kMailErrClientCanceled; } } s_critsect.Leave(); AsyncAddressLookupCancel( MailLookupProc, 0 // cancel all ); AsyncSocketConnectCancel( MailNotifyProc, 0 // cancel all ); } //=========================================================================== bool MailQueued () { bool queued; s_critsect.Enter(); queued = s_mail.Head() != nil; s_critsect.Leave(); return queued; } //============================================================================ const wchar * MailErrorToString (EMailError error) { switch (error) { case kMailSuccess: return L"kMailSuccess"; case kMailErrDnsFailed: return L"kMailErrDnsFailed"; case kMailErrConnectFailed: return L"kMailErrConnectFailed"; case kMailErrDisconnected: return L"kMailErrDisconnected"; case kMailErrClientCanceled: return L"kMailErrClientCanceled"; case kMailErrServerError: return L"kMailErrServerError"; DEFAULT_FATAL(error); } }