915 lines
28 KiB
C++
915 lines
28 KiB
C++
|
|
/****************************************************************************
|
|
** Copyright (c) 2006 - 2011, the LibQxt project.
|
|
** See the Qxt AUTHORS file for a list of authors and copyright holders.
|
|
** All rights reserved.
|
|
**
|
|
** Redistribution and use in source and binary forms, with or without
|
|
** modification, are permitted provided that the following conditions are met:
|
|
** * Redistributions of source code must retain the above copyright
|
|
** notice, this list of conditions and the following disclaimer.
|
|
** * Redistributions in binary form must reproduce the above copyright
|
|
** notice, this list of conditions and the following disclaimer in the
|
|
** documentation and/or other materials provided with the distribution.
|
|
** * Neither the name of the LibQxt project nor the
|
|
** names of its contributors may be used to endorse or promote products
|
|
** derived from this software without specific prior written permission.
|
|
**
|
|
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
** DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
**
|
|
** <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>
|
|
#include <QRegExp>
|
|
|
|
|
|
struct QxtMailMessagePrivate : public QSharedData
|
|
{
|
|
QxtMailMessagePrivate() {}
|
|
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;
|
|
};
|
|
|
|
class QxtRfc2822Parser
|
|
{
|
|
public:
|
|
QxtMailMessagePrivate* parse(const QByteArray& buffer);
|
|
private:
|
|
enum State {
|
|
Headers,
|
|
Body
|
|
};
|
|
State state;
|
|
QString currentHeaderKey;
|
|
QStringList currentHeaderValue;
|
|
void parseHeader(const QByteArray& line, QHash<QString, QString>& headers);
|
|
void parseBody(QxtMailMessagePrivate* msg);
|
|
void parseEntity(const QByteArray& buffer, QHash<QString,QString>& headers, QString& body);
|
|
QxtMailAttachment* parseAttachment(const QHash<QString,QString>& headers, const QString& body, QString& filename);
|
|
QString unfoldValue(QStringList& folded);
|
|
QString decode(const QString& charset, const QString& encoding, const QString& encoded);
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
/*!
|
|
Constructs a new QxtMailMessage object from a \a buffer that conforms to RFC 2822 and the MIME related RFCs.
|
|
*/
|
|
QxtMailMessage::QxtMailMessage(const QByteArray& buffer)
|
|
{
|
|
QxtRfc2822Parser parser;
|
|
qxt_d = parser.parse(buffer);
|
|
}
|
|
|
|
QxtMailMessage::~QxtMailMessage()
|
|
{
|
|
// trivial destructor
|
|
}
|
|
|
|
QxtMailMessage& QxtMailMessage::operator=(const QxtMailMessage & other)
|
|
{
|
|
qxt_d = other.qxt_d;
|
|
return *this;
|
|
}
|
|
|
|
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();
|
|
foreach(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 + "." + QString::number(i)))
|
|
{
|
|
i++;
|
|
}
|
|
qxt_d->attachments[filename+"."+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("=?") && latin1->canEncode(value))
|
|
{
|
|
bool firstWord = true;
|
|
foreach(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("Content-Transfer-Encoding").toLower() == "quoted-printable");
|
|
// Use base64 if requested
|
|
bool useBase64 = (extraHeader("Content-Transfer-Encoding").toLower() == "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("From"))
|
|
{
|
|
rv += qxt_fold_mime_header("From", sender(), latin1);
|
|
}
|
|
|
|
if (!qxt_d->rcptTo.isEmpty())
|
|
{
|
|
rv += qxt_fold_mime_header("To", qxt_d->rcptTo.join(", "), latin1);
|
|
}
|
|
|
|
if (!qxt_d->rcptCc.isEmpty())
|
|
{
|
|
rv += qxt_fold_mime_header("Cc", qxt_d->rcptCc.join(", "), latin1);
|
|
}
|
|
|
|
if (!subject().isEmpty())
|
|
{
|
|
rv += qxt_fold_mime_header("Subject", subject(), latin1);
|
|
}
|
|
|
|
if (!bodyIsAscii)
|
|
{
|
|
if (!hasExtraHeader("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("MIME-Version"))
|
|
rv += "MIME-Version: 1.0\r\n";
|
|
if (!hasExtraHeader("Content-Type"))
|
|
rv += "Content-Type: multipart/mixed; boundary=" + qxt_d->boundary + "\r\n";
|
|
}
|
|
else if (!bodyIsAscii && !hasExtraHeader("Content-Transfer-Encoding"))
|
|
{
|
|
if (!useQuotedPrintable)
|
|
{
|
|
// base64
|
|
rv += "Content-Transfer-Encoding: base64\r\n";
|
|
}
|
|
else
|
|
{
|
|
// quoted-printable
|
|
rv += "Content-Transfer-Encoding: quoted-printable\r\n";
|
|
}
|
|
}
|
|
|
|
foreach(const QString& r, qxt_d->extraHeaders.keys())
|
|
{
|
|
if ((r.toLower() == "content-type" || r.toLower() == "content-transfer-encoding") && attach.count())
|
|
{
|
|
// Since we're in multipart mode, we'll be outputting this later
|
|
continue;
|
|
}
|
|
rv += qxt_fold_mime_header(r.toLatin1(), 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("Content-Type"))
|
|
rv += extraHeader("Content-Type") + "\r\n";
|
|
else
|
|
rv += "text/plain; charset=UTF-8\r\n";
|
|
if (hasExtraHeader("Content-Transfer-Encoding"))
|
|
{
|
|
rv += "Content-Transfer-Encoding: " + extraHeader("Content-Transfer-Encoding") + "\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())
|
|
{
|
|
foreach(const QString& filename, attach.keys())
|
|
{
|
|
rv += "--" + qxt_d->boundary + "\r\n";
|
|
rv += qxt_fold_mime_header("Content-Disposition", QDir(filename).dirName(), latin1, "attachment; filename=");
|
|
rv += attach[filename].mimeData();
|
|
}
|
|
rv += "--" + qxt_d->boundary + "--\r\n";
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
/*!
|
|
Constructs a new QxtMailMessage object from a \a buffer that conforms to RFC 2822 and the MIME related RFCs.
|
|
*/
|
|
QxtMailMessage QxtMailMessage::fromRfc2822(const QByteArray& buffer)
|
|
{
|
|
QxtMailMessage rv;
|
|
QxtRfc2822Parser parser;
|
|
rv.qxt_d = parser.parse(buffer);
|
|
return rv;
|
|
}
|
|
|
|
QxtMailMessagePrivate* QxtRfc2822Parser::parse(const QByteArray& buffer)
|
|
{
|
|
QxtMailMessagePrivate* rv = new QxtMailMessagePrivate();
|
|
parseEntity(buffer, rv->extraHeaders, rv->body);
|
|
parseBody(rv);
|
|
return rv;
|
|
}
|
|
|
|
void QxtRfc2822Parser::parseEntity(const QByteArray& buffer, QHash<QString,QString>& headers, QString& body)
|
|
{
|
|
int pos = 0;
|
|
int crlfPos = 0;
|
|
QByteArray line;
|
|
currentHeaderKey = QString();
|
|
currentHeaderValue.clear();
|
|
state = Headers;
|
|
while (true)
|
|
{
|
|
crlfPos = buffer.indexOf("\r\n", pos);
|
|
if (crlfPos == -1)
|
|
{
|
|
break;
|
|
}
|
|
if (state == Headers && crlfPos == pos) // double CRLF reached: end of headers section
|
|
{
|
|
state = Body;
|
|
parseHeader("", headers); // to store the header currently being parsed (last one)
|
|
pos += 2;
|
|
continue;
|
|
}
|
|
line = buffer.mid(pos, crlfPos - pos);
|
|
pos = crlfPos + 2;
|
|
switch(state)
|
|
{
|
|
case Headers:
|
|
parseHeader(line, headers);
|
|
break;
|
|
case Body:
|
|
body.append(line + "\r\n");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void QxtRfc2822Parser::parseHeader(const QByteArray& line, QHash<QString, QString>& headers )
|
|
{
|
|
QRegExp spRe("^[ \\t]");
|
|
QRegExp hdrRe("^([!-9;-~]+):[ \\t](.*)$");
|
|
if (spRe.indexIn(line) == 0) // continuation line
|
|
{
|
|
currentHeaderValue.append(line);
|
|
}
|
|
else
|
|
{
|
|
// starting a new header field. Store the current one before
|
|
if (!currentHeaderKey.isEmpty())
|
|
{
|
|
QString value = unfoldValue(currentHeaderValue);
|
|
headers[currentHeaderKey.toLower()] = value;
|
|
currentHeaderKey = QString();
|
|
currentHeaderValue.clear();
|
|
}
|
|
if (hdrRe.exactMatch(line))
|
|
{
|
|
currentHeaderKey = hdrRe.cap(1);
|
|
currentHeaderValue.append(hdrRe.cap(2));
|
|
} // else: empty or malformed header line. Ignore.
|
|
}
|
|
}
|
|
|
|
// extract the attachments from a multipart body
|
|
// proceed only one level deep
|
|
// future plans may involve nested parts and dealing with inline parts too
|
|
void QxtRfc2822Parser::parseBody(QxtMailMessagePrivate* msg)
|
|
{
|
|
QString& body = msg->body;
|
|
if (!msg->extraHeaders.contains("content-type")) return;
|
|
QString contentType = msg->extraHeaders["content-type"];
|
|
if (!contentType.indexOf("multipart",0,Qt::CaseInsensitive) == 0) return;
|
|
// extract the boundary delimiter
|
|
QRegExp boundaryRe("boundary=\"?([^\"]*)\"?(?=;|$)");
|
|
if (boundaryRe.indexIn(contentType) == -1)
|
|
{
|
|
qDebug("Boundary regexp didn't match for %s", contentType.toLatin1().data());
|
|
return;
|
|
}
|
|
QString boundary = boundaryRe.cap(1);
|
|
// qDebug("Boundary=%s", boundary.toLatin1().data());
|
|
QRegExp bndRe(QString("(^|\\r?\\n)--%1(--)?[ \\t]*\\r?\\n").arg(QRegExp::escape(boundary))); // find boundary delimiters in the body
|
|
// qDebug("search for %s", bndRe.pattern().toLatin1().data());
|
|
if (!bndRe.isValid())
|
|
{
|
|
qDebug("regexp %s not valid ! %s", bndRe.pattern().toLatin1().data(), bndRe.errorString().toLatin1().data());
|
|
}
|
|
// keep track of the position of two consecutive boundary delimiters:
|
|
// begin* is the position of the delimiter first character,
|
|
// end* is the position of the first character of the part following it.
|
|
int beginFirst = 0;
|
|
int endFirst = 0;
|
|
int beginSecond = 0;
|
|
int endSecond = 0;
|
|
while(bndRe.indexIn(body, endSecond) != -1)
|
|
{
|
|
beginSecond = bndRe.pos() + bndRe.cap(1).length(); // add length of preceding line break, if any
|
|
endSecond = bndRe.pos() + bndRe.matchedLength();
|
|
|
|
// qDebug("%d captures", bndRe.numCaptures());
|
|
// foreach(QString capture, bndRe.capturedTexts())
|
|
// {
|
|
// qDebug("->%s<-", capture.toLatin1().data());
|
|
// }
|
|
// qDebug("beginFirst = %d\nendFirst = %d\nbeginSecond = %d\nendSecond = %d", beginFirst, endFirst, beginSecond, endSecond);
|
|
if (endFirst != 0)
|
|
{
|
|
// handle part here:
|
|
QByteArray part = body.mid(endFirst, beginSecond - endFirst).toLocal8Bit();
|
|
QHash<QString,QString> partHeaders;
|
|
QString partBody;
|
|
parseEntity(part, partHeaders, partBody);
|
|
// qDebug("Part headers:");
|
|
// foreach (QString key, partHeaders.keys())
|
|
// {
|
|
// qDebug("%s: %s", key.toLatin1().data(), partHeaders[key].toLatin1().data());
|
|
// }
|
|
// qDebug("Part body:\n%s", partBody.toLatin1().data());
|
|
if (partHeaders.contains("content-disposition") && partHeaders["content-disposition"].indexOf("attachment;") == 0)
|
|
{
|
|
// qDebug("Attachment!");
|
|
QString filename;
|
|
QxtMailAttachment* attachment = parseAttachment(partHeaders, partBody, filename);
|
|
if (attachment)
|
|
{
|
|
msg->attachments.insert(filename, *attachment);
|
|
delete attachment;
|
|
}
|
|
// strip part from body
|
|
body.replace(beginFirst, beginSecond - beginFirst, "");
|
|
beginSecond = beginFirst;
|
|
endSecond = endFirst;
|
|
}
|
|
}
|
|
beginFirst = beginSecond;
|
|
endFirst = endSecond;
|
|
}
|
|
}
|
|
|
|
QString QxtRfc2822Parser::unfoldValue(QStringList& folded)
|
|
{
|
|
QString unfolded;
|
|
QRegExp encRe("=\\?([^? \\t]+)\\?([qQbB])\\?([^? \\t]+)\\?="); // search for an encoded word
|
|
QStringList::iterator i;
|
|
for (i = folded.begin(); i != folded.end(); ++i)
|
|
{
|
|
int offset = 0;
|
|
while (encRe.indexIn(*i, offset) != -1)
|
|
{
|
|
QString decoded = decode(encRe.cap(1), encRe.cap(2).toLower(), encRe.cap(3));
|
|
i->replace(encRe.pos(), encRe.matchedLength(), decoded); // replace encoded word with decoded one
|
|
offset = encRe.pos() + decoded.length(); // set offset after the inserted decoded word
|
|
}
|
|
}
|
|
unfolded = folded.join("");
|
|
return unfolded;
|
|
}
|
|
|
|
QString QxtRfc2822Parser::decode(const QString& charset, const QString& encoding, const QString& encoded)
|
|
{
|
|
QString rv;
|
|
QByteArray buf;
|
|
if (encoding == "q")
|
|
{
|
|
QByteArray src = encoded.toLatin1();
|
|
int len = src.length();
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
if (src[i] == '_')
|
|
{
|
|
buf += 0x20;
|
|
}
|
|
else if (src[i] == '=')
|
|
{
|
|
if (i+2 < len)
|
|
{
|
|
buf += QByteArray::fromHex(src.mid(i+1,2));
|
|
i += 2;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
buf += src[i];
|
|
}
|
|
}
|
|
}
|
|
else if (encoding == "b")
|
|
{
|
|
buf = QByteArray::fromBase64(encoded.toLatin1());
|
|
}
|
|
QTextCodec *codec = QTextCodec::codecForName(charset.toLatin1());
|
|
if (codec)
|
|
{
|
|
rv = codec->toUnicode(buf);
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
QxtMailAttachment* QxtRfc2822Parser::parseAttachment(const QHash<QString,QString>& headers, const QString& body, QString& filename)
|
|
{
|
|
static int count = 1;
|
|
QByteArray content;
|
|
QRegExp filenameRe(";\\s+filename=\"?([^\"]*)\"?(?=;|$)");
|
|
if (filenameRe.indexIn(headers["content-disposition"]) != -1)
|
|
{
|
|
filename = filenameRe.cap(1);
|
|
}
|
|
else
|
|
{
|
|
filename = QString("attachment%1").arg(count);
|
|
}
|
|
// qDebug("Attachment %s", filename.toLocal8Bit().data());
|
|
|
|
QString ct;
|
|
if (headers.contains("content-type"))
|
|
{
|
|
ct = headers["content-type"];
|
|
}
|
|
else
|
|
{
|
|
ct = "application/octet-stream";
|
|
}
|
|
|
|
QString cte;
|
|
if (headers.contains("content-transfer-encoding"))
|
|
{
|
|
cte = headers["content-transfer-encoding"].toLower();
|
|
// qDebug("Content-Transfer-Encoding: %s", cte.toLatin1().data());
|
|
}
|
|
if ( cte == "base64")
|
|
{
|
|
content = QByteArray::fromBase64(body.toLatin1());
|
|
}
|
|
else if (cte == "quoted-printable")
|
|
{
|
|
QByteArray src = body.toLatin1();
|
|
int len = src.length();
|
|
QTextStream dest(&content);
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
if (src.mid(i,2) == "\r\n")
|
|
{
|
|
dest << endl;
|
|
i++;
|
|
}
|
|
else if (src[i] == '=')
|
|
{
|
|
if (i+2 < len)
|
|
{
|
|
if (src.mid(i+1,2) == "\r\n") // soft line break; skip
|
|
{
|
|
i +=2;
|
|
}
|
|
else
|
|
{
|
|
dest << QByteArray::fromHex(src.mid(i+1,2));
|
|
i += 2;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dest << src[i];
|
|
}
|
|
}
|
|
}
|
|
else // assume 7bit or 8bit
|
|
{
|
|
content = body.toLocal8Bit();
|
|
if (isTextMedia(ct))
|
|
{
|
|
content.replace("\r\n","\n");
|
|
}
|
|
}
|
|
QxtMailAttachment* rv = new QxtMailAttachment(content, ct);
|
|
rv->setExtraHeaders(headers);
|
|
return rv;
|
|
}
|
|
|
|
|
|
// gives only a hint, based on content-type value.
|
|
// takes value of Content-Type header field as parameter
|
|
// return true if the content-type corresponds to textual data (text/*, application/xml...)
|
|
// and false if unsure, so don't interpret a 'false' response as 'it's binary data...
|
|
bool isTextMedia(const QString& contentType)
|
|
{
|
|
// extract media type/sub-type part (e.g. text/plain, image/png...)
|
|
QRegExp mtRe("^([^;/]*)/([^;/]*)(?=;|$)");
|
|
if (mtRe.indexIn(contentType) == -1)
|
|
{
|
|
qWarning("failed to parse %s for type/subtype", contentType.toLatin1().data());
|
|
return false;
|
|
}
|
|
QString type = mtRe.cap(1).toLower();
|
|
QString subtype = mtRe.cap(2).toLower();
|
|
if (type == "text") return true;
|
|
if (type == "application" &&
|
|
(subtype == "x-csh" ||
|
|
subtype == "x-desktop" ||
|
|
subtype == "x-m4" ||
|
|
subtype == "x-perl" ||
|
|
subtype == "x-php" ||
|
|
subtype == "x-ruby" ||
|
|
subtype == "x-troff-man" ||
|
|
subtype == "xsd" ||
|
|
subtype == "xml-dtd" ||
|
|
subtype == "xml-external-parsed-entity" ||
|
|
subtype == "xslt+xml" ||
|
|
subtype == "xhtml+xml" ||
|
|
subtype == "pgp-keys" ||
|
|
subtype == "pgp-signature" ||
|
|
subtype == "javascript" ||
|
|
subtype == "ecmascript" ||
|
|
subtype == "docbook+xml" ||
|
|
subtype == "xml" ||
|
|
subtype == "html" ||
|
|
subtype == "x-shellscript")) return true;
|
|
if (type == "image" && subtype == "svg+xml") return true;
|
|
return false;
|
|
}
|