/deps/gen_smtp/src/gen_smtp_server_session.erl
Erlang | 2228 lines | 1962 code | 55 blank | 211 comment | 9 complexity | 52f8623d50fba83d7192aa47de908b6b MD5 | raw file
1%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved. 2%%% 3%%% Redistribution and use in source and binary forms, with or without 4%%% modification, are permitted provided that the following conditions are met: 5%%% 6%%% 1. Redistributions of source code must retain the above copyright notice, 7%%% this list of conditions and the following disclaimer. 8%%% 2. Redistributions in binary form must reproduce the above copyright 9%%% notice, this list of conditions and the following disclaimer in the 10%%% documentation and/or other materials provided with the distribution. 11%%% 12%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR 13%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 14%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 15%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 16%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 17%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 19%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 20%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 21%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 23%% @doc Process representing a SMTP session, extensible via a callback module. This 24%% module is implemented as a behaviour that the callback module should 25%% implement. To see the details of the required callback functions to provide, 26%% please see `smtp_server_example'. 27%% @see smtp_server_example 28 29-module(gen_smtp_server_session). 30-behaviour(gen_server). 31 32-ifdef(TEST). 33-include_lib("eunit/include/eunit.hrl"). 34-endif. 35 36-define(MAXIMUMSIZE, 10485760). %10mb 37-define(BUILTIN_EXTENSIONS, [{"SIZE", "10485670"}, {"8BITMIME", true}, {"PIPELINING", true}]). 38-define(TIMEOUT, 180000). % 3 minutes 39 40%% External API 41-export([start_link/3, start/3]). 42 43%% gen_server callbacks 44-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, 45 code_change/3]). 46 47-export([behaviour_info/1]). 48 49-record(envelope, 50 { 51 from :: binary() | 'undefined', 52 to = [] :: [binary()], 53 data = <<>> :: binary(), 54 expectedsize = 0 :: pos_integer() | 0, 55 auth = {<<>>, <<>>} :: {binary(), binary()} % {"username", "password"} 56 } 57). 58 59-record(state, 60 { 61 socket = erlang:error({undefined, socket}) :: port() | tuple(), 62 module = erlang:error({undefined, module}) :: atom(), 63 envelope = undefined :: 'undefined' | #envelope{}, 64 extensions = [] :: [{string(), string()}], 65 waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5', 66 authdata :: 'undefined' | binary(), 67 readmessage = false :: boolean(), 68 tls = false :: boolean(), 69 callbackstate :: any(), 70 options = [] :: [tuple()] 71 } 72). 73 74%% @hidden 75-spec behaviour_info(atom()) -> [{atom(), non_neg_integer()}] | 'undefined'. 76behaviour_info(callbacks) -> 77 [{init,4}, 78 {terminate,2}, 79 {code_change,3}, 80 {handle_HELO,2}, 81 {handle_EHLO,3}, 82 {handle_MAIL,2}, 83 {handle_MAIL_extension,2}, 84 {handle_RCPT,2}, 85 {handle_RCPT_extension,2}, 86 {handle_DATA,4}, 87 {handle_RSET,1}, 88 {handle_VRFY,2}, 89 {handle_other,3}]; 90behaviour_info(_Other) -> 91 undefined. 92 93%% @doc Start a SMTP session linked to the calling process. 94%% @see start/3 95-spec(start_link/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}). 96start_link(Socket, Module, Options) -> 97 gen_server:start_link(?MODULE, [Socket, Module, Options], []). 98 99%% @doc Start a SMTP session. Arguments are `Socket' (probably opened via 100%% `gen_smtp_server' or an analogue), which is an abstract socket implemented 101%% via the `socket' module, `Module' is the name of the callback module 102%% implementing the SMTP session behaviour that you'd like to use and `Options' 103%% is the optional arguments provided by the accept server. 104-spec(start/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}). 105start(Socket, Module, Options) -> 106 gen_server:start(?MODULE, [Socket, Module, Options], []). 107 108%% @private 109-spec(init/1 :: (Args :: list()) -> {'ok', #state{}, ?TIMEOUT} | {'stop', any()} | 'ignore'). 110init([Socket, Module, Options]) -> 111 {ok, {PeerName, _Port}} = socket:peername(Socket), 112 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 113 {ok, Banner, CallbackState} -> 114 socket:send(Socket, ["220 ", Banner, "\r\n"]), 115 socket:active_once(Socket), 116 {ok, #state{socket = Socket, module = Module, options = Options, callbackstate = CallbackState}, ?TIMEOUT}; 117 {stop, Reason, Message} -> 118 socket:send(Socket, [Message, "\r\n"]), 119 socket:close(Socket), 120 {stop, Reason}; 121 ignore -> 122 socket:close(Socket), 123 ignore 124 end. 125 126%% @hidden 127-spec handle_call(Message :: any(), From :: {pid(), reference()}, #state{}) -> {'stop', 'normal', 'ok', #state{}} | {'reply', {'unknown_call', any()}, #state{}}. 128handle_call(stop, _From, State) -> 129 {stop, normal, ok, State}; 130 131handle_call(Request, _From, State) -> 132 {reply, {unknown_call, Request}, State}. 133 134%% @hidden 135-spec handle_cast(Message :: any(), State :: #state{}) -> {'noreply', #state{}}. 136handle_cast(_Msg, State) -> 137 {noreply, State}. 138 139%% @hidden 140-spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}. 141handle_info({receive_data, {error, size_exceeded}}, #state{socket = Socket, readmessage = true} = State) -> 142 socket:send(Socket, "552 Message too large\r\n"), 143 socket:active_once(Socket), 144 {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; 145handle_info({receive_data, {error, bare_newline}}, #state{socket = Socket, readmessage = true} = State) -> 146 socket:send(Socket, "451 Bare newline detected\r\n"), 147 io:format("bare newline detected: ~p~n", [self()]), 148 socket:active_once(Socket), 149 {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}; 150handle_info({receive_data, Body, Rest}, #state{socket = Socket, readmessage = true, envelope = Env, module=Module, 151 callbackstate = OldCallbackState, extensions = Extensions} = State) -> 152 % send the remainder of the data... 153 case Rest of 154 <<>> -> ok; % no remaining data 155 _ -> self() ! {socket:get_proto(Socket), Socket, Rest} 156 end, 157 socket:setopts(Socket, [{packet, line}]), 158 Envelope = Env#envelope{data = Body},% size = length(Body)}, 159 Valid = case has_extension(Extensions, "SIZE") of 160 {true, Value} -> 161 case byte_size(Envelope#envelope.data) > list_to_integer(Value) of 162 true -> 163 socket:send(Socket, "552 Message too large\r\n"), 164 socket:active_once(Socket), 165 false; 166 false -> 167 true 168 end; 169 false -> 170 true 171 end, 172 case Valid of 173 true -> 174 case Module:handle_DATA(Envelope#envelope.from, Envelope#envelope.to, Envelope#envelope.data, OldCallbackState) of 175 {ok, Reference, CallbackState} -> 176 socket:send(Socket, io_lib:format("250 queued as ~s\r\n", [Reference])), 177 socket:active_once(Socket), 178 {noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT}; 179 {error, Message, CallbackState} -> 180 socket:send(Socket, [Message, "\r\n"]), 181 socket:active_once(Socket), 182 {noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT} 183 end; 184 false -> 185 % might not even be able to get here anymore... 186 {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT} 187 end; 188handle_info({_SocketType, Socket, Packet}, State) -> 189 case handle_request(parse_request(Packet), State) of 190 {ok, #state{extensions = Extensions, options = Options, readmessage = true} = NewState} -> 191 MaxSize = case has_extension(Extensions, "SIZE") of 192 {true, Value} -> 193 list_to_integer(Value); 194 false -> 195 ?MAXIMUMSIZE 196 end, 197 Session = self(), 198 Size = 0, 199 socket:setopts(Socket, [{packet, raw}]), 200 spawn_opt(fun() -> receive_data([], 201 Socket, 0, Size, MaxSize, Session, Options) end, 202 [link, {fullsweep_after, 0}]), 203 {noreply, NewState, ?TIMEOUT}; 204 {ok, NewState} -> 205 socket:active_once(NewState#state.socket), 206 {noreply, NewState, ?TIMEOUT}; 207 {stop, Reason, NewState} -> 208 {stop, Reason, NewState} 209 end; 210handle_info({tcp_closed, _Socket}, State) -> 211 {stop, normal, State}; 212handle_info({ssl_closed, _Socket}, State) -> 213 {stop, normal, State}; 214handle_info(timeout, #state{socket = Socket} = State) -> 215 socket:send(Socket, "421 Error: timeout exceeded\r\n"), 216 socket:close(Socket), 217 {stop, normal, State}; 218handle_info(Info, State) -> 219 io:format("unhandled info message ~p~n", [Info]), 220 {noreply, State}. 221 222%% @hidden 223-spec(terminate/2 :: (Reason :: any(), State :: #state{}) -> 'ok'). 224terminate(Reason, State) -> 225 socket:close(State#state.socket), 226 (State#state.module):terminate(Reason, State#state.callbackstate). 227 228%% @hidden 229-spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}. 230code_change(OldVsn, #state{module = Module} = State, Extra) -> 231 % TODO - this should probably be the callback module's version or its checksum 232 CallbackState = 233 case catch Module:code_change(OldVsn, State#state.callbackstate, Extra) of 234 {ok, NewCallbackState} -> NewCallbackState; 235 _ -> State#state.callbackstate 236 end, 237 {ok, State#state{callbackstate = CallbackState}}. 238 239-spec(parse_request/1 :: (Packet :: binary()) -> {binary(), binary()}). 240parse_request(Packet) -> 241 Request = binstr:strip(binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s), 242 case binstr:strchr(Request, $\s) of 243 0 -> 244 % io:format("got a ~s request~n", [Request]), 245 case binstr:to_upper(Request) of 246 <<"QUIT">> = Res -> {Res, <<>>}; 247 <<"DATA">> = Res -> {Res, <<>>}; 248 % likely a base64-encoded client reply 249 _ -> {Request, <<>>} 250 end; 251 Index -> 252 Verb = binstr:substr(Request, 1, Index - 1), 253 Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s), 254 %io:format("got a ~s request with parameters ~s~n", [Verb, Parameters]), 255 {binstr:to_upper(Verb), Parameters} 256 end. 257 258-spec(handle_request/2 :: ({Verb :: binary(), Args :: binary()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}). 259handle_request({<<>>, _Any}, #state{socket = Socket} = State) -> 260 socket:send(Socket, "500 Error: bad syntax\r\n"), 261 {ok, State}; 262handle_request({<<"HELO">>, <<>>}, #state{socket = Socket} = State) -> 263 socket:send(Socket, "501 Syntax: HELO hostname\r\n"), 264 {ok, State}; 265handle_request({<<"HELO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState} = State) -> 266 case Module:handle_HELO(Hostname, OldCallbackState) of 267 {ok, MaxSize, CallbackState} when is_integer(MaxSize) -> 268 socket:send(Socket,["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]), 269 {ok, State#state{extensions = [{"SIZE", integer_to_list(MaxSize)}], envelope = #envelope{}, callbackstate = CallbackState}}; 270 {ok, CallbackState} -> 271 socket:send(Socket, ["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]), 272 {ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}}; 273 {error, Message, CallbackState} -> 274 socket:send(Socket, [Message, "\r\n"]), 275 {ok, State#state{callbackstate = CallbackState}} 276 end; 277handle_request({<<"EHLO">>, <<>>}, #state{socket = Socket} = State) -> 278 socket:send(Socket, "501 Syntax: EHLO hostname\r\n"), 279 {ok, State}; 280handle_request({<<"EHLO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) -> 281 case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of 282 {ok, Extensions, CallbackState} -> 283 case Extensions of 284 [] -> 285 socket:send(Socket, ["250 ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]), 286 {ok, State#state{extensions = Extensions, callbackstate = CallbackState}}; 287 _Else -> 288 F = 289 fun({E, true}, {Pos, Len, Acc}) when Pos =:= Len -> 290 {Pos, Len, [["250 ", E, "\r\n"] | Acc]}; 291 ({E, Value}, {Pos, Len, Acc}) when Pos =:= Len -> 292 {Pos, Len, [["250 ", E, " ", Value, "\r\n"] | Acc]}; 293 ({E, true}, {Pos, Len, Acc}) -> 294 {Pos+1, Len, [["250-", E, "\r\n"] | Acc]}; 295 ({E, Value}, {Pos, Len, Acc}) -> 296 {Pos+1, Len, [["250-", E, " ", Value , "\r\n"] | Acc]} 297 end, 298 Extensions2 = case Tls of 299 true -> 300 Extensions -- [{"STARTTLS", true}]; 301 false -> 302 Extensions 303 end, 304 {_, _, Response} = lists:foldl(F, {1, length(Extensions2), [["250-", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]]}, Extensions2), 305 %?debugFmt("Respponse ~p~n", [lists:reverse(Response)]), 306 socket:send(Socket, lists:reverse(Response)), 307 {ok, State#state{extensions = Extensions2, envelope = #envelope{}, callbackstate = CallbackState}} 308 end; 309 {error, Message, CallbackState} -> 310 socket:send(Socket, [Message, "\r\n"]), 311 {ok, State#state{callbackstate = CallbackState}} 312 end; 313 314handle_request({<<"AUTH">>, _Args}, #state{envelope = undefined, socket = Socket} = State) -> 315 socket:send(Socket, "503 Error: send EHLO first\r\n"), 316 {ok, State}; 317handle_request({<<"AUTH">>, Args}, #state{socket = Socket, extensions = Extensions, envelope = Envelope, options = Options} = State) -> 318 case binstr:strchr(Args, $\s) of 319 0 -> 320 AuthType = Args, 321 Parameters = false; 322 Index -> 323 AuthType = binstr:substr(Args, 1, Index - 1), 324 Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s) 325 end, 326 327 case has_extension(Extensions, "AUTH") of 328 false -> 329 socket:send(Socket, "502 Error: AUTH not implemented\r\n"), 330 {ok, State}; 331 {true, AvailableTypes} -> 332 case lists:member(string:to_upper(binary_to_list(AuthType)), string:tokens(AvailableTypes, " ")) of 333 false -> 334 socket:send(Socket, "504 Unrecognized authentication type\r\n"), 335 {ok, State}; 336 true -> 337 case binstr:to_upper(AuthType) of 338 <<"LOGIN">> -> 339 % socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")), 340 socket:send(Socket, "334 VXNlcm5hbWU6\r\n"), 341 {ok, State#state{waitingauth = 'login', envelope = Envelope#envelope{auth = {<<>>, <<>>}}}}; 342 <<"PLAIN">> when Parameters =/= false -> 343 % TODO - duplicated below in handle_request waitingauth PLAIN 344 case binstr:split(base64:decode(Parameters), <<0>>) of 345 [_Identity, Username, Password] -> 346 try_auth('plain', Username, Password, State); 347 [Username, Password] -> 348 try_auth('plain', Username, Password, State); 349 _ -> 350 % TODO error 351 {ok, State} 352 end; 353 <<"PLAIN">> -> 354 socket:send(Socket, "334\r\n"), 355 {ok, State#state{waitingauth = 'plain', envelope = Envelope#envelope{auth = {<<>>, <<>>}}}}; 356 <<"CRAM-MD5">> -> 357 crypto:start(), % ensure crypto is started, we're gonna need it 358 String = smtp_util:get_cram_string(proplists:get_value(hostname, Options, smtp_util:guess_FQDN())), 359 socket:send(Socket, ["334 ", String, "\r\n"]), 360 {ok, State#state{waitingauth = 'cram-md5', authdata=base64:decode(String), envelope = Envelope#envelope{auth = {<<>>, <<>>}}}} 361 %"DIGEST-MD5" -> % TODO finish this? (see rfc 2831) 362 %crypto:start(), % ensure crypto is started, we're gonna need it 363 %Nonce = get_digest_nonce(), 364 %Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname), 365 %socket:send(Socket, "334 "++Response++"\r\n"), 366 %{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}} 367 end 368 end 369 end; 370 371% the client sends a response to auth-cram-md5 372handle_request({Username64, <<>>}, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {<<>>, <<>>}}, authdata = AuthData} = State) -> 373 case binstr:split(base64:decode(Username64), <<" ">>) of 374 [Username, Digest] -> 375 try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata=undefined}); 376 _ -> 377 % TODO error 378 {ok, State#state{waitingauth=false, authdata=undefined}} 379 end; 380 381% the client sends a \0username\0password response to auth-plain 382handle_request({Username64, <<>>}, #state{waitingauth = 'plain', envelope = #envelope{auth = {<<>>,<<>>}}} = State) -> 383 case binstr:split(base64:decode(Username64), <<0>>) of 384 [_Identity, Username, Password] -> 385 try_auth('plain', Username, Password, State); 386 [Username, Password] -> 387 try_auth('plain', Username, Password, State); 388 _ -> 389 % TODO error 390 {ok, State#state{waitingauth=false}} 391 end; 392 393% the client sends a username response to auth-login 394handle_request({Username64, <<>>}, #state{socket = Socket, waitingauth = 'login', envelope = #envelope{auth = {<<>>,<<>>}}} = State) -> 395 Envelope = State#state.envelope, 396 Username = base64:decode(Username64), 397 % socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")), 398 socket:send(Socket, "334 UGFzc3dvcmQ6\r\n"), 399 % store the provided username in envelope.auth 400 NewState = State#state{envelope = Envelope#envelope{auth = {Username, <<>>}}}, 401 {ok, NewState}; 402 403% the client sends a password response to auth-login 404handle_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #envelope{auth = {Username,<<>>}}} = State) -> 405 Password = base64:decode(Password64), 406 try_auth('login', Username, Password, State); 407 408handle_request({<<"MAIL">>, _Args}, #state{envelope = undefined, socket = Socket} = State) -> 409 socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"), 410 {ok, State}; 411handle_request({<<"MAIL">>, Args}, #state{socket = Socket, module = Module, envelope = Envelope, callbackstate = OldCallbackState, extensions = Extensions} = State) -> 412 case Envelope#envelope.from of 413 undefined -> 414 case binstr:strpos(binstr:to_upper(Args), "FROM:") of 415 1 -> 416 Address = binstr:strip(binstr:substr(Args, 6), left, $\s), 417 case parse_encoded_address(Address) of 418 error -> 419 socket:send(Socket, "501 Bad sender address syntax\r\n"), 420 {ok, State}; 421 {ParsedAddress, <<>>} -> 422 %io:format("From address ~s (parsed as ~s)~n", [Address, ParsedAddress]), 423 case Module:handle_MAIL(ParsedAddress, OldCallbackState) of 424 {ok, CallbackState} -> 425 socket:send(Socket, "250 sender Ok\r\n"), 426 {ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}}; 427 {error, Message, CallbackState} -> 428 socket:send(Socket, [Message, "\r\n"]), 429 {ok, State#state{callbackstate = CallbackState}} 430 end; 431 {ParsedAddress, ExtraInfo} -> 432 %io:format("From address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]), 433 Options = [binstr:to_upper(X) || X <- binstr:split(ExtraInfo, <<" ">>)], 434 %io:format("options are ~p~n", [Options]), 435 F = fun(_, {error, Message}) -> 436 {error, Message}; 437 (<<"SIZE=", Size/binary>>, InnerState) -> 438 case has_extension(Extensions, "SIZE") of 439 {true, Value} -> 440 case list_to_integer(binary_to_list(Size)) > list_to_integer(Value) of 441 true -> 442 {error, ["552 Estimated message length ", Size, " exceeds limit of ", Value, "\r\n"]}; 443 false -> 444 InnerState#state{envelope = Envelope#envelope{expectedsize = list_to_integer(binary_to_list(Size))}} 445 end; 446 false -> 447 {error, "555 Unsupported option SIZE\r\n"} 448 end; 449 (<<"BODY=", _BodyType/binary>>, InnerState) -> 450 case has_extension(Extensions, "8BITMIME") of 451 {true, _} -> 452 InnerState; 453 false -> 454 {error, "555 Unsupported option BODY\r\n"} 455 end; 456 (X, InnerState) -> 457 case Module:handle_MAIL_extension(X, OldCallbackState) of 458 {ok, CallbackState} -> 459 InnerState#state{callbackstate = CallbackState}; 460 error -> 461 {error, ["555 Unsupported option: ", ExtraInfo, "\r\n"]} 462 end 463 end, 464 case lists:foldl(F, State, Options) of 465 {error, Message} -> 466 %io:format("error: ~s~n", [Message]), 467 socket:send(Socket, Message), 468 {ok, State}; 469 NewState -> 470 %io:format("OK~n"), 471 case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of 472 {ok, CallbackState} -> 473 socket:send(Socket, "250 sender Ok\r\n"), 474 {ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}}; 475 {error, Message, CallbackState} -> 476 socket:send(Socket, [Message, "\r\n"]), 477 {ok, NewState#state{callbackstate = CallbackState}} 478 end 479 end 480 end; 481 _Else -> 482 socket:send(Socket, "501 Syntax: MAIL FROM:<address>\r\n"), 483 {ok, State} 484 end; 485 _Other -> 486 socket:send(Socket, "503 Error: Nested MAIL command\r\n"), 487 {ok, State} 488 end; 489handle_request({<<"RCPT">>, _Args}, #state{envelope = undefined, socket = Socket} = State) -> 490 socket:send(Socket, "503 Error: need MAIL command\r\n"), 491 {ok, State}; 492handle_request({<<"RCPT">>, Args}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) -> 493 case binstr:strpos(binstr:to_upper(Args), "TO:") of 494 1 -> 495 Address = binstr:strip(binstr:substr(Args, 4), left, $\s), 496 case parse_encoded_address(Address) of 497 error -> 498 socket:send(Socket, "501 Bad recipient address syntax\r\n"), 499 {ok, State}; 500 {<<>>, _} -> 501 % empty rcpt to addresses aren't cool 502 socket:send(Socket, "501 Bad recipient address syntax\r\n"), 503 {ok, State}; 504 {ParsedAddress, <<>>} -> 505 %io:format("To address ~s (parsed as ~s)~n", [Address, ParsedAddress]), 506 case Module:handle_RCPT(ParsedAddress, OldCallbackState) of 507 {ok, CallbackState} -> 508 socket:send(Socket, "250 recipient Ok\r\n"), 509 {ok, State#state{envelope = Envelope#envelope{to = Envelope#envelope.to ++ [ParsedAddress]}, callbackstate = CallbackState}}; 510 {error, Message, CallbackState} -> 511 socket:send(Socket, [Message, "\r\n"]), 512 {ok, State#state{callbackstate = CallbackState}} 513 end; 514 {ParsedAddress, ExtraInfo} -> 515 % TODO - are there even any RCPT extensions? 516 io:format("To address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]), 517 socket:send(Socket, ["555 Unsupported option: ", ExtraInfo, "\r\n"]), 518 {ok, State} 519 end; 520 _Else -> 521 socket:send(Socket, "501 Syntax: RCPT TO:<address>\r\n"), 522 {ok, State} 523 end; 524handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = undefined} = State) -> 525 socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"), 526 {ok, State}; 527handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = Envelope} = State) -> 528 case {Envelope#envelope.from, Envelope#envelope.to} of 529 {undefined, _} -> 530 socket:send(Socket, "503 Error: need MAIL command\r\n"), 531 {ok, State}; 532 {_, []} -> 533 socket:send(Socket, "503 Error: need RCPT command\r\n"), 534 {ok, State}; 535 _Else -> 536 socket:send(Socket, "354 enter mail, end with line containing only '.'\r\n"), 537 %io:format("switching to data read mode~n", []), 538 539 {ok, State#state{readmessage = true}} 540 end; 541handle_request({<<"RSET">>, _Any}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) -> 542 socket:send(Socket, "250 Ok\r\n"), 543 % if the client sends a RSET before a HELO/EHLO don't give them a valid envelope 544 NewEnvelope = case Envelope of 545 undefined -> undefined; 546 _Something -> #envelope{} 547 end, 548 {ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}}; 549handle_request({<<"NOOP">>, _Any}, #state{socket = Socket} = State) -> 550 socket:send(Socket, "250 Ok\r\n"), 551 {ok, State}; 552handle_request({<<"QUIT">>, _Any}, #state{socket = Socket} = State) -> 553 socket:send(Socket, "221 Bye\r\n"), 554 {stop, normal, State}; 555handle_request({<<"VRFY">>, Address}, #state{module= Module, socket = Socket, callbackstate = OldCallbackState} = State) -> 556 case parse_encoded_address(Address) of 557 {ParsedAddress, <<>>} -> 558 case Module:handle_VRFY(ParsedAddress, OldCallbackState) of 559 {ok, Reply, CallbackState} -> 560 socket:send(Socket, ["250 ", Reply, "\r\n"]), 561 {ok, State#state{callbackstate = CallbackState}}; 562 {error, Message, CallbackState} -> 563 socket:send(Socket, [Message, "\r\n"]), 564 {ok, State#state{callbackstate = CallbackState}} 565 end; 566 _Other -> 567 socket:send(Socket, "501 Syntax: VRFY username/address\r\n"), 568 {ok, State} 569 end; 570handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket, tls=false, extensions = Extensions, options = Options} = State) -> 571 case has_extension(Extensions, "STARTTLS") of 572 {true, _} -> 573 socket:send(Socket, "220 OK\r\n"), 574 crypto:start(), 575 application:start(public_key), 576 application:start(ssl), 577 Options1 = case proplists:get_value(certfile, Options) of 578 undefined -> 579 []; 580 CertFile -> 581 [{certfile, CertFile}] 582 end, 583 Options2 = case proplists:get_value(keyfile, Options) of 584 undefined -> 585 Options1; 586 KeyFile -> 587 [{keyfile, KeyFile} | Options1] 588 end, 589 % TODO: certfile and keyfile should be at configurable locations 590 case socket:to_ssl_server(Socket, Options2, 5000) of 591 {ok, NewSocket} -> 592 %io:format("SSL negotiation sucessful~n"), 593 {ok, State#state{socket = NewSocket, envelope=undefined, 594 authdata=undefined, waitingauth=false, readmessage=false, 595 tls=true}}; 596 {error, Reason} -> 597 io:format("SSL handshake failed : ~p~n", [Reason]), 598 socket:send(Socket, "454 TLS negotiation failed\r\n"), 599 {ok, State} 600 end; 601 false -> 602 socket:send(Socket, "500 Command unrecognized\r\n"), 603 {ok, State} 604 end; 605handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket} = State) -> 606 socket:send(Socket, "500 TLS already negotiated\r\n"), 607 {ok, State}; 608handle_request({<<"STARTTLS">>, _Args}, #state{socket = Socket} = State) -> 609 socket:send(Socket, "501 Syntax error (no parameters allowed)\r\n"), 610 {ok, State}; 611handle_request({Verb, Args}, #state{socket = Socket, module = Module, callbackstate = OldCallbackState} = State) -> 612 {Message, CallbackState} = Module:handle_other(Verb, Args, OldCallbackState), 613 socket:send(Socket, [Message, "\r\n"]), 614 {ok, State#state{callbackstate = CallbackState}}. 615 616-spec(parse_encoded_address/1 :: (Address :: binary()) -> {binary(), binary()} | 'error'). 617parse_encoded_address(<<>>) -> 618 error; % empty 619parse_encoded_address(<<"<@", Address/binary>>) -> 620 case binstr:strchr(Address, $:) of 621 0 -> 622 error; % invalid address 623 Index -> 624 parse_encoded_address(binstr:substr(Address, Index + 1), [], {false, true}) 625 end; 626parse_encoded_address(<<"<", Address/binary>>) -> 627 parse_encoded_address(Address, [], {false, true}); 628parse_encoded_address(<<" ", Address/binary>>) -> 629 parse_encoded_address(Address); 630parse_encoded_address(Address) -> 631 parse_encoded_address(Address, [], {false, false}). 632 633-spec(parse_encoded_address/3 :: (Address :: binary(), Acc :: list(), Flags :: {boolean(), boolean()}) -> {binary(), binary()} | 'error'). 634parse_encoded_address(<<>>, Acc, {_Quotes, false}) -> 635 {list_to_binary(lists:reverse(Acc)), <<>>}; 636parse_encoded_address(<<>>, _Acc, {_Quotes, true}) -> 637 error; % began with angle brackets but didn't end with them 638parse_encoded_address(_, Acc, _) when length(Acc) > 129 -> 639 error; % too long 640parse_encoded_address(<<"\\", Tail/binary>>, Acc, Flags) -> 641 <<H, NewTail/binary>> = Tail, 642 parse_encoded_address(NewTail, [H | Acc], Flags); 643parse_encoded_address(<<"\"", Tail/binary>>, Acc, {false, AB}) -> 644 parse_encoded_address(Tail, Acc, {true, AB}); 645parse_encoded_address(<<"\"", Tail/binary>>, Acc, {true, AB}) -> 646 parse_encoded_address(Tail, Acc, {false, AB}); 647parse_encoded_address(<<">", Tail/binary>>, Acc, {false, true}) -> 648 {list_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; 649parse_encoded_address(<<">", _Tail/binary>>, _Acc, {false, false}) -> 650 error; % ended with angle brackets but didn't begin with them 651parse_encoded_address(<<" ", Tail/binary>>, Acc, {false, false}) -> 652 {list_to_binary(lists:reverse(Acc)), binstr:strip(Tail, left, $\s)}; 653parse_encoded_address(<<" ", _Tail/binary>>, _Acc, {false, true}) -> 654 error; % began with angle brackets but didn't end with them 655parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $0, H =< $9 -> 656 parse_encoded_address(Tail, [H | Acc], {false, AB}); % digits 657parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $@, H =< $Z -> 658 parse_encoded_address(Tail, [H | Acc], {false, AB}); % @ symbol and uppercase letters 659parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H >= $a, H =< $z -> 660 parse_encoded_address(Tail, [H | Acc], {false, AB}); % lowercase letters 661parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H =:= $-; H =:= $.; H =:= $_ -> 662 parse_encoded_address(Tail, [H | Acc], {false, AB}); % dash, dot, underscore 663% Allowed characters in the local name: ! # $ % & ' * + - / = ? ^ _ ` . { | } ~ 664parse_encoded_address(<<H, Tail/binary>>, Acc, {false, AB}) when H =:= $+; 665 H =:= $!; H =:= $#; H =:= $$; H =:= $%; H =:= $&; H =:= $'; H =:= $*; H =:= $=; 666 H =:= $/; H =:= $?; H =:= $^; H =:= $`; H =:= ${; H =:= $|; H =:= $}; H =:= $~ -> 667 parse_encoded_address(Tail, [H | Acc], {false, AB}); % other characters 668parse_encoded_address(_, _Acc, {false, _AB}) -> 669 error; 670parse_encoded_address(<<H, Tail/binary>>, Acc, Quotes) -> 671 parse_encoded_address(Tail, [H | Acc], Quotes). 672 673-spec(has_extension/2 :: (Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false'). 674has_extension(Exts, Ext) -> 675 Extension = string:to_upper(Ext), 676 Extensions = [{string:to_upper(X), Y} || {X, Y} <- Exts], 677 %io:format("extensions ~p~n", [Extensions]), 678 case proplists:get_value(Extension, Extensions) of 679 undefined -> 680 false; 681 Value -> 682 {true, Value} 683 end. 684 685 686-spec(try_auth/4 :: (AuthType :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Credential :: binary() | {binary(), binary()}, State :: #state{}) -> {'ok', #state{}}). 687try_auth(AuthType, Username, Credential, #state{module = Module, socket = Socket, envelope = Envelope, callbackstate = OldCallbackState} = State) -> 688 % clear out waiting auth 689 NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {<<>>, <<>>}}}, 690 case erlang:function_exported(Module, handle_AUTH, 4) of 691 true -> 692 case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of 693 {ok, CallbackState} -> 694 socket:send(Socket, "235 Authentication successful.\r\n"), 695 {ok, NewState#state{callbackstate = CallbackState, 696 envelope = Envelope#envelope{auth = {Username, Credential}}}}; 697 _Other -> 698 socket:send(Socket, "535 Authentication failed.\r\n"), 699 {ok, NewState} 700 end; 701 false -> 702 io:format("Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions~n"), 703 socket:send(Socket, "535 authentication failed (#5.7.1)\r\n"), 704 {ok, NewState} 705 end. 706 707%get_digest_nonce() -> 708 %A = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))], 709 %B = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))], 710 %binary_to_list(base64:encode(lists:flatten(A ++ B))). 711 712 713%% @doc a tight loop to receive the message body 714receive_data(_Acc, _Socket, _, Size, MaxSize, Session, _Options) when MaxSize > 0, Size > MaxSize -> 715 io:format("message body size ~B exceeded maximum allowed ~B~n", [Size, MaxSize]), 716 Session ! {receive_data, {error, size_exceeded}}; 717receive_data(Acc, Socket, RecvSize, Size, MaxSize, Session, Options) -> 718 case socket:recv(Socket, RecvSize, 1000) of 719 {ok, Packet} when Acc == [] -> 720 case check_bare_crlf(Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0) of 721 error -> 722 Session ! {receive_data, {error, bare_newline}}; 723 FixedPacket -> 724 case binstr:strpos(FixedPacket, "\r\n.\r\n") of 725 0 -> 726 %io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]), 727 %io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]), 728 receive_data([FixedPacket | Acc], Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options); 729 Index -> 730 String = binstr:substr(FixedPacket, 1, Index - 1), 731 Rest = binstr:substr(FixedPacket, Index+5), 732 %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]), 733 Result = list_to_binary(lists:reverse([String | Acc])), 734 %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]), 735 Session ! {receive_data, Result, Rest} 736 end 737 end; 738 {ok, Packet} -> 739 [Last | _] = Acc, 740 case check_bare_crlf(Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0) of 741 error -> 742 Session ! {receive_data, {error, bare_newline}}; 743 FixedPacket -> 744 case binstr:strpos(FixedPacket, "\r\n.\r\n") of 745 0 -> 746 %io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]), 747 %io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]), 748 receive_data([FixedPacket | Acc], Socket, RecvSize, Size + byte_size(FixedPacket), MaxSize, Session, Options); 749 Index -> 750 String = binstr:substr(FixedPacket, 1, Index - 1), 751 Rest = binstr:substr(FixedPacket, Index+5), 752 %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]), 753 Result = list_to_binary(lists:reverse([String | Acc])), 754 %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]), 755 Session ! {receive_data, Result, Rest} 756 end 757 end; 758 {error, timeout} when RecvSize =:= 0, length(Acc) > 1 -> 759 % check that we didn't accidentally receive a \r\n.\r\n split across 2 receives 760 [A, B | Acc2] = Acc, 761 Packet = list_to_binary([B, A]), 762 case binstr:strpos(Packet, "\r\n.\r\n") of 763 0 -> 764 % uh-oh 765 %io:format("no data on socket, and no DATA terminator, retrying ~p~n", [Session]), 766 % eventually we'll either get data or a different error, just keep retrying 767 receive_data(Acc, Socket, 0, Size, MaxSize, Session, Options); 768 Index -> 769 String = binstr:substr(Packet, 1, Index - 1), 770 Rest = binstr:substr(Packet, Index+5), 771 %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]), 772 Result = list_to_binary(lists:reverse([String | Acc2])), 773 %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]), 774 Session ! {receive_data, Result, Rest} 775 end; 776 {error, timeout} -> 777 receive_data(Acc, Socket, 0, Size, MaxSize, Session, Options); 778 {error, Reason} -> 779 io:format("receive error: ~p~n", [Reason]), 780 exit(receive_error) 781 end. 782 783check_for_bare_crlf(Bin, Offset) -> 784 case {re:run(Bin, "(?<!\r)\n", [{capture, none}, {offset, Offset}]), re:run(Bin, "\r(?!\n)", [{capture, none}, {offset, Offset}])} of 785 {match, _} -> true; 786 {_, match} -> true; 787 _ -> false 788 end. 789 790fix_bare_crlf(Bin, Offset) -> 791 Options = [{offset, Offset}, {return, binary}, global], 792 re:replace(re:replace(Bin, "(?<!\r)\n", "\r\n", Options), "\r(?!\n)", "\r\n", Options). 793 794strip_bare_crlf(Bin, Offset) -> 795 Options = [{offset, Offset}, {return, binary}, global], 796 re:replace(re:replace(Bin, "(?<!\r)\n", "", Options), "\r(?!\n)", "", Options). 797 798check_bare_crlf(Binary, _, ignore, _) -> 799 Binary; 800check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, Offset) when byte_size(Prev) > 0, Offset == 0 -> 801 % check if last character of previous was a CR 802 Lastchar = binstr:substr(Prev, -1), 803 case Lastchar of 804 <<"\r">> -> 805 % okay, check again for the rest 806 check_bare_crlf(Bin, <<>>, Op, 1); 807 _ when Op == false -> % not fixing or ignoring them 808 error; 809 _ -> 810 % no dice 811 check_bare_crlf(Bin, <<>>, Op, 0) 812 end; 813check_bare_crlf(Binary, _Prev, Op, Offset) -> 814 Last = binstr:substr(Binary, -1), 815 % is the last character a CR? 816 case Last of 817 <<"\r">> -> 818 % okay, the last character is a CR, we have to assume the next packet contains the corresponding LF 819 NewBin = binstr:substr(Binary, 1, byte_size(Binary) -1), 820 case check_for_bare_crlf(NewBin, Offset) of 821 true when Op == fix -> 822 list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]); 823 true when Op == strip -> 824 list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]); 825 true -> 826 error; 827 false -> 828 Binary 829 end; 830 _ -> 831 case check_for_bare_crlf(Binary, Offset) of 832 true when Op == fix -> 833 fix_bare_crlf(Binary, Offset); 834 true when Op == strip -> 835 strip_bare_crlf(Binary, Offset); 836 true -> 837 error; 838 false -> 839 Binary 840 end 841 end. 842 843 844-ifdef(TEST). 845parse_encoded_address_test_() -> 846 [ 847 {"Valid addresses should parse", 848 fun() -> 849 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God@heaven.af.mil>">>)), 850 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\\God@heaven.af.mil>">>)), 851 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<\"God\"@heaven.af.mil>">>)), 852 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>">>)), 853 ?assertEqual({<<"God2@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God2@heaven.af.mil>">>)), 854 ?assertEqual({<<"God+extension@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God+extension@heaven.af.mil>">>)), 855 ?assertEqual({<<"God~*$@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"<God~*$@heaven.af.mil>">>)) 856 end 857 }, 858 {"Addresses that are sorta valid should parse", 859 fun() -> 860 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil">>)), 861 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<"God@heaven.af.mil ">>)), 862 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" God@heaven.af.mil ">>)), 863 ?assertEqual({<<"God@heaven.af.mil">>, <<>>}, parse_encoded_address(<<" <God@heaven.af.mil> ">>)) 864 end 865 }, 866 {"Addresses containing unescaped <> that aren't at start/end should fail", 867 fun() -> 868 ?assertEqual(error, parse_encoded_address(<<"<<">>)), 869 ?assertEqual(error, parse_encoded_address(<<"<God<@heaven.af.mil>">>)) 870 end 871 }, 872 {"Address that begins with < but doesn't end with a > should fail", 873 fun() -> 874 ?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil">>)), 875 ?assertEqual(error, parse_encoded_address(<<"<God@heaven.af.mil ">>)) 876 end 877 }, 878 {"Address that begins without < but ends with a > should fail", 879 fun() -> 880 ?assertEqual(error, parse_encoded_address(<<"God@heaven.af.mil>">>)) 881 end 882 }, 883 {"Address longer than 129 character should fail", 884 fun() -> 885 MegaAddress = list_to_binary(lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++ lists:seq(97, 122)), 886 ?assertEqual(error, parse_encoded_address(MegaAddress)) 887 end 888 }, 889 {"Address with an invalid route should fail", 890 fun() -> 891 ?assertEqual(error, parse_encoded_address(<<"<@gateway.af.mil God@heaven.af.mil>">>)) 892 end 893 }, 894 {"Empty addresses should parse OK", 895 fun() -> 896 ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<"<>">>)), 897 ?assertEqual({<<>>, <<>>}, parse_encoded_address(<<" <> ">>)) 898 end 899 }, 900 {"Completely empty addresses are an error", 901 fun() -> 902 ?assertEqual(error, parse_encoded_address(<<"">>)), 903 ?assertEqual(error, parse_encoded_address(<<" ">>)) 904 end 905 }, 906 {"addresses with trailing parameters should return the trailing parameters", 907 fun() -> 908 ?assertEqual({<<"God@heaven.af.mil">>, <<"SIZE=100 BODY=8BITMIME">>}, parse_encoded_address(<<"<God@heaven.af.mil> SIZE=100 BODY=8BITMIME">>)) 909 end 910 } 911 ]. 912 913parse_request_test_() -> 914 [ 915 {"Parsing normal SMTP requests", 916 fun() -> 917 ?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)), 918 ?assertEqual({<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>)), 919 ?assertEqual({<<"MAIL">>, <<"FROM:God@heaven.af.mil">>}, parse_request(<<"MAIL FROM:God@heaven.af.mil">>)) 920 end 921 }, 922 {"Verbs should be uppercased", 923 fun() -> 924 ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>)) 925 end 926 }, 927 {"Leading and trailing spaces are removed", 928 fun() -> 929 ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo hell.af.mil ">>)) 930 end 931 }, 932 {"Blank lines are blank", 933 fun() -> 934 ?assertEqual({<<>>, <<>>}, parse_request(<<"">>)) 935 end 936 } 937 ]. 938 939smtp_session_test_() -> 940 {foreach, 941 local, 942 fun() -> 943 Self = self(), 944 spawn(fun() -> 945 {ok, ListenSock} = socket:listen(tcp, 9876, [binary]), 946 {ok, X} = socket:accept(ListenSock), 947 socket:controlling_process(X, Self), 948 Self ! X 949 end), 950 {ok, CSock} = socket:connect(tcp, "localhost", 9876), 951 receive 952 SSock when is_port(SSock) -> 953 ok 954 end, 955 {ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}]), 956 socket:controlling_process(SSock, Pid), 957 {CSock, Pid} 958 end, 959 fun({CSock, _Pid}) -> 960 socket:close(CSock) 961 end, 962 [fun({CSock, _Pid}) -> 963 {"A new connection should get a banner", 964 fun() -> 965 socket:active_once(CSock), 966 receive {tcp, CSock, Packet} -> ok end, 967 ?assertMatch("220 localhost"++_Stuff, Packet) 968 end 969 } 970 end, 971 fun({CSock, _Pid}) -> 972 {"A correct response to HELO", 973 fun() -> 974 socket:active_once(CSock), 975 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 976 ?assertMatch("220 localhost"++_Stuff, Packet), 977 socket:send(CSock, "HELO somehost.com\r\n"), 978 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 979 ?assertMatch("250 localhost\r\n", Packet2) 980 end 981 } 982 end, 983 fun({CSock, _Pid}) -> 984 {"An error in response to an invalid HELO", 985 fun() -> 986 socket:active_once(CSock), 987 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 988 ?assertMatch("220 localhost"++_Stuff, Packet), 989 socket:send(CSock, "HELO\r\n"), 990 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 991 ?assertMatch("501 Syntax: HELO hostname\r\n", Packet2) 992 end 993 } 994 end, 995 fun({CSock, _Pid}) -> 996 {"A rejected HELO", 997 fun() -> 998 socket:active_once(CSock), 999 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1000 ?assertMatch("220 localhost"++_Stuff, Packet), 1001 socket:send(CSock, "HELO invalid\r\n"), 1002 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1003 ?assertMatch("554 invalid hostname\r\n", Packet2) 1004 end 1005 } 1006 end, 1007 fun({CSock, _Pid}) -> 1008 {"A rejected EHLO", 1009 fun() -> 1010 socket:active_once(CSock), 1011 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1012 ?assertMatch("220 localhost"++_Stuff, Packet), 1013 socket:send(CSock, "EHLO invalid\r\n"), 1014 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1015 ?assertMatch("554 invalid hostname\r\n", Packet2) 1016 end 1017 } 1018 end, 1019 fun({CSock, _Pid}) -> 1020 {"EHLO response", 1021 fun() -> 1022 socket:active_once(CSock), 1023 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1024 ?assertMatch("220 localhost"++_Stuff, Packet), 1025 socket:send(CSock, "EHLO somehost.com\r\n"), 1026 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1027 ?assertMatch("250-localhost\r\n", Packet2), 1028 Foo = fun(F) -> 1029 receive 1030 {tcp, CSock, "250-"++_Packet3} -> 1031 socket:active_once(CSock), 1032 F(F); 1033 {tcp, CSock, "250 "++_Packet3} -> 1034 socket:active_once(CSock), 1035 ok; 1036 {tcp, CSock, _R} -> 1037 socket:active_once(CSock), 1038 error 1039 end 1040 end, 1041 ?assertEqual(ok, Foo(Foo)) 1042 end 1043 } 1044 end, 1045 fun({CSock, _Pid}) -> 1046 {"Unsupported AUTH PLAIN", 1047 fun() -> 1048 socket:active_once(CSock), 1049 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1050 ?assertMatch("220 localhost"++_Stuff, Packet), 1051 socket:send(CSock, "EHLO somehost.com\r\n"), 1052 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1053 ?assertMatch("250-localhost\r\n", Packet2), 1054 Foo = fun(F) -> 1055 receive 1056 {tcp, CSock, "250-"++_Packet3} -> 1057 socket:active_once(CSock), 1058 F(F); 1059 {tcp, CSock, "250"++_Packet3} -> 1060 socket:active_once(CSock), 1061 ok; 1062 {tcp, CSock, _R} -> 1063 socket:active_once(CSock), 1064 error 1065 end 1066 end, 1067 ?assertEqual(ok, Foo(Foo)), 1068 socket:send(CSock, "AUTH PLAIN\r\n"), 1069 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1070 ?assertMatch("502 Error: AUTH not implemented\r\n", Packet4) 1071 end 1072 } 1073 end, 1074 fun({CSock, _Pid}) -> 1075 {"Sending DATA", 1076 fun() -> 1077 socket:active_once(CSock), 1078 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1079 ?assertMatch("220 localhost"++_Stuff, Packet), 1080 socket:send(CSock, "HELO somehost.com\r\n"), 1081 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1082 ?assertMatch("250 localhost\r\n", Packet2), 1083 socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"), 1084 receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end, 1085 ?assertMatch("250 "++_, Packet3), 1086 socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"), 1087 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1088 ?assertMatch("250 "++_, Packet4), 1089 socket:send(CSock, "DATA\r\n"), 1090 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1091 ?assertMatch("354 "++_, Packet5), 1092 socket:send(CSock, "Subject: tls message\r\n"), 1093 socket:send(CSock, "To: <user@otherhost>\r\n"), 1094 socket:send(CSock, "From: <user@somehost.com>\r\n"), 1095 socket:send(CSock, "\r\n"), 1096 socket:send(CSock, "message body"), 1097 socket:send(CSock, "\r\n.\r\n"), 1098 receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1099 ?assertMatch("250 queued as"++_, Packet6) 1100 end 1101 } 1102 end, 1103% fun({CSock, _Pid}) -> 1104% {"Sending DATA with a bare newline", 1105% fun() -> 1106% socket:active_once(CSock), 1107% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1108% ?assertMatch("220 localhost"++_Stuff, Packet), 1109% socket:send(CSock, "HELO somehost.com\r\n"), 1110% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1111% ?assertMatch("250 localhost\r\n", Packet2), 1112% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"), 1113% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end, 1114% ?assertMatch("250 "++_, Packet3), 1115% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"), 1116% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1117% ?assertMatch("250 "++_, Packet4), 1118% socket:send(CSock, "DATA\r\n"), 1119% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1120% ?assertMatch("354 "++_, Packet5), 1121% socket:send(CSock, "Subject: tls message\r\n"), 1122% socket:send(CSock, "To: <user@otherhost>\r\n"), 1123% socket:send(CSock, "From: <user@somehost.com>\r\n"), 1124% socket:send(CSock, "\r\n"), 1125% socket:send(CSock, "this\r\n"), 1126% socket:send(CSock, "body\r\n"), 1127% socket:send(CSock, "has\r\n"), 1128% socket:send(CSock, "a\r\n"), 1129% socket:send(CSock, "bare\n"), 1130% socket:send(CSock, "newline\r\n"), 1131% socket:send(CSock, "\r\n.\r\n"), 1132% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1133% ?assertMatch("451 "++_, Packet6), 1134% end 1135% } 1136% end, 1137 %fun({CSock, _Pid}) -> 1138% {"Sending DATA with a bare CR", 1139% fun() -> 1140% socket:active_once(CSock), 1141% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1142% ?assertMatch("220 localhost"++_Stuff, Packet), 1143% socket:send(CSock, "HELO somehost.com\r\n"), 1144% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1145% ?assertMatch("250 localhost\r\n", Packet2), 1146% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"), 1147% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end, 1148% ?assertMatch("250 "++_, Packet3), 1149% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"), 1150% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1151% ?assertMatch("250 "++_, Packet4), 1152% socket:send(CSock, "DATA\r\n"), 1153% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1154% ?assertMatch("354 "++_, Packet5), 1155% socket:send(CSock, "Subject: tls message\r\n"), 1156% socket:send(CSock, "To: <user@otherhost>\r\n"), 1157% socket:send(CSock, "From: <user@somehost.com>\r\n"), 1158% socket:send(CSock, "\r\n"), 1159% socket:send(CSock, "this\r\n"), 1160% socket:send(CSock, "\rbody\r\n"), 1161% socket:send(CSock, "has\r\n"), 1162% socket:send(CSock, "a\r\n"), 1163% socket:send(CSock, "bare\r"), 1164% socket:send(CSock, "CR\r\n"), 1165% socket:send(CSock, "\r\n.\r\n"), 1166% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1167% ?assertMatch("451 "++_, Packet6), 1168% end 1169% } 1170% end, 1171 1172% fun({CSock, _Pid}) -> 1173% {"Sending DATA with a bare newline in the headers", 1174% fun() -> 1175% socket:active_once(CSock), 1176% receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1177% ?assertMatch("220 localhost"++_Stuff, Packet), 1178% socket:send(CSock, "HELO somehost.com\r\n"), 1179% receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1180% ?assertMatch("250 localhost\r\n", Packet2), 1181% socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"), 1182% receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end, 1183% ?assertMatch("250 "++_, Packet3), 1184% socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"), 1185% receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1186% ?assertMatch("250 "++_, Packet4), 1187% socket:send(CSock, "DATA\r\n"), 1188% receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1189% ?assertMatch("354 "++_, Packet5), 1190% socket:send(CSock, "Subject: tls message\r\n"), 1191% socket:send(CSock, "To: <user@otherhost>\n"), 1192% socket:send(CSock, "From: <user@somehost.com>\r\n"), 1193% socket:send(CSock, "\r\n"), 1194% socket:send(CSock, "this\r\n"), 1195% socket:send(CSock, "body\r\n"), 1196% socket:send(CSock, "has\r\n"), 1197% socket:send(CSock, "no\r\n"), 1198% socket:send(CSock, "bare\r\n"), 1199% socket:send(CSock, "newlines\r\n"), 1200% socket:send(CSock, "\r\n.\r\n"), 1201% receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1202% ?assertMatch("451 "++_, Packet6), 1203% end 1204% } 1205% end, 1206 fun({CSock, _Pid}) -> 1207 {"Sending DATA with bare newline on first line of body", 1208 fun() -> 1209 socket:active_once(CSock), 1210 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1211 ?assertMatch("220 localhost"++_Stuff, Packet), 1212 socket:send(CSock, "HELO somehost.com\r\n"), 1213 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1214 ?assertMatch("250 localhost\r\n", Packet2), 1215 socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"), 1216 receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end, 1217 ?assertMatch("250 "++_, Packet3), 1218 socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"), 1219 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1220 ?assertMatch("250 "++_, Packet4), 1221 socket:send(CSock, "DATA\r\n"), 1222 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1223 ?assertMatch("354 "++_, Packet5), 1224 socket:send(CSock, "Subject: tls message\r\n"), 1225 socket:send(CSock, "To: <user@otherhost>\n"), 1226 socket:send(CSock, "From: <user@somehost.com>\r\n"), 1227 socket:send(CSock, "\r\n"), 1228 socket:send(CSock, "this\n"), 1229 socket:send(CSock, "body\r\n"), 1230 socket:send(CSock, "has\r\n"), 1231 socket:send(CSock, "no\r\n"), 1232 socket:send(CSock, "bare\r\n"), 1233 socket:send(CSock, "newlines\r\n"), 1234 socket:send(CSock, "\r\n.\r\n"), 1235 receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1236 ?assertMatch("451 "++_, Packet6) 1237 end 1238 } 1239 end 1240 1241 ] 1242 }. 1243 1244smtp_session_auth_test_() -> 1245 {foreach, 1246 local, 1247 fun() -> 1248 Self = self(), 1249 spawn(fun() -> 1250 {ok, ListenSock} = socket:listen(tcp, 9876, [binary]), 1251 {ok, X} = socket:accept(ListenSock), 1252 socket:controlling_process(X, Self), 1253 Self ! X 1254 end), 1255 {ok, CSock} = socket:connect(tcp, "localhost", 9876), 1256 receive 1257 SSock when is_port(SSock) -> 1258 ok 1259 end, 1260 {ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}, {callbackoptions, [{auth, true}]}]), 1261 socket:controlling_process(SSock, Pid), 1262 {CSock, Pid} 1263 end, 1264 fun({CSock, _Pid}) -> 1265 socket:close(CSock) 1266 end, 1267 [fun({CSock, _Pid}) -> 1268 {"EHLO response includes AUTH", 1269 fun() -> 1270 socket:active_once(CSock), 1271 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1272 ?assertMatch("220 localhost"++_Stuff, Packet), 1273 socket:send(CSock, "EHLO somehost.com\r\n"), 1274 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1275 ?assertMatch("250-localhost\r\n", Packet2), 1276 Foo = fun(F, Acc) -> 1277 receive 1278 {tcp, CSock, "250-AUTH"++_Packet3} -> 1279 socket:active_once(CSock), 1280 F(F, true); 1281 {tcp, CSock, "250-"++_Packet3} -> 1282 socket:active_once(CSock), 1283 F(F, Acc); 1284 {tcp, CSock, "250 AUTH"++_Packet3} -> 1285 socket:active_once(CSock), 1286 true; 1287 {tcp, CSock, "250 "++_Packet3} -> 1288 socket:active_once(CSock), 1289 Acc; 1290 {tcp, CSock, _} -> 1291 socket:active_once(CSock), 1292 error 1293 end 1294 end, 1295 ?assertEqual(true, Foo(Foo, false)) 1296 end 1297 } 1298 end, 1299 fun({CSock, _Pid}) -> 1300 {"AUTH before EHLO is error", 1301 fun() -> 1302 socket:active_once(CSock), 1303 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1304 ?assertMatch("220 localhost"++_Stuff, Packet), 1305 socket:send(CSock, "AUTH CRAZY\r\n"), 1306 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1307 ?assertMatch("503 "++_, Packet4) 1308 end 1309 } 1310 end, 1311 fun({CSock, _Pid}) -> 1312 {"Unknown authentication type", 1313 fun() -> 1314 socket:active_once(CSock), 1315 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1316 ?assertMatch("220 localhost"++_Stuff, Packet), 1317 socket:send(CSock, "EHLO somehost.com\r\n"), 1318 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1319 ?assertMatch("250-localhost\r\n", Packet2), 1320 Foo = fun(F, Acc) -> 1321 receive 1322 {tcp, CSock, "250-AUTH"++_} -> 1323 socket:active_once(CSock), 1324 F(F, true); 1325 {tcp, CSock, "250-"++_} -> 1326 socket:active_once(CSock), 1327 F(F, Acc); 1328 {tcp, CSock, "250 AUTH"++_} -> 1329 socket:active_once(CSock), 1330 true; 1331 {tcp, CSock, "250 "++_} -> 1332 socket:active_once(CSock), 1333 Acc; 1334 {tcp, CSock, _} -> 1335 socket:active_once(CSock), 1336 error 1337 end 1338 end, 1339 ?assertEqual(true, Foo(Foo, false)), 1340 socket:send(CSock, "AUTH CRAZY\r\n"), 1341 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1342 ?assertMatch("504 Unrecognized authentication type\r\n", Packet4) 1343 end 1344 } 1345 end, 1346 1347 fun({CSock, _Pid}) -> 1348 {"A successful AUTH PLAIN", 1349 fun() -> 1350 socket:active_once(CSock), 1351 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1352 ?assertMatch("220 localhost"++_Stuff, Packet), 1353 socket:send(CSock, "EHLO somehost.com\r\n"), 1354 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1355 ?assertMatch("250-localhost\r\n", Packet2), 1356 Foo = fun(F, Acc) -> 1357 receive 1358 {tcp, CSock, "250-AUTH"++_Packet3} -> 1359 socket:active_once(CSock), 1360 F(F, true); 1361 {tcp, CSock, "250-"++_Packet3} -> 1362 socket:active_once(CSock), 1363 F(F, Acc); 1364 {tcp, CSock, "250 AUTH"++_Packet3} -> 1365 socket:active_once(CSock), 1366 true; 1367 {tcp, CSock, "250 "++_Packet3} -> 1368 socket:active_once(CSock), 1369 Acc; 1370 {tcp, CSock, _} -> 1371 socket:active_once(CSock), 1372 error 1373 end 1374 end, 1375 ?assertEqual(true, Foo(Foo, false)), 1376 socket:send(CSock, "AUTH PLAIN\r\n"), 1377 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1378 ?assertMatch("334\r\n", Packet4), 1379 String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), 1380 socket:send(CSock, String++"\r\n"), 1381 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1382 ?assertMatch("235 Authentication successful.\r\n", Packet5) 1383 end 1384 } 1385 end, 1386 fun({CSock, _Pid}) -> 1387 {"A successful AUTH PLAIN with an identity", 1388 fun() -> 1389 socket:active_once(CSock), 1390 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1391 ?assertMatch("220 localhost"++_Stuff, Packet), 1392 socket:send(CSock, "EHLO somehost.com\r\n"), 1393 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1394 ?assertMatch("250-localhost\r\n", Packet2), 1395 Foo = fun(F, Acc) -> 1396 receive 1397 {tcp, CSock, "250-AUTH"++_Packet3} -> 1398 socket:active_once(CSock), 1399 F(F, true); 1400 {tcp, CSock, "250-"++_Packet3} -> 1401 socket:active_once(CSock), 1402 F(F, Acc); 1403 {tcp, CSock, "250 AUTH"++_Packet3} -> 1404 socket:active_once(CSock), 1405 true; 1406 {tcp, CSock, "250 "++_Packet3} -> 1407 socket:active_once(CSock), 1408 Acc; 1409 {tcp, CSock, _} -> 1410 socket:active_once(CSock), 1411 error 1412 end 1413 end, 1414 ?assertEqual(true, Foo(Foo, false)), 1415 socket:send(CSock, "AUTH PLAIN\r\n"), 1416 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1417 ?assertMatch("334\r\n", Packet4), 1418 String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), 1419 socket:send(CSock, String++"\r\n"), 1420 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1421 ?assertMatch("235 Authentication successful.\r\n", Packet5) 1422 end 1423 } 1424 end, 1425 fun({CSock, _Pid}) -> 1426 {"A successful immediate AUTH PLAIN", 1427 fun() -> 1428 socket:active_once(CSock), 1429 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1430 ?assertMatch("220 localhost"++_Stuff, Packet), 1431 socket:send(CSock, "EHLO somehost.com\r\n"), 1432 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1433 ?assertMatch("250-localhost\r\n", Packet2), 1434 Foo = fun(F, Acc) -> 1435 receive 1436 {tcp, CSock, "250-AUTH"++_Packet3} -> 1437 socket:active_once(CSock), 1438 F(F, true); 1439 {tcp, CSock, "250-"++_Packet3} -> 1440 socket:active_once(CSock), 1441 F(F, Acc); 1442 {tcp, CSock, "250 AUTH"++_Packet3} -> 1443 socket:active_once(CSock), 1444 true; 1445 {tcp, CSock, "250 "++_Packet3} -> 1446 socket:active_once(CSock), 1447 Acc; 1448 {tcp, CSock, _} -> 1449 socket:active_once(CSock), 1450 error 1451 end 1452 end, 1453 ?assertEqual(true, Foo(Foo, false)), 1454 String = binary_to_list(base64:encode("\0username\0PaSSw0rd")), 1455 socket:send(CSock, "AUTH PLAIN "++String++"\r\n"), 1456 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1457 ?assertMatch("235 Authentication successful.\r\n", Packet5) 1458 end 1459 } 1460 end, 1461 fun({CSock, _Pid}) -> 1462 {"A successful immediate AUTH PLAIN with an identity", 1463 fun() -> 1464 socket:active_once(CSock), 1465 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1466 ?assertMatch("220 localhost"++_Stuff, Packet), 1467 socket:send(CSock, "EHLO somehost.com\r\n"), 1468 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1469 ?assertMatch("250-localhost\r\n", Packet2), 1470 ?assertMatch("250-localhost\r\n", Packet2), 1471 Foo = fun(F, Acc) -> 1472 receive 1473 {tcp, CSock, "250-AUTH"++_Packet3} -> 1474 socket:active_once(CSock), 1475 F(F, true); 1476 {tcp, CSock, "250-"++_Packet3} -> 1477 socket:active_once(CSock), 1478 F(F, Acc); 1479 {tcp, CSock, "250 AUTH"++_Packet3} -> 1480 socket:active_once(CSock), 1481 true; 1482 {tcp, CSock, "250 "++_Packet3} -> 1483 socket:active_once(CSock), 1484 Acc; 1485 {tcp, CSock, _R} -> 1486 socket:active_once(CSock), 1487 error 1488 end 1489 end, 1490 ?assertEqual(true, Foo(Foo, false)), 1491 String = binary_to_list(base64:encode("username\0username\0PaSSw0rd")), 1492 socket:send(CSock, "AUTH PLAIN "++String++"\r\n"), 1493 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1494 ?assertMatch("235 Authentication successful.\r\n", Packet5) 1495 end 1496 } 1497 end, 1498 fun({CSock, _Pid}) -> 1499 {"An unsuccessful immediate AUTH PLAIN", 1500 fun() -> 1501 socket:active_once(CSock), 1502 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1503 ?assertMatch("220 localhost"++_Stuff, Packet), 1504 socket:send(CSock, "EHLO somehost.com\r\n"), 1505 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1506 ?assertMatch("250-localhost\r\n", Packet2), 1507 ?assertMatch("250-localhost\r\n", Packet2), 1508 Foo = fun(F, Acc) -> 1509 receive 1510 {tcp, CSock, "250-AUTH"++_Packet3} -> 1511 socket:active_once(CSock), 1512 F(F, true); 1513 {tcp, CSock, "250-"++_Packet3} -> 1514 socket:active_once(CSock), 1515 F(F, Acc); 1516 {tcp, CSock, "250 AUTH"++_Packet3} -> 1517 socket:active_once(CSock), 1518 true; 1519 {tcp, CSock, "250 "++_Packet3} -> 1520 socket:active_once(CSock), 1521 Acc; 1522 {tcp, CSock, _} -> 1523 socket:active_once(CSock), 1524 error 1525 end 1526 end, 1527 ?assertEqual(true, Foo(Foo, false)), 1528 String = binary_to_list(base64:encode("username\0username\0PaSSw0rd2")), 1529 socket:send(CSock, "AUTH PLAIN "++String++"\r\n"), 1530 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1531 ?assertMatch("535 Authentication failed.\r\n", Packet5) 1532 end 1533 } 1534 end, 1535 fun({CSock, _Pid}) -> 1536 {"An unsuccessful AUTH PLAIN", 1537 fun() -> 1538 socket:active_once(CSock), 1539 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1540 ?assertMatch("220 localhost"++_Stuff, Packet), 1541 socket:send(CSock, "EHLO somehost.com\r\n"), 1542 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1543 ?assertMatch("250-localhost\r\n", Packet2), 1544 ?assertMatch("250-localhost\r\n", Packet2), 1545 Foo = fun(F, Acc) -> 1546 receive 1547 {tcp, CSock, "250-AUTH"++_Packet3} -> 1548 socket:active_once(CSock), 1549 F(F, true); 1550 {tcp, CSock, "250-"++_Packet3} -> 1551 socket:active_once(CSock), 1552 F(F, Acc); 1553 {tcp, CSock, "250 AUTH"++_Packet3} -> 1554 socket:active_once(CSock), 1555 true; 1556 {tcp, CSock, "250 "++_Packet3} -> 1557 socket:active_once(CSock), 1558 Acc; 1559 {tcp, CSock, _} -> 1560 socket:active_once(CSock), 1561 error 1562 end 1563 end, 1564 ?assertEqual(true, Foo(Foo, false)), 1565 socket:send(CSock, "AUTH PLAIN\r\n"), 1566 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1567 ?assertMatch("334\r\n", Packet4), 1568 String = binary_to_list(base64:encode("\0username\0NotThePassword")), 1569 socket:send(CSock, String++"\r\n"), 1570 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1571 ?assertMatch("535 Authentication failed.\r\n", Packet5) 1572 end 1573 } 1574 end, 1575 fun({CSock, _Pid}) -> 1576 {"A successful AUTH LOGIN", 1577 fun() -> 1578 socket:active_once(CSock), 1579 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1580 ?assertMatch("220 localhost"++_Stuff, Packet), 1581 socket:send(CSock, "EHLO somehost.com\r\n"), 1582 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1583 ?assertMatch("250-localhost\r\n", Packet2), 1584 Foo = fun(F, Acc) -> 1585 receive 1586 {tcp, CSock, "250-AUTH"++_Packet3} -> 1587 socket:active_once(CSock), 1588 F(F, true); 1589 {tcp, CSock, "250-"++_Packet3} -> 1590 socket:active_once(CSock), 1591 F(F, Acc); 1592 {tcp, CSock, "250 AUTH"++_Packet3} -> 1593 socket:active_once(CSock), 1594 true; 1595 {tcp, CSock, "250 "++_Packet3} -> 1596 socket:active_once(CSock), 1597 Acc; 1598 {tcp, CSock, _} -> 1599 socket:active_once(CSock), 1600 error 1601 end 1602 end, 1603 ?assertEqual(true, Foo(Foo, false)), 1604 socket:send(CSock, "AUTH LOGIN\r\n"), 1605 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1606 ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), 1607 String = binary_to_list(base64:encode("username")), 1608 socket:send(CSock, String++"\r\n"), 1609 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1610 ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), 1611 PString = binary_to_list(base64:encode("PaSSw0rd")), 1612 socket:send(CSock, PString++"\r\n"), 1613 receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1614 ?assertMatch("235 Authentication successful.\r\n", Packet6) 1615 end 1616 } 1617 end, 1618 fun({CSock, _Pid}) -> 1619 {"An unsuccessful AUTH LOGIN", 1620 fun() -> 1621 socket:active_once(CSock), 1622 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1623 ?assertMatch("220 localhost"++_Stuff, Packet), 1624 socket:send(CSock, "EHLO somehost.com\r\n"), 1625 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1626 ?assertMatch("250-localhost\r\n", Packet2), 1627 Foo = fun(F, Acc) -> 1628 receive 1629 {tcp, CSock, "250-AUTH"++_Packet3} -> 1630 socket:active_once(CSock), 1631 F(F, true); 1632 {tcp, CSock, "250-"++_Packet3} -> 1633 socket:active_once(CSock), 1634 F(F, Acc); 1635 {tcp, CSock, "250 AUTH"++_Packet3} -> 1636 socket:active_once(CSock), 1637 true; 1638 {tcp, CSock, "250 "++_Packet3} -> 1639 socket:active_once(CSock), 1640 Acc; 1641 {tcp, CSock, _} -> 1642 socket:active_once(CSock), 1643 error 1644 end 1645 end, 1646 ?assertEqual(true, Foo(Foo, false)), 1647 socket:send(CSock, "AUTH LOGIN\r\n"), 1648 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1649 ?assertMatch("334 VXNlcm5hbWU6\r\n", Packet4), 1650 String = binary_to_list(base64:encode("username2")), 1651 socket:send(CSock, String++"\r\n"), 1652 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1653 ?assertMatch("334 UGFzc3dvcmQ6\r\n", Packet5), 1654 PString = binary_to_list(base64:encode("PaSSw0rd")), 1655 socket:send(CSock, PString++"\r\n"), 1656 receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end, 1657 ?assertMatch("535 Authentication failed.\r\n", Packet6) 1658 end 1659 } 1660 end, 1661 fun({CSock, _Pid}) -> 1662 {"A successful AUTH CRAM-MD5", 1663 fun() -> 1664 socket:active_once(CSock), 1665 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1666 ?assertMatch("220 localhost"++_Stuff, Packet), 1667 socket:send(CSock, "EHLO somehost.com\r\n"), 1668 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1669 ?assertMatch("250-localhost\r\n", Packet2), 1670 Foo = fun(F, Acc) -> 1671 receive 1672 {tcp, CSock, "250-AUTH"++_Packet3} -> 1673 socket:active_once(CSock), 1674 F(F, true); 1675 {tcp, CSock, "250-"++_Packet3} -> 1676 socket:active_once(CSock), 1677 F(F, Acc); 1678 {tcp, CSock, "250 AUTH"++_Packet3} -> 1679 socket:active_once(CSock), 1680 true; 1681 {tcp, CSock, "250 "++_Packet3} -> 1682 socket:active_once(CSock), 1683 Acc; 1684 {tcp, CSock, _} -> 1685 socket:active_once(CSock), 1686 error 1687 end 1688 end, 1689 ?assertEqual(true, Foo(Foo, false)), 1690 socket:send(CSock, "AUTH CRAM-MD5\r\n"), 1691 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1692 ?assertMatch("334 "++_, Packet4), 1693 1694 ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), 1695 Seed = base64:decode_to_string(Seed64), 1696 Digest = smtp_util:compute_cram_digest("PaSSw0rd", Seed), 1697 String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), 1698 socket:send(CSock, String++"\r\n"), 1699 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1700 ?assertMatch("235 Authentication successful.\r\n", Packet5) 1701 end 1702 } 1703 end, 1704 fun({CSock, _Pid}) -> 1705 {"An unsuccessful AUTH CRAM-MD5", 1706 fun() -> 1707 socket:active_once(CSock), 1708 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1709 ?assertMatch("220 localhost"++_Stuff, Packet), 1710 socket:send(CSock, "EHLO somehost.com\r\n"), 1711 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1712 ?assertMatch("250-localhost\r\n", Packet2), 1713 Foo = fun(F, Acc) -> 1714 receive 1715 {tcp, CSock, "250-AUTH"++_Packet3} -> 1716 socket:active_once(CSock), 1717 F(F, true); 1718 {tcp, CSock, "250-"++_Packet3} -> 1719 socket:active_once(CSock), 1720 F(F, Acc); 1721 {tcp, CSock, "250 AUTH"++_Packet3} -> 1722 socket:active_once(CSock), 1723 true; 1724 {tcp, CSock, "250 "++_Packet3} -> 1725 socket:active_once(CSock), 1726 Acc; 1727 {tcp, CSock, _} -> 1728 socket:active_once(CSock), 1729 error 1730 end 1731 end, 1732 ?assertEqual(true, Foo(Foo, false)), 1733 socket:send(CSock, "AUTH CRAM-MD5\r\n"), 1734 receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end, 1735 ?assertMatch("334 "++_, Packet4), 1736 1737 ["334", Seed64] = string:tokens(smtp_util:trim_crlf(Packet4), " "), 1738 Seed = base64:decode_to_string(Seed64), 1739 Digest = smtp_util:compute_cram_digest("Passw0rd", Seed), 1740 String = binary_to_list(base64:encode(list_to_binary(["username ", Digest]))), 1741 socket:send(CSock, String++"\r\n"), 1742 receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end, 1743 ?assertMatch("535 Authentication failed.\r\n", Packet5) 1744 end 1745 } 1746 end 1747 ] 1748 }. 1749 1750smtp_session_tls_test_() -> 1751 {foreach, 1752 local, 1753 fun() -> 1754 crypto:start(), 1755 application:start(public_key), 1756 application:start(ssl), 1757 Self = self(), 1758 spawn(fun() -> 1759 {ok, ListenSock} = socket:listen(tcp, 9876, [binary]), 1760 {ok, X} = socket:accept(ListenSock), 1761 socket:controlling_process(X, Self), 1762 Self ! X 1763 end), 1764 {ok, CSock} = socket:connect(tcp, "localhost", 9876), 1765 receive 1766 SSock when is_port(SSock) -> 1767 ok 1768 end, 1769 {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}]}]), 1770 socket:controlling_process(SSock, Pid), 1771 {CSock, Pid} 1772 end, 1773 fun({CSock, _Pid}) -> 1774 socket:close(CSock) 1775 end, 1776 [fun({CSock, _Pid}) -> 1777 {"EHLO response includes STARTTLS", 1778 fun() -> 1779 socket:active_once(CSock), 1780 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1781 ?assertMatch("220 localhost"++_Stuff, Packet), 1782 socket:send(CSock, "EHLO somehost.com\r\n"), 1783 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1784 ?assertMatch("250-localhost\r\n", Packet2), 1785 Foo = fun(F, Acc) -> 1786 receive 1787 {tcp, CSock, "250-STARTTLS"++_} -> 1788 socket:active_once(CSock), 1789 F(F, true); 1790 {tcp, CSock, "250-"++_Packet3} -> 1791 socket:active_once(CSock), 1792 F(F, Acc); 1793 {tcp, CSock, "250 STARTTLS"++_} -> 1794 socket:active_once(CSock), 1795 true; 1796 {tcp, CSock, "250 "++_Packet3} -> 1797 socket:active_once(CSock), 1798 Acc; 1799 {tcp, CSock, _} -> 1800 socket:active_once(CSock), 1801 error 1802 end 1803 end, 1804 ?assertEqual(true, Foo(Foo, false)) 1805 end 1806 } 1807 end, 1808 fun({CSock, _Pid}) -> 1809 {"STARTTLS does a SSL handshake", 1810 fun() -> 1811 socket:active_once(CSock), 1812 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1813 ?assertMatch("220 localhost"++_Stuff, Packet), 1814 socket:send(CSock, "EHLO somehost.com\r\n"), 1815 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1816 ?assertMatch("250-localhost\r\n", Packet2), 1817 Foo = fun(F, Acc) -> 1818 receive 1819 {tcp, CSock, "250-STARTTLS"++_} -> 1820 socket:active_once(CSock), 1821 F(F, true); 1822 {tcp, CSock, "250-"++_Packet3} -> 1823 socket:active_once(CSock), 1824 F(F, Acc); 1825 {tcp, CSock, "250 STARTTLS"++_} -> 1826 socket:active_once(CSock), 1827 true; 1828 {tcp, CSock, "250 "++_Packet3} -> 1829 socket:active_once(CSock), 1830 Acc; 1831 {tcp, CSock, _} -> 1832 socket:active_once(CSock), 1833 error 1834 end 1835 end, 1836 ?assertEqual(true, Foo(Foo, false)), 1837 socket:send(CSock, "STARTTLS\r\n"), 1838 receive {tcp, CSock, Packet4} -> ok end, 1839 ?assertMatch("220 "++_, Packet4), 1840 Result = socket:to_ssl_client(CSock), 1841 ?assertMatch({ok, _Socket}, Result), 1842 {ok, _Socket} = Result 1843 %socket:active_once(Socket), 1844 %ssl:send(Socket, "EHLO somehost.com\r\n"), 1845 %receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end, 1846 %?assertEqual("Foo", Packet5), 1847 end 1848 } 1849 end, 1850 fun({CSock, _Pid}) -> 1851 {"After STARTTLS, EHLO doesn't report STARTTLS", 1852 fun() -> 1853 socket:active_once(CSock), 1854 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1855 ?assertMatch("220 localhost"++_Stuff, Packet), 1856 socket:send(CSock, "EHLO somehost.com\r\n"), 1857 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1858 ?assertMatch("250-localhost\r\n", Packet2), 1859 Foo = fun(F, Acc) -> 1860 receive 1861 {tcp, CSock, "250-STARTTLS"++_} -> 1862 socket:active_once(CSock), 1863 F(F, true); 1864 {tcp, CSock, "250-"++_Packet3} -> 1865 socket:active_once(CSock), 1866 F(F, Acc); 1867 {tcp, CSock, "250 STARTTLS"++_} -> 1868 socket:active_once(CSock), 1869 true; 1870 {tcp, CSock, "250 "++_Packet3} -> 1871 socket:active_once(CSock), 1872 Acc; 1873 {tcp, CSock, _} -> 1874 socket:active_once(CSock), 1875 error 1876 end 1877 end, 1878 ?assertEqual(true, Foo(Foo, false)), 1879 socket:send(CSock, "STARTTLS\r\n"), 1880 receive {tcp, CSock, Packet4} -> ok end, 1881 ?assertMatch("220 "++_, Packet4), 1882 Result = socket:to_ssl_client(CSock), 1883 ?assertMatch({ok, _Socket}, Result), 1884 {ok, Socket} = Result, 1885 socket:active_once(Socket), 1886 socket:send(Socket, "EHLO somehost.com\r\n"), 1887 receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end, 1888 ?assertMatch("250-localhost\r\n", Packet5), 1889 Bar = fun(F, Acc) -> 1890 receive 1891 {ssl, Socket, "250-STARTTLS"++_} -> 1892 socket:active_once(Socket), 1893 F(F, true); 1894 {ssl, Socket, "250-"++_} -> 1895 socket:active_once(Socket), 1896 F(F, Acc); 1897 {ssl, Socket, "250 STARTTLS"++_} -> 1898 socket:active_once(Socket), 1899 true; 1900 {ssl, Socket, "250 "++_} -> 1901 socket:active_once(Socket), 1902 Acc; 1903 {ssl, Socket, _} -> 1904 socket:active_once(Socket), 1905 error 1906 end 1907 end, 1908 ?assertEqual(false, Bar(Bar, false)) 1909 end 1910 } 1911 end, 1912 fun({CSock, _Pid}) -> 1913 {"After STARTTLS, re-negotiating STARTTLS is an error", 1914 fun() -> 1915 socket:active_once(CSock), 1916 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1917 ?assertMatch("220 localhost"++_Stuff, Packet), 1918 socket:send(CSock, "EHLO somehost.com\r\n"), 1919 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1920 ?assertMatch("250-localhost\r\n", Packet2), 1921 Foo = fun(F, Acc) -> 1922 receive 1923 {tcp, CSock, "250-STARTTLS"++_} -> 1924 socket:active_once(CSock), 1925 F(F, true); 1926 {tcp, CSock, "250-"++_Packet3} -> 1927 socket:active_once(CSock), 1928 F(F, Acc); 1929 {tcp, CSock, "250 STARTTLS"++_} -> 1930 socket:active_once(CSock), 1931 true; 1932 {tcp, CSock, "250 "++_Packet3} -> 1933 socket:active_once(CSock), 1934 Acc; 1935 {tcp, CSock, _} -> 1936 socket:active_once(CSock), 1937 error 1938 end 1939 end, 1940 ?assertEqual(true, Foo(Foo, false)), 1941 socket:send(CSock, "STARTTLS\r\n"), 1942 receive {tcp, CSock, Packet4} -> ok end, 1943 ?assertMatch("220 "++_, Packet4), 1944 Result = socket:to_ssl_client(CSock), 1945 ?assertMatch({ok, _Socket}, Result), 1946 {ok, Socket} = Result, 1947 socket:active_once(Socket), 1948 socket:send(Socket, "EHLO somehost.com\r\n"), 1949 receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end, 1950 ?assertMatch("250-localhost\r\n", Packet5), 1951 Bar = fun(F, Acc) -> 1952 receive 1953 {ssl, Socket, "250-STARTTLS"++_} -> 1954 socket:active_once(Socket), 1955 F(F, true); 1956 {ssl, Socket, "250-"++_} -> 1957 socket:active_once(Socket), 1958 F(F, Acc); 1959 {ssl, Socket, "250 STARTTLS"++_} -> 1960 socket:active_once(Socket), 1961 true; 1962 {ssl, Socket, "250 "++_} -> 1963 socket:active_once(Socket), 1964 Acc; 1965 {ssl, Socket, _} -> 1966 socket:active_once(Socket), 1967 error 1968 end 1969 end, 1970 ?assertEqual(false, Bar(Bar, false)), 1971 socket:send(Socket, "STARTTLS\r\n"), 1972 receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end, 1973 ?assertMatch("500 "++_, Packet6) 1974 end 1975 } 1976 end, 1977 fun({CSock, _Pid}) -> 1978 {"STARTTLS can't take any parameters", 1979 fun() -> 1980 socket:active_once(CSock), 1981 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 1982 ?assertMatch("220 localhost"++_Stuff, Packet), 1983 socket:send(CSock, "EHLO somehost.com\r\n"), 1984 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 1985 ?assertMatch("250-localhost\r\n", Packet2), 1986 Foo = fun(F, Acc) -> 1987 receive 1988 {tcp, CSock, "250-STARTTLS"++_} -> 1989 socket:active_once(CSock), 1990 F(F, true); 1991 {tcp, CSock, "250-"++_Packet3} -> 1992 socket:active_once(CSock), 1993 F(F, Acc); 1994 {tcp, CSock, "250 STARTTLS"++_} -> 1995 socket:active_once(CSock), 1996 true; 1997 {tcp, CSock, "250 "++_Packet3} -> 1998 socket:active_once(CSock), 1999 Acc; 2000 {tcp, CSock, _} -> 2001 socket:active_once(CSock), 2002 error 2003 end 2004 end, 2005 ?assertEqual(true, Foo(Foo, false)), 2006 socket:send(CSock, "STARTTLS foo\r\n"), 2007 receive {tcp, CSock, Packet4} -> ok end, 2008 ?assertMatch("501 "++_, Packet4) 2009 end 2010 } 2011 end, 2012 fun({CSock, _Pid}) -> 2013 {"Negotiating STARTTLS twice is an error", 2014 fun() -> 2015 socket:active_once(CSock), 2016 receive {tcp, CSock, _Packet} -> socket:active_once(CSock) end, 2017 socket:send(CSock, "EHLO somehost.com\r\n"), 2018 receive {tcp, CSock, _Packet2} -> socket:active_once(CSock) end, 2019 ReadExtensions = fun(F, Acc) -> 2020 receive 2021 {tcp, CSock, "250-STARTTLS"++_} -> 2022 socket:active_once(CSock), 2023 F(F, true); 2024 {tcp, CSock, "250-"++_Packet3} -> 2025 socket:active_once(CSock), 2026 F(F, Acc); 2027 {tcp, CSock, "250 STARTTLS"++_} -> 2028 socket:active_once(CSock), 2029 true; 2030 {tcp, CSock, "250 "++_Packet3} -> 2031 socket:active_once(CSock), 2032 Acc; 2033 {tcp, CSock, _} -> 2034 socket:active_once(CSock), 2035 error 2036 end 2037 end, 2038 ?assertEqual(true, ReadExtensions(ReadExtensions, false)), 2039 socket:send(CSock, "STARTTLS\r\n"), 2040 receive {tcp, CSock, _} -> ok end, 2041 {ok, Socket} = socket:to_ssl_client(CSock), 2042 socket:active_once(Socket), 2043 socket:send(Socket, "EHLO somehost.com\r\n"), 2044 receive {ssl, Socket, PacketN} -> socket:active_once(Socket) end, 2045 ?assertMatch("250-localhost\r\n", PacketN), 2046 Bar = fun(F, Acc) -> 2047 receive 2048 {ssl, Socket, "250-STARTTLS"++_} -> 2049 socket:active_once(Socket), 2050 F(F, true); 2051 {ssl, Socket, "250-"++_} -> 2052 socket:active_once(Socket), 2053 F(F, Acc); 2054 {ssl, Socket, "250 STARTTLS"++_} -> 2055 socket:active_once(Socket), 2056 true; 2057 {ssl, Socket, "250 "++_} -> 2058 socket:active_once(Socket), 2059 Acc; 2060 {tcp, Socket, _} -> 2061 socket:active_once(Socket), 2062 error 2063 end 2064 end, 2065 ?assertEqual(false, Bar(Bar, false)), 2066 socket:send(Socket, "STARTTLS\r\n"), 2067 receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end, 2068 ?assertMatch("500 "++_, Packet6) 2069 end 2070 } 2071 end, 2072 fun({CSock, _Pid}) -> 2073 {"STARTTLS can't take any parameters", 2074 fun() -> 2075 socket:active_once(CSock), 2076 receive {tcp, CSock, Packet} -> socket:active_once(CSock) end, 2077 ?assertMatch("220 localhost"++_Stuff, Packet), 2078 socket:send(CSock, "EHLO somehost.com\r\n"), 2079 receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end, 2080 ?assertMatch("250-localhost\r\n", Packet2), 2081 Foo = fun(F, Acc) -> 2082 receive 2083 {tcp, CSock, "250-STARTTLS"++_} -> 2084 socket:active_once(CSock), 2085 F(F, true); 2086 {tcp, CSock, "250-"++_Packet3} -> 2087 socket:active_once(CSock), 2088 F(F, Acc); 2089 {tcp, CSock, "250 STARTTLS"++_} -> 2090 socket:active_once(CSock), 2091 true; 2092 {tcp, CSock, "250 "++_Packet3} -> 2093 socket:active_once(CSock), 2094 Acc; 2095 {tcp, CSock, _} -> 2096 socket:active_once(CSock), 2097 error 2098 end 2099 end, 2100 ?assertEqual(true, Foo(Foo, false)), 2101 socket:send(CSock, "STARTTLS foo\r\n"), 2102 receive {tcp, CSock, Packet4} -> ok end, 2103 ?assertMatch("501 "++_, Packet4) 2104 end 2105 } 2106 end, 2107 fun({CSock, _Pid}) -> 2108 {"After STARTTLS, message is received by server", 2109 fun() -> 2110 socket:active_once(CSock), 2111 receive {tcp, CSock, _Packet} -> socket:active_once(CSock) end, 2112 socket:send(CSock, "EHLO somehost.com\r\n"), 2113 receive {tcp, CSock, _Packet2} -> socket:active_once(CSock) end, 2114 ReadExtensions = fun(F, Acc) -> 2115 receive 2116 {tcp, CSock, "250-STARTTLS"++_} -> 2117 socket:active_once(CSock), 2118 F(F, true); 2119 {tcp, CSock, "250-"++_Packet3} -> 2120 socket:active_once(CSock), 2121 F(F, Acc); 2122 {tcp, CSock, "250 STARTTLS"++_} -> 2123 socket:active_once(CSock), 2124 true; 2125 {tcp, CSock, "250 "++_Packet3} -> 2126 socket:active_once(CSock), 2127 Acc; 2128 {tcp, CSock, _} -> 2129 socket:active_once(CSock), 2130 error 2131 end 2132 end, 2133 ?assertEqual(true, ReadExtensions(ReadExtensions, false)), 2134 socket:send(CSock, "STARTTLS\r\n"), 2135 receive {tcp, CSock, _} -> ok end, 2136 {ok, Socket} = socket:to_ssl_client(CSock), 2137 socket:active_once(Socket), 2138 socket:send(Socket, "EHLO somehost.com\r\n"), 2139 ReadSSLExtensions = fun(F, Acc) -> 2140 receive 2141 {ssl, Socket, "250-"++_Rest} -> 2142 socket:active_once(Socket), 2143 F(F, Acc); 2144 {ssl, Socket, "250 "++_} -> 2145 socket:active_once(Socket), 2146 true; 2147 {ssl, Socket, _R} -> 2148 socket:active_once(Socket), 2149 error 2150 end 2151 end, 2152 ?assertEqual(true, ReadSSLExtensions(ReadSSLExtensions, false)), 2153 socket:send(Socket, "MAIL FROM: <user@somehost.com>\r\n"), 2154 receive {ssl, Socket, Packet4} -> socket:active_once(Socket) end, 2155 ?assertMatch("250 "++_, Packet4), 2156 socket:send(Socket, "RCPT TO: <user@otherhost.com>\r\n"), 2157 receive {ssl, Socket, Packet5} -> socket:active_once(Socket) end, 2158 ?assertMatch("250 "++_, Packet5), 2159 socket:send(Socket, "DATA\r\n"), 2160 receive {ssl, Socket, Packet6} -> socket:active_once(Socket) end, 2161 ?assertMatch("354 "++_, Packet6), 2162 socket:send(Socket, "Subject: tls message\r\n"), 2163 socket:send(Socket, "To: <user@otherhost>\r\n"), 2164 socket:send(Socket, "From: <user@somehost.com>\r\n"), 2165 socket:send(Socket, "\r\n"), 2166 socket:send(Socket, "message body"), 2167 socket:send(Socket, "\r\n.\r\n"), 2168 receive {ssl, Socket, Packet7} -> socket:active_once(Socket) end, 2169 ?assertMatch("250 "++_, Packet7) 2170 end 2171 } 2172 end 2173 ] 2174 }. 2175 2176stray_newline_test_() -> 2177 [ 2178 {"Error out by default", 2179 fun() -> 2180 ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, false, 0)), 2181 ?assertEqual(error, check_bare_crlf(<<"foo\n">>, <<>>, false, 0)), 2182 ?assertEqual(error, check_bare_crlf(<<"fo\ro\n">>, <<>>, false, 0)), 2183 ?assertEqual(error, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, false, 0)), 2184 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, false, 0)), 2185 ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"foo\r">>, <<>>, false, 0)) 2186 end 2187 }, 2188 {"Fixing them should work", 2189 fun() -> 2190 ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, fix, 0)), 2191 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\n">>, <<>>, fix, 0)), 2192 ?assertEqual(<<"fo\r\no\r\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, fix, 0)), 2193 ?assertEqual(<<"fo\r\no\r\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, fix, 0)), 2194 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, fix, 0)) 2195 end 2196 }, 2197 {"Stripping them should work", 2198 fun() -> 2199 ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, strip, 0)), 2200 ?assertEqual(<<"foo">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, strip, 0)), 2201 ?assertEqual(<<"foo\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, strip, 0)), 2202 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, strip, 0)) 2203 end 2204 }, 2205 {"Ignoring them should work", 2206 fun() -> 2207 ?assertEqual(<<"foo">>, check_bare_crlf(<<"foo">>, <<>>, ignore, 0)), 2208 ?assertEqual(<<"fo\ro\n">>, check_bare_crlf(<<"fo\ro\n">>, <<>>, ignore, 0)), 2209 ?assertEqual(<<"fo\ro\n\r">>, check_bare_crlf(<<"fo\ro\n\r">>, <<>>, ignore, 0)), 2210 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"foo\r\n">>, <<>>, ignore, 0)) 2211 end 2212 }, 2213 {"Leading bare LFs should check the previous line", 2214 fun() -> 2215 ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)), 2216 ?assertEqual(<<"\r\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, fix, 0)), 2217 ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, fix, 0)), 2218 ?assertEqual(<<"foo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, strip, 0)), 2219 ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, strip, 0)), 2220 ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, ignore, 0)), 2221 ?assertEqual(error, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r\n">>, false, 0)), 2222 ?assertEqual(<<"\nfoo\r\n">>, check_bare_crlf(<<"\nfoo\r\n">>, <<"bar\r">>, false, 0)) 2223 end 2224 } 2225 ]. 2226 2227 2228-endif.