sugoi internally added
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package sugoi.mail;
|
||||
import sugoi.mail.IMail;
|
||||
import sugoi.mail.IMailer;
|
||||
|
||||
/**
|
||||
* Manage an email buffer in a table before sending them
|
||||
* @author fbarbut
|
||||
*/
|
||||
class BufferedMailer implements IMailer
|
||||
{
|
||||
var conf : Dynamic;
|
||||
var type : String;
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function init(?c:Dynamic):IMailer{
|
||||
return this;
|
||||
}
|
||||
|
||||
public function defineFinalMailer(type:String){
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{
|
||||
|
||||
var bm = new sugoi.db.BufferedMail();
|
||||
bm.headers = m.getHeaders();
|
||||
bm.title = m.getTitle();
|
||||
bm.htmlBody = m.getHtmlBody();
|
||||
bm.textBody = m.getTextBody();
|
||||
bm.recipients = m.getRecipients();
|
||||
bm.sender = m.getSender();
|
||||
|
||||
bm.mailerType = this.type;
|
||||
|
||||
//custom params
|
||||
if(params!=null){
|
||||
bm.data = params;
|
||||
if(Reflect.hasField(params,"remoteId")){
|
||||
bm.remoteId = Reflect.getProperty(params,"remoteId");
|
||||
}
|
||||
}
|
||||
|
||||
//set sending status as "queued"
|
||||
var map = new MailerResult();
|
||||
for( r in m.getRecipients() ){
|
||||
map.set( r.email , Success(Queued) );
|
||||
}
|
||||
|
||||
bm.status = map;
|
||||
bm.insert();
|
||||
|
||||
if(callback!=null) callback(map);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package sugoi.mail;
|
||||
import sugoi.mail.IMail;
|
||||
import sugoi.mail.IMailer;
|
||||
|
||||
/**
|
||||
* A Debug Mailer to use in dev environment :
|
||||
* logs the emails in the Error table + write html files in tmp folder
|
||||
*
|
||||
* @author fbarbut
|
||||
*/
|
||||
class DebugMailer implements IMailer
|
||||
{
|
||||
public function new() {}
|
||||
|
||||
public function init(?c:Dynamic):IMailer{
|
||||
return this;
|
||||
}
|
||||
|
||||
public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{
|
||||
|
||||
//log in the Error table
|
||||
var t = new StringBuf();
|
||||
t.add("to:" + m.getRecipients()+"\n");
|
||||
t.add("subject:" + m.getSubject()+"\n");
|
||||
t.add("body:" + m.getHtmlBody()+"\n");
|
||||
App.current.logError( "[DEBUG] Email sent to " + m.getRecipients(), t.toString() );
|
||||
|
||||
//log in an html file
|
||||
var tmpDir = sugoi.Web.getCwd() + "../tmp/";
|
||||
if ( !sys.FileSystem.exists(tmpDir) ) sys.FileSystem.createDirectory(tmpDir);
|
||||
var dest = m.getRecipients()[0].email;
|
||||
sys.io.File.saveContent( tmpDir + dest+"-"+Date.now().toString().substr(0,10)+ "-"+ m.getSubject() + ".html" , m.getHtmlBody() );
|
||||
|
||||
//callback
|
||||
if (callback != null){
|
||||
|
||||
var map = new MailerResult();
|
||||
for ( u in m.getRecipients() ){
|
||||
map.set( u.email , Success(Sent) );
|
||||
}
|
||||
callback(map);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package sugoi.mail;
|
||||
|
||||
/**
|
||||
* Interface that represents an email message.
|
||||
*/
|
||||
interface IMail
|
||||
{
|
||||
public function setSender(email:String, ?name:String, ?userId:Int):IMail;
|
||||
public function setRecipient(email:String, ?name:String, ?userId:Int):IMail;
|
||||
public function addRecipient(email:String, ?name:String, ?userId:Int):IMail;
|
||||
public function setSubject(subject:String):IMail;
|
||||
public function setHeader(key:String, value:String):IMail;
|
||||
public function setHtmlBody(body:String):IMail;
|
||||
public function setTextBody(body:String):IMail;
|
||||
|
||||
public function getSender(): {?userId:Int,email:String,name:String};
|
||||
public function getRecipients():Array<{?userId:Int,email:String,name:String}>;
|
||||
public function getSubject():String;
|
||||
public function getTitle():String;
|
||||
public function getHtmlBody():String;
|
||||
public function getTextBody():String;
|
||||
public function getHeaders():Map<String,String>;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package sugoi.mail;
|
||||
import tink.core.Future;
|
||||
import tink.core.Outcome;
|
||||
|
||||
/**
|
||||
* Errors that occurs after sending an email thru a mailer
|
||||
*/
|
||||
enum MailerError{
|
||||
GenericError(e:tink.core.Error);
|
||||
HardBounce; //bad mailbox
|
||||
SoftBounce; //mailbox exists but is full or not reachable
|
||||
Spam; //email is considered spam
|
||||
Unsub; //this user unsubscribed from this service/list
|
||||
Unsigned; //the sender is invalid ( i.e does not match the SPF records )
|
||||
}
|
||||
|
||||
enum MailerSuccess{
|
||||
Sent;
|
||||
Queued;
|
||||
}
|
||||
|
||||
typedef MailerResult = Map<String,Outcome<MailerSuccess,MailerError>>
|
||||
|
||||
/**
|
||||
* Interface for "Mailers"
|
||||
*
|
||||
* @author fbarbut
|
||||
*/
|
||||
interface IMailer
|
||||
{
|
||||
/**
|
||||
* init with a configuration object
|
||||
*/
|
||||
public function init(?conf:{smtp_host:String,smtp_port:Int,smtp_user:String,smtp_pass:String}):IMailer;
|
||||
|
||||
/**
|
||||
* Sends an email. A callback can be defined to handle the result
|
||||
*/
|
||||
public function send(email:IMail,?params:Dynamic,?callback:MailerResult->Void):Void;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package sugoi.mail;
|
||||
using Lambda;
|
||||
|
||||
class Mail implements IMail
|
||||
{
|
||||
|
||||
public var title : String;
|
||||
public var htmlBody : String;
|
||||
public var textBody : String;
|
||||
var headers : Map<String,String>;
|
||||
var sender : {name:String,email:String,?userId:Int};
|
||||
var recipients : Array<{name:String,email:String,?userId:Int}>;
|
||||
|
||||
|
||||
|
||||
public function new() {
|
||||
recipients = [];
|
||||
headers = new Map();
|
||||
}
|
||||
|
||||
public function getRecipients(){
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public function setSender(email, ?name,?userId) {
|
||||
if(!isValid(email)) throw "invalid sender email : \""+email+"\"";
|
||||
|
||||
sender = {name:name,email:email,userId:userId};
|
||||
return this;
|
||||
}
|
||||
|
||||
public function setReplyTo(email, ?name) {
|
||||
if(!isValid(email)) throw "invalid reply-to email : \""+email+"\"";
|
||||
|
||||
setHeader("Reply-To","<"+email+">"+(name==null?"":name));
|
||||
}
|
||||
|
||||
public function setSubject(s:String) {
|
||||
title = s;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* can add one or more recipient
|
||||
* @param email
|
||||
* @param ?name
|
||||
* @param ?userId
|
||||
*/
|
||||
public function addRecipient(email:String, ?name:String, ?userId:Int) {
|
||||
if(!isValid(email)) throw "invalid recipient \""+email+"\"";
|
||||
recipients.push( {email:email, name:name, userId:userId } );
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* alias to addRecipient()
|
||||
* @param email
|
||||
* @param ?name
|
||||
* @param ?userId
|
||||
*/
|
||||
public function setRecipient(email:String, ?name:String, ?userId:Int) {
|
||||
addRecipient(email, name, userId);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static function isValid( addr : String ){
|
||||
var reg = ~/^[^()<>@,;:\\"\[\]\s[:cntrl:]]+@[A-Z0-9][A-Z0-9-]*(\.[A-Z0-9][A-Z0-9-]*)*\.(xn--[A-Z0-9]+|[A-Z]{2,8})$/i;
|
||||
return addr != null && reg.match(addr);
|
||||
}
|
||||
|
||||
public function setHeader(k:String, v:String) {
|
||||
headers.set(k, v);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* generate a custom key for transactionnal emails, valid during the current day
|
||||
*/
|
||||
public function getKey() {
|
||||
return haxe.crypto.Md5.encode(App.config.get("key")+recipients[0].email+(Date.now().getDate())).substr(0,12);
|
||||
}
|
||||
|
||||
/**
|
||||
* render html from a template + vars
|
||||
* @param tpl A Template path
|
||||
* @param ctx Vars to send to template
|
||||
*/
|
||||
public function setHtmlBodyWithTemplate(tpl, ctx:Dynamic) {
|
||||
var app = App.current;
|
||||
var tpl = app.loadTemplate(tpl);
|
||||
if( ctx == null ) ctx = { };
|
||||
ctx.HOST = App.config.HOST;
|
||||
ctx.key = getKey();
|
||||
ctx.senderName = sender.name;
|
||||
ctx.senderEmail = sender.email;
|
||||
ctx.recipientName = recipients[0].name;
|
||||
ctx.recipientEmail = recipients[0].email;
|
||||
ctx.recipients = recipients;
|
||||
CSSInlining(ctx);
|
||||
htmlBody = tpl.execute(ctx);
|
||||
|
||||
}
|
||||
|
||||
public function setHtmlBody(s) {
|
||||
htmlBody = s;
|
||||
return this;
|
||||
}
|
||||
|
||||
public function setTextBodyWithTemplate(tpl, ctx:Dynamic) {
|
||||
var app = App.current;
|
||||
var tpl = app.loadTemplate(tpl);
|
||||
if( ctx == null ) ctx = { };
|
||||
ctx.HOST = App.config.HOST;
|
||||
ctx.key = getKey();
|
||||
textBody = tpl.execute(ctx);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
function CSSInlining(ctx) {
|
||||
// CSS inlining
|
||||
var css : Map<String,Array<String>> = new Map();
|
||||
ctx.addStyle = function(sel:String, style:String) {
|
||||
sel = sel.toLowerCase();
|
||||
if (css.exists(sel))
|
||||
css.set(sel, css.get(sel).concat(style.split(";")));
|
||||
else
|
||||
css.set(sel, style.split(";"));
|
||||
return "";
|
||||
}
|
||||
var applyStyleRec = null;
|
||||
applyStyleRec = function(x:Xml) {
|
||||
if (x.nodeType==Xml.Element) {
|
||||
var name = x.nodeName.toLowerCase();
|
||||
if( css.exists(name) )
|
||||
if (x.get("style")!=null)
|
||||
x.set("style", x.get("style")+";"+css.get(name).join(";"));
|
||||
else
|
||||
x.set("style", css.get(name).join(";"));
|
||||
for (n in x)
|
||||
applyStyleRec(n);
|
||||
}
|
||||
}
|
||||
ctx.applyStyle = function(raw:String) {
|
||||
var x = Xml.parse(raw);
|
||||
for(n in x)
|
||||
applyStyleRec(n);
|
||||
return x.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getSubject(){
|
||||
return title;
|
||||
}
|
||||
|
||||
public function getTitle(){
|
||||
return getSubject();
|
||||
}
|
||||
|
||||
public function getHtmlBody(){
|
||||
return htmlBody;
|
||||
}
|
||||
|
||||
public function getTextBody(){
|
||||
return textBody;
|
||||
}
|
||||
|
||||
public function setTextBody(t){
|
||||
textBody = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
public function getHeaders(){
|
||||
return headers;
|
||||
}
|
||||
|
||||
public function getSender(){
|
||||
return sender;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package sugoi.mail;
|
||||
import sugoi.mail.IMail;
|
||||
import sugoi.mail.IMailer;
|
||||
|
||||
|
||||
/**
|
||||
* Send an email via Mandrill.com API
|
||||
* @author fbarbut
|
||||
* @doc https://mandrillapp.com/api/docs/messages.JSON.html
|
||||
*/
|
||||
class MandrillMailer implements IMailer
|
||||
{
|
||||
|
||||
var conf : Dynamic;
|
||||
|
||||
public function new() {}
|
||||
|
||||
public function init(?c:Dynamic):IMailer{
|
||||
conf = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
public function send(m:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void):Void{
|
||||
|
||||
//build an object from headers map
|
||||
var headersObj = { };
|
||||
var headers = m.getHeaders();
|
||||
for(k in headers.keys()) {
|
||||
Reflect.setField(headersObj, k, headers.get(k));
|
||||
}
|
||||
|
||||
var data = {
|
||||
key: conf.smtp_pass,
|
||||
message: {
|
||||
html : m.getHtmlBody(),
|
||||
text : m.getTextBody(),
|
||||
subject : m.getSubject(),
|
||||
from_email : m.getSender().email,
|
||||
from_name : m.getSender().name,
|
||||
to : [],
|
||||
headers : headersObj,
|
||||
//images : images,
|
||||
}
|
||||
};
|
||||
for (r in m.getRecipients()) {
|
||||
data.message.to.push( { email:r.email, name:r.name, type:"to" } );
|
||||
}
|
||||
|
||||
var raw = curlRequest("POST", "https://mandrillapp.com/api/1.0/messages/send.json", {}, haxe.Json.stringify(data));
|
||||
|
||||
if (callback != null){
|
||||
|
||||
if (raw == null) throw "CURL response is null";
|
||||
if (raw == "") throw "CURL response is empty";
|
||||
var apiResult : MandrillApiSendResult = null;
|
||||
try{
|
||||
apiResult = haxe.Json.parse(raw);
|
||||
|
||||
}catch (e:Dynamic){
|
||||
throw "unable to decode : " + raw + ", error is "+Std.string(e);
|
||||
}
|
||||
|
||||
var map = new MailerResult();
|
||||
for ( r in apiResult){
|
||||
var v : tink.core.Outcome<sugoi.mail.IMailer.MailerSuccess,sugoi.mail.IMailer.MailerError> = null;
|
||||
|
||||
switch(r.status){
|
||||
case "sent" :
|
||||
v = Success(Sent);
|
||||
case "queued":
|
||||
v = Success(Queued);
|
||||
default:
|
||||
//"hard-bounce", "soft-bounce", "spam", "unsub", "custom", "invalid-sender", "invalid", "test-mode-limit", "unsigned", or "rule"
|
||||
switch(r.reject_reason){
|
||||
case "hard-bounce" :
|
||||
v = Failure(HardBounce);
|
||||
case "soft-bounce":
|
||||
v = Failure(SoftBounce);
|
||||
case "spam":
|
||||
v = Failure(Spam);
|
||||
case "unsub":
|
||||
v = Failure(Unsub);
|
||||
default :
|
||||
v = Failure(GenericError(new tink.core.Error(raw)));
|
||||
}
|
||||
}
|
||||
|
||||
map.set( r.email , v );
|
||||
}
|
||||
callback(map);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function curlRequest( method: String, url : String, ?headers : Dynamic, postData : String ) : Dynamic {
|
||||
var cParams = ["-X"+method,"--max-time","15"];
|
||||
for( k in Reflect.fields(headers) ){
|
||||
cParams.push("-H");
|
||||
cParams.push(k+": "+Reflect.field(headers,k));
|
||||
}
|
||||
cParams.push(url);
|
||||
if( postData != null ){
|
||||
cParams.push("-d");
|
||||
cParams.push(postData);
|
||||
}
|
||||
|
||||
var p = new sys.io.Process("curl", cParams);
|
||||
//var curlRq = "curl " + cParams.join(" ");
|
||||
|
||||
#if neko
|
||||
var str = neko.Lib.stringReference(p.stdout.readAll());
|
||||
#else
|
||||
var str = p.stdout.readAll().toString();
|
||||
#end
|
||||
|
||||
if (str == null || str == "") {
|
||||
str = neko.Lib.stringReference(p.stderr.readAll());
|
||||
}
|
||||
|
||||
p.exitCode();
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
typedef MandrillApiSendResult = Array <{
|
||||
email:String,
|
||||
status:String,
|
||||
_id:String,
|
||||
reject_reason:String,
|
||||
}>;
|
||||
@@ -0,0 +1,68 @@
|
||||
package sugoi.mail;
|
||||
import tink.core.Future;
|
||||
import tink.core.Noise;
|
||||
import sugoi.mail.IMailer;
|
||||
import smtpmailer.Address;
|
||||
|
||||
/**
|
||||
* Send emails thru SMTP by using ben merckx's library
|
||||
* @ref https://github.com/benmerckx/smtpmailer
|
||||
*/
|
||||
class SmtpMailer implements IMailer
|
||||
{
|
||||
var m : smtpmailer.SmtpMailer;
|
||||
|
||||
public function new(){}
|
||||
|
||||
public function init(?conf:{smtp_host:String,smtp_port:Int,smtp_user:String,smtp_pass:String}) :IMailer
|
||||
{
|
||||
m = new smtpmailer.SmtpMailer({
|
||||
host: conf.smtp_host,
|
||||
port: conf.smtp_port,
|
||||
auth: {
|
||||
username: conf.smtp_user,
|
||||
password: conf.smtp_pass
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public function send(e:sugoi.mail.IMail,?params:Dynamic,?callback:MailerResult->Void)
|
||||
{
|
||||
var surprise = m.send({
|
||||
subject: e.getSubject(),
|
||||
/*from: e.getSender().email,
|
||||
to: Lambda.array(Lambda.map(e.getRecipients(), function(x) return smtpmailer.Address.ofString(x.email) )),
|
||||
//headers : e.getHeaders(),*/
|
||||
from: new Address({address:e.getSender().email}),
|
||||
to: Lambda.array(Lambda.map(e.getRecipients(), function(x) return new Address({address:x.email}) )),
|
||||
headers : e.getHeaders(),
|
||||
content: {
|
||||
text: e.getTextBody(),
|
||||
html: e.getHtmlBody()
|
||||
}/*,
|
||||
attachments: []*/
|
||||
});
|
||||
|
||||
|
||||
if (callback != null){
|
||||
|
||||
surprise.handle(function(s){
|
||||
|
||||
var map = new MailerResult();
|
||||
|
||||
switch(s){
|
||||
case Success(_):
|
||||
map.set("*",Success(Sent));
|
||||
|
||||
case Failure(e):
|
||||
map.set("*",Failure(GenericError(e)));
|
||||
}
|
||||
|
||||
callback(map);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user