Packages

A Gleam library for sending emails via SMTP, inspired by the Rust mail-send crate

Current section

Files

Jump to
lumenmail src lumenmail@network.erl
Raw

src/lumenmail@network.erl

-module(lumenmail@network).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/lumenmail/network.gleam").
-export([tcp_connect/3, ssl_connect/4, send_command/4, send_data/4, read_response/3, read_multiline_response/3, close_socket/2]).
-export_type([socket/0, tcp_option/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(
" Network module for SMTP operations.\n"
" Provides TCP and SSL socket handling using Erlang's gen_tcp and ssl modules.\n"
" This is a pure Gleam implementation using @external declarations.\n"
).
-type socket() :: any().
-type tcp_option() :: binary | active_false | packet_line | reuse_addr.
-file("src/lumenmail/network.gleam", 240).
?DOC(" Format an error reason to a string.\n").
-spec format_error(binary()) -> binary().
format_error(Reason) ->
Reason.
-file("src/lumenmail/network.gleam", 29).
?DOC(" Establishes a TCP connection to the SMTP server.\n").
-spec tcp_connect(binary(), integer(), integer()) -> {ok, socket()} |
{error, lumenmail@types:smtp_error()}.
tcp_connect(Host, Port, Timeout) ->
Options = [binary, active_false, packet_line, reuse_addr],
case lumenmail_network_ffi:tcp_connect(Host, Port, Options, Timeout) of
{ok, Socket} ->
{ok, Socket};
{error, Reason} ->
{error, {connection_failed, format_error(Reason)}}
end.
-file("src/lumenmail/network.gleam", 54).
?DOC(" Upgrades a TCP socket to SSL/TLS.\n").
-spec ssl_connect(socket(), binary(), boolean(), integer()) -> {ok, socket()} |
{error, lumenmail@types:smtp_error()}.
ssl_connect(Socket, Host, Allow_invalid_certs, Timeout) ->
_ = lumenmail_network_ffi:ensure_ssl_started(),
case lumenmail_network_ffi:ssl_connect(
Socket,
Host,
Allow_invalid_certs,
Timeout
) of
{ok, Ssl_socket} ->
{ok, Ssl_socket};
{error, Reason} ->
{error, {tls_error, format_error(Reason)}}
end.
-file("src/lumenmail/network.gleam", 85).
?DOC(" Sends an SMTP command (appends CRLF).\n").
-spec send_command(socket(), boolean(), binary(), integer()) -> {ok, nil} |
{error, lumenmail@types:smtp_error()}.
send_command(Socket, Is_tls, Command, _) ->
Data = <<Command/binary, "\r\n"/utf8>>,
case lumenmail_network_ffi:send(Socket, Is_tls, Data) of
{ok, nil} ->
{ok, nil};
{error, Reason} ->
{error, {io_error, format_error(Reason)}}
end.
-file("src/lumenmail/network.gleam", 114).
?DOC(" Process email data: normalize line endings and apply dot-stuffing.\n").
-spec process_email_data(binary()) -> binary().
process_email_data(Data) ->
_pipe = Data,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
_pipe@2 = gleam@list:map(
_pipe@1,
fun(Line) ->
Clean_line = case gleam_stdlib:string_ends_with(Line, <<"\r"/utf8>>) of
true ->
gleam@string:drop_end(Line, 1);
false ->
Line
end,
case gleam_stdlib:string_starts_with(Clean_line, <<"."/utf8>>) of
true ->
<<"."/utf8, Clean_line/binary>>;
false ->
Clean_line
end
end
),
gleam@string:join(_pipe@2, <<"\r\n"/utf8>>).
-file("src/lumenmail/network.gleam", 99).
?DOC(" Sends raw data (for message body) with dot-stuffing.\n").
-spec send_data(socket(), boolean(), binary(), integer()) -> {ok, nil} |
{error, lumenmail@types:smtp_error()}.
send_data(Socket, Is_tls, Data, _) ->
Processed = process_email_data(Data),
case lumenmail_network_ffi:send(Socket, Is_tls, Processed) of
{ok, nil} ->
{ok, nil};
{error, Reason} ->
{error, {io_error, format_error(Reason)}}
end.
-file("src/lumenmail/network.gleam", 210).
?DOC(" Parse an SMTP response line into code, message, and multiline flag.\n").
-spec parse_response_line(binary()) -> {ok, lumenmail@types:smtp_response()} |
{error, lumenmail@types:smtp_error()}.
parse_response_line(Line) ->
Clean_line = begin
_pipe = Line,
gleam@string:trim_end(_pipe)
end,
case string:length(Clean_line) >= 3 of
false ->
{error, {invalid_response, Line}};
true ->
Code_str = gleam@string:slice(Clean_line, 0, 3),
case gleam_stdlib:parse_int(Code_str) of
{error, _} ->
{error, {invalid_response, Line}};
{ok, Code} ->
case string:length(Clean_line) of
3 ->
{ok, {smtp_response, Code, <<""/utf8>>, false}};
_ ->
Separator = gleam@string:slice(Clean_line, 3, 1),
Message = gleam@string:slice(
Clean_line,
4,
string:length(Clean_line) - 4
),
Is_multiline = Separator =:= <<"-"/utf8>>,
{ok, {smtp_response, Code, Message, Is_multiline}}
end
end
end.
-file("src/lumenmail/network.gleam", 140).
?DOC(" Reads a single-line SMTP response.\n").
-spec read_response(socket(), boolean(), integer()) -> {ok,
lumenmail@types:smtp_response()} |
{error, lumenmail@types:smtp_error()}.
read_response(Socket, Is_tls, Timeout) ->
case lumenmail_network_ffi:recv(Socket, Is_tls, Timeout) of
{ok, Line} ->
parse_response_line(Line);
{error, <<"closed"/utf8>>} ->
{error, connection_closed};
{error, <<"timeout"/utf8>>} ->
{error, timeout};
{error, Reason} ->
{error, {invalid_response, Reason}}
end.
-file("src/lumenmail/network.gleam", 162).
-spec read_multiline_loop(socket(), boolean(), integer(), list(binary())) -> {ok,
lumenmail@types:smtp_response()} |
{error, lumenmail@types:smtp_error()}.
read_multiline_loop(Socket, Is_tls, Timeout, Acc) ->
case lumenmail_network_ffi:recv(Socket, Is_tls, Timeout) of
{ok, Line} ->
case parse_response_line(Line) of
{ok, {smtp_response, _, Message, true}} ->
read_multiline_loop(
Socket,
Is_tls,
Timeout,
[Message | Acc]
);
{ok, {smtp_response, Code, Message@1, false}} ->
All_messages = lists:reverse([Message@1 | Acc]),
Full_message = gleam@string:join(
All_messages,
<<"\n"/utf8>>
),
{ok, {smtp_response, Code, Full_message, false}};
{error, E} ->
{error, E}
end;
{error, <<"closed"/utf8>>} ->
{error, connection_closed};
{error, <<"timeout"/utf8>>} ->
{error, timeout};
{error, Reason} ->
{error, {invalid_response, Reason}}
end.
-file("src/lumenmail/network.gleam", 154).
?DOC(" Reads a multi-line SMTP response (e.g., EHLO response).\n").
-spec read_multiline_response(socket(), boolean(), integer()) -> {ok,
lumenmail@types:smtp_response()} |
{error, lumenmail@types:smtp_error()}.
read_multiline_response(Socket, Is_tls, Timeout) ->
read_multiline_loop(Socket, Is_tls, Timeout, []).
-file("src/lumenmail/network.gleam", 198).
?DOC(" Closes the socket.\n").
-spec close_socket(socket(), boolean()) -> nil.
close_socket(Socket, Is_tls) ->
lumenmail_network_ffi:close(Socket, Is_tls).