summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Goodliffe <dan@randomdan.homeip.net>2019-05-27 17:14:35 +0100
committerDan Goodliffe <dan@randomdan.homeip.net>2019-05-27 17:14:35 +0100
commit0a072c66df1e2765ecfa0365c642868bbfeaad3e (patch)
tree15bfb1ae5031bb649c4389d4847a91b3911cf420
parentTidy fixes for latest clang (diff)
downloadicetray-0a072c66df1e2765ecfa0365c642868bbfeaad3e.tar.bz2
icetray-0a072c66df1e2765ecfa0365c642868bbfeaad3e.tar.xz
icetray-0a072c66df1e2765ecfa0365c642868bbfeaad3e.zip
Initial commit of MIME/Mail functionality
-rw-r--r--icetray/icetray/Jamfile.jam22
-rw-r--r--icetray/icetray/mail.ice33
-rw-r--r--icetray/icetray/mailServer.cpp53
-rw-r--r--icetray/icetray/mailServer.h28
-rw-r--r--icetray/icetray/mime.ice32
-rw-r--r--icetray/icetray/mimeImpl.cpp163
-rw-r--r--icetray/icetray/mimeImpl.h53
-rw-r--r--icetray/icetray/stream_support.h17
-rw-r--r--icetray/unittests/Jamfile.jam10
-rw-r--r--icetray/unittests/fixtures/mail/blank.pngbin0 -> 114 bytes
-rw-r--r--icetray/unittests/fixtures/mail/multipart-alt-imgs.eml48
-rw-r--r--icetray/unittests/fixtures/mail/multipart-alt.eml27
-rw-r--r--icetray/unittests/fixtures/mail/simple.eml11
-rw-r--r--icetray/unittests/testIceTrayMail.cpp156
14 files changed, 646 insertions, 7 deletions
diff --git a/icetray/icetray/Jamfile.jam b/icetray/icetray/Jamfile.jam
index bacda4e..09cf1b2 100644
--- a/icetray/icetray/Jamfile.jam
+++ b/icetray/icetray/Jamfile.jam
@@ -1,15 +1,22 @@
import package ;
lib boost_program_options ;
+lib esmtp ;
-obj logWriter : logWriter.ice :
- <slicer>no
- <include>.
- <toolset>tidy:<checker>none
- ;
+rule iceobj ( name : source )
+{
+ obj $(name) : $(source) :
+ <slicer>no
+ <include>.
+ <toolset>tidy:<checker>none
+ ;
+}
+iceobj logWriter : logWriter.ice ;
+iceobj mime : mime.ice ;
+iceobj mail : mail.ice ;
lib icetray :
- [ glob *.cpp *.ice ]
- logWriter
+ [ glob *.cpp *.ice : mime.ice mail.ice ]
+ logWriter mime mail
:
<library>..//adhocutil
<library>..//dbppcore
@@ -20,6 +27,7 @@ lib icetray :
<library>..//slicer-db
<library>../..//glibmm
<library>boost_program_options
+ <library>esmtp
<implicit-dependency>logWriter
<slicer>pure
<include>.
diff --git a/icetray/icetray/mail.ice b/icetray/icetray/mail.ice
new file mode 100644
index 0000000..7f8bda4
--- /dev/null
+++ b/icetray/icetray/mail.ice
@@ -0,0 +1,33 @@
+#ifndef ICETRAY_MAIL
+#define ICETRAY_MAIL
+
+#include <mime.ice>
+
+[["ice-prefix"]]
+module IceTray {
+ module Mail {
+ local struct Address {
+ string name;
+ string address;
+ };
+
+ local class Email {
+ Address to;
+ Address from;
+ string subject;
+ Mime::BasicPart content;
+ };
+
+ ["cpp:ice_print"]
+ local exception SendEmailFailed {
+ string message;
+ };
+
+ local interface MailServer {
+ idempotent void sendEmail(Email msg) throws SendEmailFailed;
+ };
+ };
+};
+
+#endif
+
diff --git a/icetray/icetray/mailServer.cpp b/icetray/icetray/mailServer.cpp
new file mode 100644
index 0000000..67287c8
--- /dev/null
+++ b/icetray/icetray/mailServer.cpp
@@ -0,0 +1,53 @@
+#include "mailServer.h"
+#include <libesmtp.h>
+#include <Ice/ObjectAdapter.h>
+#include <Ice/Communicator.h>
+#include <memstream.h>
+
+namespace IceTray {
+ namespace Mail {
+ LibesmtpMailServer::LibesmtpMailServer(std::string s) :
+ server(std::move(s))
+ {
+ }
+
+ void
+ LibesmtpMailServer::sendEmail(const EmailPtr & msg)
+ {
+ smtp_session_t session = smtp_create_session();
+ smtp_message_t message = smtp_add_message(session);
+ smtp_set_server(session, server.c_str());
+ smtp_set_header(message, "To", msg->to.name.c_str(), msg->to.address.c_str());
+ smtp_set_header(message, "From", msg->from.name.c_str(), msg->from.address.c_str());
+ smtp_set_header(message, "Subject", msg->subject.c_str());
+ smtp_add_recipient(message, msg->to.address.c_str());
+ AdHoc::MemStream ms;
+ writeEmailContent(msg, ms);
+ smtp_set_message_fp(message, ms);
+ if (!smtp_start_session(session)) {
+ char buf[BUFSIZ];
+ auto b = smtp_strerror(smtp_errno(), buf, sizeof(buf));
+ assert(b);
+ SendEmailFailed e(__FILE__, __LINE__, b);
+ smtp_destroy_session(session);
+ throw e;
+ }
+ smtp_destroy_session(session);
+ }
+
+ void
+ BasicMailServer::writeEmailContent(EmailPtr msg, FILE * ms)
+ {
+ fputs("MIME-Version: 1.0\r\n", ms);
+ msg->content->write({ ms }, 0);
+ }
+
+
+ void
+ SendEmailFailed::ice_print(std::ostream & buf) const
+ {
+ buf << "Failed to send email: " << message;
+ }
+ }
+}
+
diff --git a/icetray/icetray/mailServer.h b/icetray/icetray/mailServer.h
new file mode 100644
index 0000000..8f19877
--- /dev/null
+++ b/icetray/icetray/mailServer.h
@@ -0,0 +1,28 @@
+
+#ifndef ICETRAY_MAILSERVER_H
+#define ICETRAY_MAILSERVER_H
+
+#include <mail.h>
+#include <visibility.h>
+
+namespace IceTray {
+ namespace Mail {
+ class DLL_PUBLIC BasicMailServer {
+ public:
+ static void writeEmailContent(EmailPtr msg, FILE * ms);
+ };
+
+ class DLL_PUBLIC LibesmtpMailServer : public MailServer, BasicMailServer {
+ public:
+ LibesmtpMailServer(std::string server);
+
+ void sendEmail(const EmailPtr & msg) override;
+
+ private:
+ const std::string server;
+ };
+ }
+}
+
+#endif
+
diff --git a/icetray/icetray/mime.ice b/icetray/icetray/mime.ice
new file mode 100644
index 0000000..112b165
--- /dev/null
+++ b/icetray/icetray/mime.ice
@@ -0,0 +1,32 @@
+#ifndef ICETRAY_MIME
+#define ICETRAY_MIME
+
+[["ice-prefix"]]
+[["cpp:include:stream_support.h"]]
+module IceTray {
+ module Mime {
+ local dictionary<string, string> Headers;
+
+ local interface Writable {
+ ["cpp:const"] void write(["cpp:type:StreamPtr"] string buffer, int depth);
+ };
+
+ local class BasicPart implements Writable {
+ Headers headers;
+ };
+
+ local sequence<BasicPart> Parts;
+
+ local class BasicSinglePart extends BasicPart {
+ string mimetype;
+ };
+
+ local class BasicMultiPart extends BasicPart {
+ string subtype;
+ Parts parts;
+ };
+ };
+};
+
+#endif
+
diff --git a/icetray/icetray/mimeImpl.cpp b/icetray/icetray/mimeImpl.cpp
new file mode 100644
index 0000000..74dfad5
--- /dev/null
+++ b/icetray/icetray/mimeImpl.cpp
@@ -0,0 +1,163 @@
+#include "mimeImpl.h"
+
+namespace IceTray::Mime {
+ static const char * const DIVIDER = "//divider//";
+ static const char mime_base64[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+ void
+ PartHelper::writeHeaders(const Headers & headers, const StreamPtr & ms)
+ {
+ for (const auto & h : headers) {
+ if (h.second.empty()) {
+ continue;
+ }
+ fprintf(ms, "%s: %s\r\n", h.first.c_str(), h.second.c_str());
+ }
+ }
+
+ TextPart::TextPart(const Headers & h, const std::string & m, const std::string & p) :
+ BasicSinglePart(h, m),
+ payload(p)
+ {
+ }
+
+ void
+ TextPart::write(const StreamPtr & ms, Ice::Int) const
+ {
+ writeHeaders(headers, ms);
+ fputs("Content-Transfer-Encoding: quoted-printable\r\n", ms);
+ fprintf(ms, "Content-Type: %s; charset=\"utf-8\"\r\n\r\n",
+ mimetype.c_str());
+ quotedPrintable(payload, ms);
+ fputs("\r\n", ms);
+ }
+
+ void
+ TextPart::quotedPrintable(const std::string_view & input, FILE * ms, const size_t maxWidth)
+ {
+ size_t line = 0;
+ auto wrap = [&]() {
+ fputs("=\r\n", ms);
+ line = 0;
+ };
+ auto wrapIfNeeded = [&](size_t need) {
+ if (line + need > maxWidth) {
+ wrap();
+ }
+ };
+ for (const auto & ch : input) {
+ const auto & nextCh = *(&ch + 1);
+ if (ch == '\r') {
+ }
+ else if (ch == '\n') {
+ fputs("\r\n", ms);
+ line = 0;
+ }
+ else if (ch == ' ' || ch == '\t') {
+ if (nextCh == '\r' || nextCh == '\n') {
+ wrapIfNeeded(3);
+ fprintf(ms, "=%02X", (uint8_t)ch);
+ line += 3;
+ }
+ else {
+ wrapIfNeeded(1);
+ fputc(ch, ms);
+ line += 1;
+ }
+ }
+ else if ((ch >= 33 && ch < 61) || (ch > 61 && ch <= 126)) {
+ wrapIfNeeded(1);
+ fputc(ch, ms);
+ line += 1;
+ }
+ else {
+ wrapIfNeeded(3);
+ fprintf(ms, "=%02X", (uint8_t)ch);
+ line += 3;
+ }
+ }
+ }
+
+ BinaryViewPart::BinaryViewPart(const Headers & h, const std::string & m, const std::basic_string_view<uint8_t> & v) :
+ BasicSinglePart(h, m),
+ payload(v)
+ {
+ }
+
+ void
+ BinaryViewPart::write(const StreamPtr & ms, Ice::Int) const
+ {
+ writeHeaders(headers, ms);
+ fputs("Content-Transfer-Encoding: base64\r\n", ms);
+ fprintf(ms, "Content-Type: %s\r\n\r\n",
+ mimetype.c_str());
+ base64(payload, ms);
+ fputs("\r\n", ms);
+ }
+
+ void
+ BinaryViewPart::base64(const std::basic_string_view<uint8_t> & input, FILE * ms,
+ const size_t maxWidth)
+ {
+ auto mime_encode_base64_block = [](auto & dest, const auto & src) {
+ if (src.length() >= 1) {
+ dest[0] = mime_base64[(src[0] & 0xFC) >> 2];
+ if (src.length() >= 2) {
+ dest[1] = mime_base64[((src[0] & 0x03) << 4) | ((src[1] & 0xF0) >> 4)];
+ if (src.length() >= 3) {
+ dest[2] = mime_base64[((src[1] & 0x0F) << 2) | ((src[2] & 0xC0) >> 6)];
+ dest[3] = mime_base64[((src[2] & 0x3F))];
+ }
+ else {
+ dest[2] = mime_base64[((src[1] & 0x0F) << 2)];
+ }
+ }
+ else {
+ dest[1] = mime_base64[(src[0] & 0x03) << 4];
+ }
+ }
+ };
+
+ size_t l = 0;
+ for (size_t i = 0; i < input.length(); i += 3) {
+ if (maxWidth > 0 && l + 4 > maxWidth) {
+ fputs("\r\n", ms);
+ l = 0;
+ }
+
+ std::array<char, 4> bytes { '=', '=', '=', '=' };
+ mime_encode_base64_block(bytes, input.substr(i, 3));
+ fwrite(bytes.data(), bytes.size(),1, ms);
+ l += 4;
+ }
+ fputs("\r\n", ms);
+ }
+
+ BinaryCopyPart::BinaryCopyPart(const Headers & h, const std::string & m, std::vector<uint8_t> v) :
+ BinaryViewPart(h, m, { v.data(), v.size() }),
+ payload(std::move(v))
+ {
+ }
+
+
+ MultiPart::MultiPart(const Headers & h, const std::string & st, const Parts & p) :
+ BasicMultiPart(h, st, p)
+ {
+ }
+
+ void
+ MultiPart::write(const StreamPtr & ms, Ice::Int depth) const
+ {
+ writeHeaders(headers, ms);
+ fprintf(ms, "Content-Type: multipart/%s; boundary=\"%s%d\"\r\n\r\n",
+ subtype.c_str(), DIVIDER, depth);
+ for (const auto & p : parts) {
+ fprintf(ms, "--%s%d\r\n", DIVIDER, depth);
+ p->write(ms, depth + 1);
+ }
+ fprintf(ms, "--%s%d--\r\n", DIVIDER, depth);
+ }
+
+}
+
diff --git a/icetray/icetray/mimeImpl.h b/icetray/icetray/mimeImpl.h
new file mode 100644
index 0000000..235d747
--- /dev/null
+++ b/icetray/icetray/mimeImpl.h
@@ -0,0 +1,53 @@
+#ifndef ICETRAY_MIME_IMPL_H
+#define ICETRAY_MIME_IMPL_H
+
+#include <mime.h>
+#include <visibility.h>
+
+namespace IceTray::Mime {
+ class DLL_PUBLIC PartHelper {
+ protected:
+ static void writeHeaders(const Headers & headers, const StreamPtr & ms);
+ };
+
+ class DLL_PUBLIC TextPart : public BasicSinglePart, PartHelper {
+ public:
+ TextPart(const Headers &, const std::string &, const std::string &);
+
+ void write(const StreamPtr & ms, Ice::Int depth) const override;
+
+ static void quotedPrintable(const std::string_view & input, FILE * ms,
+ const size_t maxWidth = 74);
+
+ const std::string payload;
+ };
+
+ class DLL_PUBLIC BinaryViewPart : public BasicSinglePart, PartHelper {
+ public:
+ BinaryViewPart(const Headers &, const std::string &, const std::basic_string_view<uint8_t> &);
+
+ void write(const StreamPtr & ms, Ice::Int depth) const override;
+
+ static void base64(const std::basic_string_view<uint8_t> & input, FILE * ms,
+ const size_t maxWidth = 76);
+
+ std::basic_string_view<uint8_t> payload;
+ };
+
+ class DLL_PUBLIC BinaryCopyPart : public BinaryViewPart {
+ public:
+ BinaryCopyPart(const Headers &, const std::string &, std::vector<uint8_t>);
+
+ std::vector<uint8_t> payload;
+ };
+
+ class DLL_PUBLIC MultiPart : public BasicMultiPart, PartHelper {
+ public:
+ MultiPart(const Headers &, const std::string &, const Parts &);
+
+ void write(const StreamPtr & ms, Ice::Int depth) const override;
+ };
+}
+
+#endif
+
diff --git a/icetray/icetray/stream_support.h b/icetray/icetray/stream_support.h
new file mode 100644
index 0000000..4e01e7d
--- /dev/null
+++ b/icetray/icetray/stream_support.h
@@ -0,0 +1,17 @@
+#ifndef ICETRAY_STREAM_SUPPORT_H
+#define ICETRAY_STREAM_SUPPORT_H
+
+#include <cstdio>
+#include <memory>
+
+namespace IceTray
+{
+ class StreamPtr {
+ public:
+ operator FILE * () const { return f; }
+ FILE * const f;
+ };
+}
+
+#endif
+
diff --git a/icetray/unittests/Jamfile.jam b/icetray/unittests/Jamfile.jam
index 7486d04..655acb3 100644
--- a/icetray/unittests/Jamfile.jam
+++ b/icetray/unittests/Jamfile.jam
@@ -100,6 +100,16 @@ run
<library>testService
;
+run
+ testIceTrayMail.cpp
+ : -- :
+ fixtures/mail/multipart-alt-imgs.eml
+ fixtures/mail/multipart-alt.eml
+ fixtures/mail/simple.eml
+ :
+ <library>testCommon
+ ;
+
lib testService
:
testService.cpp
diff --git a/icetray/unittests/fixtures/mail/blank.png b/icetray/unittests/fixtures/mail/blank.png
new file mode 100644
index 0000000..bcaf83a
--- /dev/null
+++ b/icetray/unittests/fixtures/mail/blank.png
Binary files differ
diff --git a/icetray/unittests/fixtures/mail/multipart-alt-imgs.eml b/icetray/unittests/fixtures/mail/multipart-alt-imgs.eml
new file mode 100644
index 0000000..f883d39
--- /dev/null
+++ b/icetray/unittests/fixtures/mail/multipart-alt-imgs.eml
@@ -0,0 +1,48 @@
+From: from <from@test.com>
+To: to <to@test.com>
+Date: Fri, 24 May 2019 22:19:54 +0000 (UTC)
+Return-path: <bounces@test.com>
+MIME-Version: 1.0
+X-Source: multipart_top
+Content-Type: multipart/alternative; boundary="//divider//0"
+
+--//divider//0
+X-Source: multipart_plain
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="utf-8"
+
+Simple text =C2=A3
+
+--//divider//0
+X-Source: multipart_html
+Content-Type: multipart/related; boundary="//divider//1"
+
+--//divider//1
+X-Source: multipart_html_main
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset="utf-8"
+
+<html lang=3D"en">
+<head>
+<title>=C2=A3</title>
+</head>
+<html>
+
+--//divider//1
+X-Source: multipart_html_img1
+Content-Transfer-Encoding: base64
+Content-Type: image/png
+
+iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAABmJLR0QA/wD/AP+gvaeTAAAAJ0lE
+QVQYlWP8////fwbiwGomIhUyMDAwMIwqpo9iFgYGhtVEqj0BAAvPBjJ63HJVAAAAAElFTkSuQmCC
+
+--//divider//1
+X-Source: multipart_html_img2
+Content-Transfer-Encoding: base64
+Content-Type: image/png
+
+iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAABmJLR0QA/wD/AP+gvaeTAAAAJ0lE
+QVQYlWP8////fwbiwGomIhUyMDAwMIwqpo9iFgYGhtVEqj0BAAvPBjJ63HJVAAAAAElFTkSuQmCC
+
+--//divider//1--
+--//divider//0--
diff --git a/icetray/unittests/fixtures/mail/multipart-alt.eml b/icetray/unittests/fixtures/mail/multipart-alt.eml
new file mode 100644
index 0000000..e199b58
--- /dev/null
+++ b/icetray/unittests/fixtures/mail/multipart-alt.eml
@@ -0,0 +1,27 @@
+From: from <from@test.com>
+To: to <to@test.com>
+Date: Fri, 24 May 2019 22:19:54 +0000 (UTC)
+Return-path: <bounces@test.com>
+MIME-Version: 1.0
+X-Source: multipart_top
+Content-Type: multipart/alternative; boundary="//divider//0"
+
+--//divider//0
+X-Source: multipart_plain
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="utf-8"
+
+Simple text =C2=A3
+
+--//divider//0
+X-Source: multipart_html
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset="utf-8"
+
+<html lang=3D"en">
+<head>
+<title>=C2=A3</title>
+</head>
+<html>
+
+--//divider//0--
diff --git a/icetray/unittests/fixtures/mail/simple.eml b/icetray/unittests/fixtures/mail/simple.eml
new file mode 100644
index 0000000..2b411c9
--- /dev/null
+++ b/icetray/unittests/fixtures/mail/simple.eml
@@ -0,0 +1,11 @@
+From: from <from@test.com>
+To: to <to@test.com>
+Date: Fri, 24 May 2019 22:19:54 +0000 (UTC)
+Return-path: <bounces@test.com>
+MIME-Version: 1.0
+X-Source: single_part
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="utf-8"
+
+Simple text =C2=A3
+
diff --git a/icetray/unittests/testIceTrayMail.cpp b/icetray/unittests/testIceTrayMail.cpp
new file mode 100644
index 0000000..ff2220a
--- /dev/null
+++ b/icetray/unittests/testIceTrayMail.cpp
@@ -0,0 +1,156 @@
+#define BOOST_TEST_MODULE TestIceTrayMail
+#include <boost/test/unit_test.hpp>
+#include <boost/test/data/test_case.hpp>
+
+#include <memstream.h>
+#include <mailServer.h>
+#include <mimeImpl.h>
+#include <fileUtils.h>
+#include <definedDirs.h>
+
+using namespace std::string_literals;
+using namespace IceTray::Mime;
+using namespace IceTray::Mail;
+using QPTD = std::tuple<std::string_view, std::string_view>;
+using B64TD = std::tuple<size_t, std::string_view>;
+
+BOOST_DATA_TEST_CASE(quotedPrintable, boost::unit_test::data::make<QPTD>({
+ { "", "" },
+ { "Simple string", "Simple string" },
+ { " \t Leading whitespace", " \t Leading whitespace" },
+ { "Trailing whitespace \t \n", "Trailing whitespace \t=20\r\n" },
+ { "High byte values £ © ±", "High byte values =C2=A3 =\r\n=C2=A9 =C2=B1" },
+ { "<html lang=\"en\">", "<html lang=3D\"en\">" }
+}), input, expected)
+{
+ AdHoc::MemStream ms;
+ TextPart::quotedPrintable(input, ms, 25);
+ BOOST_CHECK_EQUAL(expected, ms.sv());
+}
+
+BOOST_DATA_TEST_CASE(base64, boost::unit_test::data::make<B64TD>({
+ { 0, "\r\n" },
+ { 1, "iQ==\r\n" },
+ { 2, "iVA=\r\n" },
+ { 3, "iVBO\r\n" },
+ { 4, "iVBORw==\r\n" },
+ { 5, "iVBORw0=\r\n" },
+ { 90,
+ "iVBORw0KGgoAAAANSUhEUgAA\r\n"
+ "AAsAAAALCAYAAACprHcmAAAA\r\n"
+ "BmJLR0QA/wD/AP+gvaeTAAAA\r\n"
+ "J0lEQVQYlWP8////fwbiwGom\r\n"
+ "IhUyMDAwMIwqpo9iFgYGhtVE\r\n" },
+ { 113,
+ "iVBORw0KGgoAAAANSUhEUgAA\r\n"
+ "AAsAAAALCAYAAACprHcmAAAA\r\n"
+ "BmJLR0QA/wD/AP+gvaeTAAAA\r\n"
+ "J0lEQVQYlWP8////fwbiwGom\r\n"
+ "IhUyMDAwMIwqpo9iFgYGhtVE\r\n"
+ "qj0BAAvPBjJ63HJVAAAAAElF\r\n"
+ "TkSuQmA=\r\n" },
+}), input, expected)
+{
+ AdHoc::MemStream ms;
+ AdHoc::FileUtils::MemMap png(rootDir / "fixtures" / "mail" / "blank.png");
+ BinaryViewPart::base64(png.sv<uint8_t>().substr(0, input), ms, 24);
+ BOOST_CHECK_EQUAL(expected, ms.sv());
+}
+
+struct TestBase {
+ TestBase() :
+ e(std::make_shared<Email>()),
+#ifdef DUMP
+ dump(fopen("/tmp/dump.eml", "w")),
+#endif
+ fixtures(rootDir / "fixtures" / "mail")
+ {
+ e->from = {"from", "from@test.com"};
+ e->to = {"to", "to@test.com"};
+ e->subject = "subject";
+
+ auto commonHeaders = [](FILE * s) {
+ fputs("From: from <from@test.com>\r\n", s);
+ fputs("To: to <to@test.com>\r\n", s);
+ fputs("Date: Fri, 24 May 2019 22:19:54 +0000 (UTC)\r\n", s);
+ fputs("Return-path: <bounces@test.com>\r\n", s);
+ };
+ commonHeaders(ms);
+#ifdef DUMP
+ commonHeaders(dump);
+#endif
+ }
+ ~TestBase()
+ {
+#ifdef DUMP
+ fclose(dump);
+#endif
+ }
+ EmailPtr e;
+ AdHoc::MemStream ms;
+#ifdef DUMP
+ FILE * dump;
+#endif
+ const std::filesystem::path fixtures;
+};
+
+const std::string text_content = "Simple text £\r\n";
+const std::string html_content = "<html lang=\"en\">\r\n"
+"<head>\r\n"
+"<title>£</title>\r\n"
+"</head>\r\n"
+"<html>\r\n";
+
+BOOST_FIXTURE_TEST_SUITE(base, TestBase);
+
+BOOST_AUTO_TEST_CASE(single_part)
+{
+ e->content = std::make_shared<TextPart>(Headers {
+ { "X-Source", "single_part" }
+ }, "text/plain", text_content);
+ BasicMailServer::writeEmailContent(e, ms);
+ BOOST_CHECK_EQUAL(ms, AdHoc::FileUtils::MemMap(fixtures / "simple.eml").sv());
+}
+
+BOOST_AUTO_TEST_CASE(multipart_alt)
+{
+ auto text = std::make_shared<TextPart>(Headers {
+ { "X-Source", "multipart_plain" }
+ }, "text/plain", text_content);
+ auto html = std::make_shared<TextPart>(Headers {
+ { "X-Source", "multipart_html" }
+ }, "text/html", html_content);
+ e->content = std::make_shared<MultiPart>(Headers {
+ { "X-Source", "multipart_top" }
+ }, "alternative", Parts { text, html });
+ BasicMailServer::writeEmailContent(e, ms);
+ BOOST_CHECK_EQUAL(ms, AdHoc::FileUtils::MemMap(fixtures / "multipart-alt.eml").sv());
+}
+
+BOOST_AUTO_TEST_CASE(multipart_alt_imgs)
+{
+ AdHoc::FileUtils::MemMap png(fixtures / "blank.png");
+ auto text = std::make_shared<TextPart>(Headers {
+ { "X-Source", "multipart_plain" }
+ }, "text/plain", text_content);
+ auto img1 = std::make_shared<BinaryViewPart>(Headers {
+ { "X-Source", "multipart_html_img1" }
+ }, "image/png", png.sv<uint8_t>());
+ auto img2 = std::make_shared<BinaryCopyPart>(Headers {
+ { "X-Source", "multipart_html_img2" }
+ }, "image/png", std::vector<uint8_t>{ png.sv<uint8_t>().begin(), png.sv<uint8_t>().end() });
+ auto html = std::make_shared<TextPart>(Headers {
+ { "X-Source", "multipart_html_main" }
+ }, "text/html", html_content);
+ auto htmlrel = std::make_shared<MultiPart>(Headers {
+ { "X-Source", "multipart_html" }
+ }, "related", Parts { html, img1, img2 });
+ e->content = std::make_shared<MultiPart>(Headers {
+ { "X-Source", "multipart_top" }
+ }, "alternative", Parts { text, htmlrel });
+ BasicMailServer::writeEmailContent(e, ms);
+ BOOST_CHECK_EQUAL(ms, AdHoc::FileUtils::MemMap(fixtures / "multipart-alt-imgs.eml").sv());
+}
+
+BOOST_AUTO_TEST_SUITE_END();
+