Added support for CHUNKING SMTP extension (RFC-3030). More robust unit tests for SMTP.

This commit is contained in:
Vincent Richard 2013-06-20 11:02:39 +02:00
parent 36773bd834
commit eee80fc81e
11 changed files with 661 additions and 21 deletions

View File

@ -252,6 +252,7 @@ libvmime_messaging_proto_sources = [
[
'smtp',
[
'net/smtp/SMTPChunkingOutputStreamAdapter.cpp', 'net/smtp/SMTPChunkingOutputStreamAdapter.hpp',
'net/smtp/SMTPCommand.cpp', 'net/smtp/SMTPCommand.hpp',
'net/smtp/SMTPCommandSet.cpp', 'net/smtp/SMTPCommandSet.hpp',
'net/smtp/SMTPConnection.cpp', 'net/smtp/SMTPConnection.hpp',

View File

@ -0,0 +1,145 @@
//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002-2013 Vincent Richard <vincent@vmime.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 3 of
// the License, or (at your option) any later version.
//
// This program 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
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library. Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//
#include "vmime/config.hpp"
#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP
#include "vmime/net/smtp/SMTPChunkingOutputStreamAdapter.hpp"
#include "vmime/net/smtp/SMTPConnection.hpp"
#include "vmime/net/smtp/SMTPTransport.hpp"
#include <algorithm>
namespace vmime {
namespace net {
namespace smtp {
SMTPChunkingOutputStreamAdapter::SMTPChunkingOutputStreamAdapter(ref <SMTPConnection> conn)
: m_connection(conn), m_bufferSize(0), m_chunkCount(0)
{
}
void SMTPChunkingOutputStreamAdapter::sendChunk
(const value_type* const data, const size_type count, const bool last)
{
if (count == 0 && !last)
{
// Nothing to send
return;
}
// Send this chunk
m_connection->sendRequest(SMTPCommand::BDAT(count, last));
m_connection->getSocket()->sendRaw(data, count);
++m_chunkCount;
// If PIPELINING is not supported, read one response for this BDAT command
if (!m_connection->hasExtension("PIPELINING"))
{
ref <SMTPResponse> resp = m_connection->readResponse();
if (resp->getCode() != 250)
{
m_connection->getTransport()->disconnect();
throw exceptions::command_error("BDAT", resp->getText());
}
}
// If PIPELINING is supported, read one response for each chunk (ie. number
// of BDAT commands issued) after the last chunk has been sent
else if (last)
{
bool invalidReply = false;
ref <SMTPResponse> resp;
for (unsigned int i = 0 ; i < m_chunkCount ; ++i)
{
resp = m_connection->readResponse();
if (resp->getCode() != 250)
invalidReply = true;
}
if (invalidReply)
{
m_connection->getTransport()->disconnect();
throw exceptions::command_error("BDAT", resp->getText());
}
}
}
void SMTPChunkingOutputStreamAdapter::write
(const value_type* const data, const size_type count)
{
const value_type* curData = data;
size_type curCount = count;
while (curCount != 0)
{
// Fill the buffer
const size_type remaining = sizeof(m_buffer) - m_bufferSize;
const size_type bytesToCopy = std::min(remaining, curCount);
std::copy(data, data + bytesToCopy, m_buffer);
m_bufferSize += bytesToCopy;
curData += bytesToCopy;
curCount -= bytesToCopy;
// If the buffer is full, send this chunk
if (m_bufferSize >= sizeof(m_buffer))
{
sendChunk(m_buffer, m_bufferSize, /* last */ false);
m_bufferSize = 0;
}
}
}
void SMTPChunkingOutputStreamAdapter::flush()
{
sendChunk(m_buffer, m_bufferSize, /* last */ true);
m_bufferSize = 0;
}
utility::stream::size_type SMTPChunkingOutputStreamAdapter::getBlockSize()
{
return sizeof(m_buffer);
}
} // smtp
} // net
} // vmime
#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP

View File

@ -149,6 +149,20 @@ ref <SMTPCommand> SMTPCommand::DATA()
}
// static
ref <SMTPCommand> SMTPCommand::BDAT(const unsigned long chunkSize, const bool last)
{
std::ostringstream cmd;
cmd.imbue(std::locale::classic());
cmd << "BDAT " << chunkSize;
if (last)
cmd << " LAST";
return createCommand(cmd.str());
}
// static
ref <SMTPCommand> SMTPCommand::NOOP()
{

View File

@ -31,6 +31,7 @@
#include "vmime/net/smtp/SMTPResponse.hpp"
#include "vmime/net/smtp/SMTPCommand.hpp"
#include "vmime/net/smtp/SMTPCommandSet.hpp"
#include "vmime/net/smtp/SMTPChunkingOutputStreamAdapter.hpp"
#include "vmime/exception.hpp"
#include "vmime/mailboxList.hpp"
@ -155,14 +156,10 @@ void SMTPTransport::noop()
}
void SMTPTransport::send
void SMTPTransport::sendEnvelope
(const mailbox& expeditor, const mailboxList& recipients,
utility::inputStream& is, const utility::stream::size_type size,
utility::progressListener* progress, const mailbox& sender)
const mailbox& sender, bool sendDATACommand)
{
if (!isConnected())
throw exceptions::not_connected();
// If no recipient/expeditor was found, throw an exception
if (recipients.isEmpty())
throw exceptions::no_recipient();
@ -199,7 +196,8 @@ void SMTPTransport::send
}
// Prepare sending of message data
commands->addCommand(SMTPCommand::DATA());
if (sendDATACommand)
commands->addCommand(SMTPCommand::DATA());
// Read response for "RSET" command
if (needReset)
@ -238,13 +236,29 @@ void SMTPTransport::send
}
// Read response for "DATA" command
commands->writeToSocket(m_connection->getSocket());
if ((resp = m_connection->readResponse())->getCode() != 354)
if (sendDATACommand)
{
disconnect();
throw exceptions::command_error(commands->getLastCommandSent()->getText(), resp->getText());
commands->writeToSocket(m_connection->getSocket());
if ((resp = m_connection->readResponse())->getCode() != 354)
{
disconnect();
throw exceptions::command_error(commands->getLastCommandSent()->getText(), resp->getText());
}
}
}
void SMTPTransport::send
(const mailbox& expeditor, const mailboxList& recipients,
utility::inputStream& is, const utility::stream::size_type size,
utility::progressListener* progress, const mailbox& sender)
{
if (!isConnected())
throw exceptions::not_connected();
// Send message envelope
sendEnvelope(expeditor, recipients, sender, /* sendDATACommand */ true);
// Send the message data
// Stream copy with "\n." to "\n.." transformation
@ -258,6 +272,8 @@ void SMTPTransport::send
// Send end-of-data delimiter
m_connection->getSocket()->sendRaw("\r\n.\r\n", 5);
ref <SMTPResponse> resp;
if ((resp = m_connection->readResponse())->getCode() != 250)
{
disconnect();
@ -270,21 +286,39 @@ void SMTPTransport::send
(ref <vmime::message> msg, const mailbox& expeditor, const mailboxList& recipients,
utility::progressListener* progress, const mailbox& sender)
{
if (!isConnected())
throw exceptions::not_connected();
// Generate the message with Internationalized Email support,
// if this is supported by the SMTP server
std::ostringstream oss;
utility::outputStreamAdapter ossAdapter(oss);
generationContext ctx(generationContext::getDefaultContext());
ctx.setInternationalizedEmailSupport(m_connection->hasExtension("SMTPUTF8"));
msg->generate(ctx, ossAdapter);
// If CHUNKING is not supported, generate the message to a temporary
// buffer then use the send() method which takes an inputStream
if (!m_connection->hasExtension("CHUNKING"))
{
std::ostringstream oss;
utility::outputStreamAdapter ossAdapter(oss);
const string& str(oss.str());
msg->generate(ctx, ossAdapter);
utility::inputStreamStringAdapter isAdapter(str);
const string& str(oss.str());
send(expeditor, recipients, isAdapter, str.length(), progress, sender);
utility::inputStreamStringAdapter isAdapter(str);
send(expeditor, recipients, isAdapter, str.length(), progress, sender);
}
// Send message envelope
sendEnvelope(expeditor, recipients, sender, /* sendDATACommand */ false);
// Send the message by chunks
SMTPChunkingOutputStreamAdapter chunkStream(m_connection);
msg->generate(ctx, chunkStream);
chunkStream.flush();
}

View File

@ -46,6 +46,7 @@ VMIME_TEST_SUITE_BEGIN(SMTPCommandTest)
VMIME_TEST(testRCPT_UTF8)
VMIME_TEST(testRSET)
VMIME_TEST(testDATA)
VMIME_TEST(testBDAT)
VMIME_TEST(testNOOP)
VMIME_TEST(testQUIT)
VMIME_TEST(testWriteToSocket)
@ -168,6 +169,19 @@ VMIME_TEST_SUITE_BEGIN(SMTPCommandTest)
VASSERT_EQ("Text", "DATA", cmd->getText());
}
void testBDAT()
{
vmime::ref <SMTPCommand> cmd1 = SMTPCommand::BDAT(12345, false);
VASSERT_NOT_NULL("Not null", cmd1);
VASSERT_EQ("Text", "BDAT 12345", cmd1->getText());
vmime::ref <SMTPCommand> cmd2 = SMTPCommand::BDAT(67890, true);
VASSERT_NOT_NULL("Not null", cmd2);
VASSERT_EQ("Text", "BDAT 67890 LAST", cmd2->getText());
}
void testNOOP()
{
vmime::ref <SMTPCommand> cmd = SMTPCommand::NOOP();

View File

@ -23,9 +23,14 @@
#include "tests/testUtils.hpp"
#include "vmime/net/smtp/SMTPTransport.hpp"
#include "vmime/net/smtp/SMTPChunkingOutputStreamAdapter.hpp"
class greetingErrorSMTPTestSocket;
class MAILandRCPTSMTPTestSocket;
class chunkingSMTPTestSocket;
class SMTPTestMessage;
VMIME_TEST_SUITE_BEGIN(SMTPTransportTest)
@ -33,6 +38,7 @@ VMIME_TEST_SUITE_BEGIN(SMTPTransportTest)
VMIME_TEST_LIST_BEGIN
VMIME_TEST(testGreetingError)
VMIME_TEST(testMAILandRCPT)
VMIME_TEST(testChunking)
VMIME_TEST_LIST_END
@ -77,6 +83,32 @@ VMIME_TEST_SUITE_BEGIN(SMTPTransportTest)
tr->send(exp, recips, is, 0);
}
void testChunking()
{
vmime::ref <vmime::net::session> session =
vmime::create <vmime::net::session>();
vmime::ref <vmime::net::transport> tr = session->getTransport
(vmime::utility::url("smtp://localhost"));
tr->setSocketFactory(vmime::create <testSocketFactory <chunkingSMTPTestSocket> >());
tr->setTimeoutHandlerFactory(vmime::create <testTimeoutHandlerFactory>());
tr->connect();
VASSERT("Test server should report it supports the CHUNKING extension!",
tr.dynamicCast <vmime::net::smtp::SMTPTransport>()->getConnection()->hasExtension("CHUNKING"));
vmime::mailbox exp("expeditor@test.vmime.org");
vmime::mailboxList recips;
recips.appendMailbox(vmime::create <vmime::mailbox>("recipient@test.vmime.org"));
vmime::ref <vmime::message> msg = vmime::create <SMTPTestMessage>();
tr->send(msg, exp, recips);
}
VMIME_TEST_SUITE_END
@ -121,6 +153,13 @@ public:
m_recipients.insert("recipient3@test.vmime.org");
m_state = STATE_NOT_CONNECTED;
m_ehloSent = m_heloSent = m_mailSent = m_rcptSent = m_dataSent = m_quitSent = false;
}
~MAILandRCPTSMTPTestSocket()
{
VASSERT("Client must send the DATA command", m_dataSent);
VASSERT("Client must send the QUIT command", m_quitSent);
}
void onConnected()
@ -155,15 +194,29 @@ public:
{
localSend("500 Syntax error, command unrecognized\r\n");
}
else if (cmd == "EHLO")
{
localSend("502 Command not implemented\r\n");
m_ehloSent = true;
}
else if (cmd == "HELO")
{
VASSERT("Client must send the EHLO command before HELO", m_ehloSent);
localSend("250 OK\r\n");
m_heloSent = true;
}
else if (cmd == "MAIL")
{
VASSERT("Client must send the HELO command", m_heloSent);
VASSERT_EQ("MAIL", std::string("MAIL FROM:<expeditor@test.vmime.org>"), line);
localSend("250 OK\r\n");
m_mailSent = true;
}
else if (cmd == "RCPT")
{
@ -186,15 +239,21 @@ public:
m_recipients.erase(it);
localSend("250 OK, recipient accepted\r\n");
m_rcptSent = true;
}
else if (cmd == "DATA")
{
VASSERT("Client must send the MAIL command", m_mailSent);
VASSERT("Client must send the RCPT command", m_rcptSent);
VASSERT("All recipients", m_recipients.empty());
localSend("354 Ready to accept data; end with <CRLF>.<CRLF>\r\n");
m_state = STATE_DATA;
m_msgData.clear();
m_dataSent = true;
}
else if (cmd == "NOOP")
{
@ -202,6 +261,8 @@ public:
}
else if (cmd == "QUIT")
{
m_quitSent = true;
localSend("221 test.vmime.org Service closing transmission channel\r\n");
}
else
@ -247,6 +308,225 @@ private:
std::set <vmime::string> m_recipients;
std::string m_msgData;
bool m_ehloSent, m_heloSent, m_mailSent, m_rcptSent,
m_dataSent, m_quitSent;
};
/** SMTP test server 2.
*
* Test CHUNKING extension/BDAT command.
*/
class chunkingSMTPTestSocket : public testSocket
{
public:
chunkingSMTPTestSocket()
{
m_state = STATE_NOT_CONNECTED;
m_bdatChunkCount = 0;
m_ehloSent = m_mailSent = m_rcptSent = m_quitSent = false;
}
~chunkingSMTPTestSocket()
{
VASSERT_EQ("BDAT chunk count", 3, m_bdatChunkCount);
VASSERT("Client must send the QUIT command", m_quitSent);
}
void onConnected()
{
localSend("220 test.vmime.org Service ready\r\n");
processCommand();
m_state = STATE_COMMAND;
}
void onDataReceived()
{
if (m_state == STATE_DATA)
{
if (m_bdatChunkReceived != m_bdatChunkSize)
{
const size_type remaining = m_bdatChunkSize - m_bdatChunkReceived;
const size_type received = localReceiveRaw(NULL, remaining);
m_bdatChunkReceived += received;
}
if (m_bdatChunkReceived == m_bdatChunkSize)
{
m_state = STATE_COMMAND;
}
}
processCommand();
}
void processCommand()
{
vmime::string line;
if (!localReceiveLine(line))
return;
std::istringstream iss(line);
switch (m_state)
{
case STATE_NOT_CONNECTED:
localSend("451 Requested action aborted: invalid state\r\n");
break;
case STATE_COMMAND:
{
std::string cmd;
iss >> cmd;
if (cmd == "EHLO")
{
localSend("250-test.vmime.org says hello\r\n");
localSend("250 CHUNKING\r\n");
m_ehloSent = true;
}
else if (cmd == "HELO")
{
VASSERT("Client must not send the HELO command, as EHLO succeeded", false);
}
else if (cmd == "MAIL")
{
localSend("250 OK\r\n");
m_mailSent = true;
}
else if (cmd == "RCPT")
{
localSend("250 OK, recipient accepted\r\n");
m_rcptSent = true;
}
else if (cmd == "DATA")
{
VASSERT("BDAT must be used here!", false);
}
else if (cmd == "BDAT")
{
VASSERT("Client must send the MAIL command", m_mailSent);
VASSERT("Client must send the RCPT command", m_rcptSent);
unsigned long chunkSize = 0;
iss >> chunkSize;
std::string last;
iss >> last;
if (m_bdatChunkCount == 0)
{
VASSERT_EQ("BDAT chunk1 size", 262144, chunkSize);
VASSERT_EQ("BDAT chunk1 last", "", last);
}
else if (m_bdatChunkCount == 1)
{
VASSERT_EQ("BDAT chunk2 size", 262144, chunkSize);
VASSERT_EQ("BDAT chunk2 last", "", last);
}
else if (m_bdatChunkCount == 2)
{
VASSERT_EQ("BDAT chunk3 size", 4712, chunkSize);
VASSERT_EQ("BDAT chunk3 last", "LAST", last);
}
else
{
VASSERT("No more BDAT command should be issued!", false);
}
m_bdatChunkSize = chunkSize;
m_bdatChunkReceived = 0;
m_bdatChunkCount++;
m_state = STATE_DATA;
localSend("250 chunk received\r\n");
}
else if (cmd == "NOOP")
{
localSend("250 Completed\r\n");
}
else if (cmd == "QUIT")
{
localSend("221 test.vmime.org Service closing transmission channel\r\n");
m_quitSent = true;
}
else
{
localSend("502 Command not implemented\r\n");
}
break;
}
}
processCommand();
}
private:
enum State
{
STATE_NOT_CONNECTED,
STATE_COMMAND,
STATE_DATA
};
int m_state;
int m_bdatChunkCount;
int m_bdatChunkSize, m_bdatChunkReceived;
bool m_ehloSent, m_mailSent, m_rcptSent, m_quitSent;
};
class SMTPTestMessage : public vmime::message
{
public:
vmime::utility::stream::size_type getChunkBufferSize() const
{
static vmime::net::smtp::SMTPChunkingOutputStreamAdapter chunkStream(NULL);
return chunkStream.getBlockSize();
}
const std::vector <vmime::string>& getChunks() const
{
static std::vector <vmime::string> chunks;
if (chunks.size() == 0)
{
chunks.push_back(vmime::string(1000, 'A'));
chunks.push_back(vmime::string(3000, 'B'));
chunks.push_back(vmime::string(500000, 'C'));
chunks.push_back(vmime::string(25000, 'D'));
}
return chunks;
}
void generateImpl(const vmime::generationContext& /* ctx */,
vmime::utility::outputStream& outputStream,
const vmime::string::size_type /* curLinePos */ = 0,
vmime::string::size_type* /* newLinePos */ = NULL) const
{
for (unsigned int i = 0, n = getChunks().size() ; i < n ; ++i)
{
const vmime::string& chunk = getChunks()[i];
outputStream.write(chunk.data(), chunk.size());
}
}
};

View File

@ -127,6 +127,42 @@ void testSocket::localReceive(vmime::string& buffer)
}
bool testSocket::localReceiveLine(vmime::string& line)
{
vmime::string::size_type eol;
if ((eol = m_outBuffer.find('\n')) != vmime::string::npos)
{
line = vmime::string(m_outBuffer.begin(), m_outBuffer.begin() + eol);
if (!line.empty() && line[line.length() - 1] == '\r')
line.erase(line.end() - 1, line.end());
m_outBuffer.erase(m_outBuffer.begin(), m_outBuffer.begin() + eol + 1);
return true;
}
return false;
}
testSocket::size_type testSocket::localReceiveRaw(char* buffer, const size_type count)
{
const size_type received = std::min(count, static_cast <size_type>(m_outBuffer.size()));
if (received != 0)
{
if (buffer != NULL)
std::copy(m_outBuffer.begin(), m_outBuffer.begin() + received, buffer);
m_outBuffer.erase(m_outBuffer.begin(), m_outBuffer.begin() + received);
}
return received;
}
void testSocket::onDataReceived()
{
// Override

View File

@ -256,16 +256,31 @@ public:
/** Send data to client.
*
* @buffer data to send
* @param buffer data to send
*/
void localSend(const vmime::string& buffer);
/** Receive data from client.
*
* @buffer buffer in which to store received data
* @param buffer buffer in which to store received data
*/
void localReceive(vmime::string& buffer);
/** Receive a line from client.
*
* @param buffer buffer in which to store received line
* @return true if a line has been read, or false otherwise
*/
bool localReceiveLine(vmime::string& buffer);
/** Receive data from client.
*
* @param buffer buffer in which to store received data
* @param count number of bytes to receive
* @return number of bytes received
*/
testSocket::size_type localReceiveRaw(char* buffer, const size_type count);
protected:
/** Called when the client has sent some data.

View File

@ -0,0 +1,84 @@
//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002-2013 Vincent Richard <vincent@vmime.org>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 3 of
// the License, or (at your option) any later version.
//
// This program 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
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library. Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//
#ifndef VMIME_NET_SMTP_SMTPCHUNKINGOUTPUTSTREAMADAPTER_HPP_INCLUDED
#define VMIME_NET_SMTP_SMTPCHUNKINGOUTPUTSTREAMADAPTER_HPP_INCLUDED
#include "vmime/config.hpp"
#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP
#include "vmime/utility/outputStream.hpp"
namespace vmime {
namespace net {
namespace smtp {
class SMTPConnection;
/** An output stream adapter used with ESMTP CHUNKING extension.
*/
class VMIME_EXPORT SMTPChunkingOutputStreamAdapter : public utility::outputStream
{
friend class vmime::creator;
public:
SMTPChunkingOutputStreamAdapter(ref <SMTPConnection> conn);
void write(const value_type* const data, const size_type count);
void flush();
size_type getBlockSize();
private:
SMTPChunkingOutputStreamAdapter(const SMTPChunkingOutputStreamAdapter&);
void sendChunk(const value_type* const data, const size_type count, const bool last);
ref <SMTPConnection> m_connection;
value_type m_buffer[262144]; // 256 KB
size_type m_bufferSize;
unsigned int m_chunkCount;
};
} // smtp
} // net
} // vmime
#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP
#endif // VMIME_NET_SMTP_SMTPCHUNKINGOUTPUTSTREAMADAPTER_HPP_INCLUDED

View File

@ -67,6 +67,7 @@ public:
static ref <SMTPCommand> RCPT(const mailbox& mbox, const bool utf8);
static ref <SMTPCommand> RSET();
static ref <SMTPCommand> DATA();
static ref <SMTPCommand> BDAT(const unsigned long chunkSize, const bool last);
static ref <SMTPCommand> NOOP();
static ref <SMTPCommand> QUIT();

View File

@ -91,6 +91,22 @@ public:
private:
/** Send the MAIL and RCPT commands to the server, checking the
* response, and using pipelining if supported by the server.
* Optionally, the DATA command can also be sent.
*
* @param expeditor expeditor mailbox
* @param recipients list of recipient mailboxes
* @param sender envelope sender (if empty, expeditor will be used)
* @param sendDATACommand if true, the DATA command will be sent
*/
void sendEnvelope
(const mailbox& expeditor,
const mailboxList& recipients,
const mailbox& sender,
bool sendDATACommand);
ref <SMTPConnection> m_connection;