// See the file "COPYING" in the main distribution directory for copyright.

#include "zeek/analyzer/protocol/smtp/SMTP.h"

#include "zeek/zeek-config.h"

#include <cstdlib>
#include <limits>

#include "zeek/Event.h"
#include "zeek/NetVar.h"
#include "zeek/Reporter.h"
#include "zeek/analyzer/Manager.h"
#include "zeek/analyzer/protocol/smtp/BDAT.h"
#include "zeek/analyzer/protocol/smtp/consts.bif.h"
#include "zeek/analyzer/protocol/smtp/events.bif.h"

#undef SMTP_CMD_DEF
#define SMTP_CMD_DEF(cmd) #cmd,

static const char* smtp_cmd_word[] = {
#include "SMTP_cmd.def"
};

static const char* unknown_cmd = "(UNKNOWN)";

#define SMTP_CMD_WORD(code) ((code >= 0) ? smtp_cmd_word[code] : unknown_cmd)

namespace zeek::analyzer::smtp {

SMTP_Analyzer::SMTP_Analyzer(Connection* conn) : analyzer::tcp::TCP_ApplicationAnalyzer("SMTP", conn) {
    expect_sender = false;
    expect_recver = true;
    state = detail::SMTP_CONNECTED;
    last_replied_cmd = -1;
    first_cmd = detail::SMTP_CMD_CONN_ESTABLISHMENT;
    pending_reply = 0;

    // Some clients appear to assume pipelining is always enabled
    // and do not bother to check whether "PIPELINING" appears in
    // the server reply to EHLO.
    pipelining = true;

    skip_data = false;
    orig_is_sender = true;
    line_after_gap = nullptr;
    mail = nullptr;
    UpdateState(first_cmd, 0, true);
    cl_orig = new analyzer::tcp::ContentLine_Analyzer(conn, true);
    cl_orig->SetIsNULSensitive(true);
    cl_orig->SetSkipPartial(true);
    AddSupportAnalyzer(cl_orig);

    cl_resp = new analyzer::tcp::ContentLine_Analyzer(conn, false);
    cl_resp->SetIsNULSensitive(true);
    cl_resp->SetSkipPartial(true);
    AddSupportAnalyzer(cl_resp);
}

void SMTP_Analyzer::ConnectionFinished(bool half_finished) {
    analyzer::tcp::TCP_ApplicationAnalyzer::ConnectionFinished(half_finished);

    if ( ! half_finished && mail )
        EndData();
}

SMTP_Analyzer::~SMTP_Analyzer() { delete line_after_gap; }

void SMTP_Analyzer::Done() {
    analyzer::tcp::TCP_ApplicationAnalyzer::Done();

    if ( mail )
        EndData();
}

void SMTP_Analyzer::Undelivered(uint64_t seq, int len, bool is_orig) {
    analyzer::tcp::TCP_ApplicationAnalyzer::Undelivered(seq, len, is_orig);

    if ( len <= 0 )
        return;

    const char* buf = util::fmt("seq = %" PRIu64 ", len = %d", seq, len);
    int buf_len = strlen(buf);

    Unexpected(is_orig, "content gap", buf_len, buf);

    if ( state == detail::SMTP_IN_DATA || state == detail::SMTP_CMD_BDAT ) {
        // Record the SMTP data gap and terminate the
        // ongoing mail transaction.
        if ( mail )
            mail->Undelivered(len);

        EndData();
    }

    if ( line_after_gap ) {
        delete line_after_gap;
        line_after_gap = nullptr;
    }

    pending_cmd_q.clear();

    first_cmd = last_replied_cmd = -1;

    // Missing either the sender's packets or their replies
    // (e.g. code 354) is critical, so we set state to SMTP_AFTER_GAP
    // in both cases
    state = detail::SMTP_AFTER_GAP;
}

void SMTP_Analyzer::DeliverStream(int length, const u_char* line, bool orig) {
    analyzer::tcp::TCP_ApplicationAnalyzer::DeliverStream(length, line, orig);

    // If an TLS transaction has been initiated, forward to child and abort.
    if ( state == detail::SMTP_IN_TLS ) {
        ForwardStream(length, line, orig);
        return;
    }

    // NOTE: do not use IsOrig() here, because of TURN command.
    bool is_sender = orig_is_sender ? orig : ! orig;

    if ( length > 0 && is_sender && bdat ) {
        // We're processing BDAT and have switched the ContentLine analyzer
        // into plain mode to send us the full chunk. Ensure we only use up
        // as much as we need in case we get more.
        int bdat_len = length;
        if ( bdat->RemainingChunkSize() < static_cast<uint64_t>(bdat_len) )
            bdat_len = static_cast<int>(bdat->RemainingChunkSize());

        if ( bdat_len > 0 )
            bdat->NextStream(bdat_len, line, orig);

        // All BDAT chunks seen?
        if ( bdat->IsLastChunk() && bdat->RemainingChunkSize() == 0 )
            UpdateState(detail::SMTP_CMD_END_OF_DATA, 0, orig);

        line += bdat_len;
        length -= bdat_len;
        assert(length >= 0);

        // Anything left? Usually the remainder is zero as we're doing
        // plain delivery. However, a "BDAT 0 LAST" empty chunk isn't
        // delivered by the ContentLineAnalyzer.
        if ( length == 0 )
            return;
    }

#if 0
	###
	if ( line[length] != '\r' || line[length+1] != '\n' )
		Unexpected(is_sender, "line does not end with <CR><LF>", length, line);
#endif

    // Some weird client uses '\r\r\n' for end-of-line sequence
    // So we make a compromise here to allow /(\r)*\n/ as end-of-line sequences
    if ( length > 0 && line[length - 1] == '\r' ) {
        Unexpected(is_sender, "more than one <CR> at the end of line", length, (const char*)line);
        do
            --length;
        while ( length > 0 && line[length - 1] == '\r' );
    }

    for ( int i = 0; i < length; ++i )
        if ( line[i] == '\r' || line[i] == '\n' ) {
            Unexpected(is_sender, "Bare <CR> or <LF> appears in the middle of line", length, (const char*)line);
            break;
        }

    ProcessLine(length, (const char*)line, orig);
}

void SMTP_Analyzer::ProcessLine(int length, const char* line, bool orig) {
    const char* end_of_line = line + length;
    int cmd_len = 0;
    const char* cmd = "";

    // NOTE: do not use IsOrig() here, because of TURN command.
    bool is_sender = orig_is_sender ? orig : ! orig;

    if ( ! pipelining && ((is_sender && ! expect_sender) || (! is_sender && ! expect_recver)) )
        Unexpected(is_sender, "out of order", length, line);

    if ( is_sender ) {
        int cmd_code = -1;

        if ( state == detail::SMTP_AFTER_GAP ) {
            // Don't know whether it is a command line or
            // a data line.
            delete line_after_gap;

            line_after_gap = new String((const u_char*)line, length, true);
        }

        else if ( state == detail::SMTP_IN_DATA && line[0] == '.' && length == 1 ) {
            cmd = ".";
            cmd_len = 1;
            cmd_code = detail::SMTP_CMD_END_OF_DATA;
            NewCmd(cmd_code);

            expect_sender = false;
            expect_recver = true;
        }

        else if ( state == detail::SMTP_IN_DATA ) {
            // Check "." for end of data for non-BDAT transfers.
            expect_recver = false; // ?? MAY server respond to mail data?

            if ( line[0] == '.' )
                ++line;

            int data_len = end_of_line - line;

            if ( ! mail )
                // This can happen if we're already shut
                // down the connection due to seeing a RST
                // but are now processing packets sent
                // afterwards (because, e.g., the RST was
                // dropped or ignored).
                BeginData(orig);

            ProcessData(data_len, line);

            if ( smtp_data && ! skip_data ) {
                EnqueueConnEvent(smtp_data, ConnVal(), val_mgr->Bool(orig), make_intrusive<StringVal>(data_len, line));
            }
        }

        else if ( state == detail::SMTP_IN_AUTH ) {
            cmd = "***";
            cmd_len = 2;
            cmd_code = detail::SMTP_CMD_AUTH_ANSWER;
            NewCmd(cmd_code);
        }

        else {
            expect_sender = false;
            expect_recver = true;

            util::get_word(length, line, cmd_len, cmd);
            line = util::skip_whitespace(line + cmd_len, end_of_line);
            cmd_code = ParseCmd(cmd_len, cmd);

            if ( cmd_code == -1 ) {
                Unexpected(true, "unknown command", cmd_len, cmd);
                cmd = nullptr;
            }
            else
                NewCmd(cmd_code);
        }

        // Generate smtp_request event
        if ( cmd_code >= 0 ) {
            // In order for all MIME events nested
            // between SMTP command DATA and END_OF_DATA,
            // we need to call UpdateState(), which in
            // turn calls BeginData() and EndData(),  and
            // RequestEvent() in different orders for the
            // two commands.
            if ( cmd_code == detail::SMTP_CMD_END_OF_DATA )
                UpdateState(cmd_code, 0, orig);

            if ( smtp_request ) {
                int data_len = end_of_line - line;

                if ( cmd_len > 0 || data_len > 0 )
                    RequestEvent(cmd_len, cmd, data_len, line);
            }

            // See above, might have already done so.
            bool do_update_state = cmd_code != detail::SMTP_CMD_END_OF_DATA;

            if ( cmd_code == detail::SMTP_CMD_BDAT )
                // Do not update state if this isn't a valid BDAT command.
                do_update_state = ProcessBdatArg(end_of_line - line, line, orig);
            else if ( bdat ) {
                // Non-BDAT command from client but still have BDAT state,
                // close it out. This can happen when a client started to
                // send BDAT chunks, but starts sending other commands without
                // a last BDAT chunk.
                Weird("smtp_missing_bdat_last_chunk");
                EndData();
            }

            if ( do_update_state )
                UpdateState(cmd_code, 0, orig);
        }
    }

    else {
        int reply_code;

        if ( length >= 3 && isdigit(line[0]) && isdigit(line[1]) && isdigit(line[2]) ) {
            reply_code = (line[0] - '0') * 100 + (line[1] - '0') * 10 + (line[2] - '0');
        }
        else
            reply_code = -1;

        // The first digit of reply code must be between 1 and 5,
        // and the second between 0 and 5 (RFC 2821).  But sometimes
        // we see 5xx codes larger than 559, so we still tolerate that.
        if ( reply_code < 100 || reply_code > 599 ) {
            reply_code = -1;
            Unexpected(is_sender, "reply code out of range", length, line);
            AnalyzerViolation(util::fmt("reply code %d out of range", reply_code), line, length);
        }

        else { // Valid reply code.
            if ( pending_reply && reply_code != pending_reply ) {
                Unexpected(is_sender, "reply code does not match the continuing reply", length, line);
                pending_reply = 0;
            }

            if ( ! pending_reply && reply_code >= 0 )
                // It is not a continuation.
                NewReply(reply_code, orig);

            // Update pending_reply.
            if ( reply_code >= 0 && length > 3 && line[3] == '-' ) { // A continued reply.
                pending_reply = reply_code;
                line = util::skip_whitespace(line + 4, end_of_line);
            }

            else { // This is the end of the reply.
                line = util::skip_whitespace(line + 3, end_of_line);

                pending_reply = 0;
                expect_sender = true;
                expect_recver = false;
            }

            // Generate events.
            if ( smtp_reply && reply_code >= 0 ) {
                int cmd_code = last_replied_cmd;
                switch ( cmd_code ) {
                    case detail::SMTP_CMD_CONN_ESTABLISHMENT: cmd = ">"; break;

                    case detail::SMTP_CMD_END_OF_DATA: cmd = "."; break;

                    default: cmd = SMTP_CMD_WORD(cmd_code); break;
                }

                EnqueueConnEvent(smtp_reply, ConnVal(), val_mgr->Bool(orig), val_mgr->Count(reply_code),
                                 make_intrusive<StringVal>(cmd), make_intrusive<StringVal>(end_of_line - line, line),
                                 val_mgr->Bool((pending_reply > 0)));
            }
        }

        // Process SMTP extensions, e.g. PIPELINING.
        if ( last_replied_cmd == detail::SMTP_CMD_EHLO && reply_code == 250 ) {
            const char* ext;
            int ext_len;

            util::get_word(end_of_line - line, line, ext_len, ext);
            ProcessExtension(ext_len, ext);
        }
    }
}

void SMTP_Analyzer::NewCmd(int cmd_code) {
    if ( pipelining ) {
        if ( first_cmd < 0 )
            first_cmd = cmd_code;
        else {
            auto constexpr max_pending_cmd_q_size = 1000;

            if ( pending_cmd_q.size() == max_pending_cmd_q_size ) {
                Weird("smtp_excessive_pending_cmds");
                pending_cmd_q.clear();
            }

            pending_cmd_q.push_back(cmd_code);
        }
    }
    else
        first_cmd = cmd_code;
}

void SMTP_Analyzer::StartTLS() {
    // STARTTLS was successful. Remove SMTP support analyzers, add SSL
    // analyzer, and throw event signifying the change.
    state = detail::SMTP_IN_TLS;
    expect_sender = expect_recver = true;

    RemoveSupportAnalyzer(cl_orig);
    RemoveSupportAnalyzer(cl_resp);

    Analyzer* ssl = analyzer_mgr->InstantiateAnalyzer("SSL", Conn());
    if ( ssl )
        AddChildAnalyzer(ssl);

    if ( smtp_starttls )
        EnqueueConnEvent(smtp_starttls, ConnVal());
}

// Here we keep a SMTP state machine and update it on each reply.
// However, the purpose is NOT to check correctness of SMTP commands
// and replies, but to guess the state of the SMTP session and,
// particularly, to know when we are in the SMTP_IN_DATA state.
//
// That is why state transition does not depend on the previous state,
// but only depend on the <command, reply> pair.
//
// Why not simply have two-state machine, IN_DATA/NOT_IN_DATA?  Because
// we want to understand the behavior of SMTP and check how far it may
// deviate from our knowledge.

void SMTP_Analyzer::NewReply(int reply_code, bool orig) {
    if ( state == detail::SMTP_AFTER_GAP && reply_code > 0 ) {
        state = detail::SMTP_GAP_RECOVERY;
        RequestEvent(strlen(unknown_cmd), unknown_cmd, 0, "");
        /*
        if ( line_after_gap )
            ProcessLine(sender, line_after_gap->Len(), (const char *) line_after_gap->Bytes());
        */
    }

    // Make all parameters constants.
    int cmd_code = first_cmd;

    // To recover from a gap, we detect replies -- the critical
    // assumptions here are 1) receiver does not reply during a DATA
    // session; 2) there is no TURN in the gap.

    last_replied_cmd = first_cmd;
    first_cmd = -1;

    if ( pipelining && pending_cmd_q.size() > 0 ) {
        first_cmd = pending_cmd_q.front();
        pending_cmd_q.pop_front();
    }

    UpdateState(cmd_code, reply_code, orig);
}

// Note: reply_code == 0 means we haven't seen the reply, in which case we
// still update the state as if the command will succeed, and later
// adjust the state if it turns out otherwise. This is because some
// clients are really aggressive in pipelining (beyond the restrictions
// in the RPC), and as a result we have to update the state following
// the commands in addition to the replies.

void SMTP_Analyzer::UpdateState(int cmd_code, int reply_code, bool orig) {
    int st = state;

    if ( st == detail::SMTP_QUIT && reply_code == 0 )
        UnexpectedCommand(cmd_code, reply_code);

    switch ( cmd_code ) {
        case detail::SMTP_CMD_CONN_ESTABLISHMENT:
            switch ( reply_code ) {
                case 0:
                    if ( st != detail::SMTP_CONNECTED ) {
                        // Impossible state, because the command
                        // CONN_ESTABLISHMENT should only appear
                        // in the very beginning.
                        UnexpectedCommand(cmd_code, reply_code);
                    }
                    state = detail::SMTP_INITIATED;
                    break;

                case 220: break;

                case 421:
                case 554: state = detail::SMTP_NOT_AVAILABLE; break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }
            break;

        case detail::SMTP_CMD_EHLO:
        case detail::SMTP_CMD_HELO:
            switch ( reply_code ) {
                case 0:
                    if ( st != detail::SMTP_INITIATED )
                        UnexpectedCommand(cmd_code, reply_code);
                    state = detail::SMTP_READY;
                    break;

                case 250: break;

                case 421:
                case 500:
                case 501:
                case 504:
                case 550: state = detail::SMTP_INITIATED; break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }
            break;

        case detail::SMTP_CMD_MAIL:
        case detail::SMTP_CMD_SEND:
        case detail::SMTP_CMD_SOML:
        case detail::SMTP_CMD_SAML:
            switch ( reply_code ) {
                case 0:
                    if ( st != detail::SMTP_READY )
                        UnexpectedCommand(cmd_code, reply_code);
                    state = detail::SMTP_MAIL_OK;
                    break;

                case 250: break;

                case 421:
                case 451:
                case 452:
                case 500:
                case 501:
                case 503:
                case 550:
                case 552:
                case 553:
                    if ( state != detail::SMTP_IN_DATA )
                        state = detail::SMTP_READY;
                    break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }
            break;

        case detail::SMTP_CMD_RCPT:
            switch ( reply_code ) {
                case 0:
                    if ( st != detail::SMTP_MAIL_OK && st != detail::SMTP_RCPT_OK )
                        UnexpectedCommand(cmd_code, reply_code);
                    state = detail::SMTP_RCPT_OK;
                    break;

                case 250:
                case 251: // ?? Shall we catch 251? (RFC 2821)
                    break;

                case 421:
                case 450:
                case 451:
                case 452:
                case 500:
                case 501:
                case 503:
                case 550:
                case 551: // ?? Shall we catch 551?
                case 552:
                case 553:
                case 554: // = transaction failed/recipient refused
                    break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }
            break;

        case detail::SMTP_CMD_DATA:
            switch ( reply_code ) {
                case 0:
                    if ( state != detail::SMTP_RCPT_OK )
                        UnexpectedCommand(cmd_code, reply_code);
                    BeginData(orig);
                    break;

                case 354: break;

                case 421:
                    if ( state == detail::SMTP_IN_DATA )
                        EndData();
                    state = detail::SMTP_QUIT;
                    break;

                case 500:
                case 501:
                case 503:
                case 451:
                case 554:
                    if ( state == detail::SMTP_IN_DATA )
                        EndData();
                    state = detail::SMTP_READY;
                    break;

                default:
                    UnexpectedReply(cmd_code, reply_code);
                    if ( state == detail::SMTP_IN_DATA )
                        EndData();
                    state = detail::SMTP_READY;
                    break;
            }
            break;

        case detail::SMTP_CMD_BDAT:
            switch ( reply_code ) {
                case 0:
                    if ( state != detail::SMTP_RCPT_OK )
                        UnexpectedCommand(cmd_code, reply_code);

                    assert(bdat);
                    state = detail::SMTP_IN_BDAT;
                    break;

                case 250: break; // server accepted BDAT transfer.

                case 421:
                case 500:
                case 501:
                case 503:
                case 451:
                case 554:
                    // Client will continue completing the inflight chunk no matter
                    // what the server replies, so we don't call EndData() here as
                    // it might be interesting what the client does actually send,
                    // even if the server isn't accepting it.
                    break;

                default:
                    UnexpectedReply(cmd_code, reply_code);
                    // Chunks might still be in-flight. See above.
                    break;
            }
            break;

        case detail::SMTP_CMD_END_OF_DATA:
            switch ( reply_code ) {
                case 0:
                    if ( st != detail::SMTP_IN_DATA && st != detail::SMTP_IN_BDAT )
                        UnexpectedCommand(cmd_code, reply_code);
                    EndData();
                    state = detail::SMTP_AFTER_DATA;
                    break;

                case 250: break;

                case 421:
                case 451:
                case 452:
                case 552:
                case 554: break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }

            if ( reply_code > 0 )
                state = detail::SMTP_READY;
            break;

        case detail::SMTP_CMD_RSET:
            switch ( reply_code ) {
                case 0: state = detail::SMTP_READY; break;

                case 250: break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }

            break;

        case detail::SMTP_CMD_QUIT:
            switch ( reply_code ) {
                case 0: state = detail::SMTP_QUIT; break;

                case 221: break;

                default: UnexpectedReply(cmd_code, reply_code); break;
            }

            break;

        case detail::SMTP_CMD_AUTH:
            if ( st != detail::SMTP_READY )
                UnexpectedCommand(cmd_code, reply_code);

            switch ( reply_code ) {
                case 0:
                    // Here we wait till there's a reply.
                    break;

                case 334: state = detail::SMTP_IN_AUTH; break;

                case 235: state = detail::SMTP_INITIATED; break;

                case 432:
                case 454:
                case 501:
                case 503:
                case 504:
                case 534:
                case 535:
                case 538:
                default: state = detail::SMTP_INITIATED; break;
            }
            break;

        case detail::SMTP_CMD_AUTH_ANSWER:
            if ( st != detail::SMTP_IN_AUTH )
                UnexpectedCommand(cmd_code, reply_code);

            switch ( reply_code ) {
                case 0:
                    // Here we wait till there's a reply.
                    break;

                case 334: state = detail::SMTP_IN_AUTH; break;

                case 235:
                case 535:
                default: state = detail::SMTP_INITIATED; break;
            }
            break;

        case detail::SMTP_CMD_TURN:
            if ( st != detail::SMTP_READY )
                UnexpectedCommand(cmd_code, reply_code);

            switch ( reply_code ) {
                case 0:
                    // Here we wait till there's a reply.
                    break;

                case 250:
                    // flip-side
                    orig_is_sender = ! orig_is_sender;

                    state = detail::SMTP_CONNECTED;
                    expect_sender = false;
                    expect_recver = true;
                    break;

                case 502:
                default: break;
            }
            break;

        case detail::SMTP_CMD_STARTTLS:
        case detail::SMTP_CMD_X_ANONYMOUSTLS:
            if ( st != detail::SMTP_READY )
                UnexpectedCommand(cmd_code, reply_code);

            switch ( reply_code ) {
                case 0:
                    // Here we wait till there's a reply.
                    break;

                case 220: StartTLS(); break;

                case 454:
                case 501:
                default: break;
            }
            break;

        case detail::SMTP_CMD_VRFY:
        case detail::SMTP_CMD_EXPN:
        case detail::SMTP_CMD_HELP:
        case detail::SMTP_CMD_NOOP:
            // These commands do not affect state.
            // ?? However, later we may want to add reply
            // and state check code.

        default:
            if ( st == detail::SMTP_GAP_RECOVERY && reply_code == 354 ) {
                BeginData(orig);
            }
            break;
    }

        // A hack: whenever the server makes a valid reply during a DATA
        // section, we assume that the DATA section has ended (the end
        // of data line might have been lost due to gaps in trace).  Note,
        // BeginData() won't be called till the next DATA command.
#if 0
	if ( state == detail::SMTP_IN_DATA && reply_code >= 400 )
		{
		EndData();
		state = detail::SMTP_READY;
		}
#endif
}

static bool istrequal(const char* s, const char* cmd, int s_len) {
    int cmd_len = strlen(cmd);

    if ( cmd_len != s_len )
        return false;

    return strncasecmp(s, cmd, s_len) == 0;
}

void SMTP_Analyzer::ProcessExtension(int ext_len, const char* ext) {
    if ( ! ext )
        return;

    if ( istrequal(ext, "PIPELINING", ext_len) )
        pipelining = true;
}

int SMTP_Analyzer::ParseCmd(int cmd_len, const char* cmd) {
    if ( ! cmd )
        return -1;

    // special case because we cannot define our usual macros with "-"
    if ( istrequal(cmd, "X-ANONYMOUSTLS", cmd_len) )
        return detail::SMTP_CMD_X_ANONYMOUSTLS;

    for ( int code = detail::SMTP_CMD_EHLO; code < detail::SMTP_CMD_LAST; ++code )
        if ( istrequal(cmd, smtp_cmd_word[code - detail::SMTP_CMD_EHLO], cmd_len) )
            return code;

    return -1;
}

void SMTP_Analyzer::RequestEvent(int cmd_len, const char* cmd, int arg_len, const char* arg) {
    AnalyzerConfirmation();

    if ( smtp_request ) {
        auto cmd_arg = make_intrusive<StringVal>(cmd_len, cmd);
        cmd_arg->ToUpper();

        EnqueueConnEvent(smtp_request, ConnVal(), val_mgr->Bool(orig_is_sender), std::move(cmd_arg),
                         make_intrusive<StringVal>(arg_len, arg));
    }
}

void SMTP_Analyzer::Unexpected(bool is_sender, const char* msg, int detail_len, const char* detail) {
    // Either party can send a line after an unexpected line.
    expect_sender = expect_recver = true;

    if ( smtp_unexpected ) {
        bool is_orig = is_sender;
        if ( ! orig_is_sender )
            is_orig = ! is_orig;

        EnqueueConnEvent(smtp_unexpected, ConnVal(), val_mgr->Bool(is_orig), make_intrusive<StringVal>(msg),
                         make_intrusive<StringVal>(detail_len, detail));
    }
}

void SMTP_Analyzer::UnexpectedCommand(int cmd_code, int reply_code) {
    // If this happens, please fix the SMTP state machine!
    // ### Eventually, these should be turned into "weird" events.
    static char buf[512];
    int len = snprintf(buf, sizeof(buf), "%s reply = %d state = %d", SMTP_CMD_WORD(cmd_code), reply_code, state);
    if ( len > (int)sizeof(buf) )
        len = sizeof(buf);
    Unexpected(true, "unexpected command", len, buf);
}

void SMTP_Analyzer::UnexpectedReply(int cmd_code, int reply_code) {
    // If this happens, please fix the SMTP state machine!
    // ### Eventually, these should be turned into "weird" events.
    static char buf[512];
    int len =
        snprintf(buf, sizeof(buf), "%d state = %d, last command = %s", reply_code, state, SMTP_CMD_WORD(cmd_code));
    Unexpected(true, "unexpected reply", len, buf);
}

void SMTP_Analyzer::ProcessData(int length, const char* line) { mail->Deliver(length, line, true /* trailing_CRLF */); }

bool SMTP_Analyzer::ProcessBdatArg(int arg_len, const char* arg, bool orig) {
    // For the BDAT command, parse out the chunk-size from the line
    // and switch the ContentLineAnalyzer into plain delivery mode
    // assuming things look valid.
    const auto [chunk_size, is_last_chunk, error] = detail::parse_bdat_arg(arg_len, arg);
    if ( error ) {
        Weird("smtp_invalid_bdat_command", error);
        return false;
    }

    // The ContentLine analyzer only supports int64_t, but BDAT could deal
    // with uint64_t sized chunks. Weird if the chunk size is larger and
    // do not configure the ContentLine analyzer for plain delivery.
    if ( chunk_size > std::numeric_limits<int64_t>::max() ) {
        const char* addl = zeek::util::fmt("%" PRIu64, chunk_size);
        Weird("smtp_huge_bdat_chunk", addl);
        return false;
    }

    auto* cl = orig ? cl_orig : cl_resp;
    cl->SetPlainDelivery(chunk_size);

    if ( ! bdat ) {
        // This is the first BDAT chunk.
        BeginData(orig, detail::SMTP_IN_BDAT);
        bdat = std::make_unique<detail::SMTP_BDAT_Analyzer>(Conn(), mail, zeek::BifConst::SMTP::bdat_max_line_length);
    }

    bdat->NextChunk(is_last_chunk ? detail::ChunkType::Last : detail::ChunkType::Intermediate, chunk_size);

    // All good.
    return true;
}

void SMTP_Analyzer::BeginData(bool orig, detail::SMTP_State new_state) {
    state = new_state;
    skip_data = false; // reset the flag at the beginning of the mail
    if ( mail != nullptr ) {
        Weird("smtp_nested_mail_transaction");
        mail->Done();
        delete mail;
    }

    mail = new analyzer::mime::MIME_Mail(this, orig);
}

void SMTP_Analyzer::EndData() {
    if ( bdat ) {
        if ( bdat->RemainingChunkSize() > 0 ) {
            Weird("smtp_bdat_remaining_at_end_data");
            cl_orig->SetPlainDelivery(0);
            cl_resp->SetPlainDelivery(0);
        }

        bdat->Done();
        bdat.reset();
    }

    if ( ! mail )
        Weird("smtp_unmatched_end_of_data");
    else {
        mail->Done();
        delete mail;
        mail = nullptr;
    }
}

} // namespace zeek::analyzer::smtp
