diff --git a/autotests/smtptest.cpp b/autotests/smtptest.cpp index 99d977f..e0c52ff 100644 --- a/autotests/smtptest.cpp +++ b/autotests/smtptest.cpp @@ -1,269 +1,274 @@ /* Copyright 2010 BetterInbox Author: Christophe Laveault Gregory Schlomoff 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, see . */ #include "smtptest.h" #include #include "fakeserver.h" #include "session.h" #include "loginjob.h" #include "sendjob.h" void SmtpTest::testHello_data() { QTest::addColumn >("scenario"); QTest::addColumn("hostname"); QList scenario; scenario << FakeServer::greeting() << "C: EHLO 127.0.0.1" << "S: 250 Localhost ready to roll" << FakeServer::bye(); QTest::newRow("EHLO OK") << scenario << QStringLiteral("127.0.0.1"); scenario.clear(); scenario << FakeServer::greeting() << "C: EHLO 127.0.0.1" << "S: 500 Command was not recognized" << "C: HELO 127.0.0.1" << "S: 250 Localhost ready to roll" << FakeServer::bye(); QTest::newRow("EHLO unknown") << scenario << QStringLiteral("127.0.0.1"); scenario.clear(); scenario << FakeServer::greeting() << "C: EHLO 127.0.0.1" << "S: 502 Command not implemented" << "C: HELO 127.0.0.1" << "S: 250 Localhost ready to roll" << FakeServer::bye(); QTest::newRow("EHLO not implemented") << scenario << QStringLiteral("127.0.0.1"); scenario.clear(); scenario << FakeServer::greeting() << "C: EHLO 127.0.0.1" << "S: 502 Command not implemented" << "C: HELO 127.0.0.1" << "S: 500 Command was not recognized" << FakeServer::bye(); QTest::newRow("ERROR") << scenario << QStringLiteral("127.0.0.1"); scenario.clear(); scenario << FakeServer::greeting() << "C: EHLO random.stranger" << "S: 250 random.stranger ready to roll" << FakeServer::bye(); QTest::newRow("EHLO hostname") << scenario << QStringLiteral("random.stranger"); } void SmtpTest::testHello() { QFETCH(QList, scenario); QFETCH(QString, hostname); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KSmtp::Session session(QStringLiteral("127.0.0.1"), 5989); session.setCustomHostname(hostname); session.openAndWait(); qDebug() << "### Session state is:" << session.state(); QEXPECT_FAIL("ERROR" , "Expected failure if HELO command not recognized", Continue); QVERIFY2(session.state() == KSmtp::Session::NotAuthenticated, "Handshake failed"); session.quitAndWait(); QVERIFY(fakeServer.isAllScenarioDone()); fakeServer.quit(); } void SmtpTest::testLoginJob_data() { QTest::addColumn >("scenario"); QTest::addColumn("authMode"); QTest::addColumn("errorCode"); QList scenario; scenario << FakeServer::greetingAndEhlo() << "S: 250 AUTH PLAIN LOGIN" << "C: AUTH PLAIN AGxvZ2luAHBhc3N3b3Jk" // [\0 + "login" + \0 + "password"].toBase64() << "S: 235 Authenticated" << FakeServer::bye(); QTest::newRow("Plain auth ok") << scenario << "Plain" << 0; scenario.clear(); scenario << FakeServer::greetingAndEhlo() << "S: 250 AUTH PLAIN LOGIN" << "C: AUTH LOGIN" << "S: 334 VXNlcm5hbWU6" // "Username:".toBase64() << "C: bG9naW4=" // "login".toBase64() << "S: 334 UGFzc3dvcmQ6" // "Password:".toBase64() << "C: cGFzc3dvcmQ=" // "password".toBase64() << "S: 235 Authenticated" << FakeServer::bye(); QTest::newRow("Login auth ok") << scenario << "Login" << 0; scenario.clear(); scenario << FakeServer::greetingAndEhlo() << "S: 250 AUTH PLAIN" << "C: AUTH PLAIN AGxvZ2luAHBhc3N3b3Jk" // [\0 + "login" + \0 + "password"].toBase64() << "S: 235 Authenticated" << FakeServer::bye(); QTest::newRow("Login not supported") << scenario << "Login" << 0; scenario.clear(); scenario << FakeServer::greetingAndEhlo(false) // The login job won't even try to send AUTH, because it does not // have any mechanisms to use //<< "C: AUTH PLAIN AGxvZ2luAHBhc3N3b3Jk" // [\0 + "login" + \0 + "password"].toBase64() //<< "S: 235 Authenticated" << FakeServer::bye(); QTest::newRow("Auth not supported") << scenario << "Login" << 100; scenario.clear(); scenario << FakeServer::greetingAndEhlo() << "S: 250 AUTH PLAIN" << "C: AUTH PLAIN AGxvZ2luAHBhc3N3b3Jk" // [\0 + "login" + \0 + "password"].toBase64() << "S: 535 Authorization failed" << FakeServer::bye(); QTest::newRow("Wrong password") << scenario << "Plain" << 100; } void SmtpTest::testLoginJob() { QFETCH(QList, scenario); QFETCH(QString, authMode); QFETCH(int, errorCode); KSmtp::LoginJob::AuthMode mode = KSmtp::LoginJob::UnknownAuth; if (authMode == QLatin1String("Plain")) { mode = KSmtp::LoginJob::Plain; } else if (authMode == QLatin1String("Login")) { mode = KSmtp::LoginJob::Login; } FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KSmtp::Session session(QStringLiteral("127.0.0.1"), 5989); session.setCustomHostname(QStringLiteral("127.0.0.1")); session.openAndWait(); KSmtp::LoginJob *login = new KSmtp::LoginJob(&session); login->setPreferedAuthMode(mode); login->setUserName(QStringLiteral("login")); login->setPassword(QStringLiteral("password")); login->exec(); // Checking job error code: QVERIFY2(login->error() == errorCode, "Unexpected LoginJob error code"); // Checking session state: QEXPECT_FAIL("Auth not supported" , "Expected failure if not authentication method suported", Continue); QEXPECT_FAIL("Wrong password" , "Expected failure if wrong password", Continue); QVERIFY2(session.state() == KSmtp::Session::Authenticated, "Authentication failed"); session.quitAndWait(); QVERIFY(fakeServer.isAllScenarioDone()); fakeServer.quit(); } void SmtpTest::testSendJob_data() { QTest::addColumn >("scenario"); QTest::addColumn("errorCode"); QList scenario; scenario << FakeServer::greetingAndEhlo(false) << "C: MAIL FROM:" << "S: 530 Not allowed" << FakeServer::bye(); QTest::newRow("Send not allowed") << scenario << 100; scenario.clear(); scenario << FakeServer::greetingAndEhlo(false) << "C: MAIL FROM:" << "S: 250 ok" << "C: RCPT TO:" << "S: 250 ok" << "C: DATA" << "S: 354 Ok go ahead" << "C: From: foo@bar.com" << "C: To: bar@foo.com" - << "C: Hello world" + << "C: Hello world." + << "C: .." // Single dot becomes two + << "C: .." // Single dot becomes two + << "C: ..." // Two dots become three + << "C: ..Foo" // .Foo becomes ..Foo + << "C: End" << "C: " << "C: ." << "S: 250 Ok transfer done" << FakeServer::bye(); QTest::newRow("ok") << scenario << 0; scenario.clear(); } void SmtpTest::testSendJob() { QFETCH(QList, scenario); QFETCH(int, errorCode); FakeServer fakeServer; fakeServer.setScenario(scenario); fakeServer.startAndWait(); KSmtp::Session session(QStringLiteral("127.0.0.1"), 5989); session.setCustomHostname(QStringLiteral("127.0.0.1")); session.openAndWait(); KSmtp::SendJob *send = new KSmtp::SendJob(&session); - send->setData("From: foo@bar.com\r\nTo: bar@foo.com\r\nHello world"); + send->setData("From: foo@bar.com\r\nTo: bar@foo.com\r\nHello world.\r\n.\r\n.\r\n..\r\n.Foo\r\nEnd"); send->setFrom(QStringLiteral("foo@bar.com")); send->setTo({QStringLiteral("bar@foo.com")}); send->exec(); // Checking job error code: QVERIFY2(send->error() == errorCode, qPrintable(QStringLiteral("Unexpected LoginJob error: ") + send->errorString())); session.quitAndWait(); QVERIFY(fakeServer.isAllScenarioDone()); fakeServer.quit(); } SmtpTest::SmtpTest() { } void SmtpTest::initTestCase() { } void SmtpTest::cleanupTestCase() { } QTEST_MAIN(SmtpTest) diff --git a/src/sendjob.cpp b/src/sendjob.cpp index 3a9f6cf..5b20a08 100644 --- a/src/sendjob.cpp +++ b/src/sendjob.cpp @@ -1,234 +1,234 @@ /* Copyright 2010 BetterInbox Author: Christophe Laveault Gregory Schlomoff 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, see . */ #include "sendjob.h" #include "job_p.h" #include "serverresponse_p.h" #include "ksmtp_debug.h" #include namespace KSmtp { class SendJobPrivate : public JobPrivate { public: enum Status { Idle, SendingReturnPath, SendingRecipients, SendingData }; SendJobPrivate(SendJob *job, Session *session, const QString &name) : JobPrivate(session, name), q(job), m_status(Idle) { } SendJob *q = nullptr; void sendNextRecipient(); void addRecipients(const QStringList &rcpts); bool prepare(); typedef struct { QString contentType; QString name; QByteArray content; } MessagePart; QString m_returnPath; QStringList m_recipients; QByteArray m_data; QStringList m_recipientsCopy; Status m_status; }; } using namespace KSmtp; SendJob::SendJob(Session *session) : Job(*new SendJobPrivate(this, session, i18n("SendJob"))) { } void SendJob::setFrom(const QString &from) { Q_D(SendJob); const auto start = from.indexOf(QLatin1Char('<')); if (start > -1) { const auto end = qMax(start, from.indexOf(QLatin1Char('>'), start)); d->m_returnPath = QStringLiteral("<%1>").arg(from.mid(start + 1, end - start - 1)); } else { d->m_returnPath = QStringLiteral("<%1>").arg(from); } } void SendJob::setTo(const QStringList &to) { Q_D(SendJob); d->addRecipients(to); } void SendJob::setCc(const QStringList &cc) { Q_D(SendJob); d->addRecipients(cc); } void SendJob::setBcc(const QStringList &bcc) { Q_D(SendJob); d->addRecipients(bcc); } void SendJob::setData(const QByteArray &data) { Q_D(SendJob); d->m_data = data; // A line with a single dot would make SMTP think "end of message", so use two dots in that case, // as per https://tools.ietf.org/html/rfc5321#section-4.5.2 - d->m_data.replace("\r\n.\r\n", "\r\n..\r\n"); + d->m_data.replace("\r\n.", "\r\n.."); } void SendJob::doStart() { Q_D(SendJob); if (!d->prepare()) { setError(KJob::UserDefinedError); setErrorText(i18n("Could not send the message because either the sender or recipient field is missing or invalid")); emitResult(); return; } int sizeLimit = session()->sizeLimit(); if (sizeLimit > 0 && size() > sizeLimit) { setError(KJob::UserDefinedError); setErrorText(i18n("Could not send the message because it exceeds the maximum allowed size of %1 bytes. (Message size: %2 bytes.)", sizeLimit, size())); emitResult(); return; } d->m_status = SendJobPrivate::SendingReturnPath; sendCommand("MAIL FROM:" + d->m_returnPath.toUtf8()); } void SendJob::handleResponse(const ServerResponse &r) { Q_D(SendJob); // Handle server errors handleErrors(r); switch (d->m_status) { case SendJobPrivate::Idle: //TODO: anything to do here? break; case SendJobPrivate::SendingReturnPath: // Expected response: server agreement if (r.isCode(25)) { d->m_status = SendJobPrivate::SendingRecipients; d->sendNextRecipient(); } break; case SendJobPrivate::SendingRecipients: // Expected response: server agreement if (r.isCode(25)) { if (d->m_recipientsCopy.isEmpty()) { sendCommand("DATA"); d->m_status = SendJobPrivate::SendingData; } else { d->sendNextRecipient(); } } break; case SendJobPrivate::SendingData: // Expected responses: // 354: Go ahead sending data if (r.isCode(354)) { sendCommand(d->m_data); sendCommand("\r\n."); } // 25x: Data received correctly if (r.isCode(25)) { emitResult(); } break; } } void SendJobPrivate::sendNextRecipient() { q->sendCommand("RCPT TO:<" + m_recipientsCopy.takeFirst().toUtf8() + '>'); } void SendJobPrivate::addRecipients(const QStringList &rcpts) { for (const auto &rcpt : rcpts) { if (rcpt.isEmpty()) { continue; } const int start = rcpt.indexOf(QLatin1Char('<')); if (start > -1) { const int end = qMax(start, rcpt.indexOf(QLatin1Char('>'), start)); m_recipients.push_back(rcpt.mid(start + 1, end - start - 1)); } else { m_recipients.push_back(rcpt); } } } bool SendJobPrivate::prepare() { if (m_data.isEmpty()) { qCWarning(KSMTP_LOG) << "A message has to be set before starting a SendJob"; return false; } m_recipientsCopy = m_recipients; if (m_recipients.isEmpty()) { qCWarning(KSMTP_LOG) << "Message has no recipients"; return false; } return true; } int SendJob::size() const { Q_D(const SendJob); return d->m_data.size(); }