%%% Copyright 2009 Andrew Thompson . All rights reserved. %%% %%% Redistribution and use in source and binary forms, with or without %%% modification, are permitted provided that the following conditions are met: %%% %%% 1. Redistributions of source code must retain the above copyright notice, %%% this list of conditions and the following disclaimer. %%% 2. Redistributions in binary form must reproduce the above copyright %%% notice, this list of conditions and the following disclaimer in the %%% documentation and/or other materials provided with the distribution. %%% %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. %% @doc Process representing a SMTP session, extensible via a callback module. This %% module is implemented as a behaviour that the callback module should %% implement. To see the details of the required callback functions to provide, %% please see `smtp_server_example'. %% @see smtp_server_example -module(gen_smtp_server_session). -behaviour(gen_server). -behaviour(ranch_protocol). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -endif. %10mb -define(DEFAULT_MAXSIZE, 10485760). -define(BUILTIN_EXTENSIONS, [ {"SIZE", integer_to_list(?DEFAULT_MAXSIZE)}, {"8BITMIME", true}, {"PIPELINING", true}, {"SMTPUTF8", true} ]). % 3 minutes -define(TIMEOUT, 180000). %% External API -export([start_link/3, start_link/4]). -export([ranch_init/1]). %% 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, protocol_message/0]). -include_lib("kernel/include/logger.hrl"). -define(LOGGER_META, #{domain => [gen_smtp, server]}). -record(envelope, { from :: binary() | 'undefined', to = [] :: [binary()], data = <<>> :: binary(), expectedsize = 0 :: pos_integer() | 0, % {"username", "password"} auth = {<<>>, <<>>} :: {binary(), binary()}, flags = [] :: [smtputf8 | '8bitmime' | '7bit'] }). -record(state, { socket = erlang:error({undefined, socket}) :: port() | tuple(), module = erlang:error({undefined, module}) :: atom(), transport :: module(), ranch_ref :: ranch:ref(), envelope = undefined :: 'undefined' | #envelope{}, extensions = [] :: [{string(), string()}], maxsize = ?DEFAULT_MAXSIZE :: pos_integer() | 'infinity', waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5', authdata :: 'undefined' | binary(), readmessage = false :: boolean(), tls = false :: boolean(), callbackstate :: any(), protocol = smtp :: 'smtp' | 'lmtp', options = [] :: [tuple()] }). -type tls_opt() :: ssl:tls_server_option(). -type options() :: [ {callbackoptions, any()} % deprecated, see tls_options | {certfile, file:name_all()} % deprecated, see tls_options | {keyfile, file:name_all()} | {allow_bare_newlines, false | ignore | fix | strip} | {hostname, inet:hostname()} | {protocol, smtp | lmtp} | {tls_options, [tls_opt()]} ]. -type state() :: any(). -type error_message() :: {error, string(), state()}. -type error_class() :: tcp_closed | tcp_error | ssl_closed | ssl_error | data_rejected | timeout | out_of_order | ssl_handshake_error | send_error | 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. -callback code_change(OldVsn :: any(), State :: state(), Extra :: any()) -> {ok, state()}. -callback handle_HELO(Hostname :: binary(), State :: state()) -> {ok, pos_integer() | 'infinity', state()} | {ok, state()} | error_message(). -callback handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: state()) -> {ok, list(), state()} | error_message(). -callback handle_STARTTLS(state()) -> state(). -callback handle_AUTH( AuthType :: login | plain | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: state() ) -> {ok, state()} | any(). -callback handle_MAIL(From :: binary(), State :: state()) -> {ok, state()} | {error, string(), state()}. -callback handle_MAIL_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_RCPT(To :: binary(), State :: state()) -> {ok, state()} | {error, string(), state()}. -callback handle_RCPT_extension(Extension :: binary(), State :: state()) -> {ok, state()} | error. -callback handle_DATA(From :: binary(), To :: [binary(), ...], Data :: binary(), State :: 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()) -> {ok, string(), state()} | {error, string(), state()}. -callback handle_other(Verb :: binary(), Args :: binary(), state()) -> {string() | noreply, state()}. -callback handle_info(Info :: term(), State :: state()) -> {noreply, NewState :: state()} | {noreply, NewState :: state(), timeout() | hibernate} | {stop, Reason :: term(), NewState :: term()}. -callback handle_error(error_class(), any(), state()) -> {ok, state()} | {stop, Reason :: any(), state()}. -callback terminate(Reason :: any(), state()) -> {ok, Reason :: any(), state()}. -optional_callbacks([handle_info/2, handle_AUTH/4, handle_error/3]). %% @doc Start a SMTP session linked to the calling process. -spec start_link( Ref :: ranch:ref(), Transport :: module(), {Callback :: module(), Options :: options()} ) -> {'ok', pid()}. start_link(Ref, Transport, Options) -> {ok, proc_lib:spawn_link(?MODULE, ranch_init, [{Ref, Transport, Options}])}. start_link(Ref, _Sock, Transport, Options) -> start_link(Ref, Transport, Options). ranch_init({Ref, Transport, {Callback, Opts}}) -> {ok, Socket} = ranch:handshake(Ref), case init([Ref, Transport, Socket, Callback, Opts]) of {ok, State, Timeout} -> gen_server:enter_loop(?MODULE, [], State, Timeout); {stop, Reason} -> exit(Reason); ignore -> ok end. %% @private -spec init(Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'. init([Ref, Transport, Socket, Module, Options]) -> Protocol = proplists:get_value(protocol, Options, smtp), PeerName = case Transport:peername(Socket) of {ok, {IPaddr, _Port}} -> IPaddr; {error, _} -> error end, case PeerName =/= error andalso Module:init( hostname(Options), %FIXME proplists:get_value(sessioncount, Options, 0), PeerName, proplists:get_value(callbackoptions, Options, []) ) of false -> Transport:close(Socket), ignore; {ok, Banner, CallbackState} -> Transport:send(Socket, ["220 ", Banner, "\r\n"]), ok = Transport:setopts(Socket, [ {active, once}, {packet, line}, binary ]), {ok, #state{ socket = Socket, transport = Transport, module = Module, ranch_ref = Ref, protocol = Protocol, options = Options, callbackstate = CallbackState }, ?TIMEOUT}; {stop, Reason, Message} -> Transport:send(Socket, [Message, "\r\n"]), Transport:close(Socket), {stop, Reason}; ignore -> Transport:close(Socket), ignore end. %% @hidden handle_call(stop, _From, State) -> {stop, normal, ok, State}; handle_call(Request, _From, State) -> {reply, {unknown_call, Request}, State}. %% @hidden 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) -> send(State, "552 Message too large\r\n"), setopts(State, [{active, once}]), State1 = handle_error(data_rejected, size_exceeded, State), {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; handle_info({receive_data, {error, bare_newline}}, #state{readmessage = true} = State) -> send(State, "451 Bare newline detected\r\n"), setopts(State, [{active, once}]), State1 = handle_error(data_rejected, bare_neline, State), {noreply, State1#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; handle_info({receive_data, {error, Other}}, #state{readmessage = true} = State) -> State1 = handle_error(data_receive_error, Other, State), {stop, {error_receiving_data, Other}, State1}; handle_info( {receive_data, Body, Rest}, #state{ socket = Socket, transport = Transport, readmessage = true, envelope = Env, module = Module, callbackstate = OldCallbackState, maxsize = MaxSize } = State ) -> % send the remainder of the data... case Rest of % no remaining data <<>> -> ok; _ -> self() ! {Transport:name(), Socket, Rest} end, setopts(State, [{packet, line}]), %% Unescape periods at start of line (rfc5321 4.5.2) Data = re:replace(Body, <<"^\\\.">>, <<>>, [global, multiline, {return, binary}]), #envelope{from = From, to = To} = Env, case MaxSize =:= infinity orelse byte_size(Data) =< MaxSize of true -> {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}]), % might not even be able to get here anymore... {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT} end; handle_info( {SocketType, Socket, Packet}, #state{socket = Socket, transport = Transport, waitingauth = false} = State ) when SocketType =:= tcp; SocketType =:= ssl -> case handle_request(parse_request(Packet), State) of {ok, #state{options = Options, readmessage = true, maxsize = MaxSize} = NewState} -> Session = self(), Size = 0, setopts(NewState, [{packet, raw}]), %% TODO: change to receive asynchronously in the same process spawn_opt( fun() -> receive_data([], Transport, Socket, 0, Size, MaxSize, Session, Options) end, [link, {fullsweep_after, 0}] ), {noreply, NewState, ?TIMEOUT}; {ok, NewState} -> setopts(NewState, [{active, once}]), {noreply, NewState, ?TIMEOUT}; {stop, Reason, NewState} -> {stop, Reason, NewState} end; handle_info({SocketType, Socket, Packet}, #state{socket = Socket} = State) when SocketType =:= tcp; SocketType =:= ssl -> %% We are in SASL state RFC-4954 Request = binstr:strip( binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s ), ?LOG_DEBUG("Got SASL request ~p", [Request], ?LOGGER_META), {ok, NewState} = handle_sasl(base64:decode(Request), State), setopts(NewState, [{active, once}]), {noreply, NewState, ?TIMEOUT}; handle_info({Kind, _Socket}, State) when Kind == tcp_closed; Kind == ssl_closed -> State1 = handle_error(Kind, [], State), {stop, normal, State1}; handle_info({Kind, _Socket, Reason}, State) when Kind == ssl_error; Kind == tcp_error -> State1 = handle_error(Kind, Reason, State), {stop, normal, State1}; handle_info(timeout, #state{socket = Socket, transport = Transport} = State) -> send(State, "421 Error: timeout exceeded\r\n"), Transport:close(Socket), State1 = handle_error(timeout, [], State), {stop, normal, State1}; handle_info(Info, #state{module = Module, callbackstate = OldCallbackState} = State) -> case erlang:function_exported(Module, handle_info, 2) of true -> case Module:handle_info(Info, OldCallbackState) of {noreply, NewCallbackState} -> {noreply, State#state{callbackstate = NewCallbackState}}; {noreply, NewCallbackState, Action} -> {noreply, State#state{callbackstate = NewCallbackState}, Action}; {stop, Reason, NewCallbackState} -> {stop, Reason, State#state{callbackstate = NewCallbackState}} end; false -> ?LOG_DEBUG("Ignored message ~p", [Info], ?LOGGER_META), {noreply, State, ?TIMEOUT} end. %% @hidden -spec terminate(Reason :: any(), State :: #state{}) -> 'ok'. terminate(Reason, #state{ socket = Socket, transport = Transport, module = Module, callbackstate = CallbackState }) -> ok = Transport:close(Socket), Module:terminate(Reason, CallbackState). %% @hidden -spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}. code_change(OldVsn, #state{module = Module, callbackstate = CallbackState} = State, Extra) -> % TODO - this should probably be the callback module's version or its checksum CallbackState = case catch Module:code_change(OldVsn, CallbackState, Extra) of {ok, NewCallbackState} -> NewCallbackState; _ -> CallbackState end, {ok, State#state{callbackstate = CallbackState}}. -spec parse_request(Packet :: binary()) -> {binary(), binary()}. parse_request(Packet) -> Request = binstr:strip( binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s ), case binstr:strchr(Request, $\s) of 0 -> ?LOG_DEBUG("got a ~s request", [Request], ?LOGGER_META), {binstr:to_upper(Request), <<>>}; Index -> Verb = binstr:substr(Request, 1, Index - 1), Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s), ?LOG_DEBUG("got a ~s request with parameters ~s", [Verb, Parameters], ?LOGGER_META), {binstr:to_upper(Verb), Parameters} end. -spec handle_request({Verb :: binary(), Args :: binary()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}. handle_request({<<>>, _Any}, State) -> send(State, "500 Error: bad syntax\r\n"), {ok, State}; 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">>; 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 {ok, MaxSize, CallbackState} when MaxSize =:= infinity; is_integer(MaxSize) -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{ maxsize = MaxSize, envelope = #envelope{}, callbackstate = CallbackState }}; {ok, CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; handle_request( {Msg, Hostname}, #state{options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State ) when Msg == <<"EHLO">>; Msg == <<"LHLO">> -> case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of {ok, [], CallbackState} -> Data = ["250 ", hostname(Options), "\r\n"], send(State, Data), {ok, State#state{extensions = [], callbackstate = CallbackState}}; {ok, Extensions, CallbackState} -> ExtensionsUpper = lists:map(fun({X, Y}) -> {string:to_upper(X), Y} end, Extensions), {Extensions1, MaxSize} = case lists:keyfind("SIZE", 1, ExtensionsUpper) of {"SIZE", "0"} -> {lists:keydelete("SIZE", 1, ExtensionsUpper), infinity}; {"SIZE", MaxSizeString} when is_list(MaxSizeString) -> {ExtensionsUpper, list_to_integer(MaxSizeString)}; false -> {ExtensionsUpper, State#state.maxsize} end, Extensions2 = case Tls of true -> lists:delete({"STARTTLS", true}, Extensions1); false -> Extensions1 end, Response = (fun F([{E, true}]) -> ["250 ", E, "\r\n"]; F([{E, V}]) -> ["250 ", E, " ", V, "\r\n"]; F([Line]) -> ["250 ", Line, "\r\n"]; F([{E, true} | More]) -> ["250-", E, "\r\n" | F(More)]; F([{E, V} | More]) -> ["250-", E, " ", V, "\r\n" | F(More)]; F([Line | More]) -> ["250-", Line, "\r\n" | F(More)] end)( [hostname(Options) | Extensions2] ), %?debugFmt("Respponse ~p~n", [lists:reverse(Response)]), send(State, Response), {ok, State#state{ extensions = Extensions2, maxsize = MaxSize, envelope = #envelope{}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; 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 ) -> case binstr:strchr(Args, $\s) of 0 -> AuthType = Args, Parameters = false; Index -> AuthType = binstr:substr(Args, 1, Index - 1), Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s) end, case has_extension(Extensions, "AUTH") of false -> send(State, "502 Error: AUTH not implemented\r\n"), {ok, State}; {true, AvailableTypes} -> case lists:member( string:to_upper(binary_to_list(AuthType)), string:tokens(AvailableTypes, " ") ) of false -> send(State, "504 Unrecognized authentication type\r\n"), {ok, State}; true -> case binstr:to_upper(AuthType) of <<"LOGIN">> -> % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")), send(State, "334 VXNlcm5hbWU6\r\n"), {ok, State#state{ waitingauth = 'login', envelope = Envelope#envelope{auth = {<<>>, <<>>}} }}; <<"PLAIN">> when Parameters =/= false -> % TODO - duplicated below in handle_request waitingauth PLAIN case binstr:split(base64:decode(Parameters), <<0>>) of [_Identity, Username, Password] -> try_auth('plain', Username, Password, State); [Username, Password] -> try_auth('plain', Username, Password, State); _ -> % TODO error {ok, State} end; <<"PLAIN">> -> send(State, "334\r\n"), {ok, State#state{ waitingauth = 'plain', envelope = Envelope#envelope{auth = {<<>>, <<>>}} }}; <<"CRAM-MD5">> -> % ensure crypto is started, we're gonna need it crypto:start(), String = smtp_util:get_cram_string(hostname(Options)), send(State, ["334 ", String, "\r\n"]), {ok, State#state{ waitingauth = 'cram-md5', authdata = base64:decode(String), envelope = Envelope#envelope{auth = {<<>>, <<>>}} }} %"DIGEST-MD5" -> % TODO finish this? (see rfc 2831) %crypto:start(), % ensure crypto is started, we're gonna need it %Nonce = get_digest_nonce(), %Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname), %smtp_socket:send(Socket, "334 "++Response++"\r\n"), %{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}} end end end; 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}, #state{ module = Module, envelope = Envelope0, callbackstate = OldCallbackState, extensions = Extensions, maxsize = MaxSize } = State ) -> case Envelope0#envelope.from of undefined -> case binstr:strpos(binstr:to_upper(Args), <<"FROM:">>) of 1 -> Address = binstr:strip(binstr:substr(Args, 6), left, $\s), case parse_encoded_address( Address, has_extension(Extensions, "SMTPUTF8") =/= false ) of error -> send(State, "501 Bad sender address syntax\r\n"), {ok, State}; {ParsedAddress, <<>>} -> ?LOG_DEBUG("From address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META), case Module:handle_MAIL(ParsedAddress, OldCallbackState) of {ok, CallbackState} -> send(State, "250 sender Ok\r\n"), {ok, State#state{ envelope = Envelope0#envelope{from = ParsedAddress}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; {ParsedAddress, ExtraInfo} -> ?LOG_DEBUG( "From address ~s (parsed as ~s) with extra info ~s", [ Address, ParsedAddress, ExtraInfo ], ?LOGGER_META ), Options = [binstr:to_upper(X) || X <- binstr:split(ExtraInfo, <<" ">>)], ?LOG_DEBUG("options are ~p", [Options], ?LOGGER_META), F = fun (_, {error, Message}) -> {error, Message}; ( <<"SIZE=", Size/binary>>, #state{envelope = Envelope} = InnerState ) when MaxSize =:= 'infinity' -> InnerState#state{ envelope = Envelope#envelope{ expectedsize = binary_to_integer(Size) } }; ( <<"SIZE=", Size/binary>>, #state{envelope = Envelope} = InnerState ) -> case binary_to_integer(Size) > MaxSize of true -> {error, [ "552 Estimated message length ", Size, " exceeds limit of ", integer_to_binary(MaxSize), "\r\n" ]}; false -> InnerState#state{ envelope = Envelope#envelope{ expectedsize = binary_to_integer(Size) } } end; ( <<"BODY=", BodyType/binary>>, #state{envelope = #envelope{flags = Flags} = Envelope} = InnerState ) -> case has_extension(Extensions, "8BITMIME") of {true, _} -> Flag = maps:get(BodyType, #{ <<"8BITMIME">> => '8bitmime', <<"7BIT">> => '7bit' }), InnerState#state{ envelope = Envelope#envelope{flags = [Flag | Flags]} }; false -> {error, "555 Unsupported option BODY\r\n"} end; ( <<"SMTPUTF8">>, #state{envelope = #envelope{flags = Flags} = Envelope} = InnerState ) -> case has_extension(Extensions, "SMTPUTF8") of {true, _} -> InnerState#state{ envelope = Envelope#envelope{ flags = ['smtputf8' | Flags] } }; false -> {error, "555 Unsupported option SMTPUTF8\r\n"} end; (X, InnerState) -> case Module:handle_MAIL_extension(X, OldCallbackState) of {ok, CallbackState} -> InnerState#state{callbackstate = CallbackState}; error -> {error, ["555 Unsupported option: ", ExtraInfo, "\r\n"]} end end, case lists:foldl(F, State, Options) of {error, Message} -> ?LOG_DEBUG("error: ~s", [Message], ?LOGGER_META), send(State, Message), {ok, State}; #state{envelope = Envelope} = NewState -> ?LOG_DEBUG("OK", ?LOGGER_META), case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of {ok, CallbackState} -> send(State, "250 sender Ok\r\n"), {ok, State#state{ envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, NewState#state{callbackstate = CallbackState}} end end end; _Else -> send(State, "501 Syntax: MAIL FROM:
\r\n"), {ok, State} end; _Other -> send(State, "503 Error: Nested MAIL command\r\n"), {ok, State} end; handle_request({<<"RCPT">> = C, _Args}, #state{envelope = undefined} = State) -> send(State, "503 Error: need MAIL command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request( {<<"RCPT">>, Args}, #state{ envelope = Envelope, module = Module, callbackstate = OldCallbackState, extensions = Extensions } = State ) -> case binstr:strpos(binstr:to_upper(Args), <<"TO:">>) of 1 -> Address = binstr:strip(binstr:substr(Args, 4), left, $\s), case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of error -> send(State, "501 Bad recipient address syntax\r\n"), {ok, State}; {<<>>, _} -> % empty rcpt to addresses aren't cool send(State, "501 Bad recipient address syntax\r\n"), {ok, State}; {ParsedAddress, <<>>} -> ?LOG_DEBUG("To address ~s (parsed as ~s)", [Address, ParsedAddress], ?LOGGER_META), case Module:handle_RCPT(ParsedAddress, OldCallbackState) of {ok, CallbackState} -> send(State, "250 recipient Ok\r\n"), {ok, State#state{ envelope = Envelope#envelope{ to = Envelope#envelope.to ++ [ParsedAddress] }, callbackstate = CallbackState }}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; {ParsedAddress, ExtraInfo} -> % TODO - are there even any RCPT extensions? ?LOG_DEBUG( "To address ~s (parsed as ~s) with extra info ~s", [ Address, ParsedAddress, ExtraInfo ], ?LOGGER_META ), send(State, ["555 Unsupported option: ", ExtraInfo, "\r\n"]), {ok, State} end; _Else -> send(State, "501 Syntax: RCPT TO:
\r\n"), {ok, State} end; 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) -> case {Envelope#envelope.from, Envelope#envelope.to} of {undefined, _} -> send(State, "503 Error: need MAIL command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; {_, []} -> send(State, "503 Error: need RCPT command\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; _Else -> send(State, "354 enter mail, end with line containing only '.'\r\n"), ?LOG_DEBUG("switching to data read mode", [], ?LOGGER_META), {ok, State#state{readmessage = true}} end; handle_request( {<<"RSET">>, _Any}, #state{envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State ) -> send(State, "250 Ok\r\n"), % if the client sends a RSET before a HELO/EHLO don't give them a valid envelope NewEnvelope = case Envelope of undefined -> undefined; _Something -> #envelope{} end, {ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}}; handle_request({<<"NOOP">>, _Any}, State) -> send(State, "250 Ok\r\n"), {ok, State}; handle_request({<<"QUIT">>, _Any}, State) -> send(State, "221 Bye\r\n"), {stop, normal, State}; handle_request( {<<"VRFY">>, Address}, #state{module = Module, callbackstate = OldCallbackState, extensions = Extensions} = State ) -> case parse_encoded_address(Address, has_extension(Extensions, "SMTPUTF8") =/= false) of {ParsedAddress, <<>>} -> case Module:handle_VRFY(ParsedAddress, OldCallbackState) of {ok, Reply, CallbackState} -> send(State, ["250 ", Reply, "\r\n"]), {ok, State#state{callbackstate = CallbackState}}; {error, Message, CallbackState} -> send(State, [Message, "\r\n"]), {ok, State#state{callbackstate = CallbackState}} end; _Other -> send(State, "501 Syntax: VRFY username/address\r\n"), {ok, State} end; handle_request( {<<"STARTTLS">>, <<>>}, #state{ socket = Socket, module = Module, tls = false, extensions = Extensions, callbackstate = OldCallbackState, options = Options } = State ) -> case has_extension(Extensions, "STARTTLS") of {true, _} -> send(State, "220 OK\r\n"), TlsOpts0 = proplists:get_value(tls_options, Options, []), TlsOpts1 = case proplists:get_value(certfile, Options) of undefined -> TlsOpts0; CertFile -> [{certfile, CertFile} | TlsOpts0] end, TlsOpts2 = case proplists:get_value(keyfile, Options) of undefined -> TlsOpts1; KeyFile -> [{keyfile, KeyFile} | TlsOpts1] end, %% Assert that socket is in passive state {ok, [{active, false}]} = inet:getopts(Socket, [active]), %XXX: see smtp_socket:?SSL_LISTEN_OPTIONS case ranch_ssl:handshake( Socket, [{packet, line}, {mode, list}, {ssl_imp, new} | TlsOpts2], 5000 ) of {ok, NewSocket} -> ?LOG_DEBUG("SSL negotiation successful", ?LOGGER_META), ranch_ssl:setopts(NewSocket, [{packet, line}, binary]), {ok, State#state{ socket = NewSocket, transport = ranch_ssl, envelope = undefined, authdata = undefined, waitingauth = false, readmessage = false, tls = true, callbackstate = Module:handle_STARTTLS(OldCallbackState) }}; {error, Reason} -> ?LOG_INFO("SSL handshake failed : ~p", [Reason], ?LOGGER_META), send(State, "454 TLS negotiation failed\r\n"), State1 = handle_error(ssl_handshake_error, Reason, State), {ok, State1} end; false -> send(State, "500 Command unrecognized\r\n"), {ok, State} end; handle_request({<<"STARTTLS">> = C, <<>>}, State) -> send(State, "500 TLS already negotiated\r\n"), State1 = handle_error(out_of_order, C, State), {ok, State1}; handle_request({<<"STARTTLS">>, _Args}, State) -> send(State, "501 Syntax error (no parameters allowed)\r\n"), {ok, State}; handle_request({Verb, Args}, #state{module = Module, callbackstate = OldCallbackState} = State) -> CallbackState = case Module:handle_other(Verb, Args, OldCallbackState) of {noreply, CState1} -> CState1; {Message, CState1} -> send(State, [Message, "\r\n"]), CState1 end, {ok, State#state{callbackstate = CallbackState}}. %% @doc handle SASL client response to `334' challenge - RFC-4954 % the client sends a response to auth-cram-md5 handle_sasl( UserDigest, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {<<>>, <<>>}}, authdata = AuthData} = State ) -> case binstr:split(UserDigest, <<" ">>) of [Username, Digest] -> try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata = undefined}); _ -> % TODO error {ok, State#state{waitingauth = false, authdata = undefined}} end; % the client sends a \0username\0password response to auth-plain handle_sasl( UserPass, #state{waitingauth = 'plain', envelope = #envelope{auth = {<<>>, <<>>}}} = State ) -> case binstr:split(UserPass, <<0>>) of [_Identity, Username, Password] -> try_auth('plain', Username, Password, State); [Username, Password] -> try_auth('plain', Username, Password, State); _ -> % TODO error {ok, State#state{waitingauth = false}} end; % the client sends a username response to auth-login handle_sasl( Username, #state{waitingauth = 'login', envelope = #envelope{auth = {<<>>, <<>>}}} = State ) -> Envelope = State#state.envelope, % smtp_socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")), send(State, "334 UGFzc3dvcmQ6\r\n"), % store the provided username in envelope.auth NewState = State#state{envelope = Envelope#envelope{auth = {Username, <<>>}}}, {ok, NewState}; % the client sends a password response to auth-login handle_sasl( Password, #state{waitingauth = 'login', envelope = #envelope{auth = {Username, <<>>}}} = State ) -> try_auth('login', Username, Password, State). -spec handle_error(error_class(), any(), #state{}) -> #state{}. handle_error(Kind, Details, #state{module = Module, callbackstate = OldCallbackState} = State) -> case erlang:function_exported(Module, handle_error, 3) of true -> case Module:handle_error(Kind, Details, OldCallbackState) of {ok, CallbackState} -> State#state{callbackstate = CallbackState}; {stop, Reason, CallbackState} -> throw({stop, Reason, State#state{callbackstate = CallbackState}}) end; false -> State end. %% pa = parse address %% ab = angular brackets -record(pa, { quotes = false, ab = true, utf8 = false }). %% https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2 -spec parse_encoded_address(Address :: binary(), Utf8 :: boolean()) -> {binary(), binary()} | 'error'. parse_encoded_address(<<>>, _) -> % empty error; parse_encoded_address(<<"<@", Address/binary>>, Utf8) -> %% A-d-l (source route) - should be ignored case binstr:strchr(Address, $:) of 0 -> % invalid address error; Index -> parse_encoded_address(binstr:substr(Address, Index + 1), [], #pa{ quotes = false, ab = true, utf8 = Utf8 }) end; parse_encoded_address(<<"<", Address/binary>>, Utf8) -> parse_encoded_address(Address, [], #pa{quotes = false, ab = true, utf8 = Utf8}); parse_encoded_address(<<" ", Address/binary>>, Utf8) -> parse_encoded_address(Address, Utf8); parse_encoded_address(Address, Utf8) -> parse_encoded_address(Address, [], #pa{quotes = false, ab = false, utf8 = Utf8}). -spec parse_encoded_address(Address :: binary(), Acc :: list(), Flags :: #pa{}) -> {binary(), binary()} | 'error'. parse_encoded_address(<<>>, Acc, #pa{ab = false}) -> {unicode:characters_to_binary(lists:reverse(Acc)), <<>>}; parse_encoded_address(<<>>, _Acc, #pa{ab = true}) -> % began with angle brackets but didn't end with them error; parse_encoded_address(_, Acc, _) when length(Acc) > 320 -> % too long error; parse_encoded_address(<<"\\", H, Tail/binary>>, Acc, Flags) -> parse_encoded_address(Tail, [H | Acc], Flags); parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = false} = F) -> parse_encoded_address(Tail, Acc, F#pa{quotes = true}); parse_encoded_address(<<"\"", Tail/binary>>, Acc, #pa{quotes = true} = F) -> parse_encoded_address(Tail, Acc, F#pa{quotes = false}); parse_encoded_address(<<">", Tail/binary>>, Acc, #pa{quotes = false, ab = true}) -> {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; parse_encoded_address(<<">", _Tail/binary>>, _Acc, #pa{quotes = false, ab = false}) -> % ended with angle brackets but didn't begin with them error; parse_encoded_address(<<" ", Tail/binary>>, Acc, #pa{quotes = false, ab = false}) -> {unicode:characters_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; parse_encoded_address(<<" ", _Tail/binary>>, _Acc, #pa{quotes = false, ab = true}) -> % began with angle brackets but didn't end with them error; parse_encoded_address(<>, Acc, #pa{utf8 = true} = F) when H > 127 -> %% https://datatracker.ietf.org/doc/html/rfc6531#section-3.3 % UTF-8 above 7bit (when allowed) parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $0, H =< $9 -> % digits parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $@, H =< $Z -> % @ symbol and uppercase letters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H >= $a, H =< $z -> % lowercase letters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H =:= $-; H =:= $.; H =:= $_ -> % dash, dot, underscore parse_encoded_address(Tail, [H | Acc], F); % Allowed characters in the local name: ! # $ % & ' * + - / = ? ^ _ ` . { | } ~ parse_encoded_address(<>, Acc, #pa{quotes = false} = F) when H =:= $+; H =:= $!; H =:= $#; H =:= $$; H =:= $%; H =:= $&; H =:= $'; H =:= $*; H =:= $=; H =:= $/; H =:= $?; H =:= $^; H =:= $`; H =:= ${; H =:= $|; H =:= $}; H =:= $~ -> % other characters parse_encoded_address(Tail, [H | Acc], F); parse_encoded_address(_, _Acc, #pa{quotes = false}) -> error; parse_encoded_address(<>, Acc, #pa{quotes = true} = F) -> parse_encoded_address(Tail, [H | Acc], F). -spec has_extension(Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false'. has_extension(Extensions, Ext) -> ?LOG_DEBUG("extensions ~p", [Extensions], ?LOGGER_META), case proplists:get_value(Ext, Extensions) of undefined -> false; Value -> {true, Value} end. -spec try_auth( AuthType :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: #state{} ) -> {'ok', #state{}}. try_auth( AuthType, Username, Credential, #state{module = Module, envelope = Envelope, callbackstate = OldCallbackState} = State ) -> % clear out waiting auth NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {<<>>, <<>>}}}, case erlang:function_exported(Module, handle_AUTH, 4) of true -> case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of {ok, CallbackState} -> send(State, "235 Authentication successful.\r\n"), {ok, NewState#state{ callbackstate = CallbackState, envelope = Envelope#envelope{auth = {Username, Credential}} }}; _Other -> send(State, "535 Authentication failed.\r\n"), {ok, NewState} end; false -> ?LOG_WARNING( "Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions", ?LOGGER_META ), send(State, "535 authentication failed (#5.7.1)\r\n"), {ok, NewState} end. %get_digest_nonce() -> %A = [io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))], %B = [io_lib:format("~2.16.0b", [X]) || <> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))], %binary_to_list(base64:encode(lists:flatten(A ++ B))). %% @doc a tight loop to receive the message body receive_data(_Acc, _Transport, _Socket, _, Size, MaxSize, Session, _Options) when MaxSize =/= 'infinity', Size > MaxSize -> ?LOG_INFO("SMTP message body size ~B exceeded maximum allowed ~B", [Size, MaxSize], ?LOGGER_META), Session ! {receive_data, {error, size_exceeded}}; receive_data(Acc, Transport, Socket, RecvSize, Size, MaxSize, Session, Options) -> case Transport:recv(Socket, RecvSize, 1000) of {ok, Packet} when Acc =:= [] -> case check_bare_crlf( Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0 ) of error -> Session ! {receive_data, {error, bare_newline}}; FixedPacket -> case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of 0 -> ?LOG_DEBUG( "received ~B bytes; size is now ~p", [ RecvSize, Size + size(Packet) ], ?LOGGER_META ), ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META), receive_data( [FixedPacket | Acc], Transport, Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options ); Index -> String = binstr:substr(FixedPacket, 1, Index - 1), Rest = binstr:substr(FixedPacket, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end end; {ok, Packet} -> [Last | _] = Acc, case check_bare_crlf( Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0 ) of error -> Session ! {receive_data, {error, bare_newline}}; FixedPacket -> case binstr:strpos(FixedPacket, <<"\r\n.\r\n">>) of 0 -> ?LOG_DEBUG( "received ~B bytes; size is now ~p", [ RecvSize, Size + size(Packet) ], ?LOGGER_META ), ?LOG_DEBUG("memory usage: ~p", [erlang:process_info(self(), memory)], ?LOGGER_META), receive_data( [FixedPacket | Acc], Transport, Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options ); Index -> String = binstr:substr(FixedPacket, 1, Index - 1), Rest = binstr:substr(FixedPacket, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end end; {error, timeout} when RecvSize =:= 0, length(Acc) > 1 -> % check that we didn't accidentally receive a \r\n.\r\n split across 2 receives [A, B | Acc2] = Acc, Packet = list_to_binary([B, A]), case binstr:strpos(Packet, <<"\r\n.\r\n">>) of 0 -> % uh-oh ?LOG_DEBUG( "no data on socket, and no DATA terminator, retrying ~p", [ Session ], ?LOGGER_META ), % eventually we'll either get data or a different error, just keep retrying receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options); Index -> String = binstr:substr(Packet, 1, Index - 1), Rest = binstr:substr(Packet, Index + 5), ?LOG_DEBUG( "memory usage before flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Result = list_to_binary(lists:reverse([String | Acc2])), ?LOG_DEBUG( "memory usage after flattening: ~p", [ erlang:process_info(self(), memory) ], ?LOGGER_META ), Session ! {receive_data, Result, Rest} end; {error, timeout} -> receive_data(Acc, Transport, Socket, 0, Size, MaxSize, Session, Options); {error, Reason} -> ?LOG_WARNING("SMTP receive error: ~p", [Reason], ?LOGGER_META), Session ! {receive_data, {error, Reason}} end. check_for_bare_crlf(Bin, Offset) -> case { re:run(Bin, "(? true; {_, match} -> true; _ -> false end. fix_bare_crlf(Bin, Offset) -> Options = [{offset, Offset}, {return, binary}, global], re:replace(re:replace(Bin, "(? Options = [{offset, Offset}, {return, binary}, global], re:replace(re:replace(Bin, "(? Binary; check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, 0 = _Offset) when byte_size(Prev) > 0 -> % check if last character of previous was a CR Lastchar = binstr:substr(Prev, -1), case Lastchar of <<"\r">> -> % okay, check again for the rest check_bare_crlf(Bin, <<>>, Op, 1); % not fixing or ignoring them _ when Op == false -> error; _ -> % no dice check_bare_crlf(Bin, <<>>, Op, 0) end; check_bare_crlf(Binary, _Prev, Op, Offset) -> Last = binstr:substr(Binary, -1), % is the last character a CR? case Last of <<"\r">> -> % okay, the last character is a CR, we have to assume the next packet contains the corresponding LF NewBin = binstr:substr(Binary, 1, byte_size(Binary) - 1), case check_for_bare_crlf(NewBin, Offset) of true when Op == fix -> list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]); true when Op == strip -> list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]); true -> error; false -> Binary end; _ -> case check_for_bare_crlf(Binary, Offset) of true when Op == fix -> fix_bare_crlf(Binary, Offset); true when Op == strip -> strip_bare_crlf(Binary, Offset); true -> error; false -> Binary end end. send(#state{transport = Transport, socket = Sock} = St, Data) -> case Transport:send(Sock, Data) of ok -> ok; {error, Err} -> St1 = handle_error(send_error, Err, St), throw({stop, {send_error, Err}, St1}) end. setopts(#state{transport = Transport, socket = Sock} = St, Opts) -> case Transport:setopts(Sock, Opts) of ok -> ok; {error, Err} -> St1 = handle_error(setopts_error, Err, St), throw({stop, {setopts_error, Err}, St1}) end. hostname(Opts) -> proplists:get_value(hostname, Opts, smtp_util:guess_FQDN()). %% @hidden lhlo_if_lmtp(Protocol, Fallback) -> case Protocol == lmtp of true -> "LHLO"; 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_() -> [ {"Valid addresses should parse", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\\God@heaven.af.mil>">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God\"@heaven.af.mil>">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address( <<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>, false ) ), ?assertEqual( {<<"God2@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God+extension@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God~*$@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"">>, false) ), ?assertEqual( {<<"God~!#$%^&*()_+123@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God~!#$%^&*()_+123\"@heaven.af.mil>">>, false) ) end}, {"Addresses that are sorta valid should parse", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil ">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" God@heaven.af.mil ">>, false) ), ?assertEqual( {<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" ">>, false) ) end}, {"Addresses with UTF8 characters should parse only when allowed", fun() -> %% https://www.iana.org/domains/reserved ?assertEqual( {<<"испытание@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, true) ), ?assertEqual( {<<"測試@例子.測試"/utf8>>, <<>>}, parse_encoded_address(<<"<測試@例子.測試>"/utf8>>, true) ), ?assertEqual( {<<"испытание@пример.испытание"/utf8>>, <<"SIZE=100">>}, parse_encoded_address(<<"<испытание@пример.испытание> SIZE=100"/utf8>>, true) ), ?assertEqual( {<<"test@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<""/utf8>>, true) ), ?assertEqual( {<<"испытание!#¤½§´`<>@пример.испытание"/utf8>>, <<>>}, parse_encoded_address(<<"<\"испытание!#¤½§´`<>\"@пример.испытание>"/utf8>>, true) ), ?assertEqual( error, parse_encoded_address(<<"<испытание@пример.испытание>"/utf8>>, false) ) end}, {"Addresses containing unescaped <> that aren't at start/end should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<"<<">>, false)), ?assertEqual(error, parse_encoded_address(<<"">>, false)) end}, {"Address that begins with < but doesn't end with a > should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<">, false)), ?assertEqual(error, parse_encoded_address(<<">, false)) end}, {"Address that begins without < but ends with a > should fail", fun() -> ?assertEqual(error, parse_encoded_address(<<"God@heaven.af.mil>">>, false)) end}, {"Address longer than 320 characters should fail", fun() -> MegaAddress = list_to_binary( lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ), ?assertEqual(error, parse_encoded_address(MegaAddress, false)) end}, {"Address with an invalid route should fail", fun() -> ?assertEqual( error, parse_encoded_address(<<"<@gateway.af.mil God@heaven.af.mil>">>, false) ) end}, {"Empty addresses should parse OK", fun() -> ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<"<>">>, false)), ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<" <> ">>, false)) end}, {"Completely empty addresses are an error", fun() -> ?assertEqual(error, parse_encoded_address(<<"">>, false)), ?assertEqual(error, parse_encoded_address(<<" ">>, false)) end}, {"addresses with trailing parameters should return the trailing parameters", fun() -> ?assertEqual( {<<"God@heaven.af.mil">>, <<"SIZE=100 BODY=8BITMIME">>}, parse_encoded_address(<<" SIZE=100 BODY=8BITMIME">>, false) ) end} ]. parse_request_test_() -> [ {"Parsing normal SMTP requests", 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}, {"Verbs should be uppercased", fun() -> ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>)), ?assertEqual({<<"RSET">>, <<>>}, parse_request(<<"rset\r\n">>)) end}, {"Leading and trailing spaces are removed", fun() -> ?assertEqual( {<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo hell.af.mil ">>) ) end}, {"Blank lines are blank", fun() -> ?assertEqual({<<>>, <<>>}, parse_request(<<"">>)) end} ]. smtp_session_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {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}) -> {"A new connection should get a banner", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> ok end, ?assertMatch("220 localhost" ++ _Stuff, Packet) end} end, fun({CSock, _Pid}) -> {"A correct response to HELO", 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("250 localhost\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"An error in response to an invalid HELO", 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\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("501 Syntax: HELO hostname\r\n", Packet2) 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() -> 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 invalid\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("554 invalid hostname\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"A rejected EHLO", 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, "EHLO invalid\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("554 invalid hostname\r\n", Packet2) end} end, fun({CSock, _Pid}) -> {"EHLO 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, "EHLO 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, fun({CSock, _Pid}) -> {"Unsupported AUTH PLAIN", 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, "EHLO 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)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("502 Error: AUTH not implemented\r\n", Packet4) end} end, fun({CSock, _Pid}) -> {"Sending DATA", 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("250 localhost\r\n", Packet2), 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), 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"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"Sending with spaced MAIL FROM / 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, "HELO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 localhost\r\n", Packet2), 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), 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"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"Sending with UTF8 addresses and body", 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, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), receive {tcp, CSock, Packet31} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-SIZE" ++ _, Packet31), receive {tcp, CSock, Packet32} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-8BITMIME" ++ _, Packet32), receive {tcp, CSock, Packet33} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-PIPELINING" ++ _, Packet33), receive {tcp, CSock, Packet34} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 SMTPUTF8" ++ _, Packet34), smtp_socket:send( CSock, <<"MAIL FROM: <испытание@пример.испытание> SMTPUTF8\r\n"/utf8>> ), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 sender Ok" ++ _, Packet4), smtp_socket:send(CSock, <<"RCPT TO: <測試@例子.測試>\r\n"/utf8>>), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 recipient Ok" ++ _, Packet5), smtp_socket:send(CSock, "DATA\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet6), smtp_socket:send(CSock, <<"Subject: Я помню чудное мгновенье\r\n"/utf8>>), smtp_socket:send(CSock, <<"To: <測試@例子.測試>\r\n"/utf8>>), smtp_socket:send(CSock, <<"From: <испытание@пример.испытание>\r\n"/utf8>>), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, <<"Передо мной явилась ты"/utf8>>), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 queued as" ++ _, Packet7) end} end, % fun({CSock, _Pid}) -> % {"Sending DATA with a bare newline", % 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("250 localhost\r\n", Packet2), % 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, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % 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, "this\r\n"), % smtp_socket:send(CSock, "body\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "a\r\n"), % smtp_socket:send(CSock, "bare\n"), % smtp_socket:send(CSock, "newline\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, %fun({CSock, _Pid}) -> % {"Sending DATA with a bare CR", % 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("250 localhost\r\n", Packet2), % 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, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % 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, "this\r\n"), % smtp_socket:send(CSock, "\rbody\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "a\r\n"), % smtp_socket:send(CSock, "bare\r"), % smtp_socket:send(CSock, "CR\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, % fun({CSock, _Pid}) -> % {"Sending DATA with a bare newline in the headers", % 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("250 localhost\r\n", Packet2), % 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, "DATA\r\n"), % receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, % ?assertMatch("354 "++_, Packet5), % smtp_socket:send(CSock, "Subject: tls message\r\n"), % smtp_socket:send(CSock, "To: \n"), % smtp_socket:send(CSock, "From: \r\n"), % smtp_socket:send(CSock, "\r\n"), % smtp_socket:send(CSock, "this\r\n"), % smtp_socket:send(CSock, "body\r\n"), % smtp_socket:send(CSock, "has\r\n"), % smtp_socket:send(CSock, "no\r\n"), % smtp_socket:send(CSock, "bare\r\n"), % smtp_socket:send(CSock, "newlines\r\n"), % smtp_socket:send(CSock, "\r\n.\r\n"), % receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, % ?assertMatch("451 "++_, Packet6), % end % } % end, fun({CSock, _Pid}) -> {"Sending DATA with bare newline on first line of body", 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("250 localhost\r\n", Packet2), 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), smtp_socket:send(CSock, "Subject: tls message\r\n"), smtp_socket:send(CSock, "To: \n"), smtp_socket:send(CSock, "From: \r\n"), smtp_socket:send(CSock, "\r\n"), smtp_socket:send(CSock, "this\n"), smtp_socket:send(CSock, "body\r\n"), smtp_socket:send(CSock, "has\r\n"), smtp_socket:send(CSock, "no\r\n"), smtp_socket:send(CSock, "bare\r\n"), smtp_socket:send(CSock, "newlines\r\n"), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("451 " ++ _, Packet6) end} end ]}. lmtp_session_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {protocol, lmtp}, {callbackoptions, [ {protocol, lmtp}, {size, infinity} ]} ]}, {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, 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, "250 SMTPUTF8" ++ _} -> 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 ]}. smtp_session_auth_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{auth, true}]}]}, {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}) -> {"EHLO response includes AUTH", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)) end} end, fun({CSock, _Pid}) -> {"AUTH before EHLO is error", 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, "AUTH CRAZY\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("503 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"Unknown authentication type", 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, "EHLO 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-AUTH" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAZY\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("504 Unrecognized authentication type\r\n", Packet4) end} end, fun({CSock, _Pid}) -> {"A successful AUTH PLAIN", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful AUTH PLAIN with an identity", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful immediate AUTH PLAIN", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful immediate AUTH PLAIN with an identity", 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, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _R} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful immediate AUTH PLAIN", 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, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), String = binary_to_list(base64:encode("username\0username\0PaSSw0rd2")), smtp_socket:send(CSock, "AUTH PLAIN " ++ String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH PLAIN", 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, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), ?assertMatch("250-localhost\r\n", Packet2), Foo = fun(F, Acc) -> receive {tcp, CSock, "250-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH PLAIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334\r\n", Packet4), String = binary_to_list(base64:encode("\0username\0NotThePassword")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"A successful AUTH LOGIN", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH LOGIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), String = binary_to_list(base64:encode("username")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), PString = binary_to_list(base64:encode("PaSSw0rd")), smtp_socket:send(CSock, PString ++ "\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet6) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH LOGIN", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH LOGIN\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), String = binary_to_list(base64:encode("username2")), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), PString = binary_to_list(base64:encode("PaSSw0rd")), smtp_socket:send(CSock, PString ++ "\r\n"), receive {tcp, CSock, Packet6} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet6) end} end, fun({CSock, _Pid}) -> {"A successful AUTH CRAM-MD5", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAM-MD5\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 " ++ _, Packet4), ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), Seed = base64:decode_to_string(Seed64), Digest = smtp_util:compute_cram_digest("PaSSw0rd", Seed), String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("235 Authentication successful.\r\n", Packet5) end} end, fun({CSock, _Pid}) -> {"An unsuccessful AUTH CRAM-MD5", 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, "EHLO 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-AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 AUTH" ++ _Packet3} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "AUTH CRAM-MD5\r\n"), receive {tcp, CSock, Packet4} -> smtp_socket:active_once(CSock) end, ?assertMatch("334 " ++ _, Packet4), ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), Seed = base64:decode_to_string(Seed64), Digest = smtp_util:compute_cram_digest("Passw0rd", Seed), String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), smtp_socket:send(CSock, String ++ "\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("535 Authentication failed.\r\n", Packet5) end} end ]}. smtp_session_tls_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {tls_options, [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"} ]}, {callbackoptions, [{auth, true}]} ]}, {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}) -> {"EHLO response includes STARTTLS", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)) end} end, fun({CSock, _Pid}) -> {"STARTTLS does a SSL handshake", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, _Socket} = Result %smtp_socket:active_once(Socket), %ssl:send(Socket, "EHLO somehost.com\r\n"), %receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, %?assertEqual("Foo", Packet5), end} end, fun({CSock, _Pid}) -> {"After STARTTLS, EHLO doesn't report STARTTLS", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, Socket} = Result, smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", Packet5), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {ssl, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)) end} end, fun({CSock, _Pid}) -> {"After STARTTLS, re-negotiating STARTTLS is an error", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), Result = smtp_socket:to_ssl_client(CSock), ?assertMatch({ok, _Socket}, Result), {ok, Socket} = Result, smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", Packet5), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {ssl, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)), smtp_socket:send(Socket, "STARTTLS\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("500 " ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"STARTTLS can't take any parameters", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS foo\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("501 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"Negotiating STARTTLS twice is an error", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, _Packet} -> smtp_socket:active_once(CSock) end, smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, _Packet2} -> smtp_socket:active_once(CSock) end, ReadExtensions = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, ReadExtensions(ReadExtensions, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, _} -> ok end, {ok, Socket} = smtp_socket:to_ssl_client(CSock), smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), receive {ssl, Socket, PacketN} -> smtp_socket:active_once(Socket) end, ?assertMatch("250-localhost\r\n", PacketN), Bar = fun(F, Acc) -> receive {ssl, Socket, "250-STARTTLS" ++ _} -> smtp_socket:active_once(Socket), F(F, true); {ssl, Socket, "250-" ++ _} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 STARTTLS" ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), Acc; {tcp, Socket, _} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(false, Bar(Bar, false)), smtp_socket:send(Socket, "STARTTLS\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("500 " ++ _, Packet6) end} end, fun({CSock, _Pid}) -> {"STARTTLS can't take any parameters", 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, "EHLO 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-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "STARTTLS foo\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("501 " ++ _, Packet4) end} end, fun({CSock, _Pid}) -> {"After STARTTLS, message is received by server", fun() -> smtp_socket:active_once(CSock), receive {tcp, CSock, _Packet} -> smtp_socket:active_once(CSock) end, smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, _Packet2} -> smtp_socket:active_once(CSock) end, ReadExtensions = fun(F, Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, ReadExtensions(ReadExtensions, false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, _} -> ok end, {ok, Socket} = smtp_socket:to_ssl_client(CSock), smtp_socket:active_once(Socket), smtp_socket:send(Socket, "EHLO somehost.com\r\n"), ReadSSLExtensions = fun(F, Acc) -> receive {ssl, Socket, "250-" ++ _Rest} -> smtp_socket:active_once(Socket), F(F, Acc); {ssl, Socket, "250 " ++ _} -> smtp_socket:active_once(Socket), true; {ssl, Socket, _R} -> smtp_socket:active_once(Socket), error end end, ?assertEqual(true, ReadSSLExtensions(ReadSSLExtensions, false)), smtp_socket:send(Socket, "MAIL FROM:\r\n"), receive {ssl, Socket, Packet4} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet4), smtp_socket:send(Socket, "RCPT TO:\r\n"), receive {ssl, Socket, Packet5} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet5), smtp_socket:send(Socket, "DATA\r\n"), receive {ssl, Socket, Packet6} -> smtp_socket:active_once(Socket) end, ?assertMatch("354 " ++ _, Packet6), smtp_socket:send(Socket, "Subject: tls message\r\n"), smtp_socket:send(Socket, "To: \r\n"), smtp_socket:send(Socket, "From: \r\n"), smtp_socket:send(Socket, "\r\n"), smtp_socket:send(Socket, "message body"), smtp_socket:send(Socket, "\r\n.\r\n"), receive {ssl, Socket, Packet7} -> smtp_socket:active_once(Socket) end, ?assertMatch("250 " ++ _, Packet7) end} end ]}. smtp_session_tls_sni_test_() -> {foreach, local, fun() -> SniHosts = [ {"mx1.example.com", [ {keyfile, "test/fixtures/mx1.example.com-server.key"}, {certfile, "test/fixtures/mx1.example.com-server.crt"}, {cacertfile, "test/fixtures/root.crt"} ]}, {"mx2.example.com", [ {keyfile, "test/fixtures/mx2.example.com-server.key"}, {certfile, "test/fixtures/mx2.example.com-server.crt"}, {cacertfile, "test/fixtures/root.crt"} ]} ], application:ensure_all_started(gen_smtp), {ok, _} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [ {tls_options, [{sni_hosts, SniHosts}]}, {callbackoptions, [{auth, true}]} ]}, {domain, "localhost"}, {port, 9876} ] ), [Host || {Host, _} <- SniHosts] end, fun(_Hosts) -> gen_smtp_server:stop(gen_smtp_server) end, [fun strict_sni/1]}. strict_sni(Hosts) -> {"Do strict validation based on SNI", fun() -> [ begin {ok, CSock} = smtp_socket:connect(tcp, "localhost", 9876), smtp_socket:active_once(CSock), receive {tcp, CSock, Packet} -> smtp_socket:active_once(CSock) end, ?assertMatch("220 localhost" ++ _Stuff, Packet), smtp_socket:send(CSock, "EHLO somehost.com\r\n"), receive {tcp, CSock, Packet2} -> smtp_socket:active_once(CSock) end, ?assertMatch("250-localhost\r\n", Packet2), Foo = fun Foo(Acc) -> receive {tcp, CSock, "250-STARTTLS" ++ _} -> smtp_socket:active_once(CSock), Foo(true); {tcp, CSock, "250-" ++ _Packet3} -> smtp_socket:active_once(CSock), Foo(Acc); {tcp, CSock, "250 STARTTLS" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 " ++ _Packet3} -> smtp_socket:active_once(CSock), Acc; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(false)), smtp_socket:send(CSock, "STARTTLS\r\n"), receive {tcp, CSock, Packet4} -> ok end, ?assertMatch("220 " ++ _, Packet4), {ok, TlsSocket} = ssl:connect( CSock, [ {server_name_indication, Host}, {verify, verify_peer}, {cacertfile, "test/fixtures/root.crt"} ] ), %% Make sure server selects certificate based on SNI {ok, Cert} = ssl:peercert(TlsSocket), verify_cert_hostname(Cert, Host), smtp_socket:active_once(TlsSocket), smtp_socket:send(TlsSocket, "EHLO somehost.com\r\n"), receive {ssl, TlsSocket, Packet5} -> smtp_socket:active_once(TlsSocket) end, ?assertMatch("250-localhost\r\n", Packet5), ssl:close(TlsSocket) end || Host <- Hosts ] end}. verify_cert_hostname(BinCert, Host) -> DecCert = public_key:pkix_decode_cert(BinCert, otp), ?assert(public_key:pkix_verify_hostname(DecCert, [{dns_id, Host}])). stray_newline_test_() -> [ {"Error out by default", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"foo\n">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"fo\ro\n">>, <<>>, false, 0)), ?assertEqual(error, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, false, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, false, 0)), ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"foo\r">>, <<>>, false, 0)) end}, {"Fixing them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\n">>, <<>>, fix, 0)), ?assertEqual(<<"fo\r\no\r\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, fix, 0)), ?assertEqual(<<"fo\r\no\r\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, fix, 0)) end}, {"Stripping them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, strip, 0)), ?assertEqual(<<"foo">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, strip, 0)), ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, strip, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, strip, 0)) end}, {"Ignoring them should work", fun() -> ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, ignore, 0)), ?assertEqual(<<"fo\ro\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, ignore, 0)), ?assertEqual(<<"fo\ro\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, ignore, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, ignore, 0)) end}, {"Leading bare LFs should check the previous line", fun() -> ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)), ?assertEqual( <<"\r\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, fix, 0) ), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, fix, 0)), ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, strip, 0)), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, strip, 0)), ?assertEqual( <<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, ignore, 0) ), ?assertEqual(error, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, false, 0)), ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)) end} ]. smtp_session_maxsize_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{size, 100}]}]}, {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}) -> {"Message with ok size", 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, "EHLO 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 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), 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"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with too large size", 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, "EHLO 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 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), 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 message body message body message body message body" ), smtp_socket:send( CSock, "message body message body message body message body message body" ), smtp_socket:send(CSock, "\r\n.\r\n"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("552 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with ok size in FROM extension", 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, "EHLO 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 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=100\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3) end} end, fun({CSock, _Pid}) -> {"Message with not ok size in FROM extension", 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, "EHLO 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 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=101\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("552 " ++ _, Packet3) end} end ]}. smtp_session_nomaxsize_test_() -> {foreach, local, fun() -> application:ensure_all_started(gen_smtp), {ok, Pid} = gen_smtp_server:start( smtp_server_example, [ {sessionoptions, [{callbackoptions, [{size, infinity}]}]}, {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}) -> {"Message with no max size", 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, "EHLO 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; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _Data} -> smtp_socket:active_once(CSock), error 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, "DATA\r\n"), receive {tcp, CSock, Packet5} -> smtp_socket:active_once(CSock) end, ?assertMatch("354 " ++ _, Packet5), 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"), receive {tcp, CSock, Packet7} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet7) end} end, fun({CSock, _Pid}) -> {"Message with ok huge size in FROM extension", 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, "EHLO 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 100\r\n"} -> smtp_socket:active_once(CSock), F(F, true); {tcp, CSock, "250-SIZE" ++ _} -> error; {tcp, CSock, "250-" ++ _} -> smtp_socket:active_once(CSock), F(F, Acc); {tcp, CSock, "250 PIPELINING" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, "250 SMTPUTF8" ++ _} -> smtp_socket:active_once(CSock), true; {tcp, CSock, _} -> smtp_socket:active_once(CSock), error end end, ?assertEqual(true, Foo(Foo, false)), smtp_socket:send(CSock, "MAIL FROM: SIZE=100000000\r\n"), receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end, ?assertMatch("250 " ++ _, Packet3) end} end ]}. -endif.