feat(email): SMTP via gen_smtp and CalenTIQ HTML templates. Refs EventHub/EventHubBack#68
This commit is contained in:
@@ -19,3 +19,13 @@ ADMIN_MODER_EMAIL=moderator@eventhub.local
|
|||||||
ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора
|
ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора
|
||||||
ADMIN_SUPPORT_EMAIL=support@eventhub.local
|
ADMIN_SUPPORT_EMAIL=support@eventhub.local
|
||||||
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки
|
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки
|
||||||
|
|
||||||
|
# SMTP (пусто SMTP_HOST = только лог; пароль не коммитить)
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASS=
|
||||||
|
SMTP_FROM=noreply@calentiq.com
|
||||||
|
SMTP_TLS=if_available
|
||||||
|
PUBLIC_APP_URL=https://stage.calentiq.com
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ RUN rebar3 get-deps
|
|||||||
|
|
||||||
COPY include/ include/
|
COPY include/ include/
|
||||||
COPY src/ src/
|
COPY src/ src/
|
||||||
|
COPY priv/ priv/
|
||||||
|
|
||||||
COPY src/config/sys.config ./config/sys.config
|
COPY src/config/sys.config ./config/sys.config
|
||||||
COPY src/config/vm.args ./config/vm.args
|
COPY src/config/vm.args ./config/vm.args
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ services:
|
|||||||
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
|
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
|
||||||
- CLUSTER_MODE=true
|
- CLUSTER_MODE=true
|
||||||
- DNS_NAME=eventhub-node
|
- DNS_NAME=eventhub-node
|
||||||
|
- SMTP_HOST=${SMTP_HOST:-}
|
||||||
|
- SMTP_PORT=${SMTP_PORT:-587}
|
||||||
|
- SMTP_USER=${SMTP_USER:-}
|
||||||
|
- SMTP_PASS=${SMTP_PASS:-}
|
||||||
|
- SMTP_FROM=${SMTP_FROM:-noreply@calentiq.com}
|
||||||
|
- SMTP_TLS=${SMTP_TLS:-if_available}
|
||||||
|
- PUBLIC_APP_URL=${PUBLIC_APP_URL:-https://stage.calentiq.com}
|
||||||
networks:
|
networks:
|
||||||
eventhub-net:
|
eventhub-net:
|
||||||
aliases:
|
aliases:
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>{{heading}}</title>
|
||||||
|
</head>
|
||||||
|
<body style="margin:0;padding:0;background:#F4F1EA;">
|
||||||
|
<span style="display:none!important;visibility:hidden;opacity:0;height:0;width:0;overflow:hidden;">{{preheader}}</span>
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#F4F1EA;padding:32px 12px;">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;background:#FFFFFF;border-radius:16px;overflow:hidden;border:1px solid #E4E0D6;">
|
||||||
|
<tr>
|
||||||
|
<td style="background:#324A5F;padding:22px 28px;">
|
||||||
|
<p style="margin:0;font-family:Georgia,'Times New Roman',serif;font-size:22px;letter-spacing:0.04em;color:#FFFFFF;">CalenTIQ</p>
|
||||||
|
<p style="margin:6px 0 0;font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#ABC5B5;">One calendar. Three modes for your time.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="height:4px;background:#2F7A68;font-size:0;line-height:0;"> </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:28px 28px 8px;font-family:Arial,Helvetica,sans-serif;color:#1A1A1A;">
|
||||||
|
<h1 style="margin:0 0 12px;font-size:22px;line-height:1.3;font-weight:700;color:#324A5F;">{{heading}}</h1>
|
||||||
|
<p style="margin:0 0 20px;font-size:15px;line-height:1.55;color:#3A3A3A;">{{lead}}</p>
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius:10px;background:#FF6A3D;">
|
||||||
|
<a href="{{cta_url}}" style="display:inline-block;padding:12px 22px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:700;color:#FFFFFF;text-decoration:none;">{{cta_label}}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<p style="margin:20px 0 0;font-size:12px;line-height:1.5;color:#6B6B6B;">Если кнопка не открывается, скопируйте ссылку:<br/>
|
||||||
|
<a href="{{cta_url}}" style="color:#2F7A68;word-break:break-all;">{{cta_url}}</a>
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:8px 28px 28px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:1.5;color:#8A8A8A;">
|
||||||
|
{{footnote}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+3
-2
@@ -10,7 +10,8 @@
|
|||||||
{meck, "0.9.2"},
|
{meck, "0.9.2"},
|
||||||
{gun, "2.2.0"},
|
{gun, "2.2.0"},
|
||||||
{prometheus_cowboy, "0.2.0"},
|
{prometheus_cowboy, "0.2.0"},
|
||||||
{cowboy_swagger, "2.8.0"}
|
{cowboy_swagger, "2.8.0"},
|
||||||
|
{gen_smtp, "1.3.0"}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
{shell, [
|
{shell, [
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
{profiles, [
|
{profiles, [
|
||||||
{prod, [
|
{prod, [
|
||||||
{relx, [
|
{relx, [
|
||||||
{release, {eventhub, "0.0.1"}, [eventhub, sasl,cowboy, jose, jsx, argon2, runtime_tools, os_mon, prometheus_cowboy]},
|
{release, {eventhub, "0.0.1"}, [eventhub, sasl, cowboy, jose, jsx, argon2, gen_smtp, ssl, runtime_tools, os_mon, prometheus_cowboy]},
|
||||||
{include_erts, true},
|
{include_erts, true},
|
||||||
{extended_start_script, true},
|
{extended_start_script, true},
|
||||||
{sys_config, "./src/config/sys.config"}
|
{sys_config, "./src/config/sys.config"}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
cowboy,
|
cowboy,
|
||||||
jsx,
|
jsx,
|
||||||
trails,
|
trails,
|
||||||
cowboy_swagger
|
cowboy_swagger,
|
||||||
|
ssl,
|
||||||
|
gen_smtp
|
||||||
]},
|
]},
|
||||||
{env, [
|
{env, [
|
||||||
{http_port, 8080},
|
{http_port, 8080},
|
||||||
|
|||||||
+149
-3
@@ -1,11 +1,157 @@
|
|||||||
-module(logic_email).
|
-module(logic_email).
|
||||||
-export([send_verification_email/2, send_specialist_invite/2, send_password_reset/2]).
|
-export([send_verification_email/2, send_specialist_invite/2, send_password_reset/2]).
|
||||||
|
|
||||||
|
%% SMTP via env (variant A / Back#68):
|
||||||
|
%% SMTP_HOST empty → log-only (local/tests)
|
||||||
|
%% SMTP_PORT (default 587), SMTP_USER, SMTP_PASS, SMTP_FROM, SMTP_TLS
|
||||||
|
%% PUBLIC_APP_URL — base for deep links (default https://stage.calentiq.com)
|
||||||
|
%% Tests: application:set_env(eventhub, smtp_deliver, Fun)
|
||||||
|
|
||||||
|
-spec send_verification_email(binary(), binary()) -> ok.
|
||||||
send_verification_email(Email, Token) ->
|
send_verification_email(Email, Token) ->
|
||||||
io:format("Sending verification email to ~s with token ~s~n", [Email, Token]).
|
send_kind(verify, Email, abs_url(<<"/verify?token=">>, Token)).
|
||||||
|
|
||||||
|
-spec send_specialist_invite(binary(), binary()) -> ok.
|
||||||
send_specialist_invite(Email, Token) ->
|
send_specialist_invite(Email, Token) ->
|
||||||
io:format("Sending specialist invite email to ~s with token ~s~n", [Email, Token]).
|
send_kind(invite, Email, abs_url(<<"/invites?token=">>, Token)).
|
||||||
|
|
||||||
|
-spec send_password_reset(binary(), binary()) -> ok.
|
||||||
send_password_reset(Email, Token) ->
|
send_password_reset(Email, Token) ->
|
||||||
io:format("Sending password reset email to ~s with token ~s~n", [Email, Token]).
|
send_kind(reset, Email, abs_url(<<"/reset-password?token=">>, Token)).
|
||||||
|
|
||||||
|
send_kind(Kind, To0, Url) ->
|
||||||
|
{Subject, Plain, Html} = logic_email_templates:render(Kind, Url),
|
||||||
|
send(to_bin(To0), Subject, Plain, Html).
|
||||||
|
|
||||||
|
send(To, Subject, Plain, Html) ->
|
||||||
|
case smtp_relay() of
|
||||||
|
undefined ->
|
||||||
|
logger:info(#{what => email_log_only, to => To, subject => Subject}),
|
||||||
|
ok;
|
||||||
|
Relay ->
|
||||||
|
From = from_addr(),
|
||||||
|
Raw = rfc822(From, To, Subject, Plain, Html),
|
||||||
|
Opts = smtp_opts(Relay),
|
||||||
|
safe_deliver(From, To, Raw, Opts)
|
||||||
|
end.
|
||||||
|
|
||||||
|
safe_deliver(From, To, Raw, Opts) ->
|
||||||
|
Fun = deliver_fun(),
|
||||||
|
try Fun(From, To, Raw, Opts) of
|
||||||
|
{ok, _} -> ok;
|
||||||
|
ok -> ok;
|
||||||
|
{error, Reason} ->
|
||||||
|
logger:warning(#{what => smtp_send_error, to => To, reason => Reason}),
|
||||||
|
ok;
|
||||||
|
Other ->
|
||||||
|
logger:warning(#{what => smtp_send_unexpected, to => To, result => Other}),
|
||||||
|
ok
|
||||||
|
catch
|
||||||
|
Class:Reason:Stack ->
|
||||||
|
logger:warning(#{what => smtp_send_crash, to => To,
|
||||||
|
class => Class, reason => Reason, stack => Stack}),
|
||||||
|
ok
|
||||||
|
end.
|
||||||
|
|
||||||
|
deliver_fun() ->
|
||||||
|
case application:get_env(eventhub, smtp_deliver) of
|
||||||
|
{ok, Fun} when is_function(Fun, 4) -> Fun;
|
||||||
|
_ -> fun default_deliver/4
|
||||||
|
end.
|
||||||
|
|
||||||
|
default_deliver(From, To, Raw, Opts) ->
|
||||||
|
gen_smtp_client:send({From, [To], Raw}, Opts).
|
||||||
|
|
||||||
|
smtp_relay() ->
|
||||||
|
case app_or_env(smtp_host, "SMTP_HOST") of
|
||||||
|
<<>> -> undefined;
|
||||||
|
Host -> binary_to_list(Host)
|
||||||
|
end.
|
||||||
|
|
||||||
|
smtp_opts(Relay) ->
|
||||||
|
Port = env_int("SMTP_PORT", 587),
|
||||||
|
Tls = tls_mode(env_bin("SMTP_TLS", <<"if_available">>)),
|
||||||
|
Base = [
|
||||||
|
{relay, Relay},
|
||||||
|
{port, Port},
|
||||||
|
{tls, Tls},
|
||||||
|
{hostname, "calentiq.com"}
|
||||||
|
],
|
||||||
|
User = env_bin("SMTP_USER", <<>>),
|
||||||
|
Pass = env_bin("SMTP_PASS", <<>>),
|
||||||
|
case User of
|
||||||
|
<<>> -> Base;
|
||||||
|
_ ->
|
||||||
|
[{username, binary_to_list(User)},
|
||||||
|
{password, binary_to_list(Pass)},
|
||||||
|
{auth, always} | Base]
|
||||||
|
end.
|
||||||
|
|
||||||
|
tls_mode(<<"always">>) -> always;
|
||||||
|
tls_mode(<<"never">>) -> never;
|
||||||
|
tls_mode(_) -> if_available.
|
||||||
|
|
||||||
|
from_addr() ->
|
||||||
|
env_bin("SMTP_FROM", <<"noreply@calentiq.com">>).
|
||||||
|
|
||||||
|
abs_url(PathPrefix, Token) ->
|
||||||
|
Base = strip_slash(env_bin("PUBLIC_APP_URL", <<"https://stage.calentiq.com">>)),
|
||||||
|
TokenB = to_bin(Token),
|
||||||
|
<<Base/binary, PathPrefix/binary, TokenB/binary>>.
|
||||||
|
|
||||||
|
strip_slash(Url) ->
|
||||||
|
case binary:last(Url) of
|
||||||
|
$/ -> binary:part(Url, 0, byte_size(Url) - 1);
|
||||||
|
_ -> Url
|
||||||
|
end.
|
||||||
|
|
||||||
|
rfc822(From, To, Subject, Plain, Html) ->
|
||||||
|
Bound = <<"eh-alt-01">>,
|
||||||
|
iolist_to_binary([
|
||||||
|
<<"From: CalenTIQ <">>, From, <<">\r\n">>,
|
||||||
|
<<"To: ">>, To, <<"\r\n">>,
|
||||||
|
<<"Subject: ">>, encode_subject(Subject), <<"\r\n">>,
|
||||||
|
<<"MIME-Version: 1.0\r\n">>,
|
||||||
|
<<"Content-Type: multipart/alternative; boundary=\"">>, Bound, <<"\"\r\n">>,
|
||||||
|
<<"\r\n">>,
|
||||||
|
<<"--">>, Bound, <<"\r\n">>,
|
||||||
|
<<"Content-Type: text/plain; charset=utf-8\r\n">>,
|
||||||
|
<<"Content-Transfer-Encoding: 8bit\r\n\r\n">>,
|
||||||
|
Plain, <<"\r\n">>,
|
||||||
|
<<"--">>, Bound, <<"\r\n">>,
|
||||||
|
<<"Content-Type: text/html; charset=utf-8\r\n">>,
|
||||||
|
<<"Content-Transfer-Encoding: 8bit\r\n\r\n">>,
|
||||||
|
Html, <<"\r\n">>,
|
||||||
|
<<"--">>, Bound, <<"--\r\n">>
|
||||||
|
]).
|
||||||
|
|
||||||
|
app_or_env(AppKey, EnvName) ->
|
||||||
|
case application:get_env(eventhub, AppKey) of
|
||||||
|
{ok, Val} when Val =/= undefined, Val =/= <<>>, Val =/= "" -> to_bin(Val);
|
||||||
|
_ -> env_bin(EnvName, <<>>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
env_bin(Name, Default) ->
|
||||||
|
case os:getenv(Name) of
|
||||||
|
false -> Default;
|
||||||
|
"" -> Default;
|
||||||
|
S -> unicode:characters_to_binary(S)
|
||||||
|
end.
|
||||||
|
|
||||||
|
env_int(Name, Default) ->
|
||||||
|
case os:getenv(Name) of
|
||||||
|
false -> Default;
|
||||||
|
"" -> Default;
|
||||||
|
S ->
|
||||||
|
try list_to_integer(S) of
|
||||||
|
N -> N
|
||||||
|
catch
|
||||||
|
_:_ -> Default
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
to_bin(B) when is_binary(B) -> B;
|
||||||
|
to_bin(L) when is_list(L) -> unicode:characters_to_binary(L).
|
||||||
|
|
||||||
|
encode_subject(Bin) ->
|
||||||
|
<<"=?UTF-8?B?", (base64:encode(Bin))/binary, "?=">>.
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
-module(logic_email_templates).
|
||||||
|
-export([render/2]).
|
||||||
|
|
||||||
|
%% Kind: verify | invite | reset
|
||||||
|
-spec render(verify | invite | reset, binary()) ->
|
||||||
|
{Subject :: binary(), Plain :: binary(), Html :: binary()}.
|
||||||
|
render(Kind, Url) ->
|
||||||
|
{Subject, Heading, Lead, Cta, Foot, Pre} = copy(Kind),
|
||||||
|
Plain = iolist_to_binary([Heading, <<"\n\n">>, Lead, <<"\n\n">>, Url, <<"\n">>]),
|
||||||
|
Html = fill(layout(), [
|
||||||
|
{<<"{{preheader}}">>, Pre},
|
||||||
|
{<<"{{heading}}">>, Heading},
|
||||||
|
{<<"{{lead}}">>, Lead},
|
||||||
|
{<<"{{cta_url}}">>, Url},
|
||||||
|
{<<"{{cta_label}}">>, Cta},
|
||||||
|
{<<"{{footnote}}">>, Foot}
|
||||||
|
]),
|
||||||
|
{Subject, Plain, Html}.
|
||||||
|
|
||||||
|
copy(verify) ->
|
||||||
|
{<<"CalenTIQ: подтвердите email">>,
|
||||||
|
<<"Подтвердите email">>,
|
||||||
|
<<"Один календарь. Три режима для вашего времени. Нажмите кнопку, чтобы активировать аккаунт.">>,
|
||||||
|
<<"Подтвердить email">>,
|
||||||
|
<<"Если вы не регистрировались в CalenTIQ, просто проигнорируйте письмо.">>,
|
||||||
|
<<"Подтвердите email в CalenTIQ">>};
|
||||||
|
copy(invite) ->
|
||||||
|
{<<"CalenTIQ: приглашение специалиста">>,
|
||||||
|
<<"Вас пригласили как специалиста">>,
|
||||||
|
<<"Откройте приглашение, чтобы принять или отклонить его в CalenTIQ.">>,
|
||||||
|
<<"Открыть приглашение">>,
|
||||||
|
<<"Письмо отправлено, потому что владелец календаря указал этот адрес.">>,
|
||||||
|
<<"Приглашение специалиста в CalenTIQ">>};
|
||||||
|
copy(reset) ->
|
||||||
|
{<<"CalenTIQ: сброс пароля">>,
|
||||||
|
<<"Сброс пароля">>,
|
||||||
|
<<"Если это были вы — задайте новый пароль по кнопке. Ссылка одноразовая.">>,
|
||||||
|
<<"Сменить пароль">>,
|
||||||
|
<<"Если вы не запрашивали сброс, проигнорируйте письмо — пароль не изменится.">>,
|
||||||
|
<<"Сброс пароля CalenTIQ">>}.
|
||||||
|
|
||||||
|
layout() ->
|
||||||
|
case read_priv() of
|
||||||
|
{ok, Bin} -> Bin;
|
||||||
|
{error, _} -> builtin_layout()
|
||||||
|
end.
|
||||||
|
|
||||||
|
read_priv() ->
|
||||||
|
case code:priv_dir(eventhub) of
|
||||||
|
{error, _} = E -> E;
|
||||||
|
Dir ->
|
||||||
|
Path = filename:join([Dir, "email", "layout.html"]),
|
||||||
|
file:read_file(Path)
|
||||||
|
end.
|
||||||
|
|
||||||
|
fill(Html, []) -> Html;
|
||||||
|
fill(Html, [{Needle, Val} | Rest]) ->
|
||||||
|
fill(binary:replace(Html, Needle, Val, [global]), Rest).
|
||||||
|
|
||||||
|
builtin_layout() ->
|
||||||
|
<<"<!DOCTYPE html><html><body style=\"font-family:Arial,sans-serif;color:#324A5F\">"
|
||||||
|
"<p><b>CalenTIQ</b></p><h1>{{heading}}</h1><p>{{lead}}</p>"
|
||||||
|
"<p><a href=\"{{cta_url}}\" style=\"background:#FF6A3D;color:#fff\">{{cta_label}}</a></p>"
|
||||||
|
"<p>{{cta_url}}</p><p>{{footnote}}</p></body></html>">>.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
-module(logic_email_tests).
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
|
||||||
|
logic_email_test_() ->
|
||||||
|
{foreach, fun setup/0, fun cleanup/1, [
|
||||||
|
{"log-only without host", fun test_log_only/0},
|
||||||
|
{"mock deliver gets CalenTIQ + verify url", fun test_verify_deliver/0},
|
||||||
|
{"invite and reset urls", fun test_invite_reset/0},
|
||||||
|
{"deliver error still ok", fun test_deliver_error/0},
|
||||||
|
{"html template placeholders filled", fun test_template_fill/0}
|
||||||
|
]}.
|
||||||
|
|
||||||
|
setup() ->
|
||||||
|
application:unset_env(eventhub, smtp_host),
|
||||||
|
application:unset_env(eventhub, smtp_deliver),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
cleanup(_) ->
|
||||||
|
application:unset_env(eventhub, smtp_host),
|
||||||
|
application:unset_env(eventhub, smtp_deliver),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
test_log_only() ->
|
||||||
|
application:unset_env(eventhub, smtp_host),
|
||||||
|
?assertEqual(ok, logic_email:send_verification_email(<<"a@b.c">>, <<"tok">>)).
|
||||||
|
|
||||||
|
test_verify_deliver() ->
|
||||||
|
Self = self(),
|
||||||
|
application:set_env(eventhub, smtp_host, "smtp.test"),
|
||||||
|
application:set_env(eventhub, smtp_deliver,
|
||||||
|
fun(From, To, Raw, Opts) ->
|
||||||
|
Self ! {smtp, From, To, Raw, Opts},
|
||||||
|
{ok, mock}
|
||||||
|
end),
|
||||||
|
?assertEqual(ok, logic_email:send_verification_email(<<"user@ex.com">>, <<"abc123">>)),
|
||||||
|
receive
|
||||||
|
{smtp, From, To, Raw, Opts} ->
|
||||||
|
?assertEqual(<<"noreply@calentiq.com">>, From),
|
||||||
|
?assertEqual(<<"user@ex.com">>, To),
|
||||||
|
?assertEqual("smtp.test", proplists:get_value(relay, Opts)),
|
||||||
|
?assert(binary:match(Raw, <<"multipart/alternative">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Raw, <<"text/html">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Raw, <<"CalenTIQ">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Raw, <<"/verify?token=abc123">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Raw, <<"#FF6A3D">>) =/= nomatch)
|
||||||
|
after 1000 ->
|
||||||
|
error(timeout)
|
||||||
|
end.
|
||||||
|
|
||||||
|
test_invite_reset() ->
|
||||||
|
Self = self(),
|
||||||
|
application:set_env(eventhub, smtp_host, <<"smtp.test">>),
|
||||||
|
application:set_env(eventhub, smtp_deliver,
|
||||||
|
fun(_From, To, Raw, _Opts) ->
|
||||||
|
Self ! {smtp, To, Raw},
|
||||||
|
ok
|
||||||
|
end),
|
||||||
|
ok = logic_email:send_specialist_invite(<<"i@ex.com">>, <<"invtok">>),
|
||||||
|
receive
|
||||||
|
{smtp, <<"i@ex.com">>, Raw1} ->
|
||||||
|
?assert(binary:match(Raw1, <<"/invites?token=invtok">>) =/= nomatch)
|
||||||
|
after 1000 -> error(timeout)
|
||||||
|
end,
|
||||||
|
ok = logic_email:send_password_reset(<<"r@ex.com">>, <<"rst">>),
|
||||||
|
receive
|
||||||
|
{smtp, <<"r@ex.com">>, Raw2} ->
|
||||||
|
?assert(binary:match(Raw2, <<"/reset-password?token=rst">>) =/= nomatch)
|
||||||
|
after 1000 -> error(timeout)
|
||||||
|
end.
|
||||||
|
|
||||||
|
test_deliver_error() ->
|
||||||
|
application:set_env(eventhub, smtp_host, "smtp.test"),
|
||||||
|
application:set_env(eventhub, smtp_deliver, fun(_, _, _, _) -> {error, boom} end),
|
||||||
|
?assertEqual(ok, logic_email:send_verification_email(<<"a@b.c">>, <<"t">>)),
|
||||||
|
application:set_env(eventhub, smtp_deliver, fun(_, _, _, _) -> error(crash) end),
|
||||||
|
?assertEqual(ok, logic_email:send_password_reset(<<"a@b.c">>, <<"t">>)).
|
||||||
|
|
||||||
|
test_template_fill() ->
|
||||||
|
{Sub, Plain, Html} = logic_email_templates:render(verify, <<"https://x/verify?token=z">>),
|
||||||
|
?assert(binary:match(Sub, <<"CalenTIQ">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Plain, <<"https://x/verify?token=z">>) =/= nomatch),
|
||||||
|
?assert(binary:match(Html, <<"{{">>) =:= nomatch),
|
||||||
|
?assert(binary:match(Html, <<"https://x/verify?token=z">>) =/= nomatch).
|
||||||
Reference in New Issue
Block a user