You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
730 lines
22 KiB
730 lines
22 KiB
/*==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 <http://www.gnu.org/licenses/>. |
|
|
|
Additional permissions under GNU GPL version 3 section 7 |
|
|
|
If you modify this Program, or any covered work, by linking or |
|
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, |
|
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent |
|
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK |
|
(or a modified version of those libraries), |
|
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, |
|
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG |
|
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the |
|
licensors of this Program grant you additional |
|
permission to convey the resulting work. Corresponding Source for a |
|
non-source form of such a combination shall include the source code for |
|
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered |
|
work. |
|
|
|
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 <base64data>" |
|
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:<UruClient@cyanworlds.com>\r\n" |
|
// -> "250 ok mail from accepted\r\n" |
|
// <- "RCPT TO:<UruCrashReport@cyanworlds.com>\r\n" |
|
// -> "250 ok recipient accepted\r\n" |
|
// <- "DATA\r\n" |
|
// -> "354 ok, go ahead:\r\n" |
|
// -> <message> + "\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:<UruClient@cyanworlds.com>\r\n" |
|
// -> "250 ok mail from accepted\r\n" |
|
// <- "RCPT TO:<UruCrashReport@cyanworlds.com>\r\n" |
|
// -> "250 ok recipient accepted\r\n" |
|
// <- "DATA\r\n" |
|
// -> "354 ok, go ahead:\r\n" |
|
// -> <message> + "\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); |
|
} |
|
}
|
|
|