Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

2229 lines (2173 sloc) 90.065 kB
%%% 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).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.
-define(MAXIMUMSIZE, 10485760). %10mb
-define(BUILTIN_EXTENSIONS, [{"SIZE", "10485670"}, {"8BITMIME", true}, {"PIPELINING", true}]).
-define(TIMEOUT, 180000). % 3 minutes
%% External API
-export([start_link/3, start/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
code_change/3]).
-export([behaviour_info/1]).
-record(envelope,
{
from :: binary() | 'undefined',
to = [] :: [binary()],
data = <<>> :: binary(),
expectedsize = 0 :: pos_integer() | 0,
auth = {<<>>, <<>>} :: {binary(), binary()} % {"username", "password"}
}
).
-record(state,
{
socket = erlang:error({undefined, socket}) :: port() | tuple(),
module = erlang:error({undefined, module}) :: atom(),
envelope = undefined :: 'undefined' | #envelope{},
extensions = [] :: [{string(), string()}],
waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5',
authdata :: 'undefined' | binary(),
readmessage = false :: boolean(),
tls = false :: boolean(),
callbackstate :: any(),
options = [] :: [tuple()]
}
).
%% @hidden
-spec behaviour_info(atom()) -> [{atom(), non_neg_integer()}] | 'undefined'.
behaviour_info(callbacks) ->
[{init,4},
{terminate,2},
{code_change,3},
{handle_HELO,2},
{handle_EHLO,3},
{handle_MAIL,2},
{handle_MAIL_extension,2},
{handle_RCPT,2},
{handle_RCPT_extension,2},
{handle_DATA,4},
{handle_RSET,1},
{handle_VRFY,2},
{handle_other,3}];
behaviour_info(_Other) ->
undefined.
%% @doc Start a SMTP session linked to the calling process.
%% @see start/3
-spec(start_link/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
start_link(Socket, Module, Options) ->
gen_server:start_link(?MODULE, [Socket, Module, Options], []).
%% @doc Start a SMTP session. Arguments are `Socket' (probably opened via
%% `gen_smtp_server' or an analogue), which is an abstract socket implemented
%% via the `socket' module, `Module' is the name of the callback module
%% implementing the SMTP session behaviour that you'd like to use and `Options'
%% is the optional arguments provided by the accept server.
-spec(start/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
start(Socket, Module, Options) ->
gen_server:start(?MODULE, [Socket, Module, Options], []).
%% @private
-spec(init/1 :: (Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore').
init([Socket, Module, Options]) ->
{ok, {PeerName, _Port}} = socket:peername(Socket),
case Module:init(proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), proplists:get_value(sessioncount, Options, 0), PeerName, proplists:get_value(callbackoptions, Options, [])) of
{ok, Banner, CallbackState} ->
socket:send(Socket, ["220 ", Banner, "\r\n"]),
socket:active_once(Socket),
{ok, #state{socket = Socket, module = Module, options = Options, callbackstate = CallbackState}, ?TIMEOUT};
{stop, Reason, Message} ->
socket:send(Socket, [Message, "\r\n"]),
socket:close(Socket),
{stop, Reason};
ignore ->
socket:close(Socket),
ignore
end.
%% @hidden
-spec handle_call(Message :: any(), From :: {pid(), reference()}, #state{}) -> {'stop', 'normal', 'ok', #state{}} | {'reply', {'unknown_call', any()}, #state{}}.
handle_call(stop, _From, State) ->
{stop, normal, ok, State};
handle_call(Request, _From, State) ->
{reply, {unknown_call, Request}, State}.
%% @hidden
-spec handle_cast(Message :: any(), State :: #state{}) -> {'noreply', #state{}}.
handle_cast(_Msg, State) ->
{noreply, State}.
%% @hidden
-spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}.
handle_info({receive_data, {error, size_exceeded}}, #state{socket = Socket, readmessage = true} = State) ->
socket:send(Socket, "552 Message too large\r\n"),
socket:active_once(Socket),
{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
handle_info({receive_data, {error, bare_newline}}, #state{socket = Socket, readmessage = true} = State) ->
socket:send(Socket, "451 Bare newline detected\r\n"),
io:format("bare newline detected: ~p~n", [self()]),
socket:active_once(Socket),
{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
handle_info({receive_data, Body, Rest}, #state{socket = Socket, readmessage = true, envelope = Env, module=Module,
callbackstate = OldCallbackState, extensions = Extensions} = State) ->
% send the remainder of the data...
case Rest of
<<>> -> ok; % no remaining data
_ -> self() ! {socket:get_proto(Socket), Socket, Rest}
end,
socket:setopts(Socket, [{packet, line}]),
Envelope = Env#envelope{data = Body},% size = length(Body)},
Valid = case has_extension(Extensions, "SIZE") of
{true, Value} ->
case byte_size(Envelope#envelope.data) > list_to_integer(Value) of
true ->
socket:send(Socket, "552 Message too large\r\n"),
socket:active_once(Socket),
false;
false ->
true
end;
false ->
true
end,
case Valid of
true ->
case Module:handle_DATA(Envelope#envelope.from, Envelope#envelope.to, Envelope#envelope.data, OldCallbackState) of
{ok, Reference, CallbackState} ->
socket:send(Socket, io_lib:format("250 queued as ~s\r\n", [Reference])),
socket:active_once(Socket),
{noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
socket:active_once(Socket),
{noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT}
end;
false ->
% might not even be able to get here anymore...
{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}
end;
handle_info({_SocketType, Socket, Packet}, State) ->
case handle_request(parse_request(Packet), State) of
{ok, #state{extensions = Extensions, options = Options, readmessage = true} = NewState} ->
MaxSize = case has_extension(Extensions, "SIZE") of
{true, Value} ->
list_to_integer(Value);
false ->
?MAXIMUMSIZE
end,
Session = self(),
Size = 0,
socket:setopts(Socket, [{packet, raw}]),
spawn_opt(fun() -> receive_data([],
Socket, 0, Size, MaxSize, Session, Options) end,
[link, {fullsweep_after, 0}]),
{noreply, NewState, ?TIMEOUT};
{ok, NewState} ->
socket:active_once(NewState#state.socket),
{noreply, NewState, ?TIMEOUT};
{stop, Reason, NewState} ->
{stop, Reason, NewState}
end;
handle_info({tcp_closed, _Socket}, State) ->
{stop, normal, State};
handle_info({ssl_closed, _Socket}, State) ->
{stop, normal, State};
handle_info(timeout, #state{socket = Socket} = State) ->
socket:send(Socket, "421 Error: timeout exceeded\r\n"),
socket:close(Socket),
{stop, normal, State};
handle_info(Info, State) ->
io:format("unhandled info message ~p~n", [Info]),
{noreply, State}.
%% @hidden
-spec(terminate/2 :: (Reason :: any(), State :: #state{}) -> 'ok').
terminate(Reason, State) ->
socket:close(State#state.socket),
(State#state.module):terminate(Reason, State#state.callbackstate).
%% @hidden
-spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}.
code_change(OldVsn, #state{module = Module} = State, Extra) ->
% TODO - this should probably be the callback module's version or its checksum
CallbackState =
case catch Module:code_change(OldVsn, State#state.callbackstate, Extra) of
{ok, NewCallbackState} -> NewCallbackState;
_ -> State#state.callbackstate
end,
{ok, State#state{callbackstate = CallbackState}}.
-spec(parse_request/1 :: (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 ->
% io:format("got a ~s request~n", [Request]),
case binstr:to_upper(Request) of
<<"QUIT">> = Res -> {Res, <<>>};
<<"DATA">> = Res -> {Res, <<>>};
% likely a base64-encoded client reply
_ -> {Request, <<>>}
end;
Index ->
Verb = binstr:substr(Request, 1, Index - 1),
Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s),
%io:format("got a ~s request with parameters ~s~n", [Verb, Parameters]),
{binstr:to_upper(Verb), Parameters}
end.
-spec(handle_request/2 :: ({Verb :: binary(), Args :: binary()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}).
handle_request({<<>>, _Any}, #state{socket = Socket} = State) ->
socket:send(Socket, "500 Error: bad syntax\r\n"),
{ok, State};
handle_request({<<"HELO">>, <<>>}, #state{socket = Socket} = State) ->
socket:send(Socket, "501 Syntax: HELO hostname\r\n"),
{ok, State};
handle_request({<<"HELO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState} = State) ->
case Module:handle_HELO(Hostname, OldCallbackState) of
{ok, MaxSize, CallbackState} when is_integer(MaxSize) ->
socket:send(Socket,["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
{ok, State#state{extensions = [{"SIZE", integer_to_list(MaxSize)}], envelope = #envelope{}, callbackstate = CallbackState}};
{ok, CallbackState} ->
socket:send(Socket, ["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
{ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}
end;
handle_request({<<"EHLO">>, <<>>}, #state{socket = Socket} = State) ->
socket:send(Socket, "501 Syntax: EHLO hostname\r\n"),
{ok, State};
handle_request({<<"EHLO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) ->
case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of
{ok, Extensions, CallbackState} ->
case Extensions of
[] ->
socket:send(Socket, ["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
{ok, State#state{extensions = Extensions, callbackstate = CallbackState}};
_Else ->
F =
fun({E, true}, {Pos, Len, Acc}) when Pos =:= Len ->
{Pos, Len, [["250 ", E, "\r\n"] | Acc]};
({E, Value}, {Pos, Len, Acc}) when Pos =:= Len ->
{Pos, Len, [["250 ", E, " ", Value, "\r\n"] | Acc]};
({E, true}, {Pos, Len, Acc}) ->
{Pos+1, Len, [["250-", E, "\r\n"] | Acc]};
({E, Value}, {Pos, Len, Acc}) ->
{Pos+1, Len, [["250-", E, " ", Value , "\r\n"] | Acc]}
end,
Extensions2 = case Tls of
true ->
Extensions -- [{"STARTTLS", true}];
false ->
Extensions
end,
{_, _, Response} = lists:foldl(F, {1, length(Extensions2), [["250-", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]]}, Extensions2),
%?debugFmt("Respponse ~p~n", [lists:reverse(Response)]),
socket:send(Socket, lists:reverse(Response)),
{ok, State#state{extensions = Extensions2, envelope = #envelope{}, callbackstate = CallbackState}}
end;
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}
end;
handle_request({<<"AUTH">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
socket:send(Socket, "503 Error: send EHLO first\r\n"),
{ok, State};
handle_request({<<"AUTH">>, Args}, #state{socket = Socket, 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 ->
socket:send(Socket, "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 ->
socket:send(Socket, "504 Unrecognized authentication type\r\n"),
{ok, State};
true ->
case binstr:to_upper(AuthType) of
<<"LOGIN">> ->
% socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")),
socket:send(Socket, "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">> ->
socket:send(Socket, "334\r\n"),
{ok, State#state{waitingauth = 'plain', envelope = Envelope#envelope{auth = {<<>>, <<>>}}}};
<<"CRAM-MD5">> ->
crypto:start(), % ensure crypto is started, we're gonna need it
String = smtp_util:get_cram_string(proplists:get_value(hostname, Options, smtp_util:guess_FQDN())),
socket:send(Socket, ["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),
%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;
% the client sends a response to auth-cram-md5
handle_request({Username64, <<>>}, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {<<>>, <<>>}}, authdata = AuthData} = State) ->
case binstr:split(base64:decode(Username64), <<" ">>) 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_request({Username64, <<>>}, #state{waitingauth = 'plain', envelope = #envelope{auth = {<<>>,<<>>}}} = State) ->
case binstr:split(base64:decode(Username64), <<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_request({Username64, <<>>}, #state{socket = Socket, waitingauth = 'login', envelope = #envelope{auth = {<<>>,<<>>}}} = State) ->
Envelope = State#state.envelope,
Username = base64:decode(Username64),
% socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")),
socket:send(Socket, "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_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #envelope{auth = {Username,<<>>}}} = State) ->
Password = base64:decode(Password64),
try_auth('login', Username, Password, State);
handle_request({<<"MAIL">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
{ok, State};
handle_request({<<"MAIL">>, Args}, #state{socket = Socket, module = Module, envelope = Envelope, callbackstate = OldCallbackState, extensions = Extensions} = State) ->
case Envelope#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) of
error ->
socket:send(Socket, "501 Bad sender address syntax\r\n"),
{ok, State};
{ParsedAddress, <<>>} ->
%io:format("From address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
case Module:handle_MAIL(ParsedAddress, OldCallbackState) of
{ok, CallbackState} ->
socket:send(Socket, "250 sender Ok\r\n"),
{ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}
end;
{ParsedAddress, ExtraInfo} ->
%io:format("From address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
Options = [binstr:to_upper(X) || X <- binstr:split(ExtraInfo, <<" ">>)],
%io:format("options are ~p~n", [Options]),
F = fun(_, {error, Message}) ->
{error, Message};
(<<"SIZE=", Size/binary>>, InnerState) ->
case has_extension(Extensions, "SIZE") of
{true, Value} ->
case list_to_integer(binary_to_list(Size)) > list_to_integer(Value) of
true ->
{error, ["552 Estimated message length ", Size, " exceeds limit of ", Value, "\r\n"]};
false ->
InnerState#state{envelope = Envelope#envelope{expectedsize = list_to_integer(binary_to_list(Size))}}
end;
false ->
{error, "555 Unsupported option SIZE\r\n"}
end;
(<<"BODY=", _BodyType/binary>>, InnerState) ->
case has_extension(Extensions, "8BITMIME") of
{true, _} ->
InnerState;
false ->
{error, "555 Unsupported option BODY\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} ->
%io:format("error: ~s~n", [Message]),
socket:send(Socket, Message),
{ok, State};
NewState ->
%io:format("OK~n"),
case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of
{ok, CallbackState} ->
socket:send(Socket, "250 sender Ok\r\n"),
{ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, NewState#state{callbackstate = CallbackState}}
end
end
end;
_Else ->
socket:send(Socket, "501 Syntax: MAIL FROM:<address>\r\n"),
{ok, State}
end;
_Other ->
socket:send(Socket, "503 Error: Nested MAIL command\r\n"),
{ok, State}
end;
handle_request({<<"RCPT">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
socket:send(Socket, "503 Error: need MAIL command\r\n"),
{ok, State};
handle_request({<<"RCPT">>, Args}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = 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) of
error ->
socket:send(Socket, "501 Bad recipient address syntax\r\n"),
{ok, State};
{<<>>, _} ->
% empty rcpt to addresses aren't cool
socket:send(Socket, "501 Bad recipient address syntax\r\n"),
{ok, State};
{ParsedAddress, <<>>} ->
%io:format("To address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
case Module:handle_RCPT(ParsedAddress, OldCallbackState) of
{ok, CallbackState} ->
socket:send(Socket, "250 recipient Ok\r\n"),
{ok, State#state{envelope = Envelope#envelope{to = Envelope#envelope.to ++ [ParsedAddress]}, callbackstate = CallbackState}};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}
end;
{ParsedAddress, ExtraInfo} ->
% TODO - are there even any RCPT extensions?
io:format("To address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
socket:send(Socket, ["555 Unsupported option: ", ExtraInfo, "\r\n"]),
{ok, State}
end;
_Else ->
socket:send(Socket, "501 Syntax: RCPT TO:<address>\r\n"),
{ok, State}
end;
handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = undefined} = State) ->
socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
{ok, State};
handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = Envelope} = State) ->
case {Envelope#envelope.from, Envelope#envelope.to} of
{undefined, _} ->
socket:send(Socket, "503 Error: need MAIL command\r\n"),
{ok, State};
{_, []} ->
socket:send(Socket, "503 Error: need RCPT command\r\n"),
{ok, State};
_Else ->
socket:send(Socket, "354 enter mail, end with line containing only '.'\r\n"),
%io:format("switching to data read mode~n", []),
{ok, State#state{readmessage = true}}
end;
handle_request({<<"RSET">>, _Any}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) ->
socket:send(Socket, "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{socket = Socket} = State) ->
socket:send(Socket, "250 Ok\r\n"),
{ok, State};
handle_request({<<"QUIT">>, _Any}, #state{socket = Socket} = State) ->
socket:send(Socket, "221 Bye\r\n"),
{stop, normal, State};
handle_request({<<"VRFY">>, Address}, #state{module= Module, socket = Socket, callbackstate = OldCallbackState} = State) ->
case parse_encoded_address(Address) of
{ParsedAddress, <<>>} ->
case Module:handle_VRFY(ParsedAddress, OldCallbackState) of
{ok, Reply, CallbackState} ->
socket:send(Socket, ["250 ", Reply, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}};
{error, Message, CallbackState} ->
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}
end;
_Other ->
socket:send(Socket, "501 Syntax: VRFY username/address\r\n"),
{ok, State}
end;
handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket, tls=false, extensions = Extensions, options = Options} = State) ->
case has_extension(Extensions, "STARTTLS") of
{true, _} ->
socket:send(Socket, "220 OK\r\n"),
crypto:start(),
application:start(public_key),
application:start(ssl),
Options1 = case proplists:get_value(certfile, Options) of
undefined ->
[];
CertFile ->
[{certfile, CertFile}]
end,
Options2 = case proplists:get_value(keyfile, Options) of
undefined ->
Options1;
KeyFile ->
[{keyfile, KeyFile} | Options1]
end,
% TODO: certfile and keyfile should be at configurable locations
case socket:to_ssl_server(Socket, Options2, 5000) of
{ok, NewSocket} ->
%io:format("SSL negotiation sucessful~n"),
{ok, State#state{socket = NewSocket, envelope=undefined,
authdata=undefined, waitingauth=false, readmessage=false,
tls=true}};
{error, Reason} ->
io:format("SSL handshake failed : ~p~n", [Reason]),
socket:send(Socket, "454 TLS negotiation failed\r\n"),
{ok, State}
end;
false ->
socket:send(Socket, "500 Command unrecognized\r\n"),
{ok, State}
end;
handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket} = State) ->
socket:send(Socket, "500 TLS already negotiated\r\n"),
{ok, State};
handle_request({<<"STARTTLS">>, _Args}, #state{socket = Socket} = State) ->
socket:send(Socket, "501 Syntax error (no parameters allowed)\r\n"),
{ok, State};
handle_request({Verb, Args}, #state{socket = Socket, module = Module, callbackstate = OldCallbackState} = State) ->
{Message, CallbackState} = Module:handle_other(Verb, Args, OldCallbackState),
socket:send(Socket, [Message, "\r\n"]),
{ok, State#state{callbackstate = CallbackState}}.
-spec(parse_encoded_address/1 :: (Address :: binary()) -> {binary(), binary()} | 'error').
parse_encoded_address(<<>>) ->
error; % empty
parse_encoded_address(<<"<@", Address/binary>>) ->
case binstr:strchr(Address, $:) of
0 ->
error; % invalid address
Index ->
parse_encoded_address(binstr:substr(Address, Index + 1), [], {false, true})
end;
parse_encoded_address(<<"<", Address/binary>>) ->
parse_encoded_address(Address, [], {false, true});
parse_encoded_address(<<" ", Address/binary>>) ->
parse_encoded_address(Address);
parse_encoded_address(Address) ->
parse_encoded_address(Address, [], {false, false}).
-spec(parse_encoded_address/3 :: (Address :: binary(), Acc :: list(), Flags :: {boolean(), boolean()}) -> {binary(), binary()} | 'error').
parse_encoded_address(<<>>, Acc, {_Quotes, false}) ->
{list_to_binary(lists:reverse(Acc)), <<>>};
parse_encoded_address(<<>>, _Acc, {_Quotes, true}) ->
error; % began with angle brackets but didn't end with them
parse_encoded_address(_, Acc, _) when length(Acc) > 129 ->
error; % too long
parse_encoded_address(<<"\\", Tail/binary>>, Acc, Flags) ->
<<H, NewTail/binary>> = Tail,
parse_encoded_address(NewTail, [H | Acc], Flags);
parse_encoded_address(<<"\"", Tail/binary>>, Acc, {false, AB}) ->
parse_encoded_address(Tail, Acc, {true, AB});
parse_encoded_address(<<"\"", Tail/binary>>, Acc, {true, AB}) ->
parse_encoded_address(Tail, Acc, {false, AB});
parse_encoded_address(<<">", Tail/binary>>, Acc, {false, true}) ->
{list_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)};
parse_encoded_address(<<">", _Tail/binary>>, _Acc, {false, false}) ->
error; % ended with angle brackets but didn't begin with them
parse_encoded_address(<<" ", Tail/binary>>, Acc, {false, false}) ->
{list_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)};
parse_encoded_address(<<" ", _Tail/binary>>, _Acc, {false, true}) ->
error; % began with angle brackets but didn't end with them
parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $0, H =< $9 ->
parse_encoded_address(Tail, [H | Acc], {false, AB}); % digits
parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $@, H =< $Z ->
parse_encoded_address(Tail, [H | Acc], {false, AB}); % @ symbol and uppercase letters
parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $a, H =< $z ->
parse_encoded_address(Tail, [H | Acc], {false, AB}); % lowercase letters
parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H =:= $-; H =:= $.; H =:= $_ ->
parse_encoded_address(Tail, [H | Acc], {false, AB}); % dash, dot, underscore
% Allowed characters in the local name: ! # $ % & ' * + - / = ? ^ _ ` . { | } ~
parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H =:= $+;
H =:= $!; H =:= $#; H =:= $$; H =:= $%; H =:= $&; H =:= $'; H =:= $*; H =:= $=;
H =:= $/; H =:= $?; H =:= $^; H =:= $`; H =:= ${; H =:= $|; H =:= $}; H =:= $~ ->
parse_encoded_address(Tail, [H | Acc], {false, AB}); % other characters
parse_encoded_address(_, _Acc, {false, _AB}) ->
error;
parse_encoded_address(<<H, Tail/binary>>, Acc, Quotes) ->
parse_encoded_address(Tail, [H | Acc], Quotes).
-spec(has_extension/2 :: (Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false').
has_extension(Exts, Ext) ->
Extension = string:to_upper(Ext),
Extensions = [{string:to_upper(X), Y} || {X, Y} <- Exts],
%io:format("extensions ~p~n", [Extensions]),
case proplists:get_value(Extension, Extensions) of
undefined ->
false;
Value ->
{true, Value}
end.
-spec(try_auth/4 :: (AuthType :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: #state{}) -> {'ok', #state{}}).
try_auth(AuthType, Username, Credential, #state{module = Module, socket = Socket, 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} ->
socket:send(Socket, "235 Authentication successful.\r\n"),
{ok, NewState#state{callbackstate = CallbackState,
envelope = Envelope#envelope{auth = {Username, Credential}}}};
_Other ->
socket:send(Socket, "535 Authentication failed.\r\n"),
{ok, NewState}
end;
false ->
io:format("Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions~n"),
socket:send(Socket, "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(crypto:rand_uniform(0, 4294967295)))],
%B = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))],
%binary_to_list(base64:encode(lists:flatten(A ++ B))).
%% @doc a tight loop to receive the message body
receive_data(_Acc, _Socket, _, Size, MaxSize, Session, _Options) when MaxSize > 0, Size > MaxSize ->
io:format("message body size ~B exceeded maximum allowed ~B~n", [Size, MaxSize]),
Session ! {receive_data, {error, size_exceeded}};
receive_data(Acc, Socket, RecvSize, Size, MaxSize, Session, Options) ->
case socket: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 ->
%io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
%io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
receive_data([FixedPacket | Acc], Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options);
Index ->
String = binstr:substr(FixedPacket, 1, Index - 1),
Rest = binstr:substr(FixedPacket, Index+5),
%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
Result = list_to_binary(lists:reverse([String | Acc])),
%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
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 ->
%io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
%io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
receive_data([FixedPacket | Acc], Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options);
Index ->
String = binstr:substr(FixedPacket, 1, Index - 1),
Rest = binstr:substr(FixedPacket, Index+5),
%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
Result = list_to_binary(lists:reverse([String | Acc])),
%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
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
%io:format("no data on socket, and no DATA terminator, retrying ~p~n", [Session]),
% eventually we'll either get data or a different error, just keep retrying
receive_data(Acc, Socket, 0, Size, MaxSize, Session, Options);
Index ->
String = binstr:substr(Packet, 1, Index - 1),
Rest = binstr:substr(Packet, Index+5),
%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
Result = list_to_binary(lists:reverse([String | Acc2])),
%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
Session ! {receive_data, Result, Rest}
end;
{error, timeout} ->
receive_data(Acc, Socket, 0, Size, MaxSize, Session, Options);
{error, Reason} ->
io:format("receive error: ~p~n", [Reason]),
exit(receive_error)
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, Offset) when byte_size(Prev) > 0, Offset == 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);
_ when Op == false -> % not fixing or ignoring them
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.
-ifdef(TEST).
parse_encoded_address_test_() ->
[
{"Valid addresses should parse",
fun() ->
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God@heaven.af.mil>">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\\God@heaven.af.mil>">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God\"@heaven.af.mil>">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>)),
?assertEqual({<<"God2@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God2@heaven.af.mil>">>)),
?assertEqual({<<"God+extension@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God+extension@heaven.af.mil>">>)),
?assertEqual({<<"God~*$@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God~*$@heaven.af.mil>">>))
end
},
{"Addresses that are sorta valid should parse",
fun() ->
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil ">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" God@heaven.af.mil ">>)),
?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" <God@heaven.af.mil> ">>))
end
},
{"Addresses containing unescaped <> that aren't at start/end should fail",
fun() ->
?assertEqual(error, parse_encoded_address(<<"<<">>)),
?assertEqual(error, parse_encoded_address(<<"<God<@heaven.af.mil>">>))
end
},
{"Address that begins with < but doesn't end with a > should fail",
fun() ->
?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil">>)),
?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil ">>))
end
},
{"Address that begins without < but ends with a > should fail",
fun() ->
?assertEqual(error, parse_encoded_address(<<"God@heaven.af.mil>">>))
end
},
{"Address longer than 129 character 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)),
?assertEqual(error, parse_encoded_address(MegaAddress))
end
},
{"Address with an invalid route should fail",
fun() ->
?assertEqual(error, parse_encoded_address(<<"<@gateway.af.mil God@heaven.af.mil>">>))
end
},
{"Empty addresses should parse OK",
fun() ->
?assertEqual({<<>>, <<>>}, parse_encoded_address(<<"<>">>)),
?assertEqual({<<>>, <<>>}, parse_encoded_address(<<" <> ">>))
end
},
{"Completely empty addresses are an error",
fun() ->
?assertEqual(error, parse_encoded_address(<<"">>)),
?assertEqual(error, parse_encoded_address(<<" ">>))
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">>))
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({<<"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">>))
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() ->
Self = self(),
spawn(fun() ->
{ok, ListenSock} = socket:listen(tcp, 9876, [binary]),
{ok, X} = socket:accept(ListenSock),
socket:controlling_process(X, Self),
Self ! X
end),
{ok, CSock} = socket:connect(tcp, "localhost", 9876),
receive
SSock when is_port(SSock) ->
ok
end,
{ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}]),
socket:controlling_process(SSock, Pid),
{CSock, Pid}
end,
fun({CSock, _Pid}) ->
socket:close(CSock)
end,
[fun({CSock, _Pid}) ->
{"A new connection should get a banner",
fun() ->
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() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "HELO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> 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() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "HELO\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("501 Syntax: HELO hostname\r\n", Packet2)
end
}
end,
fun({CSock, _Pid}) ->
{"A rejected HELO",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "HELO invalid\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("554 invalid hostname\r\n", Packet2)
end
}
end,
fun({CSock, _Pid}) ->
{"A rejected EHLO",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO invalid\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("554 invalid hostname\r\n", Packet2)
end
}
end,
fun({CSock, _Pid}) ->
{"EHLO response",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F) ->
receive
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F);
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
ok;
{tcp, CSock, _R} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(ok, Foo(Foo))
end
}
end,
fun({CSock, _Pid}) ->
{"Unsupported AUTH PLAIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F) ->
receive
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F);
{tcp, CSock, "250"++_Packet3} ->
socket:active_once(CSock),
ok;
{tcp, CSock, _R} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(ok, Foo(Foo)),
socket:send(CSock, "AUTH PLAIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("502 Error: AUTH not implemented\r\n", Packet4)
end
}
end,
fun({CSock, _Pid}) ->
{"Sending DATA",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "HELO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250 localhost\r\n", Packet2),
socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
?assertMatch("250 "++_, Packet3),
socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("250 "++_, Packet4),
socket:send(CSock, "DATA\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("354 "++_, Packet5),
socket:send(CSock, "Subject: tls message\r\n"),
socket:send(CSock, "To: <user@otherhost>\r\n"),
socket:send(CSock, "From: <user@somehost.com>\r\n"),
socket:send(CSock, "\r\n"),
socket:send(CSock, "message body"),
socket:send(CSock, "\r\n.\r\n"),
receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
?assertMatch("250 queued as"++_, Packet6)
end
}
end,
% fun({CSock, _Pid}) ->
% {"Sending DATA with a bare newline",
% fun() ->
% socket:active_once(CSock),
% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
% ?assertMatch("220 localhost"++_Stuff, Packet),
% socket:send(CSock, "HELO somehost.com\r\n"),
% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
% ?assertMatch("250 localhost\r\n", Packet2),
% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet4),
% socket:send(CSock, "DATA\r\n"),
% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
% ?assertMatch("354 "++_, Packet5),
% socket:send(CSock, "Subject: tls message\r\n"),
% socket:send(CSock, "To: <user@otherhost>\r\n"),
% socket:send(CSock, "From: <user@somehost.com>\r\n"),
% socket:send(CSock, "\r\n"),
% socket:send(CSock, "this\r\n"),
% socket:send(CSock, "body\r\n"),
% socket:send(CSock, "has\r\n"),
% socket:send(CSock, "a\r\n"),
% socket:send(CSock, "bare\n"),
% socket:send(CSock, "newline\r\n"),
% socket:send(CSock, "\r\n.\r\n"),
% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
% ?assertMatch("451 "++_, Packet6),
% end
% }
% end,
%fun({CSock, _Pid}) ->
% {"Sending DATA with a bare CR",
% fun() ->
% socket:active_once(CSock),
% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
% ?assertMatch("220 localhost"++_Stuff, Packet),
% socket:send(CSock, "HELO somehost.com\r\n"),
% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
% ?assertMatch("250 localhost\r\n", Packet2),
% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet4),
% socket:send(CSock, "DATA\r\n"),
% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
% ?assertMatch("354 "++_, Packet5),
% socket:send(CSock, "Subject: tls message\r\n"),
% socket:send(CSock, "To: <user@otherhost>\r\n"),
% socket:send(CSock, "From: <user@somehost.com>\r\n"),
% socket:send(CSock, "\r\n"),
% socket:send(CSock, "this\r\n"),
% socket:send(CSock, "\rbody\r\n"),
% socket:send(CSock, "has\r\n"),
% socket:send(CSock, "a\r\n"),
% socket:send(CSock, "bare\r"),
% socket:send(CSock, "CR\r\n"),
% socket:send(CSock, "\r\n.\r\n"),
% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
% ?assertMatch("451 "++_, Packet6),
% end
% }
% end,
% fun({CSock, _Pid}) ->
% {"Sending DATA with a bare newline in the headers",
% fun() ->
% socket:active_once(CSock),
% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
% ?assertMatch("220 localhost"++_Stuff, Packet),
% socket:send(CSock, "HELO somehost.com\r\n"),
% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
% ?assertMatch("250 localhost\r\n", Packet2),
% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet3),
% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
% ?assertMatch("250 "++_, Packet4),
% socket:send(CSock, "DATA\r\n"),
% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
% ?assertMatch("354 "++_, Packet5),
% socket:send(CSock, "Subject: tls message\r\n"),
% socket:send(CSock, "To: <user@otherhost>\n"),
% socket:send(CSock, "From: <user@somehost.com>\r\n"),
% socket:send(CSock, "\r\n"),
% socket:send(CSock, "this\r\n"),
% socket:send(CSock, "body\r\n"),
% socket:send(CSock, "has\r\n"),
% socket:send(CSock, "no\r\n"),
% socket:send(CSock, "bare\r\n"),
% socket:send(CSock, "newlines\r\n"),
% socket:send(CSock, "\r\n.\r\n"),
% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
% ?assertMatch("451 "++_, Packet6),
% end
% }
% end,
fun({CSock, _Pid}) ->
{"Sending DATA with bare newline on first line of body",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "HELO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250 localhost\r\n", Packet2),
socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
?assertMatch("250 "++_, Packet3),
socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("250 "++_, Packet4),
socket:send(CSock, "DATA\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("354 "++_, Packet5),
socket:send(CSock, "Subject: tls message\r\n"),
socket:send(CSock, "To: <user@otherhost>\n"),
socket:send(CSock, "From: <user@somehost.com>\r\n"),
socket:send(CSock, "\r\n"),
socket:send(CSock, "this\n"),
socket:send(CSock, "body\r\n"),
socket:send(CSock, "has\r\n"),
socket:send(CSock, "no\r\n"),
socket:send(CSock, "bare\r\n"),
socket:send(CSock, "newlines\r\n"),
socket:send(CSock, "\r\n.\r\n"),
receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
?assertMatch("451 "++_, Packet6)
end
}
end
]
}.
smtp_session_auth_test_() ->
{foreach,
local,
fun() ->
Self = self(),
spawn(fun() ->
{ok, ListenSock} = socket:listen(tcp, 9876, [binary]),
{ok, X} = socket:accept(ListenSock),
socket:controlling_process(X, Self),
Self ! X
end),
{ok, CSock} = socket:connect(tcp, "localhost", 9876),
receive
SSock when is_port(SSock) ->
ok
end,
{ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}, {callbackoptions, [{auth, true}]}]),
socket:controlling_process(SSock, Pid),
{CSock, Pid}
end,
fun({CSock, _Pid}) ->
socket:close(CSock)
end,
[fun({CSock, _Pid}) ->
{"EHLO response includes AUTH",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false))
end
}
end,
fun({CSock, _Pid}) ->
{"AUTH before EHLO is error",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "AUTH CRAZY\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("503 "++_, Packet4)
end
}
end,
fun({CSock, _Pid}) ->
{"Unknown authentication type",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH CRAZY\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("504 Unrecognized authentication type\r\n", Packet4)
end
}
end,
fun({CSock, _Pid}) ->
{"A successful AUTH PLAIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH PLAIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("334\r\n", Packet4),
String = binary_to_list(base64:encode("\0username\0PaSSw0rd")),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> 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() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH PLAIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("334\r\n", Packet4),
String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("235 Authentication successful.\r\n", Packet5)
end
}
end,
fun({CSock, _Pid}) ->
{"A successful immediate AUTH PLAIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
String = binary_to_list(base64:encode("\0username\0PaSSw0rd")),
socket:send(CSock, "AUTH PLAIN "++String++"\r\n"),
receive {tcp, CSock, Packet5} -> 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() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> 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} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _R} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")),
socket:send(CSock, "AUTH PLAIN "++String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("235 Authentication successful.\r\n", Packet5)
end
}
end,
fun({CSock, _Pid}) ->
{"An unsuccessful immediate AUTH PLAIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> 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} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
String = binary_to_list(base64:encode("username\0username\0PaSSw0rd2")),
socket:send(CSock, "AUTH PLAIN "++String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("535 Authentication failed.\r\n", Packet5)
end
}
end,
fun({CSock, _Pid}) ->
{"An unsuccessful AUTH PLAIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> 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} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH PLAIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("334\r\n", Packet4),
String = binary_to_list(base64:encode("\0username\0NotThePassword")),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("535 Authentication failed.\r\n", Packet5)
end
}
end,
fun({CSock, _Pid}) ->
{"A successful AUTH LOGIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH LOGIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4),
String = binary_to_list(base64:encode("username")),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5),
PString = binary_to_list(base64:encode("PaSSw0rd")),
socket:send(CSock, PString++"\r\n"),
receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
?assertMatch("235 Authentication successful.\r\n", Packet6)
end
}
end,
fun({CSock, _Pid}) ->
{"An unsuccessful AUTH LOGIN",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH LOGIN\r\n"),
receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4),
String = binary_to_list(base64:encode("username2")),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5),
PString = binary_to_list(base64:encode("PaSSw0rd")),
socket:send(CSock, PString++"\r\n"),
receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
?assertMatch("535 Authentication failed.\r\n", Packet6)
end
}
end,
fun({CSock, _Pid}) ->
{"A successful AUTH CRAM-MD5",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH CRAM-MD5\r\n"),
receive {tcp, CSock, Packet4} -> 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]))),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("235 Authentication successful.\r\n", Packet5)
end
}
end,
fun({CSock, _Pid}) ->
{"An unsuccessful AUTH CRAM-MD5",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-AUTH"++_Packet3} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 AUTH"++_Packet3} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "AUTH CRAM-MD5\r\n"),
receive {tcp, CSock, Packet4} -> 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]))),
socket:send(CSock, String++"\r\n"),
receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
?assertMatch("535 Authentication failed.\r\n", Packet5)
end
}
end
]
}.
smtp_session_tls_test_() ->
{foreach,
local,
fun() ->
crypto:start(),
application:start(public_key),
application:start(ssl),
Self = self(),
spawn(fun() ->
{ok, ListenSock} = socket:listen(tcp, 9876, [binary]),
{ok, X} = socket:accept(ListenSock),
socket:controlling_process(X, Self),
Self ! X
end),
{ok, CSock} = socket:connect(tcp, "localhost", 9876),
receive
SSock when is_port(SSock) ->
ok
end,
{ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{keyfile, "../testdata/server.key"}, {certfile, "../testdata/server.crt"}, {hostname, "localhost"}, {sessioncount, 1}, {callbackoptions, [{auth, true}]}]),
socket:controlling_process(SSock, Pid),
{CSock, Pid}
end,
fun({CSock, _Pid}) ->
socket:close(CSock)
end,
[fun({CSock, _Pid}) ->
{"EHLO response includes STARTTLS",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false))
end
}
end,
fun({CSock, _Pid}) ->
{"STARTTLS does a SSL handshake",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "STARTTLS\r\n"),
receive {tcp, CSock, Packet4} -> ok end,
?assertMatch("220 "++_, Packet4),
Result = socket:to_ssl_client(CSock),
?assertMatch({ok, _Socket}, Result),
{ok, _Socket} = Result
%socket:active_once(Socket),
%ssl:send(Socket, "EHLO somehost.com\r\n"),
%receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end,
%?assertEqual("Foo", Packet5),
end
}
end,
fun({CSock, _Pid}) ->
{"After STARTTLS, EHLO doesn't report STARTTLS",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "STARTTLS\r\n"),
receive {tcp, CSock, Packet4} -> ok end,
?assertMatch("220 "++_, Packet4),
Result = socket:to_ssl_client(CSock),
?assertMatch({ok, _Socket}, Result),
{ok, Socket} = Result,
socket:active_once(Socket),
socket:send(Socket, "EHLO somehost.com\r\n"),
receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end,
?assertMatch("250-localhost\r\n", Packet5),
Bar = fun(F, Acc) ->
receive
{ssl, Socket, "250-STARTTLS"++_} ->
socket:active_once(Socket),
F(F, true);
{ssl, Socket, "250-"++_} ->
socket:active_once(Socket),
F(F, Acc);
{ssl, Socket, "250 STARTTLS"++_} ->
socket:active_once(Socket),
true;
{ssl, Socket, "250 "++_} ->
socket:active_once(Socket),
Acc;
{ssl, Socket, _} ->
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() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
socket:send(CSock, "STARTTLS\r\n"),
receive {tcp, CSock, Packet4} -> ok end,
?assertMatch("220 "++_, Packet4),
Result = socket:to_ssl_client(CSock),
?assertMatch({ok, _Socket}, Result),
{ok, Socket} = Result,
socket:active_once(Socket),
socket:send(Socket, "EHLO somehost.com\r\n"),
receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end,
?assertMatch("250-localhost\r\n", Packet5),
Bar = fun(F, Acc) ->
receive
{ssl, Socket, "250-STARTTLS"++_} ->
socket:active_once(Socket),
F(F, true);
{ssl, Socket, "250-"++_} ->
socket:active_once(Socket),
F(F, Acc);
{ssl, Socket, "250 STARTTLS"++_} ->
socket:active_once(Socket),
true;
{ssl, Socket, "250 "++_} ->
socket:active_once(Socket),
Acc;
{ssl, Socket, _} ->
socket:active_once(Socket),
error
end
end,
?assertEqual(false, Bar(Bar, false)),
socket:send(Socket, "STARTTLS\r\n"),
receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end,
?assertMatch("500 "++_, Packet6)
end
}
end,
fun({CSock, _Pid}) ->
{"STARTTLS can't take any parameters",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
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() ->
socket:active_once(CSock),
receive {tcp, CSock, _Packet} -> socket:active_once(CSock) end,
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, _Packet2} -> socket:active_once(CSock) end,
ReadExtensions = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, ReadExtensions(ReadExtensions, false)),
socket:send(CSock, "STARTTLS\r\n"),
receive {tcp, CSock, _} -> ok end,
{ok, Socket} = socket:to_ssl_client(CSock),
socket:active_once(Socket),
socket:send(Socket, "EHLO somehost.com\r\n"),
receive {ssl, Socket, PacketN} -> socket:active_once(Socket) end,
?assertMatch("250-localhost\r\n", PacketN),
Bar = fun(F, Acc) ->
receive
{ssl, Socket, "250-STARTTLS"++_} ->
socket:active_once(Socket),
F(F, true);
{ssl, Socket, "250-"++_} ->
socket:active_once(Socket),
F(F, Acc);
{ssl, Socket, "250 STARTTLS"++_} ->
socket:active_once(Socket),
true;
{ssl, Socket, "250 "++_} ->
socket:active_once(Socket),
Acc;
{tcp, Socket, _} ->
socket:active_once(Socket),
error
end
end,
?assertEqual(false, Bar(Bar, false)),
socket:send(Socket, "STARTTLS\r\n"),
receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end,
?assertMatch("500 "++_, Packet6)
end
}
end,
fun({CSock, _Pid}) ->
{"STARTTLS can't take any parameters",
fun() ->
socket:active_once(CSock),
receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
?assertMatch("220 localhost"++_Stuff, Packet),
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
?assertMatch("250-localhost\r\n", Packet2),
Foo = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, Foo(Foo, false)),
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() ->
socket:active_once(CSock),
receive {tcp, CSock, _Packet} -> socket:active_once(CSock) end,
socket:send(CSock, "EHLO somehost.com\r\n"),
receive {tcp, CSock, _Packet2} -> socket:active_once(CSock) end,
ReadExtensions = fun(F, Acc) ->
receive
{tcp, CSock, "250-STARTTLS"++_} ->
socket:active_once(CSock),
F(F, true);
{tcp, CSock, "250-"++_Packet3} ->
socket:active_once(CSock),
F(F, Acc);
{tcp, CSock, "250 STARTTLS"++_} ->
socket:active_once(CSock),
true;
{tcp, CSock, "250 "++_Packet3} ->
socket:active_once(CSock),
Acc;
{tcp, CSock, _} ->
socket:active_once(CSock),
error
end
end,
?assertEqual(true, ReadExtensions(ReadExtensions, false)),
socket:send(CSock, "STARTTLS\r\n"),
receive {tcp, CSock, _} -> ok end,
{ok, Socket} = socket:to_ssl_client(CSock),
socket:active_once(Socket),
socket:send(Socket, "EHLO somehost.com\r\n"),
ReadSSLExtensions = fun(F, Acc) ->
receive
{ssl, Socket, "250-"++_Rest} ->
socket:active_once(Socket),
F(F, Acc);
{ssl, Socket, "250 "++_} ->
socket:active_once(Socket),
true;
{ssl, Socket, _R} ->
socket:active_once(Socket),
error
end
end,
?assertEqual(true, ReadSSLExtensions(ReadSSLExtensions, false)),
socket:send(Socket, "MAIL FROM: <user@somehost.com>\r\n"),
receive {ssl, Socket, Packet4} -> socket:active_once(Socket) end,
?assertMatch("250 "++_, Packet4),
socket:send(Socket, "RCPT TO: <user@otherhost.com>\r\n"),
receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end,
?assertMatch("250 "++_, Packet5),
socket:send(Socket, "DATA\r\n"),
receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end,
?assertMatch("354 "++_, Packet6),
socket:send(Socket, "Subject: tls message\r\n"),
socket:send(Socket, "To: <user@otherhost>\r\n"),
socket:send(Socket, "From: <user@somehost.com>\r\n"),
socket:send(Socket, "\r\n"),
socket:send(Socket, "message body"),
socket:send(Socket, "\r\n.\r\n"),
receive {ssl, Socket, Packet7} -> socket:active_once(Socket) end,
?assertMatch("250 "++_, Packet7)
end
}
end
]
}.
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
}
].
-endif.
Jump to Line
Something went wrong with that request. Please try again.