From 099d47b1eabd2aa9746ef62905e4f415681100e5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Mar 2021 20:53:56 +0000 Subject: [PATCH 01/12] Add LHLO handling --- src/gen_smtp_server_session.erl | 45 +++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index f03edeb5..ee5e768a 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -73,6 +73,7 @@ readmessage = false :: boolean(), tls = false :: boolean(), callbackstate :: any(), + variant = smtp :: 'smtp' | 'lmtp', options = [] :: [tuple()] } ). @@ -93,6 +94,7 @@ | {keyfile, file:name_all()} % deprecated, see tls_options | {allow_bare_newlines, false | ignore | fix | strip} | {hostname, inet:hostname()} + | {variant, smtp | lmtp} | {tls_options, [tls_opt()]}]. -type(state() :: any()). @@ -190,7 +192,8 @@ init([Ref, Transport, Socket, Module, Options]) -> module = Module, ranch_ref = Ref, options = Options, - callbackstate = CallbackState}, ?TIMEOUT}; + callbackstate = CallbackState, + variant = variant(Options)}, ?TIMEOUT}; {stop, Reason, Message} -> Transport:send(Socket, [Message, "\r\n"]), Transport:close(Socket), @@ -349,9 +352,12 @@ parse_request(Packet) -> handle_request({<<>>, _Any}, State) -> send(State, "500 Error: bad syntax\r\n"), {ok, State}; -handle_request({<<"HELO">>, <<>>}, State) -> +handle_request({<<"HELO">>, <<>>}, #state{variant = smtp} = State) -> send(State, "501 Syntax: HELO hostname\r\n"), {ok, State}; +handle_request({<<"LHLO">>, _Any}, #state{variant = smtp} = State) -> + send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"), + {ok, State}; handle_request({<<"HELO">>, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState} = State) -> case Module:handle_HELO(Hostname, OldCallbackState) of @@ -369,11 +375,19 @@ handle_request({<<"HELO">>, Hostname}, send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; -handle_request({<<"EHLO">>, <<>>}, State) -> +handle_request({<<"EHLO">>, <<>>}, #state{variant = smtp} = State) -> send(State, "501 Syntax: EHLO hostname\r\n"), {ok, State}; -handle_request({<<"EHLO">>, Hostname}, - #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) -> +handle_request({<<"LHLO">>, <<>>}, #state{variant = lmtp} = State) -> + send(State, "501 Syntax: LHLO hostname\r\n"), + {ok, State}; +handle_request({Msg, _Any}, #state{variant = lmtp} = State) + when Msg == <<"HELO">> orelse Msg == <<"EHLO">> -> + send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), + {ok, State}; +handle_request({Msg, Hostname}, + #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) + when Msg == <<"EHLO">> orelse Msg == <<"LHLO">> -> case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of {ok, [], CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], @@ -416,8 +430,8 @@ handle_request({<<"EHLO">>, Hostname}, {ok, State#state{callbackstate = CallbackState}} end; -handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined} = State) -> - send(State, "503 Error: send EHLO first\r\n"), +handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined, variant = Variant} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"AUTH">>, Args}, #state{extensions = Extensions, envelope = Envelope, options = Options} = State) -> @@ -512,8 +526,8 @@ handle_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #env Password = base64:decode(Password64), try_auth('login', Username, Password, State); -handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined} = State) -> - send(State, "503 Error: send HELO/EHLO first\r\n"), +handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined, variant = Variant} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"MAIL">>, Args}, @@ -631,8 +645,8 @@ handle_request({<<"RCPT">>, Args}, #state{envelope = Envelope, module = Module, send(State, "501 Syntax: RCPT TO:
\r\n"), {ok, State} end; -handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined} = State) -> - send(State, "503 Error: send HELO/EHLO first\r\n"), +handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined, variant = Variant} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"DATA">> = C, <<>>}, #state{envelope = Envelope} = State) -> @@ -993,6 +1007,15 @@ setopts(#state{transport = Transport, socket = Sock} = St, Opts) -> hostname(Opts) -> proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()). +variant(Opts) -> + proplists:get_value(variant, Opts, smtp). + +lhlo_if_lmtp(Variant, Fallback) -> + case Variant == lmtp of + true -> "LHLO"; + false -> Fallback + end. + -ifdef(TEST). parse_encoded_address_test_() -> [ From 7640b64129a9444d9ffeb72c04c6aa56ba630979 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Mar 2021 21:47:43 +0000 Subject: [PATCH 02/12] Add LHLO tests --- src/gen_smtp_server_session.erl | 84 +++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index ee5e768a..b7e83f9c 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -358,6 +358,10 @@ handle_request({<<"HELO">>, <<>>}, #state{variant = smtp} = State) -> handle_request({<<"LHLO">>, _Any}, #state{variant = smtp} = State) -> send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"), {ok, State}; +handle_request({Msg, _Any}, #state{variant = lmtp} = State) + when Msg == <<"HELO">> orelse Msg == <<"EHLO">> -> + send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), + {ok, State}; handle_request({<<"HELO">>, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState} = State) -> case Module:handle_HELO(Hostname, OldCallbackState) of @@ -381,10 +385,6 @@ handle_request({<<"EHLO">>, <<>>}, #state{variant = smtp} = State) -> handle_request({<<"LHLO">>, <<>>}, #state{variant = lmtp} = State) -> send(State, "501 Syntax: LHLO hostname\r\n"), {ok, State}; -handle_request({Msg, _Any}, #state{variant = lmtp} = State) - when Msg == <<"HELO">> orelse Msg == <<"EHLO">> -> - send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), - {ok, State}; handle_request({Msg, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) when Msg == <<"EHLO">> orelse Msg == <<"LHLO">> -> @@ -1091,6 +1091,7 @@ parse_request_test_() -> fun() -> ?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)), ?assertEqual({<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>)), + ?assertEqual({<<"LHLO">>, <<"hell.af.mil">>}, parse_request(<<"LHLO hell.af.mil\r\n">>)), ?assertEqual({<<"MAIL">>, <<"FROM:God@heaven.af.mil">>}, parse_request(<<"MAIL FROM:God@heaven.af.mil">>)) end }, @@ -1161,6 +1162,18 @@ smtp_session_test_() -> end } end, + fun({CSock, _Pid}) -> + {"An error in response to an LHLO sent by SMTP", + fun() -> + smtp_socket:active_once(CSock), + receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, + ?assertMatch("220 localhost"++_Stuff, Packet), + smtp_socket:send(CSock, "LHLO somehost.com\r\n"), + receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, + ?assertMatch("500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n", Packet2) + end + } + end, fun({CSock, _Pid}) -> {"A rejected HELO", fun() -> @@ -1439,6 +1452,69 @@ smtp_session_test_() -> ] }. +lmtp_session_test_() -> + {foreach, + local, + fun() -> + application:ensure_all_started(gen_smtp), + {ok, Pid} = gen_smtp_server:start( + smtp_server_example, + [{sessionoptions, + [{variant, lmtp}]}, + {domain, "localhost"}, + {port, 9876}]), + {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), + {CSock, Pid} + end, + fun({CSock, _Pid}) -> + gen_smtp_server:stop(gen_smtp_server), + smtp_socket:close(CSock), + timer:sleep(10) + end, + [fun({CSock, _Pid}) -> + {"An error in response to a HELO/EHLO sent by LMTP", + fun() -> + smtp_socket:active_once(CSock), + receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, + ?assertMatch("220 localhost"++_Stuff, Packet), + smtp_socket:send(CSock, "HELO somehost.com\r\n"), + receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, + ?assertMatch("500 Error: LMTP should replace HELO and EHLO with LHLO\r\n", Packet2), + smtp_socket:send(CSock, "EHLO somehost.com\r\n"), + receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, + ?assertMatch("500 Error: LMTP should replace HELO and EHLO with LHLO\r\n", Packet3) + end + } + end, + fun({CSock, _Pid}) -> + {"LHLO response", + fun() -> + smtp_socket:active_once(CSock), + receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, + ?assertMatch("220 localhost"++_Stuff, Packet), + smtp_socket:send(CSock, "LHLO somehost.com\r\n"), + receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250-localhost\r\n", Packet2), + Foo = fun(F) -> + receive + {tcp, CSock, "250-"++_Packet3} -> + smtp_socket:active_once(CSock), + F(F); + {tcp, CSock, "250 "++_Packet3} -> + smtp_socket:active_once(CSock), + ok; + {tcp, CSock, _R} -> + smtp_socket:active_once(CSock), + error + end + end, + ?assertEqual(ok, Foo(Foo)) + end + } + end + ] + }. + smtp_session_auth_test_() -> {foreach, local, From eb1cf1bde5b82ea25602b553709b0c75f9fd9f1b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Mar 2021 22:25:17 +0000 Subject: [PATCH 03/12] Add error for LMTP on port 25 --- src/gen_smtp_server_session.erl | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index b7e83f9c..dabf5aaa 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -170,8 +170,16 @@ ranch_init({Ref, Transport, {Callback, Opts}}) -> %% @private -spec init(Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'. init([Ref, Transport, Socket, Module, Options]) -> + Variant = variant(Options), PeerName = case Transport:peername(Socket) of - {ok, {IPaddr, _Port}} -> IPaddr; + {ok, {IPaddr, Port}} -> + case {Variant, Port} of + {lmtp, 25} -> + ?log(debug, "error: LMTP is different from SMTP, it MUST NOT be used on the TCP port 25."), + % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 + error; + _ -> IPaddr + end; {error, _} -> error end, case PeerName =/= error @@ -191,9 +199,9 @@ init([Ref, Transport, Socket, Module, Options]) -> transport = Transport, module = Module, ranch_ref = Ref, + variant = Variant, options = Options, - callbackstate = CallbackState, - variant = variant(Options)}, ?TIMEOUT}; + callbackstate = CallbackState}, ?TIMEOUT}; {stop, Reason, Message} -> Transport:send(Socket, [Message, "\r\n"]), Transport:close(Socket), From d5be1fcff4ed06f85ec27ddd569957deeac65083 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 18 Mar 2021 01:58:20 +0000 Subject: [PATCH 04/12] Change handle_DATA to return status for multiple RCPT in LMTP --- src/gen_smtp_server_session.erl | 46 ++++++++++-------- src/smtp_server_example.erl | 82 ++++++++++++++++++++++++++++----- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index dabf5aaa..419fcfb0 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -130,7 +130,8 @@ -callback handle_RCPT_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_DATA(From :: binary(), To :: [binary(),...], Data :: binary(), State :: state()) -> - {ok, string(), state()} | {error, string(), state()}. + {ok | error, string(), state()} | {multiple, [{ok | error, string()}], state()}. +% the 'multiple' reply is only available for LMTP -callback handle_RSET(State :: state()) -> state(). -callback handle_VRFY(Address :: binary(), State :: state()) -> {ok, string(), state()} | {error, string(), state()}. @@ -222,6 +223,23 @@ handle_call(Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. + +%% @hidden +-spec report_recipient(ResponseType :: 'ok' | 'error' | 'multiple', + Value :: string() | [{'ok' | 'error', string()}], + State :: #state{}) -> any(). +report_recipient(ok, Reference, State) -> + send(State, io_lib:format("250 ~s\r\n", [Reference])); +report_recipient(error, Message, State) -> + send(State, [Message, "\r\n"]); +report_recipient(multiple, _Any, #state{variant = smtp} = State) -> + Msg = "SMTP should report a single delivery status for all the recipients", + throw({stop, {handle_DATA_error, Msg}, State}); +report_recipient(multiple, [], State) -> ok; +report_recipient(multiple, [{ResponseType, Value} | Rest], State) -> + report_recipient(ResponseType, Value, State), + report_recipient(multiple, Rest, State). + %% @hidden -spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}. handle_info({receive_data, {error, size_exceeded}}, #state{readmessage = true} = State) -> @@ -247,24 +265,16 @@ handle_info({receive_data, Body, Rest}, end, setopts(State, [{packet, line}]), %% Unescape periods at start of line (rfc5321 4.5.2) - UnescapedBody = re:replace(Body, <<"^\\\.">>, <<>>, [global, multiline, {return, binary}]), - Envelope = Env#envelope{data = UnescapedBody}, - case MaxSize =:= infinity orelse byte_size(Envelope#envelope.data) =< MaxSize of + Data = re:replace(Body, <<"^\\\.">>, <<>>, [global, multiline, {return, binary}]), + #envelope{from = From, to = To} = Env, + case MaxSize =:= infinity orelse byte_size(Data) =< MaxSize of true -> - case Module:handle_DATA(Envelope#envelope.from, Envelope#envelope.to, Envelope#envelope.data, OldCallbackState) of - {ok, Reference, CallbackState} -> - send(State, io_lib:format("250 queued as ~s\r\n", [Reference])), - setopts(State, [{active, once}]), - {noreply, State#state{readmessage = false, - envelope = #envelope{}, - callbackstate = CallbackState}, ?TIMEOUT}; - {error, Message, CallbackState} -> - send(State, [Message, "\r\n"]), - setopts(State, [{active, once}]), - {noreply, State#state{readmessage = false, - envelope = #envelope{}, - callbackstate = CallbackState}, ?TIMEOUT} - end; + {ResponseType, Value, CallbackState} = Module:handle_DATA(From, To, Data, OldCallbackState), + report_recipient(ResponseType, Value, State), + setopts(State, [{active, once}]), + {noreply, State#state{readmessage = false, + envelope = #envelope{}, + callbackstate = CallbackState}, ?TIMEOUT}; false -> send(State, "552 Message too large\r\n"), setopts(State, [{active, once}]), diff --git a/src/smtp_server_example.erl b/src/smtp_server_example.erl index 7035494e..68c6ca39 100644 --- a/src/smtp_server_example.erl +++ b/src/smtp_server_example.erl @@ -14,7 +14,8 @@ -record(state, { - options = [] :: list() + options = [] :: list(), + recipients = [] :: [string()] }). -type(error_message() :: {'error', string(), #state{}}). @@ -124,10 +125,10 @@ handle_MAIL_extension(Extension, _State) -> -spec handle_RCPT(To :: binary(), State :: #state{}) -> {'ok', #state{}} | {'error', string(), #state{}}. handle_RCPT(<<"nobody@example.com">>, State) -> {error, "550 No such recipient", State}; -handle_RCPT(To, State) -> +handle_RCPT(To, #state{recipients = Recipients} = State) -> ?log(info, "Mail to ~s~n", [To]), - % you can accept or reject RCPT TO addesses here, one per call - {ok, State}. + % you can accept or reject RCPT TO addresses here, one per call + {ok, State#state{recipients = [To | Recipients]}}. -spec handle_RCPT_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. handle_RCPT_extension(<<"X-SomeExtension">> = Extension, State) -> @@ -138,20 +139,80 @@ handle_RCPT_extension(Extension, _State) -> ?log(warning, "Unknown RCPT TO extension ~s~n", [Extension]), error. --spec handle_DATA(From :: binary(), To :: [binary(),...], Data :: binary(), State :: #state{}) -> {'ok', string(), #state{}} | {'error', string(), #state{}}. +%% @doc Helps `handle_DATA' to deal with the received email. +%% This function is not directly required by the behaviour. +-spec queue_or_deliver(From :: binary(), + To :: [binary(),...], + Data :: binary(), + Reference :: string(), + State :: #state{} + ) -> {ok | error, string(), #state{}} | + {multiple, [{ok | error, string()}], #state{}}. +queue_or_deliver(From, To, Data, Reference, State) -> + % At this point, if we return ok, we've accepted responsibility for the emaill + Length = byte_size(Data), + Recipients = State#state.recipients, + Variant = proplists:get_value(variant, State#state.options, smtp), + case {Variant, Recipients} of + {smtp, _} -> + ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, Length]), + % ... should actually handle the email, + % if `ok` is returned we are taking the responsibility of the delivery. + {ok, ["queued as ~s", Reference], State}; + {lmtp, []} -> + ?log(info, "message from ~s to ~p rejected: no successful recipients~n", [From, To]), + {error, "503 Error: No successful RCPT TO command"}; + {lmtp, _} -> + ?log(info, "message from ~s delivered to ~p, body length ~p~n", [From, Recipients, Length]), + Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- Recipients], + % ... should actually handle the email for each recipient for each `ok` + {multiple, Multiple, State} + end. + +%% @doc Handle the DATA verb from the client, which corresponds to the body of +%% the message. After receiving the body, a SMTP server can put the email in +%% a queue for later delivering while a LMTP server can handle the delivery +%% directly (LMTP servers are supposed to be simpler and handle emails to +%% local users directly without the need for a queue). Relaying the email to +%% another server is also an option. +%% +%% When using the SMTP protocol, `handle_DATA' should return a single "aggregate" delivery status +%% in the form of a `{ok, SuccessMsg, State}' tuple or `{error, ErrorMsg, State}'. +%% At this point, if `ok' is returned, we have accepted the full responsibility +%% of delivering the email. +%% +%% When using the LMTP protocol, `handle_DATA' should return a status for +%% each accepted address in `handle_RCPT' in the form of a `{multiple, StatusList, State}' tuple +%% where `StatusList' is a list of `{ok, SuccessMsg}' or `{error, ErrorMsg}' tuples +%% (the statuses should be presented in the same order as the recipient addresses were accepted). +%% For each `ok' in the `StatusList', we have accepted full responsibility for +%% delivering the email to that specific recipient. When a single recipient is +%% specified the returned value can also follow the SMTP format. +%% +%% `ErrorMsg' should always start with the SMTP error code, while `SuccessMsg' +%% should not (the `250' code is automatically prepended). +%% +%% According to the SMTP specification the, responsibility of delivering an +%% email must be taken seriously and the servers MUST NOT loose the message. +-spec handle_DATA(From :: binary(), + To :: [binary(),...], + Data :: binary(), + State :: #state{} + ) -> {ok | error, string(), #state{}} | + {multiple, [{ok | error, string()}], #state{}}. handle_DATA(_From, _To, <<>>, State) -> {error, "552 Message too small", State}; handle_DATA(From, To, Data, State) -> % some kind of unique id - Reference = lists:flatten([io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(term_to_binary(unique_id()))]), % if RELAY is true, then relay email to email address, else send email data to console case proplists:get_value(relay, State#state.options, false) of true -> relay(From, To, Data); false -> - ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, byte_size(Data)]), + Reference = lists:flatten([io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(term_to_binary(unique_id()))]), case proplists:get_value(parse, State#state.options, false) of false -> ok; true -> + % In this example we try to decode the email try mimemail:decode(Data) of _Result -> ?log(info, "Message decoded successfully!~n") @@ -171,10 +232,9 @@ handle_DATA(From, To, Data, State) -> end end end - end - end, - % At this point, if we return ok, we've accepted responsibility for the email - {ok, Reference, State}. + end, + queue_or_deliver(From, To, Data, Reference, State) + end. -spec handle_RSET(State :: #state{}) -> #state{}. handle_RSET(State) -> From 7cf30cf57dcff19c31dc11a0b739e324dd181b55 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 18 Mar 2021 02:25:52 +0000 Subject: [PATCH 05/12] Simplify recipients gen_smtp_server_session already aggregate all the recipients in a non empty list of strings, no extra checks are required. --- src/gen_smtp_server_session.erl | 2 +- src/smtp_server_example.erl | 22 ++++++++-------------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index 419fcfb0..e7fb3764 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -235,7 +235,7 @@ report_recipient(error, Message, State) -> report_recipient(multiple, _Any, #state{variant = smtp} = State) -> Msg = "SMTP should report a single delivery status for all the recipients", throw({stop, {handle_DATA_error, Msg}, State}); -report_recipient(multiple, [], State) -> ok; +report_recipient(multiple, [], _State) -> ok; report_recipient(multiple, [{ResponseType, Value} | Rest], State) -> report_recipient(ResponseType, Value, State), report_recipient(multiple, Rest, State). diff --git a/src/smtp_server_example.erl b/src/smtp_server_example.erl index 68c6ca39..d6216876 100644 --- a/src/smtp_server_example.erl +++ b/src/smtp_server_example.erl @@ -14,8 +14,7 @@ -record(state, { - options = [] :: list(), - recipients = [] :: [string()] + options = [] :: list() }). -type(error_message() :: {'error', string(), #state{}}). @@ -125,10 +124,10 @@ handle_MAIL_extension(Extension, _State) -> -spec handle_RCPT(To :: binary(), State :: #state{}) -> {'ok', #state{}} | {'error', string(), #state{}}. handle_RCPT(<<"nobody@example.com">>, State) -> {error, "550 No such recipient", State}; -handle_RCPT(To, #state{recipients = Recipients} = State) -> +handle_RCPT(To, State) -> ?log(info, "Mail to ~s~n", [To]), % you can accept or reject RCPT TO addresses here, one per call - {ok, State#state{recipients = [To | Recipients]}}. + {ok, State}. -spec handle_RCPT_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'. handle_RCPT_extension(<<"X-SomeExtension">> = Extension, State) -> @@ -151,20 +150,15 @@ handle_RCPT_extension(Extension, _State) -> queue_or_deliver(From, To, Data, Reference, State) -> % At this point, if we return ok, we've accepted responsibility for the emaill Length = byte_size(Data), - Recipients = State#state.recipients, - Variant = proplists:get_value(variant, State#state.options, smtp), - case {Variant, Recipients} of - {smtp, _} -> + case proplists:get_value(variant, State#state.options, smtp) of + smtp -> ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, Length]), % ... should actually handle the email, % if `ok` is returned we are taking the responsibility of the delivery. {ok, ["queued as ~s", Reference], State}; - {lmtp, []} -> - ?log(info, "message from ~s to ~p rejected: no successful recipients~n", [From, To]), - {error, "503 Error: No successful RCPT TO command"}; - {lmtp, _} -> - ?log(info, "message from ~s delivered to ~p, body length ~p~n", [From, Recipients, Length]), - Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- Recipients], + lmtp -> + ?log(info, "message from ~s delivered to ~p, body length ~p~n", [From, To, Length]), + Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To], % ... should actually handle the email for each recipient for each `ok` {multiple, Multiple, State} end. From d5ad3ee44a695f001920df8cbd16556d92e93ad9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 18 Mar 2021 12:39:15 +0000 Subject: [PATCH 06/12] Add tests for multiple RCPT in LMTP --- src/gen_smtp_server_session.erl | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index e7fb3764..79880f2b 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -1478,7 +1478,11 @@ lmtp_session_test_() -> {ok, Pid} = gen_smtp_server:start( smtp_server_example, [{sessionoptions, - [{variant, lmtp}]}, + [{variant, lmtp}, + {callbackoptions, + [{variant, lmtp}, + {size, infinity}]} + ]}, {domain, "localhost"}, {port, 9876}]), {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), @@ -1529,6 +1533,67 @@ lmtp_session_test_() -> ?assertEqual(ok, Foo(Foo)) end } + end, + fun({CSock, _Pid}) -> + {"DATA with multiple RCPT TO", + fun() -> + smtp_socket:active_once(CSock), + receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, + ?assertMatch("220 localhost"++_Stuff, Packet), + smtp_socket:send(CSock, "LHLO somehost.com\r\n"), + receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250-localhost\r\n", Packet2), + Foo = fun(F, Acc) -> + receive + {tcp, CSock, "250-SIZE"++_ = Data} -> + {error, ["received: ", Data]}; + {tcp, CSock, "250-"++_} -> + smtp_socket:active_once(CSock), + F(F, Acc); + {tcp, CSock, "250 PIPELINING"++_} -> + smtp_socket:active_once(CSock), + true; + {tcp, CSock, Data} -> + smtp_socket:active_once(CSock), + {error, ["received: ", Data]} + end + end, + ?assertEqual(true, Foo(Foo, false)), + + smtp_socket:send(CSock, "MAIL FROM:\r\n"), + receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250 " ++ _, Packet3), + smtp_socket:send(CSock, "RCPT TO:\r\n"), + receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250 " ++ _, Packet4), + smtp_socket:send(CSock, "RCPT TO:\r\n"), + receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250 " ++ _, Packet5), + smtp_socket:send(CSock, "RCPT TO:\r\n"), + receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250 " ++ _, Packet6), + + smtp_socket:send(CSock, "DATA\r\n"), + receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, + ?assertMatch("354 " ++ _, Packet7), + + smtp_socket:send(CSock, "Subject: tls message\r\n"), + smtp_socket:send(CSock, "To: \r\n"), + smtp_socket:send(CSock, "From: \r\n"), + smtp_socket:send(CSock, "\r\n"), + smtp_socket:send(CSock, "message body"), + smtp_socket:send(CSock, "\r\n.\r\n"), + % We sent 3 RCPT TO, so we should have 3 delivery reports + AssertDelivery = fun(_) -> + receive {tcp, CSock, Packet8} -> smtp_socket:active_once(CSock) end, + ?assertMatch("250 "++_, Packet8) + end, + lists:foreach(AssertDelivery, [1, 2, 3]), + smtp_socket:send(CSock, "QUIT\r\n"), + receive {tcp, CSock, Packet9} -> smtp_socket:active_once(CSock) end, + ?assertMatch("221 " ++ _, Packet9) + end + } end ] }. From 4bd92e86fe50595b0bda7b4ab6fbcd7abc8c3085 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 18 Mar 2021 13:11:38 +0000 Subject: [PATCH 07/12] Replace 'variant' with 'protocol' This change in naming is compatible with the common jargon, and is also used in other softwares. For example, the mstmp client defines a `--protocol=smtp|lmtp` option (https://manpages.debian.org/testing/msmtp-mta/sendmail.8.en.html) --- src/gen_smtp_server_session.erl | 45 +++++++++++++++------------------ src/smtp_server_example.erl | 2 +- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index 79880f2b..76cc564b 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -73,7 +73,7 @@ readmessage = false :: boolean(), tls = false :: boolean(), callbackstate :: any(), - variant = smtp :: 'smtp' | 'lmtp', + protocol = smtp :: 'smtp' | 'lmtp', options = [] :: [tuple()] } ). @@ -94,7 +94,7 @@ | {keyfile, file:name_all()} % deprecated, see tls_options | {allow_bare_newlines, false | ignore | fix | strip} | {hostname, inet:hostname()} - | {variant, smtp | lmtp} + | {protocol, smtp | lmtp} | {tls_options, [tls_opt()]}]. -type(state() :: any()). @@ -171,10 +171,10 @@ ranch_init({Ref, Transport, {Callback, Opts}}) -> %% @private -spec init(Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'. init([Ref, Transport, Socket, Module, Options]) -> - Variant = variant(Options), + Protocol = proplists:get_value(protocol, Options, smtp), PeerName = case Transport:peername(Socket) of {ok, {IPaddr, Port}} -> - case {Variant, Port} of + case {Protocol, Port} of {lmtp, 25} -> ?log(debug, "error: LMTP is different from SMTP, it MUST NOT be used on the TCP port 25."), % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 @@ -200,7 +200,7 @@ init([Ref, Transport, Socket, Module, Options]) -> transport = Transport, module = Module, ranch_ref = Ref, - variant = Variant, + protocol = Protocol, options = Options, callbackstate = CallbackState}, ?TIMEOUT}; {stop, Reason, Message} -> @@ -232,7 +232,7 @@ report_recipient(ok, Reference, State) -> send(State, io_lib:format("250 ~s\r\n", [Reference])); report_recipient(error, Message, State) -> send(State, [Message, "\r\n"]); -report_recipient(multiple, _Any, #state{variant = smtp} = State) -> +report_recipient(multiple, _Any, #state{protocol = smtp} = State) -> Msg = "SMTP should report a single delivery status for all the recipients", throw({stop, {handle_DATA_error, Msg}, State}); report_recipient(multiple, [], _State) -> ok; @@ -370,13 +370,13 @@ parse_request(Packet) -> handle_request({<<>>, _Any}, State) -> send(State, "500 Error: bad syntax\r\n"), {ok, State}; -handle_request({<<"HELO">>, <<>>}, #state{variant = smtp} = State) -> +handle_request({<<"HELO">>, <<>>}, #state{protocol = smtp} = State) -> send(State, "501 Syntax: HELO hostname\r\n"), {ok, State}; -handle_request({<<"LHLO">>, _Any}, #state{variant = smtp} = State) -> +handle_request({<<"LHLO">>, _Any}, #state{protocol = smtp} = State) -> send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"), {ok, State}; -handle_request({Msg, _Any}, #state{variant = lmtp} = State) +handle_request({Msg, _Any}, #state{protocol = lmtp} = State) when Msg == <<"HELO">> orelse Msg == <<"EHLO">> -> send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), {ok, State}; @@ -397,10 +397,10 @@ handle_request({<<"HELO">>, Hostname}, send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; -handle_request({<<"EHLO">>, <<>>}, #state{variant = smtp} = State) -> +handle_request({<<"EHLO">>, <<>>}, #state{protocol = smtp} = State) -> send(State, "501 Syntax: EHLO hostname\r\n"), {ok, State}; -handle_request({<<"LHLO">>, <<>>}, #state{variant = lmtp} = State) -> +handle_request({<<"LHLO">>, <<>>}, #state{protocol = lmtp} = State) -> send(State, "501 Syntax: LHLO hostname\r\n"), {ok, State}; handle_request({Msg, Hostname}, @@ -448,8 +448,8 @@ handle_request({Msg, Hostname}, {ok, State#state{callbackstate = CallbackState}} end; -handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined, variant = Variant} = State) -> - send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "EHLO"), " first\r\n"]), +handle_request({<<"AUTH">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"AUTH">>, Args}, #state{extensions = Extensions, envelope = Envelope, options = Options} = State) -> @@ -544,8 +544,8 @@ handle_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #env Password = base64:decode(Password64), try_auth('login', Username, Password, State); -handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined, variant = Variant} = State) -> - send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "HELO/EHLO"), " first\r\n"]), +handle_request({<<"MAIL">> = C, _Args}, #state{envelope = undefined, protocol = Protocol} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"MAIL">>, Args}, @@ -663,8 +663,8 @@ handle_request({<<"RCPT">>, Args}, #state{envelope = Envelope, module = Module, send(State, "501 Syntax: RCPT TO:
\r\n"), {ok, State} end; -handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined, variant = Variant} = State) -> - send(State, ["503 Error: send ", lhlo_if_lmtp(Variant, "HELO/EHLO"), " first\r\n"]), +handle_request({<<"DATA">> = C, <<>>}, #state{envelope = undefined, protocol = Protocol} = State) -> + send(State, ["503 Error: send ", lhlo_if_lmtp(Protocol, "HELO/EHLO"), " first\r\n"]), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"DATA">> = C, <<>>}, #state{envelope = Envelope} = State) -> @@ -1025,11 +1025,8 @@ setopts(#state{transport = Transport, socket = Sock} = St, Opts) -> hostname(Opts) -> proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()). -variant(Opts) -> - proplists:get_value(variant, Opts, smtp). - -lhlo_if_lmtp(Variant, Fallback) -> - case Variant == lmtp of +lhlo_if_lmtp(Protocol, Fallback) -> + case Protocol == lmtp of true -> "LHLO"; false -> Fallback end. @@ -1478,9 +1475,9 @@ lmtp_session_test_() -> {ok, Pid} = gen_smtp_server:start( smtp_server_example, [{sessionoptions, - [{variant, lmtp}, + [{protocol, lmtp}, {callbackoptions, - [{variant, lmtp}, + [{protocol, lmtp}, {size, infinity}]} ]}, {domain, "localhost"}, diff --git a/src/smtp_server_example.erl b/src/smtp_server_example.erl index d6216876..92e1142f 100644 --- a/src/smtp_server_example.erl +++ b/src/smtp_server_example.erl @@ -150,7 +150,7 @@ handle_RCPT_extension(Extension, _State) -> queue_or_deliver(From, To, Data, Reference, State) -> % At this point, if we return ok, we've accepted responsibility for the emaill Length = byte_size(Data), - case proplists:get_value(variant, State#state.options, smtp) of + case proplists:get_value(protocol, State#state.options, smtp) of smtp -> ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, Length]), % ... should actually handle the email, From 6f682c94f910ba4a923646b644accbf25686e9e7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 19 Mar 2021 17:39:10 +0000 Subject: [PATCH 08/12] Mention LMTP in README --- README.markdown | 6 ++++++ src/gen_smtp_server_session.erl | 8 +++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.markdown b/README.markdown index d79a70df..71be0800 100644 --- a/README.markdown +++ b/README.markdown @@ -195,6 +195,12 @@ Session options are: * `{tls_options, [ssl:server_option()]}` - options to pass to `ssl:handshake/3` (OTP-21+) / `ssl:ssl_accept/3` when `STARTTLS` command is sent by the client. Only needed if `STARTTLS` extension is enabled +* `{protocol, smtp | lmtp}` - when `lmtp` is passed, the control flow of the + [Local Mail Tranfer Protocol](https://tools.ietf.org/html/rfc2033) is applied. + LMTP is derived from SMTP with just a few variations and is used by standard + [Mail Transfer Agents (MTA)](https://en.wikipedia.org/wiki/Message_transfer_agent), like Postfix, Exim and OpenSMTPD to + send incoming email to local mail-handling applications that usually don't have a delivery queue. + The default value of this option is `smtp`. * `{callbackoptions, any()}` - value will be passed as 4th argument to callback module's `init/4` You can connect and test this using the `gen_smtp_client` via something like: diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index 76cc564b..4547e7f0 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -45,7 +45,7 @@ %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --export_type([options/0, error_class/0]). +-export_type([options/0, error_class/0, protocol_message/0]). -include_lib("hut/include/hut.hrl"). @@ -109,6 +109,8 @@ | setopts_error | data_receive_error. +-type protocol_message() :: string() | iodata(). + -callback init(Hostname :: inet:hostname(), _SessionCount, Peername :: inet:ip_address(), Opts :: any()) -> {ok, Banner :: iodata(), CallbackState :: state()} | {stop, Reason :: any(), Message :: iodata()} | ignore. @@ -130,7 +132,7 @@ -callback handle_RCPT_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_DATA(From :: binary(), To :: [binary(),...], Data :: binary(), State :: state()) -> - {ok | error, string(), state()} | {multiple, [{ok | error, string()}], state()}. + {ok | error, protocol_message(), state()} | {multiple, [{ok | error, protocol_message()}], state()}. % the 'multiple' reply is only available for LMTP -callback handle_RSET(State :: state()) -> state(). -callback handle_VRFY(Address :: binary(), State :: state()) -> @@ -229,7 +231,7 @@ handle_cast(_Msg, State) -> Value :: string() | [{'ok' | 'error', string()}], State :: #state{}) -> any(). report_recipient(ok, Reference, State) -> - send(State, io_lib:format("250 ~s\r\n", [Reference])); + send(State, ["250 ", Reference, "\r\n"]); report_recipient(error, Message, State) -> send(State, [Message, "\r\n"]); report_recipient(multiple, _Any, #state{protocol = smtp} = State) -> From 215046a7a2993e53fe8b7c159bc5219884ee47ed Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 22 Mar 2021 21:43:44 +0000 Subject: [PATCH 09/12] Add note on README about the LMTP callbacks --- README.markdown | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.markdown b/README.markdown index 71be0800..909355ac 100644 --- a/README.markdown +++ b/README.markdown @@ -213,6 +213,12 @@ gen_smtp_client:send( If you want to listen on IPv6, you can use the `{family, inet6}` and `{address, "::"}` options to enable listening on IPv6. +Please notice that when using the LMTP protocol, the `handle_EHLO` callback will be used +to handle the `LHLO` command as defined in [RFC2033](https://tools.ietf.org/html/rfc2033), +due to their similarities. Although not used, the implementation of `handle_HELO` is still +mandatory for the general `gen_smtp_server_session` behaviour (you can simply +return a 500 error, e.g. `{error, "500 LMTP server, not SMTP"}`). + ## Dependency on iconv gen_smtp relies on iconv for text encoding and decoding when parsing is activated. From ed1f3c47aab9bcd2897a89eeddc51048d1168ca3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 24 Mar 2021 12:31:00 +0000 Subject: [PATCH 10/12] Ensure LMTP doesn't run on port 25 before starting --- src/gen_smtp_server.erl | 58 +++++++++++++++++++++------------ src/gen_smtp_server_session.erl | 9 +---- test/gen_smtp_server_test.erl | 15 +++++++++ 3 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 test/gen_smtp_server_test.erl diff --git a/src/gen_smtp_server.erl b/src/gen_smtp_server.erl index 6e5d1eaf..0e185430 100644 --- a/src/gen_smtp_server.erl +++ b/src/gen_smtp_server.erl @@ -26,6 +26,8 @@ -define(PORT, 2525). +-include_lib("hut/include/hut.hrl"). + %% External API -export([ start/3, start/2, start/1, @@ -47,16 +49,24 @@ CallbackModule :: module(), Options :: options()) -> {'ok', pid()} | {'error', any()}. start(ServerName, CallbackModule, Options) when is_list(Options) -> - {ok, Transport, TransportOpts, ProtocolOpts} - = convert_options(CallbackModule, Options), - ranch:start_listener( - ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts). + case convert_options(CallbackModule, Options) of + {ok, Transport, TransportOpts, ProtocolOpts} -> + ranch:start_listener( + ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts); + {error, Reason} -> {error, Reason} + end. child_spec(ServerName, CallbackModule, Options) -> - {ok, Transport, TransportOpts, ProtocolOpts} - = convert_options(CallbackModule, Options), - ranch:child_spec( - ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts). + case convert_options(CallbackModule, Options) of + {ok, Transport, TransportOpts, ProtocolOpts} -> + ranch:child_spec( + ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts); + {error, Reason} -> + % `supervisor:child_spec' is not compatible with ok/error tuples. + % This error is likely to occur when starting the application, + % so the user can sort out the configuration parameters and try again. + erlang:error(Reason) + end. convert_options(CallbackModule, Options) -> Transport = case proplists:get_value(protocol, Options, tcp) of @@ -68,18 +78,26 @@ convert_options(CallbackModule, Options) -> Port = proplists:get_value(port, Options, ?PORT), Hostname = proplists:get_value(domain, Options, smtp_util:guess_FQDN()), ProtocolOpts = proplists:get_value(sessionoptions, Options, []), - ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]}, - RanchOpts = proplists:get_value(ranch_opts, Options, #{}), - SocketOpts = maps:get(socket_opts, RanchOpts, []), - TransportOpts = RanchOpts#{ - socket_opts => - [{port, Port}, - {ip, Address}, - {keepalive, true}, - %% binary, {active, false}, {reuseaddr, true} - ranch defaults - Family - | SocketOpts]}, - {ok, Transport, TransportOpts, ProtocolOpts1}. + EmailTransferProtocol = proplists:get_value(protocol, ProtocolOpts, smtp), + case {EmailTransferProtocol, Port} of + {lmtp, 25} -> + ?log(error, "LMTP is different from SMTP, it MUST NOT be used on the TCP port 25"), + % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 + {error, invalid_lmtp_port}; + _ -> + ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]}, + RanchOpts = proplists:get_value(ranch_opts, Options, #{}), + SocketOpts = maps:get(socket_opts, RanchOpts, []), + TransportOpts = RanchOpts#{ + socket_opts => + [{port, Port}, + {ip, Address}, + {keepalive, true}, + %% binary, {active, false}, {reuseaddr, true} - ranch defaults + Family + | SocketOpts]}, + {ok, Transport, TransportOpts, ProtocolOpts1} + end. %% @doc Start the listener with callback module `Module' with options `Options' linked to no process. diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index 4547e7f0..fe80c2c2 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -175,14 +175,7 @@ ranch_init({Ref, Transport, {Callback, Opts}}) -> init([Ref, Transport, Socket, Module, Options]) -> Protocol = proplists:get_value(protocol, Options, smtp), PeerName = case Transport:peername(Socket) of - {ok, {IPaddr, Port}} -> - case {Protocol, Port} of - {lmtp, 25} -> - ?log(debug, "error: LMTP is different from SMTP, it MUST NOT be used on the TCP port 25."), - % Error defined in section 5 of https://tools.ietf.org/html/rfc2033 - error; - _ -> IPaddr - end; + {ok, {IPaddr, _Port}} -> IPaddr; {error, _} -> error end, case PeerName =/= error diff --git a/test/gen_smtp_server_test.erl b/test/gen_smtp_server_test.erl new file mode 100644 index 00000000..3f222c35 --- /dev/null +++ b/test/gen_smtp_server_test.erl @@ -0,0 +1,15 @@ +-module(gen_smtp_server_test). + +-compile([export_all, nowarn_export_all]). + +-include_lib("eunit/include/eunit.hrl"). + +invalid_lmtp_port_test_() -> + {"gen_smtp_server should prevent starting LMTP on port 25 (RFC2023, secion 5)", + fun() -> + Options = [{port, 25}, {sessionoptions, [{protocol, lmtp}]}], + [?_assertMatch({error, invalid_lmtp_port}, + gen_smtp_server:start(gen_smtp_server, Options)), + ?_assertError(invalid_lmtp_port, + gen_smtp_server:child_spec("LMTP Server", gen_smtp_server, Options))] + end}. From 97a0456e436b3b3269e9bc464d2c14bbb1d4e358 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 24 Mar 2021 12:32:26 +0000 Subject: [PATCH 11/12] Improve function organisation - Combine similar clauses of `gen_smtp_server_session:randle_request` for HELLO, EHLO and LHLO. - Move `report_recipient` and `queue_or_delivery` to the ends of their documents, so functions are defined "from top to down". --- src/gen_smtp_server_session.erl | 47 ++++++++++++++----------------- src/smtp_server_example.erl | 50 ++++++++++++++++----------------- 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index fe80c2c2..a1682cb4 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -219,22 +219,6 @@ handle_cast(_Msg, State) -> {noreply, State}. -%% @hidden --spec report_recipient(ResponseType :: 'ok' | 'error' | 'multiple', - Value :: string() | [{'ok' | 'error', string()}], - State :: #state{}) -> any(). -report_recipient(ok, Reference, State) -> - send(State, ["250 ", Reference, "\r\n"]); -report_recipient(error, Message, State) -> - send(State, [Message, "\r\n"]); -report_recipient(multiple, _Any, #state{protocol = smtp} = State) -> - Msg = "SMTP should report a single delivery status for all the recipients", - throw({stop, {handle_DATA_error, Msg}, State}); -report_recipient(multiple, [], _State) -> ok; -report_recipient(multiple, [{ResponseType, Value} | Rest], State) -> - report_recipient(ResponseType, Value, State), - report_recipient(multiple, Rest, State). - %% @hidden -spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}. handle_info({receive_data, {error, size_exceeded}}, #state{readmessage = true} = State) -> @@ -365,14 +349,15 @@ parse_request(Packet) -> handle_request({<<>>, _Any}, State) -> send(State, "500 Error: bad syntax\r\n"), {ok, State}; -handle_request({<<"HELO">>, <<>>}, #state{protocol = smtp} = State) -> - send(State, "501 Syntax: HELO hostname\r\n"), +handle_request({Command, <<>>}, State) + when Command == <<"HELO">>; Command == <<"EHLO">>; Command == <<"LHLO">> -> + send(State, ["501 Syntax: ", Command, " hostname\r\n"]), {ok, State}; handle_request({<<"LHLO">>, _Any}, #state{protocol = smtp} = State) -> send(State, "500 Error: SMTP should send HELO or EHLO instead of LHLO\r\n"), {ok, State}; handle_request({Msg, _Any}, #state{protocol = lmtp} = State) - when Msg == <<"HELO">> orelse Msg == <<"EHLO">> -> + when Msg == <<"HELO">>; Msg == <<"EHLO">> -> send(State, "500 Error: LMTP should replace HELO and EHLO with LHLO\r\n"), {ok, State}; handle_request({<<"HELO">>, Hostname}, @@ -392,15 +377,9 @@ handle_request({<<"HELO">>, Hostname}, send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; -handle_request({<<"EHLO">>, <<>>}, #state{protocol = smtp} = State) -> - send(State, "501 Syntax: EHLO hostname\r\n"), - {ok, State}; -handle_request({<<"LHLO">>, <<>>}, #state{protocol = lmtp} = State) -> - send(State, "501 Syntax: LHLO hostname\r\n"), - {ok, State}; handle_request({Msg, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) - when Msg == <<"EHLO">> orelse Msg == <<"LHLO">> -> + when Msg == <<"EHLO">>; Msg == <<"LHLO">> -> case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of {ok, [], CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], @@ -1026,6 +1005,22 @@ lhlo_if_lmtp(Protocol, Fallback) -> false -> Fallback end. +%% @hidden +-spec report_recipient(ResponseType :: 'ok' | 'error' | 'multiple', + Value :: string() | [{'ok' | 'error', string()}], + State :: #state{}) -> any(). +report_recipient(ok, Reference, State) -> + send(State, ["250 ", Reference, "\r\n"]); +report_recipient(error, Message, State) -> + send(State, [Message, "\r\n"]); +report_recipient(multiple, _Any, #state{protocol = smtp} = State) -> + Msg = "SMTP should report a single delivery status for all the recipients", + throw({stop, {handle_DATA_error, Msg}, State}); +report_recipient(multiple, [], _State) -> ok; +report_recipient(multiple, [{ResponseType, Value} | Rest], State) -> + report_recipient(ResponseType, Value, State), + report_recipient(multiple, Rest, State). + -ifdef(TEST). parse_encoded_address_test_() -> [ diff --git a/src/smtp_server_example.erl b/src/smtp_server_example.erl index 92e1142f..75e22e6a 100644 --- a/src/smtp_server_example.erl +++ b/src/smtp_server_example.erl @@ -138,31 +138,6 @@ handle_RCPT_extension(Extension, _State) -> ?log(warning, "Unknown RCPT TO extension ~s~n", [Extension]), error. -%% @doc Helps `handle_DATA' to deal with the received email. -%% This function is not directly required by the behaviour. --spec queue_or_deliver(From :: binary(), - To :: [binary(),...], - Data :: binary(), - Reference :: string(), - State :: #state{} - ) -> {ok | error, string(), #state{}} | - {multiple, [{ok | error, string()}], #state{}}. -queue_or_deliver(From, To, Data, Reference, State) -> - % At this point, if we return ok, we've accepted responsibility for the emaill - Length = byte_size(Data), - case proplists:get_value(protocol, State#state.options, smtp) of - smtp -> - ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, Length]), - % ... should actually handle the email, - % if `ok` is returned we are taking the responsibility of the delivery. - {ok, ["queued as ~s", Reference], State}; - lmtp -> - ?log(info, "message from ~s delivered to ~p, body length ~p~n", [From, To, Length]), - Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To], - % ... should actually handle the email for each recipient for each `ok` - {multiple, Multiple, State} - end. - %% @doc Handle the DATA verb from the client, which corresponds to the body of %% the message. After receiving the body, a SMTP server can put the email in %% a queue for later delivering while a LMTP server can handle the delivery @@ -306,3 +281,28 @@ relay(From, [To|Rest], Data) -> [_User, Host] = string:tokens(binary_to_list(To), "@"), gen_smtp_client:send({From, [To], erlang:binary_to_list(Data)}, [{relay, Host}]), relay(From, Rest, Data). + +%% @doc Helps `handle_DATA' to deal with the received email. +%% This function is not directly required by the behaviour. +-spec queue_or_deliver(From :: binary(), + To :: [binary(),...], + Data :: binary(), + Reference :: string(), + State :: #state{} + ) -> {ok | error, string(), #state{}} | + {multiple, [{ok | error, string()}], #state{}}. +queue_or_deliver(From, To, Data, Reference, State) -> + % At this point, if we return ok, we've accepted responsibility for the emaill + Length = byte_size(Data), + case proplists:get_value(protocol, State#state.options, smtp) of + smtp -> + ?log(info, "message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, Length]), + % ... should actually handle the email, + % if `ok` is returned we are taking the responsibility of the delivery. + {ok, ["queued as ~s", Reference], State}; + lmtp -> + ?log(info, "message from ~s delivered to ~p, body length ~p~n", [From, To, Length]), + Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To], + % ... should actually handle the email for each recipient for each `ok` + {multiple, Multiple, State} + end. From e43d40232e870380f1a87eac881fd45a733eb79e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 24 Mar 2021 18:09:12 +0000 Subject: [PATCH 12/12] Minor comment/whitespace change --- src/gen_smtp_server_session.erl | 2 +- src/smtp_server_example.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gen_smtp_server_session.erl b/src/gen_smtp_server_session.erl index a1682cb4..bf90f489 100644 --- a/src/gen_smtp_server_session.erl +++ b/src/gen_smtp_server_session.erl @@ -218,7 +218,6 @@ handle_call(Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. - %% @hidden -spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}. handle_info({receive_data, {error, size_exceeded}}, #state{readmessage = true} = State) -> @@ -999,6 +998,7 @@ setopts(#state{transport = Transport, socket = Sock} = St, Opts) -> hostname(Opts) -> proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()). +%% @hidden lhlo_if_lmtp(Protocol, Fallback) -> case Protocol == lmtp of true -> "LHLO"; diff --git a/src/smtp_server_example.erl b/src/smtp_server_example.erl index 75e22e6a..7ec2b486 100644 --- a/src/smtp_server_example.erl +++ b/src/smtp_server_example.erl @@ -172,11 +172,11 @@ handle_RCPT_extension(Extension, _State) -> handle_DATA(_From, _To, <<>>, State) -> {error, "552 Message too small", State}; handle_DATA(From, To, Data, State) -> - % some kind of unique id % if RELAY is true, then relay email to email address, else send email data to console case proplists:get_value(relay, State#state.options, false) of true -> relay(From, To, Data); false -> + % some kind of unique id Reference = lists:flatten([io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(term_to_binary(unique_id()))]), case proplists:get_value(parse, State#state.options, false) of false -> ok;