%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. 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:<address>\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:<address>\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(<<H/utf8, Tail/binary>>, 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(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $0, H =< $9 ->
% digits
parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $@, H =< $Z ->
% @ symbol and uppercase letters
parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, Acc, #pa{quotes = false} = F) when H >= $a, H =< $z ->
% lowercase letters
parse_encoded_address(Tail, [H | Acc], F);
parse_encoded_address(<<H, Tail/binary>>, 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(<<H, Tail/binary>>, 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(<<H, Tail/binary>>, 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]) || <<X>> <= erlang:md5(integer_to_list(rand:uniform(4294967295)))],
%B = [io_lib:format("~2.16.0b", [X]) || <<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, "(?<!\r)\n", [{capture, none}, {offset, Offset}]),
re:run(Bin, "\r(?!\n)", [{capture, none}, {offset, Offset}])
}
of
{match, _} -> true;
{_, match} -> true;
_ -> false
end.
fix_bare_crlf(Bin, Offset) ->
Options = [{offset, Offset}, {return, binary}, global],
re:replace(re:replace(Bin, "(?<!\r)\n", "\r\n", Options), "\r(?!\n)", "\r\n", Options).
strip_bare_crlf(Bin, Offset) ->
Options = [{offset, Offset}, {return, binary}, global],
re:replace(re:replace(Bin, "(?<!\r)\n", "", Options), "\r(?!\n)", "", Options).
check_bare_crlf(Binary, _, ignore, _) ->
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(<<"<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(
<<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>, false
)
),
?assertEqual(
{<<"God2@heaven.af.mil">>, <<>>},
parse_encoded_address(<<"<God2@heaven.af.mil>">>, false)
),
?assertEqual(
{<<"God+extension@heaven.af.mil">>, <<>>},
parse_encoded_address(<<"<God+extension@heaven.af.mil>">>, false)
),
?assertEqual(
{<<"God~*$@heaven.af.mil">>, <<>>},
parse_encoded_address(<<"<God~*$@heaven.af.mil>">>, 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(<<" <God@heaven.af.mil> ">>, 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(<<"<test@пример.испытание>"/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(<<"<God<@heaven.af.mil>">>, false))
end},
{"Address that begins with < but doesn't end with a > should fail", fun() ->
?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil">>, false)),
?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil ">>, 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(<<"<God@heaven.af.mil> 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:<user@somehost.com>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<user@otherhost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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: <user@somehost.com>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO: <user@otherhost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% smtp_socket:send(CSock, "RCPT TO: <user@otherhost.com>\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: <user@otherhost>\r\n"),
% smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% smtp_socket:send(CSock, "RCPT TO: <user@otherhost.com>\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: <user@otherhost>\r\n"),
% smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> smtp_socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% smtp_socket:send(CSock, "RCPT TO: <user@otherhost.com>\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: <user@otherhost>\n"),
% smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@somehost.com>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<user@otherhost.com>\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: <user@otherhost>\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@otherhost>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<test1@somehost.com>\r\n"),
receive
{tcp, CSock, Packet4} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet4),
smtp_socket:send(CSock, "RCPT TO:<test2@somehost.com>\r\n"),
receive
{tcp, CSock, Packet5} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet5),
smtp_socket:send(CSock, "RCPT TO:<test3@somehost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@somehost.com>\r\n"),
receive
{ssl, Socket, Packet4} -> smtp_socket:active_once(Socket)
end,
?assertMatch("250 " ++ _, Packet4),
smtp_socket:send(Socket, "RCPT TO:<user@otherhost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(Socket, "From: <user@somehost.com>\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:<user@otherhost>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<test@somehost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@otherhost>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<test@somehost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@otherhost> 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:<user@otherhost> 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:<user@otherhost>\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3),
smtp_socket:send(CSock, "RCPT TO:<test@somehost.com>\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: <user@otherhost>\r\n"),
smtp_socket:send(CSock, "From: <user@somehost.com>\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:<user@otherhost> SIZE=100000000\r\n"),
receive
{tcp, CSock, Packet3} -> smtp_socket:active_once(CSock)
end,
?assertMatch("250 " ++ _, Packet3)
end}
end
]}.
-endif.