mirror of https://github.com/adamdruppe/arsd.git
285 lines
6.5 KiB
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;
|
|
}
|
|
}
|