/****************************************************************************
 **
 ** Copyright (C) Qxt Foundation. Some rights reserved.
 **
 ** This file is part of the QxtWeb module of the Qxt library.
 **
 ** This library is free software; you can redistribute it and/or modify it
 ** under the terms of the Common Public License, version 1.0, as published
 ** by IBM, and/or under the terms of the GNU Lesser General Public License,
 ** version 2.1, as published by the Free Software Foundation.
 **
 ** This file is provided "AS IS", without WARRANTIES OR CONDITIONS OF ANY
 ** KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
 ** WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR
 ** FITNESS FOR A PARTICULAR PURPOSE.
 **
 ** You should have received a copy of the CPL and the LGPL along with this
 ** file. See the LICENSE file and the cpl1.0.txt/lgpl-2.1.txt files
 ** included with the source distribution for more information.
 ** If you did not receive a copy of the licenses, contact the Qxt Foundation.
 **
 ** <http://libqxt.org>  <foundation@libqxt.org>
 **
 ****************************************************************************/


/*!
 * \class QxtMailMessage
 * \inmodule QxtNetwork
 * \brief The QxtMailMessage class encapsulates an e-mail according to RFC 2822 and related specifications
 * TODO: {implicitshared}
 */


#include "qxtmailmessage.h"
#include "qxtmail_p.h"
#include <QTextCodec>
#include <QUuid>
#include <QDir>
#include <QtDebug>


struct QxtMailMessagePrivate : public QSharedData
{
    QxtMailMessagePrivate() = default;
    QxtMailMessagePrivate(const QxtMailMessagePrivate& other)
            : QSharedData(other), rcptTo(other.rcptTo), rcptCc(other.rcptCc), rcptBcc(other.rcptBcc),
            subject(other.subject), body(other.body), sender(other.sender),
            extraHeaders(other.extraHeaders), attachments(other.attachments) {}
    QStringList rcptTo, rcptCc, rcptBcc;
    QString subject, body, sender;
    QHash<QString, QString> extraHeaders;
    QHash<QString, QxtMailAttachment> attachments;
    mutable QByteArray boundary;
};

QxtMailMessage::QxtMailMessage()
{
    qxt_d = new QxtMailMessagePrivate;
}

QxtMailMessage::QxtMailMessage(const QxtMailMessage& other) : qxt_d(other.qxt_d)
{
    // trivial copy constructor
}

QxtMailMessage::QxtMailMessage(const QString& sender, const QString& recipient)
{
    qxt_d = new QxtMailMessagePrivate;
    setSender(sender);
    addRecipient(recipient);
}

QxtMailMessage::~QxtMailMessage()
{
    // trivial destructor
}

QxtMailMessage& QxtMailMessage::operator=(const QxtMailMessage & other) = default;

QString QxtMailMessage::sender() const
{
    return qxt_d->sender;
}

void QxtMailMessage::setSender(const QString& a)
{
    qxt_d->sender = a;
}

QString QxtMailMessage::subject() const
{
    return qxt_d->subject;
}

void QxtMailMessage::setSubject(const QString& a)
{
    qxt_d->subject = a;
}

QString QxtMailMessage::body() const
{
    return qxt_d->body;
}

void QxtMailMessage::setBody(const QString& a)
{
    qxt_d->body = a;
}

QStringList QxtMailMessage::recipients(QxtMailMessage::RecipientType type) const
{
    if (type == Bcc)
        return qxt_d->rcptBcc;
    if (type == Cc)
        return qxt_d->rcptCc;
    return qxt_d->rcptTo;
}

void QxtMailMessage::addRecipient(const QString& a, QxtMailMessage::RecipientType type)
{
    if (type == Bcc)
        qxt_d->rcptBcc.append(a);
    else if (type == Cc)
        qxt_d->rcptCc.append(a);
    else
        qxt_d->rcptTo.append(a);
}

void QxtMailMessage::removeRecipient(const QString& a)
{
    qxt_d->rcptTo.removeAll(a);
    qxt_d->rcptCc.removeAll(a);
    qxt_d->rcptBcc.removeAll(a);
}

QHash<QString, QString> QxtMailMessage::extraHeaders() const
{
    return qxt_d->extraHeaders;
}

QString QxtMailMessage::extraHeader(const QString& key) const
{
    return qxt_d->extraHeaders[key.toLower()];
}

bool QxtMailMessage::hasExtraHeader(const QString& key) const
{
    return qxt_d->extraHeaders.contains(key.toLower());
}

void QxtMailMessage::setExtraHeader(const QString& key, const QString& value)
{
    qxt_d->extraHeaders[key.toLower()] = value;
}

void QxtMailMessage::setExtraHeaders(const QHash<QString, QString>& a)
{
    QHash<QString, QString>& headers = qxt_d->extraHeaders;
    headers.clear();
    for(const QString& key: a.keys())
    {
        headers[key.toLower()] = a[key];
    }
}

void QxtMailMessage::removeExtraHeader(const QString& key)
{
    qxt_d->extraHeaders.remove(key.toLower());
}

QHash<QString, QxtMailAttachment> QxtMailMessage::attachments() const
{
    return qxt_d->attachments;
}

QxtMailAttachment QxtMailMessage::attachment(const QString& filename) const
{
    return qxt_d->attachments[filename];
}

void QxtMailMessage::addAttachment(const QString& filename, const QxtMailAttachment& attach)
{
    if (qxt_d->attachments.contains(filename))
    {
        qWarning() << "QxtMailMessage::addAttachment: " << filename << " already in use";
        int i = 1;
		while (qxt_d->attachments.contains(filename + QStringLiteral(".") + QString::number(i)))
        {
            i++;
        }
		qxt_d->attachments[filename+QStringLiteral(".")+QString::number(i)] = attach;
    }
    else
    {
        qxt_d->attachments[filename] = attach;
    }
}

void QxtMailMessage::removeAttachment(const QString& filename)
{
    qxt_d->attachments.remove(filename);
}

QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextCodec* latin1, const QByteArray& prefix)
{
    QByteArray rv = "";
    QByteArray line = key.toLatin1() + ": ";
    if (!prefix.isEmpty()) line += prefix;
	if (!value.contains(QStringLiteral("=?")) && latin1->canEncode(value))
    {
        bool firstWord = true;
        for(const QByteArray& word: value.toLatin1().split(' '))
        {
            if (line.size() > 78)
            {
                rv = rv + line + "\r\n";
                line.clear();
            }
            if (firstWord)
                line += word;
            else
                line += " " + word;
            firstWord = false;
        }
    }
    else
    {
        // The text cannot be losslessly encoded as Latin-1. Therefore, we
        // must use quoted-printable or base64 encoding. This is a quick
        // heuristic based on the first 100 characters to see which
        // encoding to use.
        QByteArray utf8 = value.toUtf8();
        int ct = utf8.length();
        int nonAscii = 0;
        for (int i = 0; i < ct && i < 100; i++)
        {
            if (QXT_MUST_QP(utf8[i])) nonAscii++;
        }
        if (nonAscii > 20)
        {
            // more than 20%-ish non-ASCII characters: use base64
            QByteArray base64 = utf8.toBase64();
            ct = base64.length();
            line += "=?utf-8?b?";
            for (int i = 0; i < ct; i += 4)
            {
                if (line.length() > 72)
                {
                    rv += line + "?\r\n";
                    line = " =?utf-8?b?";
                }
                line = line + base64.mid(i, 4);
            }
        }
        else
        {
            // otherwise use Q-encoding
            line += "=?utf-8?q?";
            for (int i = 0; i < ct; i++)
            {
                if (line.length() > 73)
                {
                    rv += line + "?\r\n";
                    line = " =?utf-8?q?";
                }
                if (QXT_MUST_QP(utf8[i]) || utf8[i] == ' ')
                {
                    line += "=" + utf8.mid(i, 1).toHex().toUpper();
                }
                else
                {
                    line += utf8[i];
                }
            }
        }
        line += "?="; // end encoded-word atom
    }
    return rv + line + "\r\n";
}

QByteArray QxtMailMessage::rfc2822() const
{
    // Use quoted-printable if requested
	bool useQuotedPrintable = (extraHeader(QStringLiteral("Content-Transfer-Encoding")).toLower() == QLatin1String("quoted-printable"));
    // Use base64 if requested
	bool useBase64 = (extraHeader(QStringLiteral("Content-Transfer-Encoding")).toLower() == QLatin1String("base64"));
    // Check to see if plain text is ASCII-clean; assume it isn't if QP or base64 was requested
    QTextCodec* latin1 = QTextCodec::codecForName("latin1");
    bool bodyIsAscii = latin1->canEncode(body()) && !useQuotedPrintable && !useBase64;

    QHash<QString, QxtMailAttachment> attach = attachments();
    QByteArray rv;

	if (!sender().isEmpty() && !hasExtraHeader(QStringLiteral("From")))
    {
		rv += qxt_fold_mime_header(QStringLiteral("From"), sender(), latin1);
    }

    if (!qxt_d->rcptTo.isEmpty())
    {
		rv += qxt_fold_mime_header(QStringLiteral("To"), qxt_d->rcptTo.join(QStringLiteral(", ")), latin1);
    }

    if (!qxt_d->rcptCc.isEmpty())
    {
		rv += qxt_fold_mime_header(QStringLiteral("Cc"), qxt_d->rcptCc.join(QStringLiteral(", ")), latin1);
    }

    if (!subject().isEmpty())
    {
		rv += qxt_fold_mime_header(QStringLiteral("Subject"), subject(), latin1);
    }

    if (!bodyIsAscii)
    {
		if (!hasExtraHeader(QStringLiteral("MIME-Version")) && !attach.count())
            rv += "MIME-Version: 1.0\r\n";

        // If no transfer encoding has been requested, guess.
        // Heuristic: If >20% of the first 100 characters aren't
        // 7-bit clean, use base64, otherwise use Q-P.
        if(!bodyIsAscii && !useQuotedPrintable && !useBase64)
        {
            QString b = body();
            int nonAscii = 0;
            int ct = b.length();
            for (int i = 0; i < ct && i < 100; i++)
            {
				if (QXT_MUST_QP(b[i])) nonAscii++;
			}
            useQuotedPrintable = !(nonAscii > 20);
            useBase64 = !useQuotedPrintable;
        }
    }

    if (attach.count())
    {
        if (qxt_d->boundary.isEmpty())
            qxt_d->boundary = QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", "");
		if (!hasExtraHeader(QStringLiteral("MIME-Version")))
            rv += "MIME-Version: 1.0\r\n";
		if (!hasExtraHeader(QStringLiteral("Content-Type")))
            rv += "Content-Type: multipart/mixed; boundary=" + qxt_d->boundary + "\r\n";
    }
	else if (!bodyIsAscii && !hasExtraHeader(QStringLiteral("Content-Transfer-Encoding")))
    {
        if (!useQuotedPrintable)
        {
            // base64
            rv += "Content-Transfer-Encoding: base64\r\n";
        }
        else
        {
            // quoted-printable
            rv += "Content-Transfer-Encoding: quoted-printable\r\n";
        }
    }

    for(const QString& r: qxt_d->extraHeaders.keys())
    {
		if ((r.toLower() == QLatin1String("content-type") || r.toLower() == QLatin1String("content-transfer-encoding")) && attach.count())
        {
            // Since we're in multipart mode, we'll be outputting this later
            continue;
        }
		rv += qxt_fold_mime_header(r, extraHeader(r), latin1);
    }

    rv += "\r\n";

    if (attach.count())
    {
        // we're going to have attachments, so output the lead-in for the message body
        rv += "This is a message with multiple parts in MIME format.\r\n";
        rv += "--" + qxt_d->boundary + "\r\nContent-Type: ";
		if (hasExtraHeader(QStringLiteral("Content-Type")))
			rv += extraHeader(QStringLiteral("Content-Type")).toLatin1() + "\r\n";
        else
            rv += "text/plain; charset=UTF-8\r\n";
		if (hasExtraHeader(QStringLiteral("Content-Transfer-Encoding")))
        {
			rv += "Content-Transfer-Encoding: " + extraHeader(QStringLiteral("Content-Transfer-Encoding")).toLatin1() + "\r\n";
        }
        else if (!bodyIsAscii)
        {
            if (!useQuotedPrintable)
            {
                // base64
                rv += "Content-Transfer-Encoding: base64\r\n";
            }
            else
            {
                // quoted-printable
                rv += "Content-Transfer-Encoding: quoted-printable\r\n";
            }
        }
        rv += "\r\n";
    }

    if (bodyIsAscii)
    {
        QByteArray b = latin1->fromUnicode(body());
        int len = b.length();
        QByteArray line = "";
        QByteArray word = "";
        for (int i = 0; i < len; i++)
        {
            if (b[i] == '\n' || b[i] == '\r')
            {
                if (line.isEmpty()) 
                {
                    line = word;
                    word = "";
                }
                else if (line.length() + word.length() + 1 <= 78)
                {
                    line = line + ' ' + word;
                    word = "";
                }
                if(line[0] == '.')
                    rv += ".";
                rv += line + "\r\n";
                if ((b[i+1] == '\n' || b[i+1] == '\r') && b[i] != b[i+1])
                {
                    // If we're looking at a CRLF pair, skip the second half
                    i++;
                }
                line = word;
            }
            else if (b[i] == ' ')
            {
                if (line.length() + word.length() + 1 > 78)
                {
                    if(line[0] == '.')
                        rv += ".";
                    rv += line + "\r\n";
                    line = word;
                }
                else if (line.isEmpty())
                {
                    line = word;
                }
                else
                {
                    line = line + ' ' + word;
                }
                word = "";
            }
            else
            {
                word += b[i];
            }
        }
        if (line.length() + word.length() + 1 > 78)
        {
            if(line[0] == '.')
                rv += ".";
            rv += line + "\r\n";
            line = word;
        }
        else if (!word.isEmpty())
        {
            line += ' ' + word;
        }
        if(!line.isEmpty()) {
            if(line[0] == '.')
                rv += ".";
            rv += line + "\r\n";
        }
    }
    else if (useQuotedPrintable)
    {
        QByteArray b = body().toUtf8();
        int ct = b.length();
        QByteArray line;
        for (int i = 0; i < ct; i++)
        {
            if(b[i] == '\n' || b[i] == '\r')
            {
                if(line[0] == '.')
                    rv += ".";
                rv += line + "\r\n";
                line = "";
                if ((b[i+1] == '\n' || b[i+1] == '\r') && b[i] != b[i+1])
                {
                    // If we're looking at a CRLF pair, skip the second half
                    i++;
                }
            }
            else if (line.length() > 74)
            {
                rv += line + "=\r\n";
                line = "";
            }
            if (QXT_MUST_QP(b[i]))
            {
                line += "=" + b.mid(i, 1).toHex().toUpper();
            }
            else
            {
                line += b[i];
            }
        }
        if(!line.isEmpty()) {
            if(line[0] == '.')
                rv += ".";
            rv += line + "\r\n";
        }
    }
    else /* base64 */
    {
        QByteArray b = body().toUtf8().toBase64();
        int ct = b.length();
        for (int i = 0; i < ct; i += 78)
        {
            rv += b.mid(i, 78) + "\r\n";
        }
    }

    if (attach.count())
    {
        for(const QString& filename: attach.keys())
        {
            rv += "--" + qxt_d->boundary + "\r\n";
			rv += qxt_fold_mime_header(QStringLiteral("Content-Disposition"), QDir(filename).dirName(), latin1, "attachment; filename=");
            rv += attach[filename].mimeData();
        }
        rv += "--" + qxt_d->boundary + "--\r\n";
    }

    return rv;
}
