PageRenderTime 144ms CodeModel.GetById 14ms app.highlight 117ms RepoModel.GetById 1ms app.codeStats 1ms

/deps/gen_smtp/src/gen_smtp_server_session.erl

http://github.com/zotonic/zotonic
Erlang | 2228 lines | 1962 code | 55 blank | 211 comment | 9 complexity | 52f8623d50fba83d7192aa47de908b6b 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 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} -> 

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