Skip to main content

src/sendr_lettermint.erl

-module(sendr_lettermint).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/sendr_lettermint.gleam").
-export([config/1, set_route/2, request/2, response/1]).
-export_type([lettermint_error/0, lettermint_config/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.

?MODULEDOC(
    " sendr_lettermint — Lettermint email backend for sendr.\n"
    "\n"
    " Builds HTTP `Request` values for the Lettermint send API and parses\n"
    " `Response` values returned by it.\n"
).

-type lettermint_error() :: {invalid_uri, binary()} |
    {invalid_response, integer(), gleam@json:decode_error()} |
    {api_error, integer(), gleam@option:option(binary()), binary()}.

-opaque lettermint_config() :: {lettermint_config, binary(), binary()}.

-file("src/sendr_lettermint.gleam", 45).
?DOC(" Create a new `LettermintConfig` with the given API token.\n").
-spec config(binary()) -> lettermint_config().
config(Token) ->
    {lettermint_config, Token, <<""/utf8>>}.

-file("src/sendr_lettermint.gleam", 52).
?DOC(
    " Set the route for this config.\n"
    "\n"
    " Routes control deliverability settings in your Lettermint account.\n"
).
-spec set_route(lettermint_config(), binary()) -> lettermint_config().
set_route(Config, Route) ->
    {lettermint_config, erlang:element(2, Config), Route}.

-file("src/sendr_lettermint.gleam", 296).
-spec add(list({binary(), gleam@json:json()}), binary(), gleam@json:json()) -> list({binary(),
    gleam@json:json()}).
add(Entries, Key, Value) ->
    lists:append(Entries, [{Key, Value}]).

-file("src/sendr_lettermint.gleam", 304).
-spec add_if_some(
    list({binary(), gleam@json:json()}),
    binary(),
    gleam@option:option(gleam@json:json())
) -> list({binary(), gleam@json:json()}).
add_if_some(Entries, Key, Value) ->
    case Value of
        {some, Value@1} ->
            add(Entries, Key, Value@1);

        none ->
            Entries
    end.

-file("src/sendr_lettermint.gleam", 271).
-spec attachment_to_json(sendr@message@attachment:attachment()) -> gleam@json:json().
attachment_to_json(Attachment) ->
    Filename@1 = case erlang:element(3, Attachment) of
        <<""/utf8>> ->
            erlang:element(4, Attachment);

        Filename ->
            Filename
    end,
    Content = gleam_stdlib:base64_encode(erlang:element(5, Attachment), true),
    Content_type = case erlang:element(2, Attachment) of
        <<""/utf8>> ->
            <<"application/octet-stream"/utf8>>;

        Ct ->
            Ct
    end,
    gleam@json:object(
        begin
            _pipe = [{<<"filename"/utf8>>, gleam@json:string(Filename@1)},
                {<<"content"/utf8>>, gleam@json:string(Content)},
                {<<"content_type"/utf8>>, gleam@json:string(Content_type)}],
            add_if_some(_pipe, <<"content_id"/utf8>>, case Attachment of
                    {inlined_attachment, _, _, _, _, Cid} when Cid =/= <<""/utf8>> ->
                        {some, gleam@json:string(Cid)};

                    _ ->
                        none
                end)
        end
    ).

-file("src/sendr_lettermint.gleam", 264).
-spec mailbox_to_json(sendr@message@mailbox:mailbox()) -> gleam@json:json().
mailbox_to_json(Mailbox) ->
    case Mailbox of
        {mailbox, <<""/utf8>>, Address} ->
            gleam@json:string(Address);

        {mailbox, Name, Address@1} ->
            gleam@json:string(
                <<<<<<Name/binary, " <"/utf8>>/binary, Address@1/binary>>/binary,
                    ">"/utf8>>
            )
    end.

-file("src/sendr_lettermint.gleam", 232).
-spec build_body(sendr@message:message(), lettermint_config()) -> gleam@json:json().
build_body(Message, Config) ->
    _pipe = [],
    _pipe@1 = add_if_some(
        _pipe,
        <<"route"/utf8>>,
        case erlang:element(3, Config) of
            <<""/utf8>> ->
                none;

            Route ->
                {some, gleam@json:string(Route)}
        end
    ),
    _pipe@2 = add_if_some(
        _pipe@1,
        <<"from"/utf8>>,
        gleam@option:map(erlang:element(2, Message), fun mailbox_to_json/1)
    ),
    _pipe@3 = add_if_some(
        _pipe@2,
        <<"reply_to"/utf8>>,
        gleam@option:map(
            erlang:element(3, Message),
            fun(Mailboxes) ->
                gleam@json:array(Mailboxes, fun mailbox_to_json/1)
            end
        )
    ),
    _pipe@4 = add_if_some(
        _pipe@3,
        <<"to"/utf8>>,
        gleam@option:map(
            erlang:element(4, Message),
            fun(_capture) ->
                gleam@json:array(_capture, fun mailbox_to_json/1)
            end
        )
    ),
    _pipe@5 = add_if_some(
        _pipe@4,
        <<"cc"/utf8>>,
        gleam@option:map(
            erlang:element(5, Message),
            fun(_capture@1) ->
                gleam@json:array(_capture@1, fun mailbox_to_json/1)
            end
        )
    ),
    _pipe@6 = add_if_some(
        _pipe@5,
        <<"bcc"/utf8>>,
        gleam@option:map(
            erlang:element(6, Message),
            fun(_capture@2) ->
                gleam@json:array(_capture@2, fun mailbox_to_json/1)
            end
        )
    ),
    _pipe@7 = add_if_some(
        _pipe@6,
        <<"subject"/utf8>>,
        gleam@option:map(erlang:element(7, Message), fun gleam@json:string/1)
    ),
    _pipe@8 = add_if_some(
        _pipe@7,
        <<"text"/utf8>>,
        gleam@option:map(
            erlang:element(2, erlang:element(9, Message)),
            fun gleam@json:string/1
        )
    ),
    _pipe@9 = add_if_some(
        _pipe@8,
        <<"html"/utf8>>,
        gleam@option:map(
            erlang:element(3, erlang:element(9, Message)),
            fun gleam@json:string/1
        )
    ),
    _pipe@10 = add(
        _pipe@9,
        <<"attachments"/utf8>>,
        gleam@json:array(erlang:element(8, Message), fun attachment_to_json/1)
    ),
    gleam@json:object(_pipe@10).

-file("src/sendr_lettermint.gleam", 219).
-spec validate_body(sendr@message:message()) -> {ok, nil} |
    {error, sendr:sendr_error(any())}.
validate_body(Message) ->
    Text_length = begin
        _pipe = erlang:element(2, erlang:element(9, Message)),
        gleam@option:map(_pipe, fun string:length/1)
    end,
    Html_length = begin
        _pipe@1 = erlang:element(3, erlang:element(9, Message)),
        gleam@option:map(_pipe@1, fun string:length/1)
    end,
    case {Text_length, Html_length} of
        {none, none} ->
            {error, {invalid_body, no_body}};

        {{some, 0}, {some, 0}} ->
            {error, {invalid_body, no_body}};

        {{some, Length}, _} when Length < 3 ->
            {error, {invalid_body, {text_to_short, 3, Length}}};

        {_, {some, Length@1}} when Length@1 < 3 ->
            {error, {invalid_body, {html_to_short, 3, Length@1}}};

        {_, _} ->
            {ok, nil}
    end.

-file("src/sendr_lettermint.gleam", 205).
-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_lettermint.gleam", 194).
-spec validate_subject(sendr@message:message()) -> {ok, nil} |
    {error, sendr:sendr_error(any())}.
validate_subject(Message) ->
    Subject_length = begin
        _pipe = erlang:element(7, Message),
        gleam@option:map(_pipe, fun string:length/1)
    end,
    case Subject_length of
        none ->
            {error, {required_field_missing, subject}};

        {some, 0} ->
            {error, {required_field_missing, subject}};

        {some, Length} when Length > 998 ->
            {error, {field_exceeds_maximum_length, subject, 998, Length}};

        _ ->
            {ok, nil}
    end.

-file("src/sendr_lettermint.gleam", 184).
-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_lettermint.gleam", 155).
-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,
                begin
                    _pipe@6 = Validate(erlang:element(4, Message), to),
                    gleam@result:'try'(
                        _pipe@6,
                        fun(_) ->
                            case gleam@option:unwrap(
                                erlang:element(4, Message),
                                []
                            ) of
                                [] ->
                                    {error, {required_field_missing, to}};

                                _ ->
                                    {ok, nil}
                            end
                        end
                    )
                end
            )
    end.

-file("src/sendr_lettermint.gleam", 146).
-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_lettermint.gleam", 139).
-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_lettermint.gleam", 63).
?DOC(
    " Build an HTTP `Request` for the Lettermint send API from a sendr `Message`.\n"
    "\n"
    " Validates the message (from, reply-to, recipients, subject, attachments)\n"
    " and returns `Error(SendrError(LettermintError))` on validation failure.\n"
).
-spec request(sendr@message:message(), lettermint_config()) -> {ok,
        gleam@http@request:request(binary())} |
    {error, sendr:sendr_error(lettermint_error())}.
request(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(_) ->
                                                    Body = build_body(
                                                        Message,
                                                        Config
                                                    ),
                                                    _pipe = <<"https://api.lettermint.co/v1/send"/utf8>>,
                                                    _pipe@1 = gleam_stdlib:uri_parse(
                                                        _pipe
                                                    ),
                                                    _pipe@2 = gleam@result:'try'(
                                                        _pipe@1,
                                                        fun gleam@http@request:from_uri/1
                                                    ),
                                                    _pipe@3 = gleam@result:replace_error(
                                                        _pipe@2,
                                                        {invalid_uri,
                                                            <<"https://api.lettermint.co/v1/send"/utf8>>}
                                                    ),
                                                    _pipe@8 = gleam@result:map(
                                                        _pipe@3,
                                                        fun(Req) ->
                                                            _pipe@4 = Req,
                                                            _pipe@5 = gleam@http@request:set_method(
                                                                _pipe@4,
                                                                post
                                                            ),
                                                            _pipe@6 = gleam@http@request:set_header(
                                                                _pipe@5,
                                                                <<"content-type"/utf8>>,
                                                                <<"application/json"/utf8>>
                                                            ),
                                                            _pipe@7 = gleam@http@request:set_header(
                                                                _pipe@6,
                                                                <<"x-lettermint-token"/utf8>>,
                                                                erlang:element(
                                                                    2,
                                                                    Config
                                                                )
                                                            ),
                                                            gleam@http@request:set_body(
                                                                _pipe@7,
                                                                gleam@json:to_string(
                                                                    Body
                                                                )
                                                            )
                                                        end
                                                    ),
                                                    gleam@result:map_error(
                                                        _pipe@8,
                                                        fun(Field@0) -> {backend_error, Field@0} end
                                                    )
                                                end
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/sendr_lettermint.gleam", 95).
?DOC(
    " Parse a `Response` from the Lettermint send API.\n"
    "\n"
    " On HTTP 202 the response body is decoded and the `message_id` value\n"
    " is returned. On HTTP 422 the `detail` array is decoded into\n"
    " `ApiErrorDetail` entries. All other statuses are treated as generic\n"
    " API errors.\n"
).
-spec response(gleam@http@response:response(binary())) -> {ok, binary()} |
    {error, sendr:sendr_error(lettermint_error())}.
response(Response) ->
    case erlang:element(2, Response) of
        202 = Status ->
            Success_decoder = begin
                gleam@dynamic@decode:field(
                    <<"message_id"/utf8>>,
                    {decoder, fun gleam@dynamic@decode:decode_string/1},
                    fun(Id) -> gleam@dynamic@decode:success(Id) end
                )
            end,
            _pipe = gleam@json:parse(
                erlang:element(4, Response),
                Success_decoder
            ),
            gleam@result:map_error(
                _pipe,
                fun(Error) ->
                    {backend_error, {invalid_response, Status, Error}}
                end
            );

        422 = Status@1 ->
            Error_decoder = begin
                gleam@dynamic@decode:field(
                    <<"error"/utf8>>,
                    begin
                        gleam@dynamic@decode:field(
                            <<"code"/utf8>>,
                            {decoder, fun gleam@dynamic@decode:decode_string/1},
                            fun(Code) ->
                                gleam@dynamic@decode:field(
                                    <<"message"/utf8>>,
                                    {decoder,
                                        fun gleam@dynamic@decode:decode_string/1},
                                    fun(Message) ->
                                        gleam@dynamic@decode:success(
                                            {api_error,
                                                Status@1,
                                                {some, Code},
                                                Message}
                                        )
                                    end
                                )
                            end
                        )
                    end,
                    fun(Error@1) -> gleam@dynamic@decode:success(Error@1) end
                )
            end,
            case gleam@json:parse(erlang:element(4, Response), Error_decoder) of
                {ok, Api_error} ->
                    {error, {backend_error, Api_error}};

                {error, Error@2} ->
                    {error,
                        {backend_error, {invalid_response, Status@1, Error@2}}}
            end;

        Status@2 ->
            Error_decoder@1 = begin
                gleam@dynamic@decode:field(
                    <<"message"/utf8>>,
                    {decoder, fun gleam@dynamic@decode:decode_string/1},
                    fun(Message@1) ->
                        gleam@dynamic@decode:success(
                            {api_error, Status@2, none, Message@1}
                        )
                    end
                )
            end,
            case gleam@json:parse(erlang:element(4, Response), Error_decoder@1) of
                {ok, Api_error@1} ->
                    {error, {backend_error, Api_error@1}};

                {error, Error@3} ->
                    {error,
                        {backend_error, {invalid_response, Status@2, Error@3}}}
            end
    end.