/*
  SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com>
  SPDX-FileContributor: Christophe Laveault <christophe@betterinbox.com>
  SPDX-FileContributor: Gregory Schlomoff <gregory.schlomoff@gmail.com>

  SPDX-License-Identifier: LGPL-2.1-or-later
*/

#include "sendjob.h"
#include "job_p.h"
#include "ksmtp_debug.h"
#include "serverresponse_p.h"

#include <KLocalizedString>
#include <QUrl>

namespace KSmtp
{
class SendJobPrivate : public JobPrivate
{
public:
    enum Status {
        Idle,
        SendingReturnPath,
        SendingRecipients,
        SendingData
    };

    SendJobPrivate(SendJob *job, Session *session, const QString &name)
        : JobPrivate(session, name)
        , q(job)
    {
    }

    SendJob *const q;

    void sendNextRecipient();
    void addRecipients(const QStringList &rcpts);
    bool prepare();
    void analyzeAddresses();
    [[nodiscard]] QByteArray addressForSmtp(const QString &addr) const;

    QString m_returnPath;
    QStringList m_recipients;
    QByteArray m_data;

    QStringList m_recipientsCopy;
    Status m_status = Idle;
    bool m_dsn = false;
    bool m_smtputf8 = false;
    bool m_punycode = false;
};
}

using namespace KSmtp;

SendJob::SendJob(Session *session)
    : Job(*new SendJobPrivate(this, session, i18n("SendJob")))
{
}

void SendJob::setFrom(const QString &from)
{
    Q_D(SendJob);
    const auto start = from.indexOf(u'<');
    if (start > -1) {
        const auto end = qMax(start, from.indexOf(u'>', start));
        d->m_returnPath = u"<%1>"_s.arg(from.mid(start + 1, end - start - 1));
    } else {
        d->m_returnPath = u"<%1>"_s.arg(from);
    }
}

void SendJob::setTo(const QStringList &to)
{
    Q_D(SendJob);
    d->addRecipients(to);
}

void SendJob::setCc(const QStringList &cc)
{
    Q_D(SendJob);
    d->addRecipients(cc);
}

void SendJob::setBcc(const QStringList &bcc)
{
    Q_D(SendJob);
    d->addRecipients(bcc);
}

void SendJob::setData(const QByteArray &data)
{
    Q_D(SendJob);
    d->m_data = data;
    // A line with a single dot would make SMTP think "end of message", so use two dots in that case,
    // as per https://tools.ietf.org/html/rfc5321#section-4.5.2
    d->m_data.replace("\r\n.", "\r\n..");
}

void SendJob::doStart()
{
    Q_D(SendJob);

    if (!d->prepare()) {
        setError(KJob::UserDefinedError);
        setErrorText(i18n("Could not send the message because either the sender or recipient field is missing or invalid"));
        emitResult();
        return;
    }

    const int sizeLimit = session()->sizeLimit();
    if (sizeLimit > 0 && size() > sizeLimit) {
        setError(KJob::UserDefinedError);
        setErrorText(i18n("Could not send the message because it exceeds the maximum allowed size of %1 bytes. (Message size: %2 bytes.)", sizeLimit, size()));
        emitResult();
        return;
    }

    d->analyzeAddresses();

    if (d->m_smtputf8 && !session()->allowsSmtpUtf8()) {
        setError(KJob::UserDefinedError);
        setErrorText(i18n("The message needs and server does not support UTF-8 email addresses"));
        emitResult();
        return;
    }

    d->m_status = SendJobPrivate::SendingReturnPath;
    const QString bareFrom = d->m_returnPath.mid(1, d->m_returnPath.length() - 2);
    QByteArray mailFrom = "MAIL FROM:<" + d->addressForSmtp(bareFrom) + '>';
    if (d->m_smtputf8)
        mailFrom += " SMTPUTF8";
    sendCommand(mailFrom);
}

void SendJob::handleResponse(const ServerResponse &r)
{
    Q_D(SendJob);

    // Handle server errors
    handleErrors(r);

    switch (d->m_status) {
    case SendJobPrivate::Idle:
        // TODO: anything to do here?
        break;

    case SendJobPrivate::SendingReturnPath:

        // Expected response: server agreement
        if (r.isCode(25)) {
            d->m_status = SendJobPrivate::SendingRecipients;
            d->sendNextRecipient();
        }
        break;

    case SendJobPrivate::SendingRecipients:

        // Expected response: server agreement
        if (r.isCode(25)) {
            if (d->m_recipientsCopy.isEmpty()) {
                sendCommand("DATA");
                d->m_status = SendJobPrivate::SendingData;
            } else {
                d->sendNextRecipient();
            }
        }
        break;

    case SendJobPrivate::SendingData:

        // Expected responses:
        // 354: Go ahead sending data
        if (r.isCode(354)) {
            sendCommand(d->m_data);
            sendCommand("\r\n.");
        }

        // 25x: Data received correctly
        if (r.isCode(25)) {
            emitResult();
        }
        break;
    }
}

void SendJobPrivate::sendNextRecipient()
{
    const bool dsnSupport = m_session->allowsDsn() ? m_dsn : false;
    // qDebug() << " void SendJobPrivate::sendNextRecipient()" << m_session->allowsDsn() << " dsnSupport " << dsnSupport;
    q->sendCommand("RCPT TO:<" + addressForSmtp(m_recipientsCopy.takeFirst()) + '>' + (dsnSupport ? " NOTIFY=success,failure" : ""));
}

void SendJobPrivate::analyzeAddresses()
{
    m_smtputf8 = false;
    m_punycode = false;

    // Collect all bare addresses: strip <> from return path, then all recipients
    QStringList addresses = m_recipients;
    if (!m_returnPath.isEmpty()) {
        addresses.prepend(m_returnPath.mid(1, m_returnPath.length() - 2));
    }

    for (const QString &addr : std::as_const(addresses)) {
        const int at = addr.indexOf(u'@');
        if (at < 0) {
            continue;
        }
        const bool localNonAscii = std::any_of(addr.cbegin(), addr.cbegin() + at, [](QChar c) {
            return c.unicode() > 127;
        });
        if (localNonAscii) {
            m_smtputf8 = true;
            return; // highest priority: non-ASCII localpart requires SMTPUTF8
        }
        const bool domainNonAscii = std::any_of(addr.cbegin() + at + 1, addr.cend(), [](QChar c) {
            return c.unicode() > 127;
        });
        if (domainNonAscii) {
            m_punycode = true; // non-ASCII domain: needs punycode encoding
        }
    }
}

QByteArray SendJobPrivate::addressForSmtp(const QString &addr) const
{
    if (m_smtputf8) {
        return addr.toUtf8();
    }
    if (m_punycode) {
        const int at = addr.indexOf(u'@');
        if (at < 0) {
            return addr.toUtf8();
        }
        return addr.left(at).toUtf8() + '@' + QUrl::toAce(addr.mid(at + 1));
    }
    return addr.toUtf8();
}

void SendJobPrivate::addRecipients(const QStringList &rcpts)
{
    for (const auto &rcpt : rcpts) {
        if (rcpt.isEmpty()) {
            continue;
        }

        const int start = rcpt.indexOf(u'<');
        if (start > -1) {
            const int end = qMax(start, rcpt.indexOf(u'>', start));
            m_recipients.push_back(rcpt.mid(start + 1, end - start - 1));
        } else {
            m_recipients.push_back(rcpt);
        }
    }
}

bool SendJobPrivate::prepare()
{
    if (m_data.isEmpty()) {
        qCWarning(KSMTP_LOG) << "A message has to be set before starting a SendJob";
        return false;
    }

    m_recipientsCopy = m_recipients;

    if (m_recipients.isEmpty()) {
        qCWarning(KSMTP_LOG) << "Message has no recipients";
        return false;
    }

    return true;
}

int SendJob::size() const
{
    Q_D(const SendJob);

    return d->m_data.size();
}

void SendJob::setDeliveryStatusNotification(bool enabled)
{
    Q_D(SendJob);
    d->m_dsn = enabled;
}

#include "moc_sendjob.cpp"
