From 4ff49846d461336f970879608be061b4f09c99d7 Mon Sep 17 00:00:00 2001
From: "Adam D. Ruppe" <destructionator@gmail.com>
Date: Sat, 16 Feb 2013 11:18:00 -0500
Subject: [PATCH] better mime fixing

---
 email.d | 219 ++++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 173 insertions(+), 46 deletions(-)

diff --git a/email.d b/email.d
index dbadf73..d4da6d9 100644
--- a/email.d
+++ b/email.d
@@ -4,6 +4,7 @@ import std.net.curl;
 pragma(lib, "curl");
 
 import std.base64;
+import std.string;
 
 // SEE ALSO: std.net.curl.SMTP
 
@@ -50,49 +51,136 @@ class EmailMessage {
 		string type;
 		string filename;
 		const(void)[] content;
+		string id;
 	}
 
 	const(MimeAttachment)[] attachments;
 
-	void addAttachment(string mimeType, string filename, in void[] content) {
+	void addAttachment(string mimeType, string filename, in void[] content, string id = null) {
 		isMime = true;
-		attachments ~= MimeAttachment(mimeType, filename, content);
+		attachments ~= MimeAttachment(mimeType, filename, content, id);
 	}
 
+	// in the html, use img src="cid:ID_GIVEN_HERE"
+	void addInlineImage(string id, string mimeType, string filename, in void[] content) {
+		assert(isHtml);
+		isMime = true;
+		inlineImages ~= MimeAttachment(mimeType, filename, content, id);
+	}
+
+	const(MimeAttachment)[] inlineImages;
+
+
+	/* we should build out the mime thingy
+		related
+			mixed
+			alternate
+	*/
 
 	override string toString() {
-		string boundary = "0016e64be86203dd36047610926a"; // FIXME
-
 		assert(!isHtml || (isHtml && isMime));
 
 		auto headers = this.headers;
 
-		string toHeader = "To: ";
-		bool toHeaderOutputted = false;
-		foreach(t; to) {
-			if(toHeaderOutputted)
-				toHeader ~= ", ";
-			else
-				toHeaderOutputted = true;
-
-			toHeader ~= t;
-		}
-
 		if(to.length)
-			headers ~= toHeader;
+			headers ~= "To: " ~ join(to, ", ");
+		if(cc.length)
+			headers ~= "Cc: " ~ join(cc, ", ");
+
+		if(from.length)
+			headers ~= "From: " ~ from;
 
 		if(subject !is null)
 			headers ~= "Subject: " ~ subject;
+		if(replyTo !is null)
+			headers ~= "Reply-To: " ~ replyTo;
+		if(inReplyTo !is null)
+			headers ~= "In-Reply-To: " ~ inReplyTo;
 
 		if(isMime)
 			headers ~= "MIME-Version: 1.0";
 
+	/+
+		if(inlineImages.length) {
+			headers ~= "Content-Type: multipart/related; boundary=" ~ boundary;
+			// so we put the alternative inside asthe first attachment with as seconary boundary
+			// then we do the images
+		} else
 		if(attachments.length)
 			headers ~= "Content-Type: multipart/mixed; boundary=" ~ boundary;
 		else if(isHtml)
 			headers ~= "Content-Type: multipart/alternative; boundary=" ~ boundary;
 		else
 			headers ~= "Content-Type: text/plain; charset=UTF-8";
+	+/
+
+
+		string msgContent;
+
+		if(isMime) {
+			MimeContainer top;
+
+			{
+				MimeContainer mimeMessage;
+				if(isHtml) {
+					auto alternative = new MimeContainer("multipart/alternative");
+					alternative.stuff ~= new MimeContainer("text/plain; charset=UTF-8", textBody);
+					alternative.stuff ~= new MimeContainer("text/html; charset=UTF-8", htmlBody);
+					mimeMessage = alternative;
+				} else {
+					mimeMessage = new MimeContainer("text/plain; charset=UTF-8", textBody);
+				}
+				top = mimeMessage;
+			}
+
+			{
+				MimeContainer mimeRelated;
+				if(inlineImages.length) {
+					mimeRelated = new MimeContainer("multipart/related");
+
+					mimeRelated.stuff ~= top;
+					top = mimeRelated;
+
+					foreach(attachment; inlineImages) {
+						auto mimeAttachment = new MimeContainer(attachment.type ~ "; name=\""~attachment.filename~"\"");
+						mimeAttachment.headers ~= "Content-Transfer-Encoding: base64";
+						mimeAttachment.headers ~= "Content-ID: <" ~ attachment.id ~ ">";
+						mimeAttachment.content = Base64.encode(cast(const(ubyte)[]) attachment.content);
+
+						mimeRelated.stuff ~= mimeAttachment;
+					}
+				}
+			}
+
+			{
+				MimeContainer mimeMixed;
+				if(attachments.length) {
+					mimeMixed = new MimeContainer("multipart/mixed");
+
+					mimeMixed.stuff ~= top;
+					top = mimeMixed;
+
+					foreach(attachment; attachments) {
+						auto mimeAttachment = new MimeContainer(attachment.type);
+						mimeAttachment.headers ~= "Content-Disposition: attachment; filename=\""~attachment.filename~"\"";
+						mimeAttachment.headers ~= "Content-Transfer-Encoding: base64";
+						if(attachment.id.length)
+							mimeAttachment.headers ~= "Content-ID: <" ~ attachment.id ~ ">";
+
+						mimeAttachment.content = Base64.encode(cast(const(ubyte)[]) attachment.content);
+
+						mimeMixed.stuff ~= mimeAttachment;
+					}
+				}
+			}
+
+			headers ~= top.contentType;
+			msgContent = top.toMimeString(true);
+		} else {
+			headers ~= "Content-Type: text/plain; charset=UTF-8";
+			msgContent = textBody;
+		}
+
 
 		string msg;
 		msg.reserve(htmlBody.length + textBody.length + 1024);
@@ -102,46 +190,28 @@ class EmailMessage {
 		if(msg.length) // has headers
 			msg ~= "\r\n";
 
-		if(isMime) {
-			msg ~= "--" ~ boundary ~ "\r\n";
-			msg ~= "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
-		}
-
-		msg ~= textBody;
-
-		if(isMime)
-			msg ~= "\r\n--" ~ boundary;
-		if(isHtml) {
-			msg ~= "\r\n";
-			msg ~= "Content-Type: text/html; charset=UTF-8\r\n\r\n";
-			msg ~= htmlBody;
-			msg ~= "\r\n--" ~ boundary;
-		}
-
-		foreach(attachment; attachments) {
-			assert(isMime);
-			msg ~= "\r\n";
-			msg ~= "Content-Type: " ~ attachment.type ~ "\r\n";
-			msg ~= "Content-Disposition: attachment; filename=\""~attachment.filename~"\"\r\n";
-			msg ~= "Content-Transfer-Encoding: base64\r\n";
-			msg ~= "\r\n";
-			msg ~= Base64.encode(cast(const(ubyte)[]) attachment.content);
-			msg ~= "\r\n--" ~ boundary;
-		}
-
-		if(isMime)
-			msg ~= "--\r\n";
+		msg ~= msgContent;
 
 		return msg;
 	}
 
 	void send(RelayInfo mailServer = RelayInfo("smtp://localhost")) {
 		auto smtp = new SMTP(mailServer.server);
+		// smtp.verbose = true;
 		if(mailServer.username.length)
 			smtp.setAuthentication(mailServer.username, mailServer.password);
 		const(char)[][] allRecipients = cast(const(char)[][]) (to ~ cc ~ bcc); // WTF cast
 		smtp.mailTo(allRecipients);
-		smtp.mailFrom = from;
+
+		auto mailFrom = from;
+		auto idx = mailFrom.indexOf("<");
+		if(idx != -1)
+			mailFrom = mailFrom[idx + 1 .. $];
+		idx = mailFrom.indexOf(">");
+		if(idx != -1)
+			mailFrom = mailFrom[0 .. idx];
+
+		smtp.mailFrom = mailFrom;
 		smtp.message = this.toString();
 		smtp.perform();
 	}
@@ -155,3 +225,60 @@ void email(string to, string subject, string message, string from, RelayInfo mai
 	msg.textBody = message;
 	msg.send(mailServer);
 }
+
+// private:
+
+import std.conv;
+
+class MimeContainer {
+	private static int sequence;
+
+	immutable string _contentType;
+	immutable string boundary;
+
+	string[] headers; // NOT including content-type
+	string content;
+	MimeContainer[] stuff;
+
+	this(string contentType, string content = null) {
+		this._contentType = contentType;
+		this.content = content;
+		sequence++;
+		if(_contentType.indexOf("multipart/") == 0)
+			boundary = "0016e64be86203dd36047610926a" ~ to!string(sequence);
+	}
+
+	@property string contentType() {
+		string ct = "Content-Type: "~_contentType;
+		if(boundary.length)
+			ct ~= "; boundary=" ~ boundary;
+		return ct;
+	}
+
+
+	string toMimeString(bool isRoot = false) {
+		string ret;
+
+		if(!isRoot) {
+			ret ~= contentType;
+			foreach(header; headers) {
+				ret ~= "\r\n";
+				ret ~= header;
+			}
+			ret ~= "\r\n\r\n";
+		}
+
+		ret ~= content;
+
+		foreach(idx, thing; stuff) {
+			assert(boundary.length);
+			ret ~= "\r\n--" ~ boundary ~ "\r\n";
+			ret ~= thing.toMimeString(false);
+		}
+
+		if(boundary.length)
+			ret ~= "\r\n--" ~ boundary ~ "--";
+
+		return ret;
+	}
+}