-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, <>); {ssl_connection, Socket@1, _} -> sendr_smtp_ffi:ssl_send(Socket@1, <>) 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 ).