arsd/email.d

285 lines
6.5 KiB
D

module arsd.email;
import std.net.curl;
pragma(lib, "curl");
import std.base64;
import std.string;
// SEE ALSO: std.net.curl.SMTP
struct RelayInfo {
string server;
string username;
string password;
}
class EmailMessage {
void setHeader(string name, string value) {
headers ~= name ~ ": " ~ value;
}
string[] to;
string[] cc;
string[] bcc;
string from;
string replyTo;
string inReplyTo;
string textBody;
string htmlBody;
string subject;
string[] headers;
private bool isMime = false;
private bool isHtml = false;
void setTextBody(string text) {
textBody = text;
}
void setHtmlBody(string html) {
isMime = true;
isHtml = true;
htmlBody = html;
import arsd.htmltotext;
if(textBody is null)
textBody = htmlToText(html);
}
struct MimeAttachment {
string type;
string filename;
const(void)[] content;
string id;
}
const(MimeAttachment)[] attachments;
void addAttachment(string mimeType, string filename, in void[] content, string id = null) {
isMime = true;
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() {
assert(!isHtml || (isHtml && isMime));
auto headers = this.headers;
if(to.length)
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);
foreach(header; headers)
msg ~= header ~ "\r\n";
if(msg.length) // has headers
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);
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();
}
}
void email(string to, string subject, string message, string from, RelayInfo mailServer = RelayInfo("smtp://localhost")) {
auto msg = new EmailMessage();
msg.from = from;
msg.to = [to];
msg.subject = subject;
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;
}
}