From 5d8e8f7e81bae00d19dff2c591bf1e1067c7f703 Mon Sep 17 00:00:00 2001 From: rion Date: Fri, 3 Jun 2016 03:39:06 +0500 Subject: [PATCH 1/5] Apply changes from https://bitbucket.org/libqxt/libqxt/pull-requests/42/smtp-reposnse-parser-rewritten-as-separate/diff and bring back qt4 support --- src/mail/mailglobal.h | 4 + src/mail/mailmessage.cpp | 10 +- src/mail/mailsmtp.cpp | 326 +++++++++++++++++++++++++-------------- src/mail/mailsmtp_p.h | 52 ++++++- 4 files changed, 272 insertions(+), 120 deletions(-) diff --git a/src/mail/mailglobal.h b/src/mail/mailglobal.h index 4a3467f..967a73e 100644 --- a/src/mail/mailglobal.h +++ b/src/mail/mailglobal.h @@ -44,4 +44,8 @@ # define Q_MAIL_EXPORT #endif +#if QT_VERSION < QT_VERSION_CHECK(5,0,0) +# define QStringLiteral QLatin1String +#endif + #endif // MAILGLOBAL_H diff --git a/src/mail/mailmessage.cpp b/src/mail/mailmessage.cpp index 65a8c01..89fa3c1 100644 --- a/src/mail/mailmessage.cpp +++ b/src/mail/mailmessage.cpp @@ -686,8 +686,12 @@ void QxtRfc2822Parser::parseBody(QxtMailMessagePrivate* msg) } QString boundary = boundaryRe.cap(1); // qDebug("Boundary=%s", boundary.toLatin1().data()); +#if QT_VERSION < QT_VERSION_CHECK(5,0,0) + QRegExp bndRe(QString("(^|\\r?\\n)--%1(--)?[ \\t]*\\r?\\n").arg(QRegExp::escape(boundary))); // find boundary delimiters in the body +#else QRegExp bndRe(QStringLiteral("(^|\\r?\\n)--%1(--)?[ \\t]*\\r?\\n").arg(QRegExp::escape(boundary))); // find boundary delimiters in the body -// qDebug("search for %s", bndRe.pattern().toLatin1().data()); +#endif + // 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()); @@ -825,7 +829,11 @@ QxtMailAttachment* QxtRfc2822Parser::parseAttachment(const QHash #endif +#define QXT_SMTP_DEBUG 0 +#if QXT_SMTP_DEBUG +# include +# define smtpWrite(data) do { qDebug() << "SEND:" << data; socket->write((data)); } while(false) +#else +# define smtpWrite(data) socket->write((data)) +#endif + +QByteArray QxtSmtpResponse::domain() const +{ + if (textLines.isEmpty()) { + return QByteArray(); + } + const QByteArray &buffer = textLines.at(0); + int spindex = buffer.indexOf(' '); + if (spindex == -1) { + return buffer.trimmed(); + } else { + return buffer.mid(0, spindex).trimmed(); + } +} + +QByteArray QxtSmtpResponse::singleLine() const +{ + QByteArray result; + foreach (const QByteArray &line, textLines) { + if (result.size()) { + result += "\n"; + } + result += line; + } + return result; +} + +bool QxtSmtpResponseParser::feed(const QByteArray &data) +{ +#if QXT_SMTP_DEBUG + qDebug() << "RECV:" << data; +#endif + if (buffer.isEmpty()) { // prepare for fresh parse + state = StateStart; + lastIndex = 0; + } + buffer += data; + while (true) { + if (state == StateStart) { + state = StateFirst; + currentResponse = QxtSmtpResponse(); + } + int rnindex = buffer.indexOf("\r\n", lastIndex); + if (rnindex == -1) { + return true; + } + bool ok; + if (rnindex - lastIndex < 3) { // code + break; + } + const char *line = buffer.constData() + lastIndex; + int statusCode = QByteArray::fromRawData(line, 3).toInt(&ok); + if (!ok || !statusCode) { + break; + } + if (state != StateFirst && statusCode != currentResponse.code) { // all codes must be the same + break; + } + currentResponse.code = statusCode; + char codeSep = line[3]; + + if (codeSep != '\r' && codeSep != ' ' && codeSep != '-') { + break; // invalid separator + } + + if ((codeSep == ' ' && line[4] != '\r') || codeSep == '-') { + // we ignore SP before \r. it's by rfc + // it's text line after code + QByteArray text = QByteArray(line + 4, rnindex - lastIndex - 4); + if (text.isEmpty()) { + break; + } + currentResponse.textLines.append(text); + } + + if (codeSep == ' ') { // last line + responses.enqueue(currentResponse); + if (rnindex + 2 != buffer.size()) { // unparsed data left. + if (usePipelining) { + lastIndex = rnindex + 2; + state = StateStart; + continue; + } + // pipeling unsupported. error + break; + } + buffer.clear(); + return true; + } + lastIndex = rnindex + 2; + state = StateNext; + } + return false; +} + + QxtSmtpPrivate::QxtSmtpPrivate(QxtSmtp *q) : QObject(0), q_ptr(q) , allowedAuthTypes(QxtSmtp::AuthPlain | QxtSmtp::AuthLogin | QxtSmtp::AuthCramMD5) @@ -204,22 +307,24 @@ void QxtSmtpPrivate::socketError(QAbstractSocket::SocketError err) void QxtSmtpPrivate::socketRead() { - buffer += socket->readAll(); - while (true) - { - int pos = buffer.indexOf("\r\n"); - if (pos < 0) return; - QByteArray line = buffer.left(pos); - buffer = buffer.mid(pos + 2); - QByteArray code = line.left(3); + if (!responseParser.feed(socket->readAll())) { + state = Disconnected; + emit q_func()->connectionFailed(); + emit q_func()->connectionFailed("response parse error"); + socket->disconnectFromHost(); + return; + } + + while (responseParser.hasResponse()) { + response = responseParser.takeResponse(); switch (state) { case StartState: - if (code[0] != '2') + if (!response.hasGoodResponseCode()) { state = Disconnected; emit q_func()->connectionFailed(); - emit q_func()->connectionFailed(line); + emit q_func()->connectionFailed(response.textLines.value(0)); socket->disconnectFromHost(); } else @@ -229,12 +334,11 @@ void QxtSmtpPrivate::socketRead() break; case HeloSent: case EhloSent: - case EhloGreetReceived: - parseEhlo(code, (line[3] != ' '), QString::fromLatin1(line.mid(4))); + parseEhlo(); break; #ifndef QT_NO_OPENSSL case StartTLSSent: - if (code == "220") + if (response.code == 220) { socket->startClientEncryption(); ehlo(); @@ -249,10 +353,10 @@ void QxtSmtpPrivate::socketRead() case AuthUsernameSent: if (authType == QxtSmtp::AuthPlain) authPlain(); else if (authType == QxtSmtp::AuthLogin) authLogin(); - else authCramMD5(line.mid(4)); + else authCramMD5(); break; case AuthSent: - if (code[0] == '2') + if (response.hasGoodResponseCode()) { state = Authenticated; emit q_func()->authenticated(); @@ -261,49 +365,49 @@ void QxtSmtpPrivate::socketRead() { state = Disconnected; emit q_func()->authenticationFailed(); - emit q_func()->authenticationFailed( line ); + emit q_func()->authenticationFailed( response.singleLine() ); socket->disconnectFromHost(); } break; case MailToSent: case RcptAckPending: - if (code[0] != '2') { - emit q_func()->mailFailed( pending.first().first, code.toInt() ); - emit q_func()->mailFailed(pending.first().first, code.toInt(), line); - // pending.removeFirst(); - // DO NOT remove it, the body sent state needs this message to assigned the next mail failed message that will - // the sendNext - // a reset will be sent to clear things out + if (!response.hasGoodResponseCode()) { + emit q_func()->mailFailed( pending.first().first, response.code); + emit q_func()->mailFailed(pending.first().first, response.code, response.singleLine()); + // pending.removeFirst(); + // DO NOT remove it, the body sent state needs this message to assigned the next mail failed message that will + // the sendNext + // a reset will be sent to clear things out sendNext(); state = BodySent; } else - sendNextRcpt(code, line); + sendNextRcpt(); break; case SendingBody: - sendBody(code, line); + sendBody(); break; case BodySent: - if ( pending.count() ) - { - // if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty, - // you will get into this state and then crash because no check is done. CHeck added but shouldnt - // be necessary since I commented out the removeFirst - if (code[0] != '2') - { - emit q_func()->mailFailed(pending.first().first, code.toInt() ); - emit q_func()->mailFailed(pending.first().first, code.toInt(), line); - } - else + if ( pending.count() ) + { + // if you removeFirst in RcpActpending/MailToSent on an error, and the queue is now empty, + // you will get into this state and then crash because no check is done. CHeck added but shouldnt + // be necessary since I commented out the removeFirst + if (!response.hasGoodResponseCode()) + { + emit q_func()->mailFailed(pending.first().first, response.code ); + emit q_func()->mailFailed(pending.first().first, response.code, response.singleLine()); + } + else emit q_func()->mailSent(pending.first().first); - pending.removeFirst(); - } + pending.removeFirst(); + } sendNext(); break; case Resetting: - if (code[0] != '2') { + if (!response.hasGoodResponseCode()) { emit q_func()->connectionFailed(); - emit q_func()->connectionFailed( line ); + emit q_func()->connectionFailed( response.singleLine() ); } else { state = Waiting; @@ -327,66 +431,61 @@ void QxtSmtpPrivate::ehlo() address = addr.toString().toLatin1(); break; } - socket->write("ehlo " + address + "\r\n"); + smtpWrite("EHLO [" + address + "]\r\n"); extensions.clear(); state = EhloSent; } -void QxtSmtpPrivate::parseEhlo(const QByteArray& code, bool cont, const QString& line) +void QxtSmtpPrivate::parseEhlo() { - if (code != "250") - { - // error! - if (state != HeloSent) + while (true) { + if (response.code != 250) { - // maybe let's try HELO - socket->write("helo\r\n"); - state = HeloSent; + // error! + if (state != HeloSent) + { + smtpWrite("HELO\r\n"); + state = HeloSent; + } + else + break; + return; } - else - { - // nope - socket->write("QUIT\r\n"); - socket->flush(); - socket->disconnectFromHost(); + if (state != EhloDone) + state = EhloDone; + + if (response.domain().isEmpty()) + break; + + QList::ConstIterator it = response.textLines.constBegin(); + if (it == response.textLines.constEnd()) + break; + + while (++it != response.textLines.constEnd()) { + QString line = QString::fromLatin1(it->constData(), it->size()); + extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1); } - return; - } - else if (state != EhloGreetReceived) - { - if (!cont) + + responseParser.usePipelining = extensions.contains("PIPELINING"); + if (extensions.contains("STARTTLS") && !disableStartTLS) { - // greeting only, no extensions - state = EhloDone; + startTLS(); } else { - // greeting followed by extensions - state = EhloGreetReceived; - return; + authenticate(); } + return; } - else - { - extensions[line.section(' ', 0, 0).toUpper()] = line.section(' ', 1); - if (!cont) - state = EhloDone; - } - if (state != EhloDone) return; - if (extensions.contains(QStringLiteral("STARTTLS")) && !disableStartTLS) - { - startTLS(); - } - else - { - authenticate(); - } + smtpWrite("QUIT\r\n"); + socket->flush(); + socket->disconnectFromHost(); } void QxtSmtpPrivate::startTLS() { #ifndef QT_NO_OPENSSL - socket->write("starttls\r\n"); + smtpWrite("starttls\r\n"); state = StartTLSSent; #else authenticate(); @@ -395,23 +494,23 @@ void QxtSmtpPrivate::startTLS() void QxtSmtpPrivate::authenticate() { - if (!extensions.contains(QStringLiteral("AUTH")) || username.isEmpty() || password.isEmpty()) + if (!extensions.contains("AUTH") || username.isEmpty() || password.isEmpty()) { state = Authenticated; emit q_func()->authenticated(); } else { - QStringList auth = extensions[QStringLiteral("AUTH")].toUpper().split(' ', QString::SkipEmptyParts); - if (auth.contains(QStringLiteral("CRAM-MD5")) && (allowedAuthTypes & QxtSmtp::AuthCramMD5)) + QStringList auth = extensions["AUTH"].toUpper().split(' ', QString::SkipEmptyParts); + if (auth.contains("CRAM-MD5") && (allowedAuthTypes & QxtSmtp::AuthCramMD5)) { authCramMD5(); } - else if (auth.contains(QStringLiteral("PLAIN")) && (allowedAuthTypes & QxtSmtp::AuthPlain)) + else if (auth.contains("PLAIN") && (allowedAuthTypes & QxtSmtp::AuthPlain)) { authPlain(); } - else if (auth.contains(QStringLiteral("LOGIN")) && (allowedAuthTypes & QxtSmtp::AuthLogin)) + else if (auth.contains("LOGIN") && (allowedAuthTypes & QxtSmtp::AuthLogin)) { authLogin(); } @@ -423,11 +522,11 @@ void QxtSmtpPrivate::authenticate() } } -void QxtSmtpPrivate::authCramMD5(const QByteArray& challenge) +void QxtSmtpPrivate::authCramMD5() { if (state != AuthRequestSent) { - socket->write("auth cram-md5\r\n"); + smtpWrite("auth cram-md5\r\n"); authType = QxtSmtp::AuthCramMD5; state = AuthRequestSent; } @@ -435,9 +534,9 @@ void QxtSmtpPrivate::authCramMD5(const QByteArray& challenge) { QxtHmac hmac(QCryptographicHash::Md5); hmac.setKey(password); - hmac.addData(QByteArray::fromBase64(challenge)); + hmac.addData(QByteArray::fromBase64(response.singleLine())); QByteArray response = username + ' ' + hmac.result().toHex(); - socket->write(response.toBase64() + "\r\n"); + smtpWrite(response.toBase64() + "\r\n"); state = AuthSent; } } @@ -446,7 +545,7 @@ void QxtSmtpPrivate::authPlain() { if (state != AuthRequestSent) { - socket->write("auth plain\r\n"); + smtpWrite("auth plain\r\n"); authType = QxtSmtp::AuthPlain; state = AuthRequestSent; } @@ -457,7 +556,7 @@ void QxtSmtpPrivate::authPlain() auth += username; auth += '\0'; auth += password; - socket->write(auth.toBase64() + "\r\n"); + smtpWrite(auth.toBase64() + "\r\n"); state = AuthSent; } } @@ -466,18 +565,18 @@ void QxtSmtpPrivate::authLogin() { if (state != AuthRequestSent && state != AuthUsernameSent) { - socket->write("auth login\r\n"); + smtpWrite("auth login\r\n"); authType = QxtSmtp::AuthLogin; state = AuthRequestSent; } else if (state == AuthRequestSent) { - socket->write(username.toBase64() + "\r\n"); + smtpWrite(username.toBase64() + "\r\n"); state = AuthUsernameSent; } else { - socket->write(password.toBase64() + "\r\n"); + smtpWrite(password.toBase64() + "\r\n"); state = AuthSent; } } @@ -543,7 +642,7 @@ void QxtSmtpPrivate::sendNext() if(state != Waiting) { state = Resetting; - socket->write("rset\r\n"); + smtpWrite("rset\r\n"); return; } const QxtMailMessage& msg = pending.first().second; @@ -563,12 +662,12 @@ void QxtSmtpPrivate::sendNext() // We explicitly use lowercase keywords because for some reason gmail // interprets any string starting with an uppercase R as a request // to renegotiate the SSL connection. - socket->write("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n"); - if (extensions.contains(QStringLiteral("PIPELINING"))) // almost all do nowadays + smtpWrite("mail from:<" + qxt_extract_address(msg.sender()) + ">\r\n"); + if (extensions.contains("PIPELINING")) // almost all do nowadays { foreach(const QString& rcpt, recipients) { - socket->write("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n"); + smtpWrite("rcpt to:<" + qxt_extract_address(rcpt) + ">\r\n"); } state = RcptAckPending; } @@ -578,23 +677,23 @@ void QxtSmtpPrivate::sendNext() } } -void QxtSmtpPrivate::sendNextRcpt(const QByteArray& code, const QByteArray&line) +void QxtSmtpPrivate::sendNextRcpt() { int messageID = pending.first().first; const QxtMailMessage& msg = pending.first().second; - if (code[0] != '2') + if (!response.hasGoodResponseCode()) { // on failure, emit a warning signal if (!mailAck) { emit q_func()->senderRejected(messageID, msg.sender()); - emit q_func()->senderRejected(messageID, msg.sender(), line ); + emit q_func()->senderRejected(messageID, msg.sender(), response.singleLine()); } else { emit q_func()->recipientRejected(messageID, msg.sender()); - emit q_func()->recipientRejected(messageID, msg.sender(), line); + emit q_func()->recipientRejected(messageID, msg.sender(), response.singleLine()); } } else if (!mailAck) @@ -612,22 +711,22 @@ void QxtSmtpPrivate::sendNextRcpt(const QByteArray& code, const QByteArray&line) if (rcptAck == 0) { // no recipients were considered valid - emit q_func()->mailFailed(messageID, code.toInt() ); - emit q_func()->mailFailed(messageID, code.toInt(), line); + emit q_func()->mailFailed(messageID, response.code ); + emit q_func()->mailFailed(messageID, response.code, response.singleLine()); pending.removeFirst(); sendNext(); } else { // at least one recipient was acknowledged, send mail body - socket->write("data\r\n"); + smtpWrite("data\r\n"); state = SendingBody; } } else if (state != RcptAckPending) { // send the next recipient unless we're only waiting on acks - socket->write("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n"); + smtpWrite("rcpt to:<" + qxt_extract_address(recipients[rcptNumber]) + ">\r\n"); rcptNumber++; } else @@ -637,21 +736,22 @@ void QxtSmtpPrivate::sendNextRcpt(const QByteArray& code, const QByteArray&line) } } -void QxtSmtpPrivate::sendBody(const QByteArray& code, const QByteArray & line) +void QxtSmtpPrivate::sendBody() { int messageID = pending.first().first; const QxtMailMessage& msg = pending.first().second; - if (code[0] != '3') + if (response.code != 354) { - emit q_func()->mailFailed(messageID, code.toInt() ); - emit q_func()->mailFailed(messageID, code.toInt(), line); + emit q_func()->mailFailed(messageID, response.code ); + emit q_func()->mailFailed(messageID, response.code, response.singleLine()); pending.removeFirst(); sendNext(); return; } - socket->write(msg.rfc2822()); - socket->write(".\r\n"); + QByteArray data = msg.rfc2822(); + smtpWrite(data); + smtpWrite(".\r\n"); state = BodySent; } diff --git a/src/mail/mailsmtp_p.h b/src/mail/mailsmtp_p.h index 8a4160f..86016ed 100644 --- a/src/mail/mailsmtp_p.h +++ b/src/mail/mailsmtp_p.h @@ -37,6 +37,45 @@ #include #include #include +#include + +class QxtSmtpResponse +{ +public: + inline QxtSmtpResponse() : code(0) { } + + int code; + QList textLines; + + QByteArray domain() const; + inline bool hasGoodResponseCode() const { return code >= 200 && code < 300; } + QByteArray singleLine() const; +}; + +class QxtSmtpResponseParser +{ +public: + /* in case of multiline response but not pipelining */ + enum State { + StateStart, + StateFirst, + StateNext + }; + + bool usePipelining; + State state; + int lastIndex; + QByteArray buffer; + QxtSmtpResponse currentResponse; + QQueue responses; + + inline QxtSmtpResponseParser() : + usePipelining(false), state(StateFirst), lastIndex(0) {} + bool feed(const QByteArray &data); // return false on parse error + inline bool hasResponse() const { return !responses.isEmpty(); } + inline QxtSmtpResponse takeResponse() { return responses.dequeue(); } +}; + class QxtSmtpPrivate : public QObject { @@ -52,7 +91,6 @@ class QxtSmtpPrivate : public QObject Disconnected, StartState, EhloSent, - EhloGreetReceived, EhloExtensionsReceived, EhloDone, HeloSent, @@ -73,12 +111,14 @@ class QxtSmtpPrivate : public QObject SmtpState state; // rather then an int use the enum. makes sure invalid states are entered at compile time, and makes debugging easier QxtSmtp::AuthType authType; int allowedAuthTypes; - QByteArray buffer, username, password; + QByteArray username, password; + QxtSmtpResponseParser responseParser; QHash extensions; QList > pending; QStringList recipients; int nextID, rcptNumber, rcptAck; bool mailAck; + QxtSmtpResponse response; #ifndef QT_NO_OPENSSL QSslSocket* socket; @@ -86,16 +126,16 @@ class QxtSmtpPrivate : public QObject QTcpSocket* socket; #endif - void parseEhlo(const QByteArray& code, bool cont, const QString& line); + void parseEhlo(); void startTLS(); void authenticate(); - void authCramMD5(const QByteArray& challenge = QByteArray()); + void authCramMD5(); void authPlain(); void authLogin(); - void sendNextRcpt(const QByteArray& code, const QByteArray & line); - void sendBody(const QByteArray& code, const QByteArray & line); + void sendNextRcpt(); + void sendBody(); public slots: void socketError(QAbstractSocket::SocketError err); From 6619e3a45a9c375b49be4934e9592646b6fc1117 Mon Sep 17 00:00:00 2001 From: rion Date: Fri, 3 Jun 2016 12:28:35 +0500 Subject: [PATCH 2/5] Date header support for smtp --- src/mail/mailmessage.cpp | 21 ++++++ src/mail/mailsmtp.cpp | 9 ++- src/mail/mailtimezone.cpp | 133 ++++++++++++++++++++++++++++++++++++++ src/mail/mailtimezone.h | 33 ++++++++++ src/mail/mailutility_p.h | 1 + src/mail/qtmail.pri | 6 +- 6 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 src/mail/mailtimezone.cpp create mode 100644 src/mail/mailtimezone.h diff --git a/src/mail/mailmessage.cpp b/src/mail/mailmessage.cpp index 89fa3c1..c4e5856 100644 --- a/src/mail/mailmessage.cpp +++ b/src/mail/mailmessage.cpp @@ -40,6 +40,7 @@ #include "mailmessage.h" #include "mailutility_p.h" +#include "mailtimezone.h" #include #include #include @@ -944,3 +945,23 @@ bool isTextMedia(const QString& contentType) if (type == QLatin1String("image") && subtype == QLatin1String("svg+xml")) return true; return false; } + +QString dateTimeToRFC2822(const QDateTime &dt) +{ + int tz = QxtTimeZone::offsetFromUtc(); + QString ret = QLocale::c().toString(dt, "ddd, dd MMM yyyy HH:mm:ss"); + if (tz) { + const char *tzs; + if (tz < 0) { + tz = -tz; + tzs = " -"; + } else { + tzs = " +"; + } + int h = tz / 60; + int m = tz - (h * 60); + QString tzf; + return ret + tzf.sprintf("%s%02d%02d", tzs, h, m); + } + return ret; +} diff --git a/src/mail/mailsmtp.cpp b/src/mail/mailsmtp.cpp index 369c32d..76de771 100644 --- a/src/mail/mailsmtp.cpp +++ b/src/mail/mailsmtp.cpp @@ -39,9 +39,11 @@ #include "mailsmtp.h" #include "mailsmtp_p.h" #include "mailhmac.h" +#include "mailutility_p.h" #include #include #include +#include #ifndef QT_NO_OPENSSL # include #endif @@ -207,7 +209,12 @@ int QxtSmtp::send(const QxtMailMessage& message) { int messageID = ++d_func()->nextID; d_func()->pending.append(qMakePair(messageID, message)); - if (d_func()->state == QxtSmtpPrivate::Waiting) + + QxtMailMessage& m = d_func()->pending.last().second; + if (!m.hasExtraHeader(QStringLiteral("Date"))) + m.setExtraHeader(QStringLiteral("Date"), dateTimeToRFC2822(QDateTime::currentDateTime())); + + if (d_func()->state == QxtSmtpPrivate::Waiting) d_func()->sendNext(); return messageID; } diff --git a/src/mail/mailtimezone.cpp b/src/mail/mailtimezone.cpp new file mode 100644 index 0000000..af175f1 --- /dev/null +++ b/src/mail/mailtimezone.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) Psi Development Team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +#if QT_VERSION < QT_VERSION_CHECK(5, 2, 0) +#include +#include +#ifdef Q_OS_UNIX +#include +#endif +#ifdef Q_OS_WIN +#include +#endif +#else +#include +#endif + +#include "mailtimezone.h" + +#if QT_VERSION < QT_VERSION_CHECK(5,2,0) +static bool inited = false; +static int timezone_offset_; +static QString timezone_str_; + +static void init() +{ +#if defined(Q_OS_UNIX) + time_t x; + time(&x); + char str[256]; + char fmt[32]; + int size; + strcpy(fmt, "%z"); + size = strftime(str, 256, fmt, localtime(&x)); + if(size && strncmp(fmt, str, size)) { + timezone_offset_ = QByteArray::fromRawData(str + 1, 2).toInt() * 60 + QByteArray::fromRawData(str + 3, 2).toInt(); + if(str[0] == '-') + timezone_offset_ = -timezone_offset_; + } + strcpy(fmt, "%Z"); + strftime(str, 256, fmt, localtime(&x)); + if(strcmp(fmt, str)) + timezone_str_ = str; + +#elif defined(Q_OS_WIN) + TIME_ZONE_INFORMATION i; + memset(&i, 0, sizeof(i)); + bool inDST = (GetTimeZoneInformation(&i) == TIME_ZONE_ID_DAYLIGHT); + int bias = i.Bias; + if(inDST) + bias += i.DaylightBias; + timezone_offset_ = -bias; + timezone_str_ = ""; + for(int n = 0; n < 32; ++n) { + int w = inDST ? i.DaylightName[n] : i.StandardName[n]; + if(w == 0) + break; + timezone_str_ += QChar(w); + } + +#else + qWarning("Failed to properly init timezone data. Use UTC offset instead"); + inited = true; + timezone_offset_ = 0; + timezone_str_ = QLatin1String("N/A"); +#endif +} +#endif + +int QxtTimeZone::offsetFromUtc() +{ +#if QT_VERSION < QT_VERSION_CHECK(5,2,0) + if (!inited) { + init(); + } + return timezone_offset_; +#else + return QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()) / 60; +#endif +} + +QString QxtTimeZone::abbreviation() +{ +#if QT_VERSION < QT_VERSION_CHECK(5,2,0) + return timezone_str_; +#else + return QTimeZone::systemTimeZone().abbreviation(QDateTime::currentDateTime()); +#endif +} + +int QxtTimeZone::tzdToInt(const QString &tzd) +{ + int tzoSign = 1; + if (tzd.isEmpty() || tzd == QLatin1String("0000")) { + return 0; + } else if (tzd.startsWith('+') || tzd.startsWith('-')) { + QTime time = QTime::fromString(tzd.mid(1), "hhmm"); + if (time.isValid()) { + if (tzd[0] == '-') { + tzoSign = -1; + } + return tzoSign * (time.hour() * 60 + time.second()); + } + } + return -1; /* we don't have -1 sec offset. and usually the value is common for errors */ +} + +/** + * \fn int TimeZone::timezoneOffset() + * \brief Local timezone offset in minutes. + */ + +/** + * \fn QString TimeZone::timezoneString() + * \brief Local timezone name. + */ diff --git a/src/mail/mailtimezone.h b/src/mail/mailtimezone.h new file mode 100644 index 0000000..6b2636c --- /dev/null +++ b/src/mail/mailtimezone.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) Psi Development Team + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef IRIS_TIMEZONE_H +#define IRIS_TIMEZONE_H + +#include + +class QxtTimeZone +{ +public: + static int offsetFromUtc(); // in minutes + static QString abbreviation(); + static int tzdToInt(const QString &tzd); +}; + +#endif // IRIS_TIMEZONE_H diff --git a/src/mail/mailutility_p.h b/src/mail/mailutility_p.h index ea1d5e3..04ee840 100644 --- a/src/mail/mailutility_p.h +++ b/src/mail/mailutility_p.h @@ -37,5 +37,6 @@ QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextCodec* latin1, const QByteArray& prefix = QByteArray()); bool isTextMedia(const QString& contentType); +QString dateTimeToRFC2822(const QDateTime &dt); #endif // MAILUTILITY_P_H diff --git a/src/mail/qtmail.pri b/src/mail/qtmail.pri index 6e0d5de..9eb5c8c 100644 --- a/src/mail/qtmail.pri +++ b/src/mail/qtmail.pri @@ -18,7 +18,8 @@ HEADERS += \ $$PWD/mailpop3reply.h \ $$PWD/mailpop3reply_p.h \ $$PWD/mailpop3retrreply.h \ - $$PWD/mailpop3statreply.h + $$PWD/mailpop3statreply.h \ + $$PWD/mailtimezone.h SOURCES += \ $$PWD/mailhmac.cpp \ @@ -26,4 +27,5 @@ SOURCES += \ $$PWD/mailmessage.cpp \ $$PWD/mailsmtp.cpp \ $$PWD/mailpop3.cpp \ - $$PWD/mailpop3reply.cpp + $$PWD/mailpop3reply.cpp \ + $$PWD/mailtimezone.cpp From 364d79aab2a4a175fdf2a2ad5d238948f5a5fe1f Mon Sep 17 00:00:00 2001 From: rion Date: Sun, 26 Jun 2016 14:36:04 +0500 Subject: [PATCH 3/5] Added support for hierarchical mime messages QxtAttachment was hacked to keep children attachments. QxtAttahcment does not handle text bodies as QxtMessage do though. So here is a still a big room for improvements. But if someone wants really good support for mime based messages it's probably better to look at another projects, like Qt Messaging Framework, vmime library, Simple Mail project and others. --- src/mail/mailattachment.cpp | 94 ++++++++++++- src/mail/mailattachment.h | 6 + src/mail/mailmessage.cpp | 270 +++++++++++++++++++++--------------- src/mail/mailmessage.h | 10 ++ src/mail/mailutility_p.h | 1 + 5 files changed, 261 insertions(+), 120 deletions(-) diff --git a/src/mail/mailattachment.cpp b/src/mail/mailattachment.cpp index 76ef650..793aad9 100644 --- a/src/mail/mailattachment.cpp +++ b/src/mail/mailattachment.cpp @@ -50,6 +50,8 @@ class QxtMailAttachmentPrivate : public QSharedData { public: QHash extraHeaders; + QByteArray boundary; // in case of embedded multipart + QHash attachments; // in case of embedded multipart QString contentType; // those two members are mutable because they may change in the const rawData() method of QxtMailAttachment, // while caching the raw data for the attachment if needed. @@ -146,6 +148,15 @@ QString QxtMailAttachment::contentType() const void QxtMailAttachment::setContentType(const QString& contentType) { qxt_d->contentType = contentType; + if (contentType.startsWith("multipart", Qt::CaseInsensitive)) { + QRegExp re(QStringLiteral("boundary=([^ ;\\r]+)")); + if (re.indexIn(contentType) != -1) + qxt_d->boundary = re.capturedTexts()[1].toLatin1(); + else { + qxt_d->boundary = qxt_gen_boundary(); + qxt_d->contentType += (QStringLiteral("; boundary=") + qxt_d->boundary); + } + } } QHash QxtMailAttachment::extraHeaders() const @@ -165,16 +176,18 @@ bool QxtMailAttachment::hasExtraHeader(const QString& key) const void QxtMailAttachment::setExtraHeader(const QString& key, const QString& value) { - qxt_d->extraHeaders[key.toLower()] = value; + if (key.compare(QStringLiteral("Content-Type"), Qt::CaseInsensitive) == 0) + setContentType(value); + else + qxt_d->extraHeaders[key.toLower()] = value; } void QxtMailAttachment::setExtraHeaders(const QHash& a) { - QHash& headers = qxt_d->extraHeaders; - headers.clear(); + qxt_d->extraHeaders.clear(); foreach(const QString& key, a.keys()) { - headers[key.toLower()] = a[key]; + setExtraHeader(key, a[key]); } } @@ -183,10 +196,57 @@ void QxtMailAttachment::removeExtraHeader(const QString& key) qxt_d->extraHeaders.remove(key.toLower()); } +QHash QxtMailAttachment::attachments() const +{ + return qxt_d->attachments; +} + +QxtMailAttachment QxtMailAttachment::attachment(const QString& filename) const +{ + return qxt_d->attachments[filename]; +} + +void QxtMailAttachment::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 + QLatin1Char('.') + QString::number(i))) + { + i++; + } + qxt_d->attachments[filename+QLatin1Char('.')+QString::number(i)] = attach; + } + else + { + qxt_d->attachments[filename] = attach; + } +} + +void QxtMailAttachment::removeAttachment(const QString& filename) +{ + qxt_d->attachments.remove(filename); +} + QByteArray QxtMailAttachment::mimeData() { + bool isMultipart = false; QTextCodec* latin1 = QTextCodec::codecForName("latin1"); - QByteArray rv = "Content-Type: " + qxt_d->contentType.toLatin1() + "\r\nContent-Transfer-Encoding: base64\r\n"; + + if (qxt_d->attachments.count()) { + if (!qxt_d->contentType.startsWith("multipart/", Qt::CaseInsensitive)) + setExtraHeader(QStringLiteral("Content-Type"), QStringLiteral("multipart/mixed")); + } + + QByteArray rv = "Content-Type: " + qxt_d->contentType.toLatin1() + "\r\n"; + if (qxt_d->contentType.startsWith("multipart/", Qt::CaseInsensitive)) { + isMultipart = true; + } + else { + rv += "Content-Transfer-Encoding: base64\r\n"; + } + foreach(const QString& r, qxt_d->extraHeaders.keys()) { rv += qxt_fold_mime_header(r, extraHeader(r), latin1); @@ -194,10 +254,25 @@ QByteArray QxtMailAttachment::mimeData() rv += "\r\n"; const QByteArray& d = rawData(); - for (int pos = 0; pos < d.length(); pos += 57) + int chars = isMultipart? 73 : 57; // multipart preamble supposed to be 7bit latin1 + for (int pos = 0; pos < d.length(); pos += chars) { - rv += d.mid(pos, 57).toBase64() + "\r\n"; + if (isMultipart) { + rv += d.mid(pos, chars) + "\r\n"; + } else { + rv += d.mid(pos, chars).toBase64() + "\r\n"; + } + } + + if (isMultipart) { + QMutableHashIterator it(qxt_d->attachments); + while (it.hasNext()) { + rv += "--" + qxt_d->boundary + "\r\n"; + rv += it.next().value().mimeData(); + } + rv += "--" + qxt_d->boundary + "--\r\n"; } + return rv; } @@ -244,6 +319,11 @@ bool QxtMailAttachment::isText() const return isTextMedia(contentType()); } +bool QxtMailAttachment::isMultipart() const +{ + return qxt_d->attachments.count() || qxt_d->contentType.startsWith("multipart/", Qt::CaseInsensitive); +} + QxtMailAttachment QxtMailAttachment::fromFile(const QString& filename) { QxtMailAttachment rv(new QFile(filename)); diff --git a/src/mail/mailattachment.h b/src/mail/mailattachment.h index 6c177b3..e47cba2 100644 --- a/src/mail/mailattachment.h +++ b/src/mail/mailattachment.h @@ -69,9 +69,15 @@ class Q_MAIL_EXPORT QxtMailAttachment void setExtraHeaders(const QHash&); void removeExtraHeader(const QString& key); + QHash attachments() const; + QxtMailAttachment attachment(const QString& filename) const; + void addAttachment(const QString& filename, const QxtMailAttachment& attach); + void removeAttachment(const QString& filename); + QByteArray mimeData(); const QByteArray& rawData() const; bool isText() const; + bool isMultipart() const; private: QSharedDataPointer qxt_d; diff --git a/src/mail/mailmessage.cpp b/src/mail/mailmessage.cpp index c4e5856..d2f1b38 100644 --- a/src/mail/mailmessage.cpp +++ b/src/mail/mailmessage.cpp @@ -53,16 +53,18 @@ struct QxtMailMessagePrivate : public QSharedData { - QxtMailMessagePrivate() {} + QxtMailMessagePrivate() : multipartType(QxtMailMessage::Mixed), wordWrapLimit(78), preserveStartSpaces(false) {} 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), - wordWrapLimit(78), preserveStartSpaces(false) {} + multipartType(other.multipartType), wordWrapLimit(other.wordWrapLimit), + preserveStartSpaces(other.preserveStartSpaces) {} QStringList rcptTo, rcptCc, rcptBcc; QString subject, body, sender; QHash extraHeaders; QHash attachments; + QxtMailMessage::MultipartType multipartType; mutable QByteArray boundary; int wordWrapLimit; bool preserveStartSpaces; @@ -198,7 +200,21 @@ bool QxtMailMessage::hasExtraHeader(const QString& key) const void QxtMailMessage::setExtraHeader(const QString& key, const QString& value) { - qxt_d->extraHeaders[key.toLower()] = value; +#if 0 + if (key.compare(QStringLiteral("Content-Type"), Qt::CaseInsensitive) == 0 && + value.contains(QStringLiteral("multipart/"))) + { + QRegExp re(QStringLiteral("boundary=([^ ;\r]+)")); + if (re.indexIn(value) != -1) + qxt_d->boundary = re.capturedTexts()[1].toLatin1(); + else { + qxt_d->boundary = qxt_gen_boundary(); + qxt_d->extraHeaders[key.toLower()] = value + "; boundary=" + qxt_d->boundary; + } + } + else +#endif + qxt_d->extraHeaders[key.toLower()] = value; } void QxtMailMessage::setExtraHeaders(const QHash& a) @@ -207,7 +223,7 @@ void QxtMailMessage::setExtraHeaders(const QHash& a) headers.clear(); foreach(const QString& key, a.keys()) { - headers[key.toLower()] = a[key]; + setExtraHeader(key, a[key]); } } @@ -249,6 +265,11 @@ void QxtMailMessage::removeAttachment(const QString& filename) qxt_d->attachments.remove(filename); } +void QxtMailMessage::setMultipartType(QxtMailMessage::MultipartType mpt) +{ + qxt_d->multipartType = mpt; +} + /*! * \brief Rewrites default 78 word wrap line length limit with new \a limit */ @@ -403,13 +424,23 @@ QByteArray QxtMailMessage::rfc2822() const if (attach.count()) { if (qxt_d->boundary.isEmpty()) - qxt_d->boundary = QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", ""); + qxt_d->boundary = qxt_gen_boundary(); 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"; + if (!hasExtraHeader(QStringLiteral("Content-Type"))) { + static QMap mptMap; + if (mptMap.isEmpty()) { + mptMap.insert(Mixed, "mixed"); + mptMap.insert(Alternative, "alternative"); + mptMap.insert(Digest, "digest"); + mptMap.insert(Parallel, "parallel"); + mptMap.insert(Related, "related"); + } + + rv += "Content-Type: multipart/"+mptMap[qxt_d->multipartType]+"; boundary=" + qxt_d->boundary + "\r\n"; + } } - else if (!bodyIsAscii && !hasExtraHeader(QStringLiteral("Content-Transfer-Encoding"))) + else if (body().size() && !bodyIsAscii && !hasExtraHeader(QStringLiteral("Content-Transfer-Encoding"))) { if (!useQuotedPrintable) { @@ -439,135 +470,142 @@ QByteArray QxtMailMessage::rfc2822() const { // 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 (body().size()) { + if (attach.count()) { - if (!useQuotedPrintable) + // we're going to have attachments, so output the lead-in for the message body + 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"))) { - // base64 - rv += "Content-Transfer-Encoding: base64\r\n"; + rv += "Content-Transfer-Encoding: " + extraHeader(QStringLiteral("Content-Transfer-Encoding")).toLatin1() + "\r\n"; } - else + else if (!bodyIsAscii) { - // quoted-printable - rv += "Content-Transfer-Encoding: quoted-printable\r\n"; + if (!useQuotedPrintable) + { + // base64 + rv += "Content-Transfer-Encoding: base64\r\n"; + } + else + { + // quoted-printable + rv += "Content-Transfer-Encoding: quoted-printable\r\n"; + } } + rv += "\r\n"; } - rv += "\r\n"; - } - if (bodyIsAscii) - { - QByteArray b = latin1->fromUnicode(body()); - int len = b.length(); - QByteArray line; - QByteArray word; - QByteArray spaces; - QByteArray startSpaces; - for (int i = 0; i <= len; i++) + if (bodyIsAscii) { - char ignoredChar = 0; - if (i != len) { - ignoredChar = b[i] == '\n'? '\r' : b[i] == '\r'? '\n' : 0; - } - if (!(ignoredChar || (i == len) || (b[i] == ' ') || (b[i] == '\t'))) { - // the char is part of word - if (word.isEmpty()) { // start of new word / end of spaces - if (line.isEmpty()) { - startSpaces = spaces; + QByteArray b = latin1->fromUnicode(body()); + int len = b.length(); + QByteArray line; + QByteArray word; + QByteArray spaces; + QByteArray startSpaces; + for (int i = 0; i <= len; i++) + { + char ignoredChar = 0; + if (i != len) { + ignoredChar = b[i] == '\n'? '\r' : b[i] == '\r'? '\n' : 0; + } + if (!(ignoredChar || (i == len) || (b[i] == ' ') || (b[i] == '\t'))) { + // the char is part of word + if (word.isEmpty()) { // start of new word / end of spaces + if (line.isEmpty()) { + startSpaces = spaces; + } } + word += b[i]; + continue; } - word += b[i]; - continue; - } - // space char, so end of word or continuous spaces - if (!word.isEmpty()) { // start of new space area / end of word - if (line.length() + spaces.length() + - word.length() > qxt_d->wordWrapLimit) { - // have to wrap word to next line + // space char, so end of word or continuous spaces + if (!word.isEmpty()) { // start of new space area / end of word + if (line.length() + spaces.length() + + word.length() > qxt_d->wordWrapLimit) { + // have to wrap word to next line + if(line[0] == '.') + rv += "."; + rv += line + "\r\n"; + if (qxt_d->preserveStartSpaces) { + line = startSpaces + word; + } else { + line = word; + } + } else { // no wrap required + line += spaces + word; + } + word.clear(); + spaces.clear(); + } + + if (ignoredChar || i == len) { // new line or eof + // trailing `spaces` are ignored here if(line[0] == '.') rv += "."; rv += line + "\r\n"; - if (qxt_d->preserveStartSpaces) { - line = startSpaces + word; - } else { - line = word; - } - } else { // no wrap required - line += spaces + word; + line.clear(); + startSpaces.clear(); + spaces.clear(); + } else { + spaces += b[i]; } - word.clear(); - spaces.clear(); - } - - if (ignoredChar || i == len) { // new line or eof - // trailing `spaces` are ignored here - if(line[0] == '.') - rv += "."; - rv += line + "\r\n"; - line.clear(); - startSpaces.clear(); - spaces.clear(); - } else { - spaces += b[i]; } } - } - else if (useQuotedPrintable) - { - QByteArray b = body().toUtf8(); - int ct = b.length(); - QByteArray line; - for (int i = 0; i < ct; i++) + else if (useQuotedPrintable) { - if(b[i] == '\n' || b[i] == '\r') + QByteArray b = body().toUtf8(); + int ct = b.length(); + QByteArray line; + for (int i = 0; i < ct; i++) { - if(line[0] == '.') - rv += "."; - rv += line + "\r\n"; - line = ""; - if ((b[i+1] == '\n' || b[i+1] == '\r') && b[i] != b[i+1]) + if(b[i] == '\n' || b[i] == '\r') { - // If we're looking at a CRLF pair, skip the second half - i++; + 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 (MUST_QP(b[i])) + { + line += "=" + b.mid(i, 1).toHex().toUpper(); + } + else + { + line += b[i]; } } - else if (line.length() > 74) - { - rv += line + "=\r\n"; - line = ""; - } - if (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"; } } - 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) + else /* base64 */ { - rv += b.mid(i, 78) + "\r\n"; + QByteArray b = body().toUtf8().toBase64(); + int ct = b.length(); + for (int i = 0; i < ct; i += 78) + { + rv += b.mid(i, 78) + "\r\n"; + } } } @@ -576,7 +614,8 @@ QByteArray QxtMailMessage::rfc2822() const foreach(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="); + if (qxt_d->multipartType != Alternative && !attach[filename].isMultipart()) // REVIEW: may be other types too + rv += qxt_fold_mime_header(QStringLiteral("Content-Disposition"), QDir(filename).dirName(), latin1, "attachment; filename="); rv += attach[filename].mimeData(); } rv += "--" + qxt_d->boundary + "--\r\n"; @@ -965,3 +1004,8 @@ QString dateTimeToRFC2822(const QDateTime &dt) } return ret; } + +QByteArray qxt_gen_boundary() +{ + return QUuid::createUuid().toString().toLatin1().replace("{", "").replace("}", ""); +} diff --git a/src/mail/mailmessage.h b/src/mail/mailmessage.h index e704ebc..5af5439 100644 --- a/src/mail/mailmessage.h +++ b/src/mail/mailmessage.h @@ -51,6 +51,15 @@ class Q_MAIL_EXPORT QxtMailMessage Bcc }; + enum MultipartType + { + Mixed, + Alternative, + Digest, + Parallel, + Related // rfc 2387 + }; + QxtMailMessage(); QxtMailMessage(const QxtMailMessage& other); QxtMailMessage(const QString& sender, const QString& recipient); @@ -82,6 +91,7 @@ class Q_MAIL_EXPORT QxtMailMessage QxtMailAttachment attachment(const QString& filename) const; void addAttachment(const QString& filename, const QxtMailAttachment& attach); void removeAttachment(const QString& filename); + void setMultipartType(MultipartType); void setWordWrapLimit(int limit); void setWordWrapPreserveStartSpaces(bool state); diff --git a/src/mail/mailutility_p.h b/src/mail/mailutility_p.h index 04ee840..68e3e57 100644 --- a/src/mail/mailutility_p.h +++ b/src/mail/mailutility_p.h @@ -38,5 +38,6 @@ QByteArray qxt_fold_mime_header(const QString& key, const QString& value, QTextC const QByteArray& prefix = QByteArray()); bool isTextMedia(const QString& contentType); QString dateTimeToRFC2822(const QDateTime &dt); +QByteArray qxt_gen_boundary(); #endif // MAILUTILITY_P_H From 19c7fec55f76717769728937aed5c359c0b55d0e Mon Sep 17 00:00:00 2001 From: rion Date: Mon, 4 Jul 2016 23:07:58 +0500 Subject: [PATCH 4/5] Added forgotten header for Qt5 --- src/mail/mailattachment.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mail/mailattachment.h b/src/mail/mailattachment.h index e47cba2..bd8d098 100644 --- a/src/mail/mailattachment.h +++ b/src/mail/mailattachment.h @@ -39,6 +39,7 @@ #include #include #include +#include class QxtMailAttachmentPrivate; class Q_MAIL_EXPORT QxtMailAttachment From 47f64c2eb740869a3f75edca05d63c859348c5ee Mon Sep 17 00:00:00 2001 From: rion Date: Thu, 18 Aug 2016 03:32:06 +0500 Subject: [PATCH 5/5] Use QMap instead of QHash so we could set proper order for parts. makes sense for multipart/alternative --- src/mail/mailattachment.cpp | 6 +++--- src/mail/mailattachment.h | 2 +- src/mail/mailmessage.cpp | 6 +++--- src/mail/mailmessage.h | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/mail/mailattachment.cpp b/src/mail/mailattachment.cpp index 793aad9..92a19a8 100644 --- a/src/mail/mailattachment.cpp +++ b/src/mail/mailattachment.cpp @@ -51,7 +51,7 @@ class QxtMailAttachmentPrivate : public QSharedData public: QHash extraHeaders; QByteArray boundary; // in case of embedded multipart - QHash attachments; // in case of embedded multipart + QMap attachments; // in case of embedded multipart. QMap because order makes sense QString contentType; // those two members are mutable because they may change in the const rawData() method of QxtMailAttachment, // while caching the raw data for the attachment if needed. @@ -196,7 +196,7 @@ void QxtMailAttachment::removeExtraHeader(const QString& key) qxt_d->extraHeaders.remove(key.toLower()); } -QHash QxtMailAttachment::attachments() const +QMap QxtMailAttachment::attachments() const { return qxt_d->attachments; } @@ -265,7 +265,7 @@ QByteArray QxtMailAttachment::mimeData() } if (isMultipart) { - QMutableHashIterator it(qxt_d->attachments); + QMutableMapIterator it(qxt_d->attachments); while (it.hasNext()) { rv += "--" + qxt_d->boundary + "\r\n"; rv += it.next().value().mimeData(); diff --git a/src/mail/mailattachment.h b/src/mail/mailattachment.h index bd8d098..ce32473 100644 --- a/src/mail/mailattachment.h +++ b/src/mail/mailattachment.h @@ -70,7 +70,7 @@ class Q_MAIL_EXPORT QxtMailAttachment void setExtraHeaders(const QHash&); void removeExtraHeader(const QString& key); - QHash attachments() const; + QMap attachments() const; QxtMailAttachment attachment(const QString& filename) const; void addAttachment(const QString& filename, const QxtMailAttachment& attach); void removeAttachment(const QString& filename); diff --git a/src/mail/mailmessage.cpp b/src/mail/mailmessage.cpp index d2f1b38..519fb1b 100644 --- a/src/mail/mailmessage.cpp +++ b/src/mail/mailmessage.cpp @@ -63,7 +63,7 @@ struct QxtMailMessagePrivate : public QSharedData QStringList rcptTo, rcptCc, rcptBcc; QString subject, body, sender; QHash extraHeaders; - QHash attachments; + QMap attachments; // QMap because order makes sense QxtMailMessage::MultipartType multipartType; mutable QByteArray boundary; int wordWrapLimit; @@ -232,7 +232,7 @@ void QxtMailMessage::removeExtraHeader(const QString& key) qxt_d->extraHeaders.remove(key.toLower()); } -QHash QxtMailMessage::attachments() const +QMap QxtMailMessage::attachments() const { return qxt_d->attachments; } @@ -376,7 +376,7 @@ QByteArray QxtMailMessage::rfc2822() const QTextCodec* latin1 = QTextCodec::codecForName("latin1"); bool bodyIsAscii = latin1->canEncode(body()) && !useQuotedPrintable && !useBase64; - QHash attach = attachments(); + QMap attach = attachments(); QByteArray rv; if (!sender().isEmpty() && !hasExtraHeader(QStringLiteral("From"))) diff --git a/src/mail/mailmessage.h b/src/mail/mailmessage.h index 5af5439..77b6d8d 100644 --- a/src/mail/mailmessage.h +++ b/src/mail/mailmessage.h @@ -87,7 +87,7 @@ class Q_MAIL_EXPORT QxtMailMessage void setExtraHeaders(const QHash&); void removeExtraHeader(const QString& key); - QHash attachments() const; + QMap attachments() const; QxtMailAttachment attachment(const QString& filename) const; void addAttachment(const QString& filename, const QxtMailAttachment& attach); void removeAttachment(const QString& filename);