%% vim: ts=4 sw=4 sts=4 et
-module(sparkler).
-behaviour(gen_server).
%-compile(export_all).
-define(TIMEOUT,get_env(timeout, 300)).
-define(API_KEY, get_env(api_key, "")).
-define(API_PREFIX,get_env(api_prefix, "https://api.sparkpost.com/api/v1/")).
-define(HTTP_OPTIONS, []).
-define(OPTIONS, [{full_result,false}]).
-export([
start_link/0,
init/1,
stop/0,
reset/0,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
status/0,
code_change/3
]).
-export([
send_text/4,
send_text/5,
send_html/4,
send_html/5,
send/4,
send/5,
send/6
]).
-export([fix_floating_linefeeds/1]).
-record(mail,{from,to,subject,text,html,headers}).
-record(data,{queue}).
get_env(Key, Default) ->
case application:get_env(sparkler, Key) of
undefined -> Default;
{ok, Val} -> Val
end.
send_text(From, To, Subject, Text) ->
send_text(From, To, Subject, Text, []).
send_text(From, To, Subject, Text, Headers) ->
send(From, To, Subject, Text, "", Headers).
send_html(From, To, Subject, Html) ->
send_html(From, To, Subject, Html, []).
send_html(From, To, Subject, Html, Headers) ->
send(From, To, Subject, "", Html, Headers).
send(From,To,Subject,Text,Html,Headers) ->
Mail = #mail{
to=To,
from=From,
subject=Subject,
text=Text,
html=Html,
headers=Headers
},
gen_server:call(?MODULE,{queue,Mail}).
%% kept for backwards compatibitlity only
send(From,To,Subject,Text) ->
send(From,To,Subject,Text,[],[]).
send(From,To,Subject,Text,Html) ->
send(From,To,Subject,Text,Html,[]).
start_link() ->
gen_server:start_link({local,?MODULE},?MODULE,#data{},[]).
stop() ->
gen_server:call(?MODULE,stop).
reset() ->
gen_server:call(?MODULE,reset).
status() ->
gen_server:call(?MODULE,status).
terminate(_Reason,_Data) ->
ok.
init(Data) ->
{ok,Data#data{queue=queue:new()},?TIMEOUT}.
handle_call({queue,Mail},_From,#data{queue=Queue} = Data) ->
NewQueue = queue:in(Mail,Queue),
NewLen = queue:len(NewQueue),
{reply,{queued,NewLen},Data#data{queue=NewQueue},?TIMEOUT};
handle_call(reset,_From,Data) ->
{reply,ok,Data#data{queue=queue:new()},?TIMEOUT};
handle_call(status,_From,#data{queue=Queue} = Data) ->
Len = queue:len(Queue),
Status = [
{queue_size,Len}
],
{reply,Status,Data,?TIMEOUT};
handle_call(stop,_From,Data) ->
{stop,stopped,Data}.
handle_info(timeout,#data{queue=Queue} = Data) ->
NewQueue = case queue:out(Queue) of
{{value,Mail},NQ} ->
#mail{
from=From,
to=To,
subject=Subject,
text=Text,
html=Html,
headers=Headers
} = Mail,
spawn(fun() ->
try
int_send(?API_KEY,?API_PREFIX,From,To,Subject,Text,Html,Headers)
catch E:T:S ->
error_logger:error_msg("~p:~p~n~p~n",[E,T,S])
end
end),
NQ;
{empty,Queue} ->
Queue
end,
{noreply,Data#data{queue=NewQueue},?TIMEOUT}.
handle_cast(_Msg, State) ->
{noreply, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Headers is a proplist of {header,value}
int_send(0,APIKey,Prefix,From,To,Sub,_Text,_Html,_Headers) ->
error_logger:error_report([
failed_send,
{api_key,APIKey},
{prefix,Prefix},
{from,From},
{to,To},
{subject,Sub}
]);
int_send(TryNum,_APIKey,Prefix,From,To,Subject,Text,Html,Headers) when TryNum > 0 ->
URL = Prefix ++ "transmissions",
EncodedJson = make_json(From, To, Subject, Text, Html, Headers),
Body = iolist_to_binary(EncodedJson),
error_logger:info_msg("Sending: ~nAPI KEY: ~s~nURL: ~s~nMessage: ~s~n",[?API_KEY, URL, Body]),
case ibrowse:send_req(URL,[{authorization, ?API_KEY}],post,Body,[{content_type,"application/json"}]) of
{ok, _, _, Result} ->
error_logger:info_msg("Sent: ~p~n",[Result]),
do_nothing;
{error, Reason} ->
error_logger:info_msg("Error In Send: ~p~n",[Reason])
end.
int_send(Server,Port,From,To,Subject,Text,Html,Headers) ->
int_send(5,Server,Port,From,To,Subject,Text,Html,Headers).
make_json(From, To, Subject, Text, Html, Headers) ->
{_Headers2, ReplyTo} = extract_reply_to(Headers),
ReplyToOption = case ReplyTo of
undefined -> [];
_ -> [{reply_to, i2b(ReplyTo)}]
end,
HtmlOption = case Html of
[] -> [];
_ -> [{html, i2b(Html)}]
end,
{FromName, FromEmail} = normalize_email_name(From),
{ToName, ToEmail} = normalize_email_name(To),
Proplist = [
{content, [
{text, i2b(Text)},
{subject, i2b(Subject)},
{from, [{email, i2b(FromEmail)}, {name, i2b(FromName)}]}
] ++ ReplyToOption ++ HtmlOption},
{recipients,[
[{address, [{email,i2b(ToEmail)},{name,i2b(ToName)}]}]
]},
{options, [{click_tracking, false}]}
],
jsx:encode(Proplist).
extract_reply_to(Headers) ->
lists:foldl(fun({K,V}, {HeaderAcc, ReplyTo}) ->
case normalized_header(K) == "replyto" of
true -> {HeaderAcc, V};
false -> {[{K,V}|HeaderAcc], ReplyTo}
end
end, {[], undefined}, Headers).
normalized_header(H) when is_atom(H) ->
normalized_header(atom_to_list(H));
normalized_header(H) when is_list(H) ->
H2 = re:replace(H, "[^a-zA-Z0-9]", "", [{return, list}]),
string:to_lower(H2).
normalize_email_name({FromName, FromEmail}) ->
{FromName, FromEmail};
normalize_email_name(EmailString) ->
case re:run(EmailString, "^\"(.*)\"\s*<(.*@.*)>$", [{capture, all_but_first, list}]) of
{match, [Name, Email]} -> {Email, Name};
nomatch -> {EmailString, EmailString}
end.
fix_floating_linefeeds([]) ->
[];
fix_floating_linefeeds([13,10 | Text]) ->
[13,10 | fix_floating_linefeeds(Text)];
fix_floating_linefeeds([13 | Text]) ->
[13,10 | fix_floating_linefeeds(Text)];
fix_floating_linefeeds([10 | Text]) ->
[13,10 | fix_floating_linefeeds(Text)];
fix_floating_linefeeds([H | Text]) ->
[H | fix_floating_linefeeds(Text)].
i2b(IO) ->
unicode:characters_to_binary(IO).