/*==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/>.

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);
    }
}