PageRenderTime 142ms CodeModel.GetById 4ms app.highlight 125ms RepoModel.GetById 1ms app.codeStats 1ms

/src/gen_smtp_server_session.erl

https://github.com/pedroaxl/gen_smtp
Erlang | 2251 lines | 1983 code | 59 blank | 209 comment | 9 complexity | afab91831c9db5e9e76df0b75a6885e7 MD5 | raw file

Large files files are truncated, but you can click here to view the full 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 A Per-connection SMTP server, extensible via a callback module.
  24
  25-module(gen_smtp_server_session).
  26-behaviour(gen_server).
  27
  28-ifdef(TEST).
  29-import(smtp_util, [compute_cram_digest/2]).
  30-include_lib("eunit/include/eunit.hrl").
  31-endif.
  32
  33-define(MAXIMUMSIZE, 10485760). %10mb
  34-define(BUILTIN_EXTENSIONS, [{"SIZE", "10485670"}, {"8BITMIME", true}, {"PIPELINING", true}]).
  35-define(TIMEOUT, 180000). % 3 minutes
  36
  37%% External API
  38-export([start_link/3, start/3]).
  39
  40%% gen_server callbacks
  41-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
  42		code_change/3]).
  43
  44-export([behaviour_info/1]).
  45
  46-record(envelope,
  47	{
  48		from :: string() | 'undefined',
  49		to = [] :: [string()],
  50		data = <<>> :: binary(),
  51		expectedsize = 0 :: pos_integer() | 0,
  52		auth = {[], []} :: {string() | [], string() | []} % {"username", "password"}
  53	}
  54).
  55
  56-record(state,
  57	{
  58		socket = erlang:error({undefined, socket}) :: port() | tuple(),
  59		module = erlang:error({undefined, module}) :: atom(),
  60		envelope = undefined :: 'undefined' | #envelope{},
  61		extensions = [] :: [{string(), string()}],
  62		waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5',
  63		authdata :: 'undefined' | string(),
  64		readmessage = false :: boolean(),
  65		tls = false :: boolean(),
  66		callbackstate :: any(),
  67		options = [] :: [tuple()]
  68	}
  69).
  70
  71behaviour_info(callbacks) ->
  72	[{init,4},
  73	  {terminate,2},
  74	  {code_change,3},
  75	  {handle_HELO,2},
  76	  {handle_EHLO,3},
  77	  {handle_MAIL,2},
  78	  {handle_MAIL_extension,2},
  79	  {handle_RCPT,2},
  80	  {handle_RCPT_extension,2},
  81	  {handle_DATA,4},
  82	  {handle_RSET,1},
  83	  {handle_VRFY,2},
  84	  {handle_other,3}];
  85behaviour_info(_Other) ->
  86	undefined.
  87
  88-spec(start_link/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
  89start_link(Socket, Module, Options) ->
  90	gen_server:start_link(?MODULE, [Socket, Module, Options], []).
  91
  92-spec(start/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
  93start(Socket, Module, Options) ->
  94	gen_server:start(?MODULE, [Socket, Module, Options], []).
  95
  96-spec(init/1 :: (Args :: list()) -> {'ok', #state{}} | {'stop', any()} | 'ignore').
  97init([Socket, Module, Options]) ->
  98	{ok, {PeerName, _Port}} = socket:peername(Socket),
  99	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
 100		{ok, Banner, CallbackState} ->
 101			socket:send(Socket, io_lib:format("220 ~s\r\n", [Banner])),
 102			socket:active_once(Socket),
 103			{ok, #state{socket = Socket, module = Module, options = Options, callbackstate = CallbackState}, ?TIMEOUT};
 104		{stop, Reason, Message} ->
 105			socket:send(Socket, Message ++ "\r\n"),
 106			socket:close(Socket),
 107			{stop, Reason};
 108		ignore ->
 109			socket:close(Socket),
 110			ignore
 111	end.
 112
 113%% @hidden
 114handle_call(stop, _From, State) ->
 115	{stop, normal, ok, State};
 116
 117handle_call(Request, _From, State) ->
 118	{reply, {unknown_call, Request}, State}.
 119
 120%% @hidden
 121handle_cast(_Msg, State) ->
 122	{noreply, State}.
 123
 124
 125handle_info({receive_data, {error, size_exceeded}}, #state{socket = Socket, readmessage = true} = State) ->
 126	socket:send(Socket, "552 Message too large\r\n"),
 127	socket:active_once(Socket),
 128	{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
 129handle_info({receive_data, {error, bare_newline}}, #state{socket = Socket, readmessage = true} = State) ->
 130	socket:send(Socket, "451 Bare newline detected\r\n"),
 131	io:format("bare newline detected: ~p~n", [self()]),
 132	socket:active_once(Socket),
 133	{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
 134handle_info({receive_data, Body, Rest}, #state{socket = Socket, readmessage = true, envelope = Env, module=Module,
 135		callbackstate = OldCallbackState,  extensions = Extensions} = State) ->
 136	% send the remainder of the data...
 137	case Rest of
 138		<<>> -> ok; % no remaining data
 139		_ -> self() ! {socket:get_proto(Socket), Socket, Rest}
 140	end,
 141	socket:setopts(Socket, [{packet, line}]),
 142	Envelope = Env#envelope{data = Body},% size = length(Body)},
 143	%io:format("received body from child process, remainder was ~p (~p)~n", [Rest, self()]),
 144
 145%handle_info({_Proto, Socket, <<".\r\n">>}, #state{readmessage = true, envelope = Env, module = Module} = State) ->
 146	%io:format("done reading message~n"),
 147	%io:format("entire message~n~s~n", [Envelope#envelope.data]),
 148	%Envelope = Env#envelope{data = list_to_binary(lists:reverse(Env#envelope.data))},
 149	Valid = case has_extension(Extensions, "SIZE") of
 150		{true, Value} ->
 151			case byte_size(Envelope#envelope.data) > list_to_integer(Value) of
 152				true ->
 153					socket:send(Socket, "552 Message too large\r\n"),
 154					socket:active_once(Socket),
 155					false;
 156				false ->
 157					true
 158			end;
 159		false ->
 160			true
 161	end,
 162	case Valid of
 163		true ->
 164			case Module:handle_DATA(Envelope#envelope.from, Envelope#envelope.to, Envelope#envelope.data, OldCallbackState) of
 165				{ok, Reference, CallbackState} ->
 166					socket:send(Socket, io_lib:format("250 queued as ~s\r\n", [Reference])),
 167					socket:active_once(Socket),
 168					{noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT};
 169				{error, Message, CallbackState} ->
 170					socket:send(Socket, Message++"\r\n"),
 171					socket:active_once(Socket),
 172					{noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT}
 173			end;
 174		false ->
 175			% might not even be able to get here anymore...
 176			{noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}
 177	end;
 178handle_info({_SocketType, Socket, Packet}, State) ->
 179	case handle_request(parse_request(Packet), State) of
 180		{ok,  #state{envelope = Envelope, extensions = Extensions,  options = Options, readmessage = true} = NewState} ->
 181			MaxSize = case has_extension(Extensions, "SIZE") of
 182				{true, Value} ->
 183					list_to_integer(Value);
 184				false ->
 185					?MAXIMUMSIZE
 186			end,
 187			Session = self(),
 188			Size = 0,
 189			socket:setopts(Socket, [{packet, raw}]),
 190			spawn_opt(fun() -> receive_data([],
 191							Socket, {0, Envelope#envelope.expectedsize div 2}, Size, MaxSize, Session, Options) end,
 192				[link, {fullsweep_after, 0}]),
 193			{noreply, NewState, ?TIMEOUT};
 194		{ok, NewState} ->
 195			socket:active_once(NewState#state.socket),
 196			{noreply, NewState, ?TIMEOUT};
 197		{stop, Reason, NewState} ->
 198			{stop, Reason, NewState}
 199	end;
 200handle_info({tcp_closed, _Socket}, State) ->
 201	{stop, normal, State};
 202handle_info({ssl_closed, _Socket}, State) ->
 203	{stop, normal, State};
 204handle_info(timeout, #state{socket = Socket} = State) ->
 205	socket:send(Socket, "421 Error: timeout exceeded\r\n"),
 206	socket:close(Socket),
 207	{stop, normal, State};
 208handle_info(Info, State) ->
 209	io:format("unhandled info message ~p~n", [Info]),
 210	{noreply, State}.
 211
 212%% @hidden
 213-spec(terminate/2 :: (Reason :: any(), State :: #state{}) -> 'ok').
 214terminate(Reason, State) ->
 215	socket:close(State#state.socket),
 216	(State#state.module):terminate(Reason, State#state.callbackstate).
 217
 218%% @hidden
 219code_change(OldVsn, #state{module = Module} = State, Extra) ->
 220	% TODO - this should probably be the callback module's version or its checksum
 221	CallbackState =
 222		case catch Module:code_change(OldVsn, State#state.callbackstate, Extra) of
 223			{ok, NewCallbackState} -> NewCallbackState;
 224			_                      -> State#state.callbackstate
 225		end,
 226        {ok, State#state{callbackstate = CallbackState}}.
 227
 228-spec(parse_request/1 :: (Packet :: binary()) -> {string(), list()}).
 229parse_request(Packet) ->
 230	Request = binstr:strip(binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s),
 231	case binstr:strchr(Request, $\s) of
 232		0 ->
 233			% io:format("got a ~s request~n", [Request]),
 234			case binstr:to_upper(Request) of
 235				<<"QUIT">> = Res -> {Res, <<>>};
 236				<<"DATA">> = Res -> {Res, <<>>};
 237				% likely a base64-encoded client reply
 238				_ -> {Request, <<>>}
 239			end;
 240		Index ->
 241			Verb = binstr:substr(Request, 1, Index - 1),
 242			Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s),
 243			%io:format("got a ~s request with parameters ~s~n", [Verb, Parameters]),
 244			{binstr:to_upper(Verb), Parameters}
 245	end.
 246
 247-spec(handle_request/2 :: ({Verb :: string(), Args :: string()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}).
 248handle_request({<<>>, _Any}, #state{socket = Socket} = State) ->
 249	socket:send(Socket, "500 Error: bad syntax\r\n"),
 250	{ok, State};
 251handle_request({<<"HELO">>, <<>>}, #state{socket = Socket} = State) ->
 252	socket:send(Socket, "501 Syntax: HELO hostname\r\n"),
 253	{ok, State};
 254handle_request({<<"HELO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState} = State) ->
 255	case Module:handle_HELO(Hostname, OldCallbackState) of
 256		{ok, CallbackState} ->
 257			socket:send(Socket, io_lib:format("250 ~s\r\n", [proplists:get_value(hostname, Options, smtp_util:guess_FQDN())])),
 258			{ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}};
 259		{error, Message, CallbackState} ->
 260			socket:send(Socket, Message ++ "\r\n"),
 261			{ok, State#state{callbackstate = CallbackState}}
 262	end;
 263handle_request({<<"EHLO">>, <<>>}, #state{socket = Socket} = State) ->
 264	socket:send(Socket, "501 Syntax: EHLO hostname\r\n"),
 265	{ok, State};
 266handle_request({<<"EHLO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) ->
 267	case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of
 268		{ok, Extensions, CallbackState} ->
 269			case Extensions of
 270				[] ->
 271					socket:send(Socket, io_lib:format("250 ~s\r\n", [proplists:get_value(hostname, Options, smtp_util:guess_FQDN())])),
 272					{ok, State#state{extensions = Extensions, callbackstate = CallbackState}};
 273				_Else ->
 274					F =
 275					fun({E, true}, {Pos, Len, Acc}) when Pos =:= Len ->
 276							{Pos, Len, string:concat(string:concat(string:concat(Acc, "250 "), E), "\r\n")};
 277						({E, Value}, {Pos, Len, Acc}) when Pos =:= Len ->
 278							{Pos, Len, string:concat(Acc, io_lib:format("250 ~s ~s\r\n", [E, Value]))};
 279						({E, true}, {Pos, Len, Acc}) ->
 280							{Pos+1, Len, string:concat(string:concat(string:concat(Acc, "250-"), E), "\r\n")};
 281						({E, Value}, {Pos, Len, Acc}) ->
 282							{Pos+1, Len, string:concat(Acc, io_lib:format("250-~s ~s\r\n", [E, Value]))}
 283					end,
 284					Extensions2 = case Tls of
 285						true ->
 286							Extensions -- [{"STARTTLS", true}];
 287						false ->
 288							Extensions
 289					end,
 290					{_, _, Response} = lists:foldl(F, {1, length(Extensions2), string:concat(string:concat("250-", proplists:get_value(hostname, Options, smtp_util:guess_FQDN())), "\r\n")}, Extensions2),
 291					socket:send(Socket, Response),
 292					{ok, State#state{extensions = Extensions2, envelope = #envelope{}, callbackstate = CallbackState}}
 293			end;
 294		{error, Message, CallbackState} ->
 295			socket:send(Socket, Message++"\r\n"),
 296			{ok, State#state{callbackstate = CallbackState}}
 297	end;
 298
 299handle_request({<<"AUTH">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
 300	socket:send(Socket, "503 Error: send EHLO first\r\n"),
 301	{ok, State};
 302handle_request({<<"AUTH">>, Args}, #state{socket = Socket, extensions = Extensions, envelope = Envelope, options = Options} = State) ->
 303	case binstr:strchr(Args, $\s) of
 304		0 ->
 305			AuthType = Args,
 306			Parameters = false;
 307		Index ->
 308			AuthType = binstr:substr(Args, 1, Index - 1),
 309			Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s)
 310	end,
 311
 312	case has_extension(Extensions, "AUTH") of
 313		false ->
 314			socket:send(Socket, "502 Error: AUTH not implemented\r\n"),
 315			{ok, State};
 316		{true, AvailableTypes} ->
 317			case lists:member(string:to_upper(binary_to_list(AuthType)), string:tokens(AvailableTypes, " ")) of
 318				false ->
 319					socket:send(Socket, "504 Unrecognized authentication type\r\n"),
 320					{ok, State};
 321				true ->
 322					case binstr:to_upper(AuthType) of
 323						<<"LOGIN">> ->
 324							% socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")),
 325							socket:send(Socket, "334 VXNlcm5hbWU6\r\n"),
 326							{ok, State#state{waitingauth = 'login', envelope = Envelope#envelope{auth = {[], []}}}};
 327						<<"PLAIN">> when Parameters =/= false ->
 328							% TODO - duplicated below in handle_request waitingauth PLAIN
 329							case string:tokens(base64:decode_to_string(Parameters), [0]) of
 330								[_Identity, Username, Password] ->
 331									try_auth('plain', Username, Password, State);
 332								[Username, Password] ->
 333									try_auth('plain', Username, Password, State);
 334								_ ->
 335									% TODO error
 336									{ok, State}
 337							end;
 338						<<"PLAIN">> ->
 339							socket:send(Socket, "334\r\n"),
 340							{ok, State#state{waitingauth = 'plain', envelope = Envelope#envelope{auth = {[], []}}}};
 341						<<"CRAM-MD5">> ->
 342							crypto:start(), % ensure crypto is started, we're gonna need it
 343							String = smtp_util:get_cram_string(proplists:get_value(hostname, Options, smtp_util:guess_FQDN())),
 344							socket:send(Socket, "334 "++String++"\r\n"),
 345							{ok, State#state{waitingauth = 'cram-md5', authdata=base64:decode_to_string(String), envelope = Envelope#envelope{auth = {[], []}}}}
 346						%"DIGEST-MD5" -> % TODO finish this? (see rfc 2831)
 347							%crypto:start(), % ensure crypto is started, we're gonna need it
 348							%Nonce = get_digest_nonce(),
 349							%Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname),
 350							%socket:send(Socket, "334 "++Response++"\r\n"),
 351							%{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}}
 352					end
 353			end
 354	end;
 355
 356% the client sends a response to auth-cram-md5
 357handle_request({Username64, <<>>}, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {[],[]}}, authdata = AuthData} = State) ->
 358	case string:tokens(base64:decode_to_string(Username64), " ") of
 359		[Username, Digest] ->
 360			try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata=undefined});
 361		_ ->
 362			% TODO error
 363			{ok, State#state{waitingauth=false, authdata=undefined}}
 364	end;
 365
 366% the client sends a \0username\0password response to auth-plain
 367handle_request({Username64, <<>>}, #state{waitingauth = 'plain', envelope = #envelope{auth = {[],[]}}} = State) ->
 368	case string:tokens(base64:decode_to_string(Username64), [0]) of
 369		[_Identity, Username, Password] ->
 370			try_auth('plain', Username, Password, State);
 371		[Username, Password] ->
 372			try_auth('plain', Username, Password, State);
 373		_ ->
 374			% TODO error
 375			{ok, State#state{waitingauth=false}}
 376	end;
 377
 378% the client sends a username response to auth-login
 379handle_request({Username64, <<>>}, #state{socket = Socket, waitingauth = 'login', envelope = #envelope{auth = {[],[]}}} = State) ->
 380	Envelope = State#state.envelope,
 381	Username = base64:decode_to_string(Username64),
 382	% socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")),
 383	socket:send(Socket, "334 UGFzc3dvcmQ6\r\n"),
 384	% store the provided username in envelope.auth
 385	NewState = State#state{envelope = Envelope#envelope{auth = {Username, []}}},
 386	{ok, NewState};
 387
 388% the client sends a password response to auth-login
 389handle_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #envelope{auth = {Username,[]}}} = State) ->
 390	Password = base64:decode_to_string(Password64),
 391	try_auth('login', Username, Password, State);
 392
 393handle_request({<<"MAIL">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
 394	socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
 395	{ok, State};
 396handle_request({<<"MAIL">>, Args}, #state{socket = Socket, module = Module, envelope = Envelope, callbackstate = OldCallbackState,  extensions = Extensions} = State) ->
 397	case Envelope#envelope.from of
 398		undefined ->
 399			case binstr:strpos(binstr:to_upper(Args), "FROM:") of
 400				1 ->
 401					Address = binstr:strip(binstr:substr(Args, 6), left, $\s),
 402					case parse_encoded_address(binary_to_list(Address)) of
 403						error ->
 404							socket:send(Socket, "501 Bad sender address syntax\r\n"),
 405							{ok, State};
 406						{ParsedAddress, []} ->
 407							%io:format("From address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
 408							case Module:handle_MAIL(ParsedAddress, OldCallbackState) of
 409								{ok, CallbackState} ->
 410									socket:send(Socket, "250 sender Ok\r\n"),
 411									{ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
 412								{error, Message, CallbackState} ->
 413									socket:send(Socket, Message ++ "\r\n"),
 414									{ok, State#state{callbackstate = CallbackState}}
 415							end;
 416						{ParsedAddress, ExtraInfo} ->
 417							%io:format("From address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
 418							Options = [binstr:to_upper(X) || X <- binstr:split(list_to_binary(ExtraInfo), <<" ">>)],
 419							%io:format("options are ~p~n", [Options]),
 420							 F = fun(_, {error, Message}) ->
 421									 {error, Message};
 422								 (<<"SIZE=", Size/binary>>, InnerState) ->
 423									case has_extension(Extensions, "SIZE") of
 424										{true, Value} ->
 425											case list_to_integer(binary_to_list(Size)) > list_to_integer(Value) of
 426												true ->
 427													{error, io_lib:format("552 Estimated message length ~s exceeds limit of ~s\r\n", [Size, Value])};
 428												false ->
 429													InnerState#state{envelope = Envelope#envelope{expectedsize = list_to_integer(binary_to_list(Size))}}
 430											end;
 431										false ->
 432											{error, "555 Unsupported option SIZE\r\n"}
 433									end;
 434								(<<"BODY=", _BodyType/binary>>, InnerState) ->
 435									case has_extension(Extensions, "8BITMIME") of
 436										{true, _} ->
 437											InnerState;
 438										false ->
 439											{error, "555 Unsupported option BODY\r\n"}
 440									end;
 441								(X, InnerState) ->
 442									case Module:handle_MAIL_extension(X, OldCallbackState) of
 443										{ok, CallbackState} ->
 444											InnerState#state{callbackstate = CallbackState};
 445										error ->
 446											{error, io_lib:format("555 Unsupported option: ~s\r\n", [ExtraInfo])}
 447									end
 448							end,
 449							case lists:foldl(F, State, Options) of
 450								{error, Message} ->
 451									%io:format("error: ~s~n", [Message]),
 452									socket:send(Socket, Message),
 453									{ok, State};
 454								NewState ->
 455									%io:format("OK~n"),
 456									case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of
 457										{ok, CallbackState} ->
 458											socket:send(Socket, "250 sender Ok\r\n"),
 459											{ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
 460										{error, Message, CallbackState} ->
 461											socket:send(Socket, Message ++ "\r\n"),
 462											{ok, NewState#state{callbackstate = CallbackState}}
 463									end
 464							end
 465					end;
 466				_Else ->
 467					socket:send(Socket, "501 Syntax: MAIL FROM:<address>\r\n"),
 468					{ok, State}
 469			end;
 470		_Other ->
 471			socket:send(Socket, "503 Error: Nested MAIL command\r\n"),
 472			{ok, State}
 473	end;
 474handle_request({<<"RCPT">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
 475	socket:send(Socket, "503 Error: need MAIL command\r\n"),
 476	{ok, State};
 477handle_request({<<"RCPT">>, Args}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) ->
 478	case binstr:strpos(binstr:to_upper(Args), "TO:") of
 479		1 ->
 480			Address = binstr:strip(binstr:substr(Args, 4), left, $\s),
 481			case parse_encoded_address(binary_to_list(Address)) of
 482				error ->
 483					socket:send(Socket, "501 Bad recipient address syntax\r\n"),
 484					{ok, State};
 485				{[], _} ->
 486					% empty rcpt to addresses aren't cool
 487					socket:send(Socket, "501 Bad recipient address syntax\r\n"),
 488					{ok, State};
 489				{ParsedAddress, []} ->
 490					%io:format("To address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
 491					case Module:handle_RCPT(ParsedAddress, OldCallbackState) of
 492						{ok, CallbackState} ->
 493							socket:send(Socket, "250 recipient Ok\r\n"),
 494							{ok, State#state{envelope = Envelope#envelope{to = Envelope#envelope.to ++ [ParsedAddress]}, callbackstate = CallbackState}};
 495						{error, Message, CallbackState} ->
 496							socket:send(Socket, Message++"\r\n"),
 497							{ok, State#state{callbackstate = CallbackState}}
 498					end;
 499				{ParsedAddress, ExtraInfo} ->
 500					% TODO - are there even any RCPT extensions?
 501					io:format("To address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
 502					socket:send(Socket, io_lib:format("555 Unsupported option: ~s\r\n", [ExtraInfo])),
 503					{ok, State}
 504			end;
 505		_Else ->
 506			socket:send(Socket, "501 Syntax: RCPT TO:<address>\r\n"),
 507			{ok, State}
 508	end;
 509handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = undefined} = State) ->
 510	socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
 511	{ok, State};
 512handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = Envelope} = State) ->
 513	case {Envelope#envelope.from, Envelope#envelope.to} of
 514		{undefined, _} ->
 515			socket:send(Socket, "503 Error: need MAIL command\r\n"),
 516			{ok, State};
 517		{_, []} ->
 518			socket:send(Socket, "503 Error: need RCPT command\r\n"),
 519			{ok, State};
 520		_Else ->
 521			socket:send(Socket, "354 enter mail, end with line containing only '.'\r\n"),
 522			%io:format("switching to data read mode~n", []),
 523
 524			{ok, State#state{readmessage = true}}
 525	end;
 526handle_request({<<"RSET">>, _Any}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) ->
 527	socket:send(Socket, "250 Ok\r\n"),
 528	% if the client sends a RSET before a HELO/EHLO don't give them a valid envelope
 529	NewEnvelope = case Envelope of
 530		undefined -> undefined;
 531		_Something -> #envelope{}
 532	end,
 533	{ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}};
 534handle_request({<<"NOOP">>, _Any}, #state{socket = Socket} = State) ->
 535	socket:send(Socket, "250 Ok\r\n"),
 536	{ok, State};
 537handle_request({<<"QUIT">>, _Any}, #state{socket = Socket} = State) ->
 538	socket:send(Socket, "221 Bye\r\n"),
 539	{stop, normal, State};
 540handle_request({<<"VRFY">>, Address}, #state{module= Module, socket = Socket, callbackstate = OldCallbackState} = State) ->
 541	case parse_encoded_address(binary_to_list(Address)) of
 542		{ParsedAddress, []} ->
 543			case Module:handle_VRFY(ParsedAddress, OldCallbackState) of
 544				{ok, Reply, CallbackState} ->
 545					socket:send(Socket, io_lib:format("250 ~s\r\n", [Reply])),
 546					{ok, State#state{callbackstate = CallbackState}};
 547				{error, Message, CallbackState} ->
 548					socket:send(Socket, Message++"\r\n"),
 549					{ok, State#state{callbackstate = CallbackState}}
 550			end;
 551		_Other ->
 552			socket:send(Socket, "501 Syntax: VRFY username/address\r\n"),
 553			{ok, State}
 554	end;
 555handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket, tls=false, extensions = Extensions} = State) ->
 556	case has_extension(Extensions, "STARTTLS") of
 557		{true, _} ->
 558			socket:send(Socket, "220 OK\r\n"),
 559			crypto:start(),
 560			application:start(public_key),
 561			application:start(ssl),
 562			% TODO: certfile and keyfile should be at configurable locations
 563			case socket:to_ssl_server(Socket, [], 5000) of
 564				{ok, NewSocket} ->
 565					%io:format("SSL negotiation sucessful~n"),
 566					{ok, State#state{socket = NewSocket, envelope=undefined,
 567							authdata=undefined, waitingauth=false, readmessage=false,
 568							tls=true}};
 569				{error, Reason} ->
 570					io:format("SSL handshake failed : ~p~n", [Reason]),
 571					socket:send(Socket, "454 TLS negotiation failed\r\n"),
 572					{ok, State}
 573			end;
 574		false ->
 575			socket:send(Socket, "500 Command unrecognized\r\n"),
 576			{ok, State}
 577	end;
 578handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket} = State) ->
 579	socket:send(Socket, "500 TLS already negotiated\r\n"),
 580	{ok, State};
 581handle_request({<<"STARTTLS">>, _Args}, #state{socket = Socket} = State) ->
 582	socket:send(Socket, "501 Syntax error (no parameters allowed)\r\n"),
 583	{ok, State};
 584handle_request({Verb, Args}, #state{socket = Socket, module = Module, callbackstate = OldCallbackState} = State) ->
 585	{Message, CallbackState} = Module:handle_other(Verb, Args, OldCallbackState),
 586	socket:send(Socket, Message++"\r\n"),
 587	{ok, State#state{callbackstate = CallbackState}}.
 588
 589-spec(parse_encoded_address/1 :: (Address :: string()) -> {string(), string()} | 'error').
 590parse_encoded_address([]) ->
 591	error; % empty
 592parse_encoded_address("<@" ++ Address) ->
 593	case string:str(Address, ":") of
 594		0 ->
 595			error; % invalid address
 596		Index ->
 597			parse_encoded_address(string:substr(Address, Index + 1), "", {false, true})
 598	end;
 599parse_encoded_address("<" ++ Address) ->
 600	parse_encoded_address(Address, "", {false, true});
 601parse_encoded_address(" " ++ Address) ->
 602	parse_encoded_address(Address);
 603parse_encoded_address(Address) ->
 604	parse_encoded_address(Address, "", {false, false}).
 605
 606-spec(parse_encoded_address/3 :: (Address :: string(), Acc :: string(), Flags :: {boolean(), boolean()}) -> {string(), string()}).
 607parse_encoded_address([], Acc, {_Quotes, false}) ->
 608	{lists:reverse(Acc), []};
 609parse_encoded_address([], _Acc, {_Quotes, true}) ->
 610	error; % began with angle brackets but didn't end with them
 611parse_encoded_address(_, Acc, _) when length(Acc) > 129 ->
 612	error; % too long
 613parse_encoded_address("\\" ++ Tail, Acc, Flags) ->
 614	[H | NewTail] = Tail,
 615	parse_encoded_address(NewTail, [H | Acc], Flags);
 616parse_encoded_address("\"" ++ Tail, Acc, {false, AB}) ->
 617	parse_encoded_address(Tail, Acc, {true, AB});
 618parse_encoded_address("\"" ++ Tail, Acc, {true, AB}) ->
 619	parse_encoded_address(Tail, Acc, {false, AB});
 620parse_encoded_address(">" ++ Tail, Acc, {false, true}) ->
 621	{lists:reverse(Acc), string:strip(Tail, left, $\s)};
 622parse_encoded_address(">" ++ _Tail, _Acc, {false, false}) ->
 623	error; % ended with angle brackets but didn't begin with them
 624parse_encoded_address(" " ++ Tail, Acc, {false, false}) ->
 625	{lists:reverse(Acc), string:strip(Tail, left, $\s)};
 626parse_encoded_address(" " ++ _Tail, _Acc, {false, true}) ->
 627	error; % began with angle brackets but didn't end with them
 628parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $0, H =< $9 ->
 629	parse_encoded_address(Tail, [H | Acc], {false, AB}); % digits
 630parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $@, H =< $Z ->
 631	parse_encoded_address(Tail, [H | Acc], {false, AB}); % @ symbol and uppercase letters
 632parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $a, H =< $z ->
 633	parse_encoded_address(Tail, [H | Acc], {false, AB}); % lowercase letters
 634parse_encoded_address([H | Tail], Acc, {false, AB}) when H =:= $-; H =:= $.; H =:= $_ ->
 635	parse_encoded_address(Tail, [H | Acc], {false, AB}); % dash, dot, underscore
 636parse_encoded_address([_H | _Tail], _Acc, {false, _AB}) ->
 637	error;
 638parse_encoded_address([H | Tail], Acc, Quotes) ->
 639	parse_encoded_address(Tail, [H | Acc], Quotes).
 640
 641-spec(has_extension/2 :: (Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false').
 642has_extension(Exts, Ext) ->
 643	Extension = string:to_upper(Ext),
 644	Extensions = [{string:to_upper(X), Y} || {X, Y} <- Exts],
 645	%io:format("extensions ~p~n", [Extensions]),
 646	case proplists:get_value(Extension, Extensions) of
 647		undefined ->
 648			false;
 649		Value ->
 650			{true, Value}
 651	end.
 652
 653
 654-spec(try_auth/4 :: (AuthType :: 'login' | 'plain' | 'cram-md5', Username :: string(), Credential :: string() | {string(), string()}, State :: #state{}) -> {'ok', #state{}}).
 655try_auth(AuthType, Username, Credential, #state{module = Module, socket = Socket, envelope = Envelope, callbackstate = OldCallbackState} = State) ->
 656	% clear out waiting auth
 657	NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {[], []}}},
 658	case erlang:function_exported(Module, handle_AUTH, 4) of
 659		true ->
 660			case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of
 661				{ok, CallbackState} ->
 662					socket:send(Socket, "235 Authentication successful.\r\n"),
 663					{ok, NewState#state{callbackstate = CallbackState,
 664					                    envelope = Envelope#envelope{auth = {Username, Credential}}}};
 665				_Other ->
 666					socket:send(Socket, "535 Authentication failed.\r\n"),
 667					{ok, NewState}
 668				end;
 669		false ->
 670			io:format("Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions~n"),
 671			socket:send(Socket, "535 authentication failed (#5.7.1)\r\n"),
 672			{ok, NewState}
 673	end.
 674
 675%get_digest_nonce() ->
 676	%A = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))],
 677	%B = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))],
 678	%binary_to_list(base64:encode(lists:flatten(A ++ B))).
 679
 680
 681%% @doc a tight loop to receive the message body
 682receive_data(_Acc, _Socket, _, Size, MaxSize, Session, _Options) when Size > MaxSize ->
 683	io:format("message body size ~B exceeded maximum allowed ~B~n", [Size, MaxSize]),
 684	Session ! {receive_data, {error, size_exceeded}};
 685receive_data(Acc, Socket, {OldCount, OldRecvSize}, Size, MaxSize, Session, Options) ->
 686	{Count, RecvSize} = case Size of
 687		Size when OldCount > 2, OldRecvSize =:= 262144 ->
 688			%io:format("increasing receive size to ~B~n", [1048576]),
 689			{0, 1048576};% 1m
 690		Size when OldCount > 5, OldRecvSize =:= 65536 ->
 691			%io:format("increasing receive size to ~B~n", [262144]),
 692			{0, 262144};% 256k
 693		Size when OldCount > 5, OldRecvSize =:= 8192 ->
 694			%io:format("increasing receive size to ~B~n", [65536]),
 695			{0, 65536};% 64k
 696		Size when OldCount > 2, Size > 8192, OldRecvSize =:= 0 ->
 697			%io:format("increasing receive size to ~B~n", [8192]),
 698			{0, 8192}; % 8k
 699		_ ->
 700			{OldCount + 1, OldRecvSize} % don't change anything
 701	end,
 702	%socket:setopts(Socket, [{packet, raw}]),
 703	case socket:recv(Socket, RecvSize, 1000) of
 704		{ok, Packet} when Acc == [] ->
 705			case check_bare_crlf(Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0) of
 706				error ->
 707					Session ! {receive_data, {error, bare_newline}};
 708				FixedPacket ->
 709					case binstr:strpos(FixedPacket, "\r\n.\r\n") of
 710						0 ->
 711							%io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
 712							%io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
 713							receive_data([FixedPacket | Acc], Socket, {Count, RecvSize}, Size + byte_size(FixedPacket), MaxSize, Session, Options);
 714						Index ->
 715							String = binstr:substr(FixedPacket, 1, Index - 1),
 716							Rest = binstr:substr(FixedPacket, Index+5),
 717							%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
 718							Result = list_to_binary(lists:reverse([String | Acc])),
 719							%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
 720							Session ! {receive_data, Result, Rest}
 721					end
 722			end;
 723		{ok, Packet} ->
 724			[Last | _] = Acc,
 725			case check_bare_crlf(Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0) of
 726				error ->
 727					Session ! {receive_data, {error, bare_newline}};
 728				FixedPacket ->
 729					case binstr:strpos(FixedPacket, "\r\n.\r\n") of
 730						0 ->
 731							%io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
 732							%io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
 733							receive_data([FixedPacket | Acc], Socket, {Count, RecvSize}, Size + byte_size(FixedPacket), MaxSize, Session, Options);
 734						Index ->
 735							String = binstr:substr(FixedPacket, 1, Index - 1),
 736							Rest = binstr:substr(FixedPacket, Index+5),
 737							%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
 738							Result = list_to_binary(lists:reverse([String | Acc])),
 739							%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
 740							Session ! {receive_data, Result, Rest}
 741					end
 742			end;
 743		{error, timeout} when RecvSize =:= 0, length(Acc) > 1 ->
 744			% check that we didn't accidentally receive a \r\n.\r\n split across 2 receives
 745			[A, B | Acc2] = Acc,
 746			Packet = list_to_binary([B, A]),
 747			case binstr:strpos(Packet, "\r\n.\r\n") of
 748				0 ->
 749					% uh-oh
 750					%io:format("no data on socket, and no DATA terminator, retrying ~p~n", [Session]),
 751					% eventually we'll either get data or a different error, just keep retrying
 752					receive_data(Acc, Socket, {Count - 1, RecvSize}, Size, MaxSize, Session, Options);
 753				Index ->
 754					String = binstr:substr(Packet, 1, Index - 1),
 755					Rest = binstr:substr(Packet, Index+5),
 756					%io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
 757					Result = list_to_binary(lists:reverse([String | Acc2])),
 758					%io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
 759					Session ! {receive_data, Result, Rest}
 760			end;
 761		{error, timeout} ->
 762			NewRecvSize = adjust_receive_size_down(Size, RecvSize),
 763			%io:format("timeout when trying to read ~B bytes, lowering receive size to ~B~n", [RecvSize, NewRecvSize]),
 764			receive_data(Acc, Socket, {-5, NewRecvSize}, Size, MaxSize, Session, Options);
 765		{error, Reason} ->
 766			io:format("receive error: ~p~n", [Reason]),
 767			exit(receive_error)
 768	end.
 769
 770
 771adjust_receive_size_down(_Size, RecvSize) when RecvSize > 262144 ->
 772	262144;
 773adjust_receive_size_down(_Size, RecvSize) when RecvSize > 65536 ->
 774	65536;
 775adjust_receive_size_down(_Size, RecvSize) when RecvSize > 8192 ->
 776	8192;
 777adjust_receive_size_down(_Size, _RecvSize) ->
 778	0.
 779
 780check_for_bare_crlf(Bin, Offset) ->
 781	case {re:run(Bin, "(?<!\r)\n", [{capture, none}, {offset, Offset}]), re:run(Bin, "\r(?!\n)", [{capture, none}, {offset, Offset}])}  of
 782		{match, _} -> true;
 783		{_, match} -> true;
 784		_ -> false
 785	end.
 786
 787fix_bare_crlf(Bin, Offset) ->
 788	Options = [{offset, Offset}, {return, binary}, global],
 789	re:replace(re:replace(Bin, "(?<!\r)\n", "\r\n", Options), "\r(?!\n)", "\r\n", Options).
 790
 791strip_bare_crlf(Bin, Offset) ->
 792	Options = [{offset, Offset}, {return, binary}, global],
 793	re:replace(re:replace(Bin, "(?<!\r)\n", "", Options), "\r(?!\n)", "", Options).
 794
 795check_bare_crlf(Binary, _, ignore, _) ->
 796	Binary;
 797check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, Offset) when byte_size(Prev) > 0, Offset == 0 ->
 798	% check if last character of previous was a CR
 799	Lastchar = binstr:substr(Prev, -1),
 800	case Lastchar of
 801		<<"\r">> ->
 802			% okay, check again for the rest
 803			check_bare_crlf(Bin, <<>>, Op, 1);
 804		_ when Op == false -> % not fixing or ignoring them
 805			error;
 806		_ ->
 807			% no dice
 808			check_bare_crlf(Bin, <<>>, Op, 0)
 809	end;
 810check_bare_crlf(Binary, _Prev, Op, Offset) ->
 811	Last = binstr:substr(Binary, -1),
 812	% is the last character a CR?
 813	case Last of
 814		<<"\r">> ->
 815			% okay, the last character is a CR, we have to assume the next packet contains the corresponding LF
 816			NewBin = binstr:substr(Binary, 1, byte_size(Binary) -1),
 817			case check_for_bare_crlf(NewBin, Offset) of
 818				true when Op == fix ->
 819					list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]);
 820				true when Op == strip ->
 821					list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]);
 822				true ->
 823					error;
 824				false ->
 825					Binary
 826			end;
 827		_ ->
 828			case check_for_bare_crlf(Binary, Offset) of
 829				true when Op == fix ->
 830					fix_bare_crlf(Binary, Offset);
 831				true when Op == strip ->
 832					strip_bare_crlf(Binary, Offset);
 833				true ->
 834					error;
 835				false ->
 836					Binary
 837			end
 838	end.
 839
 840
 841-ifdef(TEST).
 842parse_encoded_address_test_() ->
 843	[
 844		{"Valid addresses should parse",
 845			fun() ->
 846					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<God@heaven.af.mil>")),
 847					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<\\God@heaven.af.mil>")),
 848					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<\"God\"@heaven.af.mil>")),
 849					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>")),
 850					?assertEqual({"God2@heaven.af.mil", []}, parse_encoded_address("<God2@heaven.af.mil>"))
 851			end
 852		},
 853		{"Addresses that are sorta valid should parse",
 854			fun() ->
 855					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("God@heaven.af.mil")),
 856					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("God@heaven.af.mil ")),
 857					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address(" God@heaven.af.mil ")),
 858					?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address(" <God@heaven.af.mil> "))
 859			end
 860		},
 861		{"Addresses containing unescaped <> that aren't at start/end should fail",
 862			fun() ->
 863					?assertEqual(error, parse_encoded_address("<<")),
 864					?assertEqual(error, parse_encoded_address("<God<@heaven.af.mil>"))
 865			end
 866		},
 867		{"Address that begins with < but doesn't end with a > should fail",
 868			fun() ->
 869					?assertEqual(error, parse_encoded_address("<God@heaven.af.mil")),
 870					?assertEqual(error, parse_encoded_address("<God@heaven.af.mil "))
 871			end
 872		},
 873		{"Address that begins without < but ends with a > should fail",
 874			fun() ->
 875					?assertEqual(error, parse_encoded_address("God@heaven.af.mil>"))
 876			end
 877		},
 878		{"Address longer than 129 character should fail",
 879			fun() ->
 880					MegaAddress = lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++ lists:seq(97, 122),
 881					?assertEqual(error, parse_encoded_address(MegaAddress))
 882			end
 883		},
 884		{"Address with an invalid route should fail",
 885			fun() ->
 886					?assertEqual(error, parse_encoded_address("<@gateway.af.mil God@heaven.af.mil>"))
 887			end
 888		},
 889		{"Empty addresses should parse OK",
 890			fun() ->
 891					?assertEqual({[], []}, parse_encoded_address("<>")),
 892					?assertEqual({[], []}, parse_encoded_address(" <> "))
 893			end
 894		},
 895		{"Completely empty addresses are an error",
 896			fun() ->
 897					?assertEqual(error, parse_encoded_address("")),
 898					?assertEqual(error, parse_encoded_address(" "))
 899			end
 900		},
 901		{"addresses with trailing parameters should return the trailing parameters",
 902			fun() ->
 903					?assertEqual({"God@heaven.af.mil", "SIZE=100 BODY=8BITMIME"}, parse_encoded_address("<God@heaven.af.mil> SIZE=100 BODY=8BITMIME"))
 904			end
 905		}
 906	].
 907
 908parse_request_test_() ->
 909	[
 910		{"Parsing normal SMTP requests",
 911			fun() ->
 912					?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)),
 913					?assertEqual({<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>)),
 914					?assertEqual({<<"MAIL">>, <<"FROM:God@heaven.af.mil">>}, parse_request(<<"MAIL FROM:God@heaven.af.mil">>))
 915			end
 916		},
 917		{"Verbs should be uppercased",
 918			fun() ->
 919					?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>))
 920			end
 921		},
 922		{"Leading and trailing spaces are removed",
 923			fun() ->
 924					?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo   hell.af.mil           ">>))
 925			end
 926		},
 927		{"Blank lines are blank",
 928			fun() ->
 929					?assertEqual({<<>>, <<>>}, parse_request(<<"">>))
 930			end
 931		}
 932	].
 933
 934smtp_session_test_() ->
 935	{foreach,
 936		local,
 937		fun() ->
 938				Self = self(),
 939				spawn(fun() ->
 940							{ok, ListenSock} = socket:listen(tcp, 9876, [binary]),
 941							{ok, X} = socket:accept(ListenSock),
 942							socket:controlling_process(X, Self),
 943							Self ! X
 944					end),
 945				{ok, CSock} = socket:connect(tcp, "localhost", 9876),
 946				receive
 947					SSock when is_port(SSock) ->
 948						?debugFmt("Got server side of the socket ~p, client is ~p~n", [SSock, CSock])
 949				end,
 950				{ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}]),
 951				socket:controlling_process(SSock, Pid),
 952				{CSock, Pid}
 953		end,
 954		fun({CSock, _Pid}) ->
 955				socket:close(CSock)
 956		end,
 957		[fun({CSock, _Pid}) ->
 958					{"A new connection should get a banner",
 959						fun() ->
 960								socket:active_once(CSock),
 961								receive {tcp, CSock, Packet} -> ok end,
 962								?assertMatch("220 localhost"++_Stuff,  Packet)
 963						end
 964					}
 965			end,
 966			fun({CSock, _Pid}) ->
 967					{"A correct response to HELO",
 968						fun() ->
 969								socket:active_once(CSock),
 970								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
 971								?assertMatch("220 localhost"++_Stuff,  Packet),
 972								socket:send(CSock, "HELO somehost.com\r\n"),
 973								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
 974								?debugFmt("~nHere 5", []),
 975								?assertMatch("250 localhost\r\n",  Packet2)
 976						end
 977					}
 978			end,
 979			fun({CSock, _Pid}) ->
 980					{"An error in response to an invalid HELO",
 981						fun() ->
 982								socket:active_once(CSock),
 983								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
 984								?assertMatch("220 localhost"++_Stuff,  Packet),
 985								socket:send(CSock, "HELO\r\n"),
 986								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
 987								?assertMatch("501 Syntax: HELO hostname\r\n",  Packet2)
 988						end
 989					}
 990			end,
 991			fun({CSock, _Pid}) ->
 992					{"A rejected HELO",
 993						fun() ->
 994								socket:active_once(CSock),
 995								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
 996								?assertMatch("220 localhost"++_Stuff,  Packet),
 997								socket:send(CSock, "HELO invalid\r\n"),
 998								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
 999								?assertMatch("554 invalid hostname\r\n",  Packet2)
1000						end
1001					}
1002			end,
1003			fun({CSock, _Pid}) ->
1004					{"A rejected EHLO",
1005						fun() ->
1006								socket:active_once(CSock),
1007								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1008								?assertMatch("220 localhost"++_Stuff,  Packet),
1009								socket:send(CSock, "EHLO invalid\r\n"),
1010								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1011								?assertMatch("554 invalid hostname\r\n",  Packet2)
1012						end
1013					}
1014			end,
1015			fun({CSock, _Pid}) ->
1016					{"EHLO response",
1017						fun() ->
1018								socket:active_once(CSock),
1019								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1020								?assertMatch("220 localhost"++_Stuff,  Packet),
1021								socket:send(CSock, "EHLO somehost.com\r\n"),
1022								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1023								?assertMatch("250-localhost\r\n",  Packet2),
1024								Foo = fun(F) ->
1025										receive
1026											{tcp, CSock, "250-"++Packet3} ->
1027												socket:active_once(CSock),
1028												F(F);
1029											{tcp, CSock, "250 "++Packet3} ->
1030												socket:active_once(CSock),
1031												ok;
1032											R ->
1033												socket:active_once(CSock),
1034												error
1035										end
1036								end,
1037								?assertEqual(ok, Foo(Foo))
1038						end
1039					}
1040			end,
1041			fun({CSock, _Pid}) ->
1042					{"Unsupported AUTH PLAIN",
1043						fun() ->
1044								socket:active_once(CSock),
1045								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1046								?assertMatch("220 localhost"++_Stuff,  Packet),
1047								socket:send(CSock, "EHLO somehost.com\r\n"),
1048								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1049								?assertMatch("250-localhost\r\n",  Packet2),
1050								Foo = fun(F) ->
1051										receive
1052											{tcp, CSock, "250-"++Packet3} ->
1053												socket:active_once(CSock),
1054												F(F);
1055											{tcp, CSock, "250"++Packet3} ->
1056												socket:active_once(CSock),
1057												ok;
1058											R ->
1059												socket:active_once(CSock),
1060												error
1061										end
1062								end,
1063								?assertEqual(ok, Foo(Foo)),
1064								socket:send(CSock, "AUTH PLAIN\r\n"),
1065								receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
1066								?assertMatch("502 Error: AUTH not implemented\r\n",  Packet4)
1067						end
1068					}
1069			end,
1070			fun({CSock, _Pid}) ->
1071					{"Sending DATA",
1072						fun() ->
1073								socket:active_once(CSock),
1074								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1075								?assertMatch("220 localhost"++_Stuff,  Packet),
1076								socket:send(CSock, "HELO somehost.com\r\n"),
1077								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1078								?assertMatch("250 localhost\r\n",  Packet2),
1079								socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
1080								receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
1081								?assertMatch("250 "++_, Packet3),
1082								socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
1083								receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
1084								?assertMatch("250 "++_, Packet4),
1085								socket:send(CSock, "DATA\r\n"),
1086								receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
1087								?assertMatch("354 "++_, Packet5),
1088								socket:send(CSock, "Subject: tls message\r\n"),
1089								socket:send(CSock, "To: <user@otherhost>\r\n"),
1090								socket:send(CSock, "From: <user@somehost.com>\r\n"),
1091								socket:send(CSock, "\r\n"),
1092								socket:send(CSock, "message body"),
1093								socket:send(CSock, "\r\n.\r\n"),
1094								receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
1095								?assertMatch("250 queued as"++_, Packet6),
1096								?debugFmt("Message send, received: ~p~n", [Packet6])
1097						end
1098					}
1099			end,
1100%			fun({CSock, _Pid}) ->
1101%					{"Sending DATA with a bare newline",
1102%						fun() ->
1103%								socket:active_once(CSock),
1104%								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1105%								?assertMatch("220 localhost"++_Stuff,  Packet),
1106%								socket:send(CSock, "HELO somehost.com\r\n"),
1107%								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1108%								?assertMatch("250 localhost\r\n",  Packet2),
1109%								socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
1110%								receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
1111%								?assertMatch("250 "++_, Packet3),
1112%								socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
1113%								receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
1114%								?assertMatch("250 "++_, Packet4),
1115%								socket:send(CSock, "DATA\r\n"),
1116%								receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
1117%								?assertMatch("354 "++_, Packet5),
1118%								socket:send(CSock, "Subject: tls message\r\n"),
1119%								socket:send(CSock, "To: <user@otherhost>\r\n"),
1120%								socket:send(CSock, "From: <user@somehost.com>\r\n"),
1121%								socket:send(CSock, "\r\n"),
1122%								socket:send(CSock, "this\r\n"),
1123%								socket:send(CSock, "body\r\n"),
1124%								socket:send(CSock, "has\r\n"),
1125%								socket:send(CSock, "a\r\n"),
1126%								socket:send(CSock, "bare\n"),
1127%								socket:send(CSock, "newline\r\n"),
1128%								socket:send(CSock, "\r\n.\r\n"),
1129%								receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
1130%								?assertMatch("451 "++_, Packet6),
1131%								?debugFmt("Message send, received: ~p~n", [Packet6])
1132%						end
1133%					}
1134%			end,
1135			%fun({CSock, _Pid}) ->
1136%					{"Sending DATA with a bare CR",
1137%						fun() ->
1138%								socket:active_once(CSock),
1139%								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1140%								?assertMatch("220 localhost"++_Stuff,  Packet),
1141%								socket:send(CSock, "HELO somehost.com\r\n"),
1142%								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1143%								?assertMatch("250 localhost\r\n",  Packet2),
1144%								socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
1145%								receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
1146%								?assertMatch("250 "++_, Packet3),
1147%								socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
1148%								receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
1149%								?assertMatch("250 "++_, Packet4),
1150%								socket:send(CSock, "DATA\r\n"),
1151%								receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
1152%								?assertMatch("354 "++_, Packet5),
1153%								socket:send(CSock, "Subject: tls message\r\n"),
1154%								socket:send(CSock, "To: <user@otherhost>\r\n"),
1155%								socket:send(CSock, "From: <user@somehost.com>\r\n"),
1156%								socket:send(CSock, "\r\n"),
1157%								socket:send(CSock, "this\r\n"),
1158%								socket:send(CSock, "\rbody\r\n"),
1159%								socket:send(CSock, "has\r\n"),
1160%								socket:send(CSock, "a\r\n"),
1161%								socket:send(CSock, "bare\r"),
1162%								socket:send(CSock, "CR\r\n"),
1163%								socket:send(CSock, "\r\n.\r\n"),
1164%								receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
1165%								?assertMatch("451 "++_, Packet6),
1166%								?debugFmt("Message send, received: ~p~n", [Packet6])
1167%						end
1168%					}
1169%			end,
1170
1171%			fun({CSock, _Pid}) ->
1172%					{"Sending DATA with a bare newline in the headers",
1173%						fun() ->
1174%								socket:active_once(CSock),
1175%								receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
1176%								?assertMatch("220 localhost"++_Stuff,  Packet),
1177%								socket:send(CSock, "HELO somehost.com\r\n"),
1178%								receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
1179%								?assertMatch("250 localhost\r\n",  Packet2),
1180%								socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
1181%								receive {tcp, CSo

Large files files are truncated, but you can click here to view the full file