diff options
| -rw-r--r-- | icetray/icetray/Jamfile.jam | 22 | ||||
| -rw-r--r-- | icetray/icetray/mail.ice | 33 | ||||
| -rw-r--r-- | icetray/icetray/mailServer.cpp | 53 | ||||
| -rw-r--r-- | icetray/icetray/mailServer.h | 28 | ||||
| -rw-r--r-- | icetray/icetray/mime.ice | 32 | ||||
| -rw-r--r-- | icetray/icetray/mimeImpl.cpp | 163 | ||||
| -rw-r--r-- | icetray/icetray/mimeImpl.h | 53 | ||||
| -rw-r--r-- | icetray/icetray/stream_support.h | 17 | ||||
| -rw-r--r-- | icetray/unittests/Jamfile.jam | 10 | ||||
| -rw-r--r-- | icetray/unittests/fixtures/mail/blank.png | bin | 0 -> 114 bytes | |||
| -rw-r--r-- | icetray/unittests/fixtures/mail/multipart-alt-imgs.eml | 48 | ||||
| -rw-r--r-- | icetray/unittests/fixtures/mail/multipart-alt.eml | 27 | ||||
| -rw-r--r-- | icetray/unittests/fixtures/mail/simple.eml | 11 | ||||
| -rw-r--r-- | icetray/unittests/testIceTrayMail.cpp | 156 | 
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.pngBinary files differ new file mode 100644 index 0000000..bcaf83a --- /dev/null +++ b/icetray/unittests/fixtures/mail/blank.png 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(); + | 
