diff --git a/src/mail/mailattachment.cpp b/src/mail/mailattachment.cpp index 76ef650..92a19a8 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 + 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. @@ -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()); } +QMap 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) { + QMutableMapIterator 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..ce32473 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 @@ -69,9 +70,15 @@ class Q_MAIL_EXPORT QxtMailAttachment void setExtraHeaders(const QHash&); void removeExtraHeader(const QString& key); + QMap 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/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..519fb1b 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 @@ -52,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; + QMap attachments; // QMap because order makes sense + QxtMailMessage::MultipartType multipartType; mutable QByteArray boundary; int wordWrapLimit; bool preserveStartSpaces; @@ -197,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) @@ -206,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]); } } @@ -215,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; } @@ -248,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 */ @@ -354,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"))) @@ -402,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) { @@ -438,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"; + } } } @@ -575,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"; @@ -686,8 +726,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 +869,11 @@ QxtMailAttachment* QxtRfc2822Parser::parseAttachment(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); + void setMultipartType(MultipartType); void setWordWrapLimit(int limit); void setWordWrapPreserveStartSpaces(bool state); diff --git a/src/mail/mailsmtp.cpp b/src/mail/mailsmtp.cpp index 58955dc..76de771 100644 --- a/src/mail/mailsmtp.cpp +++ b/src/mail/mailsmtp.cpp @@ -39,13 +39,118 @@ #include "mailsmtp.h" #include "mailsmtp_p.h" #include "mailhmac.h" +#include "mailutility_p.h" #include #include #include +#include #ifndef QT_NO_OPENSSL # include #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) @@ -104,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; } @@ -204,22 +314,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 +341,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 +360,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 +372,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 +438,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 +501,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 +529,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 +541,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 +552,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 +563,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 +572,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 +649,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 +669,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 +684,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 +718,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 +743,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); 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..68e3e57 100644 --- a/src/mail/mailutility_p.h +++ b/src/mail/mailutility_p.h @@ -37,5 +37,7 @@ 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); +QByteArray qxt_gen_boundary(); #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