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; + } +}