/*==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);
}
}