-module(sendr_smtp).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sendr_smtp.gleam").
-export([config/2, port/2, tls_mode/2, allow_invalid_certs/2, credentials/3, helo_host/2, deliver/2]).
-export_type([smtp_error/0, connection_error/0, tls_alert/0, posix_error/0, credentials/0, tls_mode/0, tls/0, smtp_config/0, connect_options/0, socket/0, connection/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type smtp_error() :: {connection_error, connection_error()} |
{protocol_error, internal@protocol:protocol_error()}.
-type connection_error() :: closed |
timeout |
{posix_error, posix_error()} |
ssl_not_started |
{tls_alert, tls_alert(), binary()}.
-type tls_alert() :: close_notify |
unexpected_message |
bad_record_mac |
record_overflow |
handshake_failure |
bad_certificate |
unsupported_certificate |
certificate_revoked |
certificate_expired |
certificate_unknown |
illegal_parameter |
unknown_ca |
access_denied |
decode_error |
decrypt_error |
export_restriction |
protocol_version |
insufficient_security |
internal_error |
inappropriate_fallback |
user_canceled |
no_renegotiation |
unsupported_extension |
certificate_unobtainable |
unrecognized_name |
bad_certificate_status_response |
bad_certificate_hash_value |
unknown_psk_identity |
no_application_protocol.
-type posix_error() :: eaddrinuse |
eaddrnotavail |
eafnosupport |
ealready |
econnaborted |
econnrefused |
econnreset |
edestaddrreq |
ehostdown |
ehostunreach |
einprogress |
eisconn |
emsgsize |
enetdown |
enetreset |
enetunreach |
enopkg |
enoprotoopt |
enotconn |
enotty |
enotsock |
eproto |
eprotonosupport |
eprototype |
esocktnosupport |
etimedout |
ewouldblock |
exbadport |
exbadseq |
nxdomain |
eacces |
eagain |
ebadf |
ebadmsg |
ebusy |
edeadlk |
edeadlock |
edquot |
eexist |
efault |
efbig |
eftype |
eintr |
einval |
eio |
eisdir |
eloop |
emfile |
emlink |
emultihop |
enametoolong |
enfile |
enobufs |
enodev |
enolck |
enolink |
enoent |
enomem |
enospc |
enosr |
enostr |
enosys |
enotblk |
enotdir |
enotsup |
enxio |
eopnotsupp |
eoverflow |
eperm |
epipe |
erange |
erofs |
eshutdown |
espipe |
esrch |
estale |
etxtbsy |
exdev.
-type credentials() :: {credentials, binary(), binary()}.
-type tls_mode() :: implicit_tls | start_tls.
-type tls() :: {tls, tls_mode(), boolean()}.
-type smtp_config() :: {smtp_config,
binary(),
integer(),
tls(),
integer(),
gleam@option:option(binary()),
gleam@option:option(credentials())}.
-type connect_options() :: {connect_options,
binary(),
integer(),
tls(),
integer()}.
-type socket() :: any().
-type connection() :: {tcp_connection, socket(), integer()} |
{ssl_connection, socket(), integer()}.
-file("src/sendr_smtp.gleam", 227).
?DOC(
" Create a default `SmtpConfig` for the given hostname and port.\n"
"\n"
" Automatically sets TLS mode based on the port:\n"
" - Port 465: `ImplicitTls` with `allow_invalid_certs: False`\n"
" - Other ports: `StartTls` with `allow_invalid_certs: True`\n"
" Default timeout is 10 seconds.\n"
"\n"
" - `hostname`: The SMTP server hostname.\n"
" - `port`: The SMTP server port.\n"
"\n"
" Returns a new `SmtpConfig`.\n"
).
-spec config(binary(), integer()) -> smtp_config().
config(Hostname, Port) ->
Tls = case Port of
465 ->
{tls, implicit_tls, false};
_ ->
{tls, start_tls, true}
end,
{smtp_config, Hostname, Port, Tls, 10000, none, none}.
-file("src/sendr_smtp.gleam", 244).
?DOC(" Set the SMTP server port on the config.\n").
-spec port(smtp_config(), integer()) -> smtp_config().
port(Config, Port) ->
{smtp_config,
erlang:element(2, Config),
Port,
erlang:element(4, Config),
erlang:element(5, Config),
erlang:element(6, Config),
erlang:element(7, Config)}.
-file("src/sendr_smtp.gleam", 249).
?DOC(" Set the TLS mode on the config.\n").
-spec tls_mode(smtp_config(), tls_mode()) -> smtp_config().
tls_mode(Config, Tls_mode) ->
{smtp_config,
erlang:element(2, Config),
erlang:element(3, Config),
{tls, Tls_mode, erlang:element(3, erlang:element(4, Config))},
erlang:element(5, Config),
erlang:element(6, Config),
erlang:element(7, Config)}.
-file("src/sendr_smtp.gleam", 257).
?DOC(" Set whether to allow invalid TLS certificates.\n").
-spec allow_invalid_certs(smtp_config(), boolean()) -> smtp_config().
allow_invalid_certs(Config, Allow_invalid_certs) ->
{smtp_config,
erlang:element(2, Config),
erlang:element(3, Config),
{tls, erlang:element(2, erlang:element(4, Config)), Allow_invalid_certs},
erlang:element(5, Config),
erlang:element(6, Config),
erlang:element(7, Config)}.
-file("src/sendr_smtp.gleam", 265).
?DOC(" Set the SMTP authentication credentials on the config.\n").
-spec credentials(smtp_config(), binary(), binary()) -> smtp_config().
credentials(Config, Username, Password) ->
{smtp_config,
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
erlang:element(6, Config),
{some, {credentials, Username, Password}}}.
-file("src/sendr_smtp.gleam", 274).
?DOC(" Set a custom hostname for the EHLO/HELO command.\n").
-spec helo_host(smtp_config(), binary()) -> smtp_config().
helo_host(Config, Helo_host) ->
{smtp_config,
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config),
{some, Helo_host},
erlang:element(7, Config)}.
-file("src/sendr_smtp.gleam", 461).
-spec disconnect(connection()) -> nil.
disconnect(Connection) ->
case Connection of
{tcp_connection, Socket, _} ->
sendr_smtp_ffi:tcp_close(Socket);
{ssl_connection, Socket@1, _} ->
sendr_smtp_ffi:ssl_close(Socket@1)
end.
-file("src/sendr_smtp.gleam", 484).
-spec upgrade(connection(), connect_options()) -> {ok, connection()} |
{error, smtp_error()}.
upgrade(Connection, Options) ->
case Connection of
{tcp_connection, Socket, Timeout_ms} ->
_pipe = sendr_smtp_ffi:upgrade(
Socket,
erlang:element(2, Options),
erlang:element(3, erlang:element(4, Options)),
erlang:element(5, Options)
),
_pipe@1 = gleam@result:map(
_pipe,
fun(_capture) -> {ssl_connection, _capture, Timeout_ms} end
),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {connection_error, Field@0} end
);
{ssl_connection, _, _} ->
{ok, Connection}
end.
-file("src/sendr_smtp.gleam", 476).
-spec send(connection(), binary()) -> {ok, nil} | {error, smtp_error()}.
send(Connection, Command) ->
_pipe = case Connection of
{tcp_connection, Socket, _} ->
sendr_smtp_ffi:tcp_send(Socket, <<Command/binary>>);
{ssl_connection, Socket@1, _} ->
sendr_smtp_ffi:ssl_send(Socket@1, <<Command/binary>>)
end,
gleam@result:map_error(
_pipe,
fun(Field@0) -> {connection_error, Field@0} end
).
-file("src/sendr_smtp.gleam", 434).
-spec handle_response(
binary(),
fun((binary()) -> {ok, IDM} | {error, internal@protocol:protocol_error()})
) -> {ok, IDM} | {error, smtp_error()}.
handle_response(Response, Callback) ->
_pipe = Response,
_pipe@1 = Callback(_pipe),
gleam@result:map_error(
_pipe@1,
fun(Field@0) -> {protocol_error, Field@0} end
).
-file("src/sendr_smtp.gleam", 468).
-spec 'receive'(connection()) -> {ok, binary()} | {error, smtp_error()}.
'receive'(Connection) ->
_pipe = case Connection of
{tcp_connection, Socket, Timeout_ms} ->
sendr_smtp_ffi:tcp_receive(Socket, Timeout_ms);
{ssl_connection, Socket@1, Timeout_ms@1} ->
sendr_smtp_ffi:ssl_receive(Socket@1, Timeout_ms@1)
end,
gleam@result:map_error(
_pipe,
fun(Field@0) -> {connection_error, Field@0} end
).
-file("src/sendr_smtp.gleam", 409).
-spec smtp_session(
connection(),
connect_options(),
{ok, internal@protocol:action()} |
{error, internal@protocol:protocol_error()}
) -> {ok, nil} | {error, smtp_error()}.
smtp_session(Connection, Options, Action) ->
case Action of
{ok, {'receive', Callback}} ->
_pipe = Connection,
_pipe@1 = 'receive'(_pipe),
_pipe@2 = gleam@result:'try'(
_pipe@1,
fun(_capture) -> handle_response(_capture, Callback) end
),
gleam@result:'try'(
_pipe@2,
fun(R) -> smtp_session(Connection, Options, {ok, R}) end
);
{ok, {send, Command, Callback@1}} ->
_pipe@3 = Connection,
_pipe@4 = send(_pipe@3, Command),
_pipe@5 = gleam@result:map(_pipe@4, fun(_) -> Callback@1() end),
gleam@result:'try'(
_pipe@5,
fun(_capture@1) ->
smtp_session(Connection, Options, _capture@1)
end
);
{ok, {upgrade, Callback@2}} ->
_pipe@6 = Connection,
_pipe@7 = upgrade(_pipe@6, Options),
gleam@result:'try'(
_pipe@7,
fun(_capture@2) ->
smtp_session(_capture@2, Options, Callback@2())
end
);
{ok, done} ->
{ok, nil};
{error, Error} ->
{error, {protocol_error, Error}}
end.
-file("src/sendr_smtp.gleam", 443).
-spec connect(connect_options()) -> {ok, connection()} | {error, smtp_error()}.
connect(Options) ->
_pipe@2 = case erlang:element(2, erlang:element(4, Options)) of
implicit_tls ->
_pipe = sendr_smtp_ffi:ssl_connect(
erlang:element(2, Options),
erlang:element(3, Options),
erlang:element(3, erlang:element(4, Options)),
erlang:element(5, Options)
),
gleam@result:map(
_pipe,
fun(_capture) ->
{ssl_connection, _capture, erlang:element(5, Options)}
end
);
start_tls ->
_pipe@1 = sendr_smtp_ffi:tcp_connect(
erlang:element(2, Options),
erlang:element(3, Options),
erlang:element(5, Options)
),
gleam@result:map(
_pipe@1,
fun(_capture@1) ->
{tcp_connection, _capture@1, erlang:element(5, Options)}
end
)
end,
gleam@result:map_error(
_pipe@2,
fun(Field@0) -> {connection_error, Field@0} end
).
-file("src/sendr_smtp.gleam", 403).
-spec fqdn() -> binary().
fqdn() ->
_pipe = sendr_smtp_ffi:get_hostname(),
_pipe@1 = sendr_smtp_ffi:get_host_by_name(_pipe),
gleam@result:unwrap(_pipe@1, <<"localhost"/utf8>>).
-file("src/sendr_smtp.gleam", 381).
-spec do_deliver(sendr@message:message(), smtp_config()) -> {ok, nil} |
{error, smtp_error()}.
do_deliver(Message, Config) ->
Connection_options = {connect_options,
erlang:element(2, Config),
erlang:element(3, Config),
erlang:element(4, Config),
erlang:element(5, Config)},
Protocol_config = {protocol_config,
gleam@option:lazy_unwrap(erlang:element(6, Config), fun fqdn/0),
gleam@option:map(
erlang:element(7, Config),
fun(Credentials) ->
{erlang:element(2, Credentials), erlang:element(3, Credentials)}
end
)},
gleam@result:'try'(
connect(Connection_options),
fun(Connection) ->
Result = smtp_session(
Connection,
Connection_options,
internal@protocol:start_session(Message, Protocol_config)
),
disconnect(Connection),
Result
end
).
-file("src/sendr_smtp.gleam", 374).
-spec validate_body(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_body(Message) ->
case {erlang:element(2, erlang:element(9, Message)),
erlang:element(3, erlang:element(9, Message))} of
{none, none} ->
{error, {invalid_body, no_body}};
{_, _} ->
{ok, nil}
end.
-file("src/sendr_smtp.gleam", 360).
-spec validate_attachments(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_attachments(Message) ->
gleam@list:try_each(
erlang:element(8, Message),
fun(Attachment) ->
case {erlang:element(3, Attachment), erlang:element(4, Attachment)} of
{<<""/utf8>>, <<""/utf8>>} ->
{error,
{invalid_attachment,
required_filename_missing,
Attachment}};
{_, _} ->
case Attachment of
{inlined_attachment, _, _, _, _, <<""/utf8>>} ->
{error,
{invalid_attachment,
required_content_id_missing,
Attachment}};
_ ->
{ok, nil}
end
end
end
).
-file("src/sendr_smtp.gleam", 353).
-spec validate_subject(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_subject(Message) ->
case erlang:element(7, Message) of
none ->
{error, {required_field_missing, subject}};
{some, <<""/utf8>>} ->
{error, {required_field_missing, subject}};
_ ->
{ok, nil}
end.
-file("src/sendr_smtp.gleam", 343).
-spec validate_mailbox(sendr@message@mailbox:mailbox(), sendr:field()) -> {ok,
nil} |
{error, sendr:sendr_error(any())}.
validate_mailbox(Mailbox, Field) ->
case gleam@string:split_once(erlang:element(3, Mailbox), <<"@"/utf8>>) of
{ok, {Local, Domain}} when (Local =/= <<""/utf8>>) andalso (Domain =/= <<""/utf8>>) ->
{ok, nil};
_ ->
{error, {invalid_mailbox, Field, Mailbox}}
end.
-file("src/sendr_smtp.gleam", 322).
-spec validate_recipients(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_recipients(Message) ->
Recipients = begin
_pipe = [erlang:element(4, Message),
erlang:element(5, Message),
erlang:element(6, Message)],
_pipe@1 = gleam@option:values(_pipe),
lists:append(_pipe@1)
end,
Validate = fun(Mailboxes, Field) -> _pipe@2 = Mailboxes,
_pipe@3 = gleam@option:unwrap(_pipe@2, []),
gleam@list:try_each(
_pipe@3,
fun(_capture) -> validate_mailbox(_capture, Field) end
) end,
case Recipients of
[] ->
{error, no_recipients};
_ ->
_pipe@4 = Validate(erlang:element(6, Message), bcc),
_pipe@5 = gleam@result:'or'(
_pipe@4,
Validate(erlang:element(5, Message), cc)
),
gleam@result:'or'(_pipe@5, Validate(erlang:element(4, Message), to))
end.
-file("src/sendr_smtp.gleam", 313).
-spec validate_reply_to(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_reply_to(Message) ->
case erlang:element(3, Message) of
none ->
{ok, nil};
{some, []} ->
{ok, nil};
{some, Mailboxes} ->
_pipe = gleam@list:try_each(
Mailboxes,
fun(_capture) -> validate_mailbox(_capture, reply_to) end
),
gleam@result:replace(_pipe, nil)
end.
-file("src/sendr_smtp.gleam", 306).
-spec validate_from(sendr@message:message()) -> {ok, nil} |
{error, sendr:sendr_error(any())}.
validate_from(Message) ->
case erlang:element(2, Message) of
none ->
{error, {required_field_missing, from}};
{some, Mailbox} ->
validate_mailbox(Mailbox, from)
end.
-file("src/sendr_smtp.gleam", 291).
?DOC(
" Deliver an email message via SMTP.\n"
"\n"
" Validates the message fields upfront, then connects to the SMTP server,\n"
" performs the SMTP protocol session, and disconnects.\n"
"\n"
" - `message`: The `sendr/message.Message` to deliver.\n"
" - `config`: The `SmtpConfig` for the connection.\n"
"\n"
" Returns `Ok(Nil)` on success, or `Error(SendrError(SmtpError))` if\n"
" validation or delivery fails.\n"
).
-spec deliver(sendr@message:message(), smtp_config()) -> {ok, nil} |
{error, sendr:sendr_error(smtp_error())}.
deliver(Message, Config) ->
gleam@result:'try'(
validate_from(Message),
fun(_) ->
gleam@result:'try'(
validate_reply_to(Message),
fun(_) ->
gleam@result:'try'(
validate_recipients(Message),
fun(_) ->
gleam@result:'try'(
validate_subject(Message),
fun(_) ->
gleam@result:'try'(
validate_attachments(Message),
fun(_) ->
gleam@result:'try'(
validate_body(Message),
fun(_) ->
_pipe = do_deliver(
Message,
Config
),
gleam@result:map_error(
_pipe,
fun(E) ->
{backend_error, E}
end
)
end
)
end
)
end
)
end
)
end
)
end
).