-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 = <>, 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).