PageRenderTime 131ms CodeModel.GetById 11ms app.highlight 103ms RepoModel.GetById 1ms app.codeStats 0ms

/deps/gen_smtp/src/gen_smtp_client.erl

https://code.google.com/p/zotonic/
Erlang | 1019 lines | 903 code | 38 blank | 78 comment | 7 complexity | 31b1c68e5a429b5d7ca3496a09e9eb7b MD5 | raw file
   1%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
   2%%%
   3%%% Redistribution and use in source and binary forms, with or without
   4%%% modification, are permitted provided that the following conditions are met:
   5%%%
   6%%%   1. Redistributions of source code must retain the above copyright notice,
   7%%%      this list of conditions and the following disclaimer.
   8%%%   2. Redistributions in binary form must reproduce the above copyright
   9%%%      notice, this list of conditions and the following disclaimer in the
  10%%%      documentation and/or other materials provided with the distribution.
  11%%%
  12%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
  13%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
  14%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
  15%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
  16%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  17%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  18%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  19%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  20%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  21%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  22
  23%% @doc A simple SMTP client used for sending mail - assumes relaying via a
  24%% smarthost.
  25
  26-module(gen_smtp_client).
  27
  28-define(DEFAULT_OPTIONS, [
  29		{ssl, false}, % whether to connect on 465 in ssl mode
  30		{tls, if_available}, % always, never, if_available
  31		{auth, if_available},
  32		{hostname, smtp_util:guess_FQDN()},
  33		{retries, 1} % how many retries per smtp host on temporary failure
  34	]).
  35
  36-define(AUTH_PREFERENCE, [
  37		"CRAM-MD5",
  38		"LOGIN",
  39		"PLAIN"
  40	]).
  41
  42-define(TIMEOUT, 1200000).
  43
  44-ifdef(TEST).
  45-include_lib("eunit/include/eunit.hrl").
  46-compile(export_all).
  47-else.
  48-export([send/2, send/3, send_blocking/2]).
  49-endif.
  50
  51-type email() :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}. 
  52
  53-spec send(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> {'ok', pid()} | {'error', any()}.
  54%% @doc Send an email in a non-blocking fashion via a spawned_linked process.
  55%% The process will exit abnormally on a send failure.
  56send(Email, Options) ->
  57	send(Email, Options, undefined).
  58
  59%% @doc Send an email nonblocking and invoke a callback with the result of the send.
  60%% The callback will receive either `{ok, Receipt}' where Receipt is the SMTP server's receipt
  61%% identifier,  `{error, Type, Message}' or `{exit, ExitReason}', as the single argument.
  62-spec send(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list(), Callback :: function() | 'undefined') -> {'ok', pid()} | {'error', any()}.
  63send(Email, Options, Callback) ->
  64	NewOptions = lists:ukeymerge(1, lists:sort(Options),
  65		lists:sort(?DEFAULT_OPTIONS)),
  66	case check_options(NewOptions) of
  67		ok when is_function(Callback) ->
  68			spawn(fun() ->
  69						process_flag(trap_exit, true),
  70						Pid = spawn_link(fun() ->
  71									send_it_nonblock(Email, NewOptions, Callback)
  72							end
  73						),
  74						receive
  75							{'EXIT', Pid, Reason} ->
  76								case Reason of
  77									X when X == normal; X == shutdown ->
  78										ok;
  79									Error ->
  80										Callback({exit, Error})
  81								end
  82						end
  83				end);
  84		ok ->
  85			Pid = spawn_link(fun () ->
  86						send_it_nonblock(Email, NewOptions, Callback)
  87				end
  88			),
  89			{ok, Pid};
  90		{error, Reason} ->
  91			{error, Reason}
  92	end.
  93
  94-spec send_blocking(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> binary() | {'error', atom(), any()} | {'error', any()}.
  95%% @doc Send an email and block waiting for the reply. Returns either a binary that contains
  96%% the SMTP server's receipt or `{error, Type, Message}' or `{error, Reason}'.
  97send_blocking(Email, Options) ->
  98	NewOptions = lists:ukeymerge(1, lists:sort(Options),
  99		lists:sort(?DEFAULT_OPTIONS)),
 100	case check_options(NewOptions) of
 101		ok ->
 102			send_it(Email, NewOptions);
 103		{error, Reason} ->
 104			{error, Reason}
 105	end.
 106
 107-spec send_it_nonblock(Email :: email(), Options :: list(), Callback :: function() | 'undefined') ->{'ok', binary()} | {'error', any(), any()}.
 108send_it_nonblock(Email, Options, Callback) ->
 109	case send_it(Email, Options) of
 110		{error, Type, Message} when is_function(Callback) ->
 111			Callback({error, Type, Message}),
 112			{error, Type, Message};
 113		{error, Type, Message} ->
 114			erlang:exit({error, Type, Message});
 115		Receipt when is_function(Callback) ->
 116			Callback({ok, Receipt}),
 117			{ok, Receipt};
 118		Receipt ->
 119			{ok, Receipt}
 120	end.
 121
 122-spec send_it(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> binary() | {'error', any(), any()}.
 123send_it(Email, Options) ->
 124	RelayDomain = proplists:get_value(relay, Options),
 125	MXRecords = case proplists:get_value(no_mx_lookups, Options) of
 126		true ->
 127			[];
 128		_ ->
 129			smtp_util:mxlookup(RelayDomain)
 130	end,
 131	%io:format("MX records for ~s are ~p~n", [RelayDomain, MXRecords]),
 132	Hosts = case MXRecords of
 133		[] ->
 134			[{0, RelayDomain}]; % maybe we're supposed to relay to a host directly
 135		_ ->
 136			MXRecords
 137	end,
 138	try_smtp_sessions(Hosts, Email, Options, []).
 139
 140-spec try_smtp_sessions(Hosts :: [{non_neg_integer(), string()}, ...], Email :: email(), Options :: list(), RetryList :: list()) -> binary() | {'error', any(), any()}.
 141try_smtp_sessions([{Distance, Host} | Tail], Email, Options, RetryList) ->
 142	Retries = proplists:get_value(retries, Options),
 143	try do_smtp_session(Host, Email, Options) of
 144		Res -> Res
 145	catch
 146		throw:{permanent_failure, Message} ->
 147			% permanent failure means no retries, and don't even continue with other hosts
 148			{error, no_more_hosts, {permanent_failure, Host, Message}};
 149		throw:{FailureType, Message} ->
 150			case proplists:get_value(Host, RetryList) of
 151				RetryCount when is_integer(RetryCount), RetryCount >= Retries ->
 152					% out of chances
 153					%io:format("retries for ~s exceeded (~p of ~p)~n", [Host, RetryCount, Retries]),
 154					NewHosts = Tail,
 155					NewRetryList = lists:keydelete(Host, 1, RetryList);
 156				RetryCount when is_integer(RetryCount) ->
 157					%io:format("scheduling ~s for retry (~p of ~p)~n", [Host, RetryCount, Retries]),
 158					NewHosts = Tail ++ [{Distance, Host}],
 159					NewRetryList = lists:keydelete(Host, 1, RetryList) ++ [{Host, RetryCount + 1}];
 160				_ when Retries == 0 ->
 161					% done retrying completely
 162					NewHosts = Tail,
 163					NewRetryList = lists:keydelete(Host, 1, RetryList);
 164				_ ->
 165					% otherwise...
 166					%io:format("scheduling ~s for retry (~p of ~p)~n", [Host, 1, Retries]),
 167					NewHosts = Tail ++ [{Distance, Host}],
 168					NewRetryList = lists:keydelete(Host, 1, RetryList) ++ [{Host, 1}]
 169			end,
 170			case NewHosts of
 171				[] ->
 172					{error, retries_exceeded, {FailureType, Host, Message}};
 173				_ ->
 174					try_smtp_sessions(NewHosts, Email, Options, NewRetryList)
 175			end
 176	end.
 177
 178-spec do_smtp_session(Host :: string(), Email :: email(), Options :: list()) -> binary().
 179do_smtp_session(Host, Email, Options) ->
 180	{ok, Socket, _Host, _Banner} = connect(Host, Options),
 181	%io:format("connected to ~s; banner was ~s~n", [Host, Banner]),
 182	{ok, Extensions} = try_EHLO(Socket, Options),
 183	%io:format("Extensions are ~p~n", [Extensions]),
 184	{Socket2, Extensions2} = try_STARTTLS(Socket, Options, Extensions),
 185	%io:format("Extensions are ~p~n", [Extensions2]),
 186	_Authed = try_AUTH(Socket2, Options, proplists:get_value(<<"AUTH">>, Extensions2)),
 187	%io:format("Authentication status is ~p~n", [Authed]),
 188	Receipt = try_sending_it(Email, Socket2, Extensions2),
 189	%io:format("Mail sending successful~n"),
 190	quit(Socket2),
 191	Receipt.
 192
 193-spec try_sending_it(Email :: email(), Socket :: socket:socket(), Extensions :: list()) -> binary().
 194try_sending_it({From, To, Body}, Socket, Extensions) ->
 195	try_MAIL_FROM(From, Socket, Extensions),
 196	try_RCPT_TO(To, Socket, Extensions),
 197	try_DATA(Body, Socket, Extensions).
 198
 199-spec try_MAIL_FROM(From :: string() | binary(), Socket :: socket:socket(), Extensions :: list()) -> true.
 200try_MAIL_FROM(From, Socket, Extensions) when is_binary(From) ->
 201	try_MAIL_FROM(binary_to_list(From), Socket, Extensions);
 202try_MAIL_FROM("<" ++ _ = From, Socket, _Extensions) ->
 203	% TODO do we need to bother with SIZE?
 204	socket:send(Socket, ["MAIL FROM: ", From, "\r\n"]),
 205	case read_possible_multiline_reply(Socket) of
 206		{ok, <<"250", _Rest/binary>>} ->
 207			true;
 208		{ok, <<"4", _Rest/binary>> = Msg} ->
 209			quit(Socket),
 210			throw({temporary_failure, Msg});
 211		{ok, Msg} ->
 212			%io:format("Mail FROM rejected: ~p~n", [Msg]),
 213			quit(Socket),
 214			throw({permanent_failure, Msg})
 215	end;
 216try_MAIL_FROM(From, Socket, Extensions) ->
 217	% someone was bad and didn't put in the angle brackets
 218	try_MAIL_FROM("<"++From++">", Socket, Extensions).
 219
 220-spec try_RCPT_TO(Tos :: [binary() | string()], Socket :: socket:socket(), Extensions :: list()) -> true.
 221try_RCPT_TO([], _Socket, _Extensions) ->
 222	true;
 223try_RCPT_TO([To | Tail], Socket, Extensions) when is_binary(To) ->
 224	try_RCPT_TO([binary_to_list(To) | Tail], Socket, Extensions);
 225try_RCPT_TO(["<" ++ _ = To | Tail], Socket, Extensions) ->
 226	socket:send(Socket, ["RCPT TO: ",To,"\r\n"]),
 227	case read_possible_multiline_reply(Socket) of
 228		{ok, <<"250", _Rest/binary>>} ->
 229			try_RCPT_TO(Tail, Socket, Extensions);
 230		{ok, <<"251", _Rest/binary>>} ->
 231			try_RCPT_TO(Tail, Socket, Extensions);
 232		{ok, <<"4", _Rest/binary>> = Msg} ->
 233			quit(Socket),
 234			throw({temporary_failure, Msg});
 235		{ok, Msg} ->
 236			quit(Socket),
 237			throw({permanent_failure, Msg})
 238	end;
 239try_RCPT_TO([To | Tail], Socket, Extensions) ->
 240	% someone was bad and didn't put in the angle brackets
 241	try_RCPT_TO(["<"++To++">" | Tail], Socket, Extensions).
 242
 243-spec try_DATA(Body :: binary() | function(), Socket :: socket:socket(), Extensions :: list()) -> binary().
 244try_DATA(Body, Socket, Extensions) when is_function(Body) ->
 245    try_DATA(Body(), Socket, Extensions);
 246try_DATA(Body, Socket, _Extensions) ->
 247	socket:send(Socket, "DATA\r\n"),
 248	case read_possible_multiline_reply(Socket) of
 249		{ok, <<"354", _Rest/binary>>} ->
 250			socket:send(Socket, [Body, "\r\n.\r\n"]),
 251			case read_possible_multiline_reply(Socket) of
 252				{ok, <<"250 ", Receipt/binary>>} ->
 253					Receipt;
 254				{ok, <<"4", _Rest2/binary>> = Msg} ->
 255					quit(Socket),
 256					throw({temporary_failure, Msg});
 257				{ok, Msg} ->
 258					quit(Socket),
 259					throw({permanent_failure, Msg})
 260			end;
 261		{ok, <<"4", _Rest/binary>> = Msg} ->
 262			quit(Socket),
 263			throw({temporary_failure, Msg});
 264		{ok, Msg} ->
 265			quit(Socket),
 266			throw({permanent_failure, Msg})
 267	end.
 268
 269-spec try_AUTH(Socket :: socket:socket(), Options :: list(), AuthTypes :: [string()]) -> boolean().
 270try_AUTH(Socket, Options, []) ->
 271	case proplists:get_value(auth, Options) of
 272		always ->
 273			quit(Socket),
 274			erlang:throw({missing_requirement, auth});
 275		_ ->
 276			false
 277	end;
 278try_AUTH(Socket, Options, undefined) ->
 279	case proplists:get_value(auth, Options) of
 280		always ->
 281			quit(Socket),
 282			erlang:throw({missing_requirement, auth});
 283		_ ->
 284			false
 285	end;
 286try_AUTH(Socket, Options, AuthTypes) ->
 287	case proplists:is_defined(username, Options) and
 288		proplists:is_defined(password, Options) and
 289		(proplists:get_value(auth, Options) =/= never) of
 290		false ->
 291			case proplists:get_value(auth, Options) of
 292				always ->
 293					quit(Socket),
 294					erlang:throw({missing_requirement, auth});
 295				_ ->
 296					false
 297			end;
 298		true ->
 299			Username = proplists:get_value(username, Options),
 300			Password = proplists:get_value(password, Options),
 301			%io:format("Auth types: ~p~n", [AuthTypes]),
 302			Types = re:split(AuthTypes, " ", [{return, list}, trim]),
 303			case do_AUTH(Socket, Username, Password, Types) of
 304				false ->
 305					case proplists:get_value(auth, Options) of
 306						always ->
 307							quit(Socket),
 308							erlang:throw({permanent_failure, auth_failed});
 309						_ ->
 310							false
 311					end;
 312				true ->
 313					true
 314			end
 315	end.
 316
 317-spec do_AUTH(Socket :: socket:socket(), Username :: string(), Password :: string(), Types :: [string()]) -> boolean().
 318do_AUTH(Socket, Username, Password, Types) ->
 319	FixedTypes = [string:to_upper(X) || X <- Types],
 320	%io:format("Fixed types: ~p~n", [FixedTypes]),
 321	AllowedTypes = [X  || X <- ?AUTH_PREFERENCE, lists:member(X, FixedTypes)],
 322	%io:format("available authentication types, in order of preference: ~p~n",
 323	%	[AllowedTypes]),
 324	do_AUTH_each(Socket, Username, Password, AllowedTypes).
 325
 326-spec do_AUTH_each(Socket :: socket:socket(), Username :: string() | binary(), Password :: string() | binary(), AuthTypes :: [string()]) -> boolean().
 327do_AUTH_each(_Socket, _Username, _Password, []) ->
 328	false;
 329do_AUTH_each(Socket, Username, Password, ["CRAM-MD5" | Tail]) ->
 330	socket:send(Socket, "AUTH CRAM-MD5\r\n"),
 331	case read_possible_multiline_reply(Socket) of
 332		{ok, <<"334 ", Rest/binary>>} ->
 333			Seed64 = binstr:strip(binstr:strip(Rest, right, $\n), right, $\r),
 334			Seed = base64:decode_to_string(Seed64),
 335			Digest = smtp_util:compute_cram_digest(Password, Seed),
 336			String = base64:encode(list_to_binary([Username, " ", Digest])),
 337			socket:send(Socket, [String, "\r\n"]),
 338			case read_possible_multiline_reply(Socket) of
 339				{ok, <<"235", _Rest/binary>>} ->
 340					%io:format("authentication accepted~n"),
 341					true;
 342				{ok, _Msg} ->
 343					%io:format("authentication rejected: ~s~n", [Msg]),
 344					do_AUTH_each(Socket, Username, Password, Tail)
 345			end;
 346		{ok, _Something} ->
 347			%io:format("got ~s~n", [Something]),
 348			do_AUTH_each(Socket, Username, Password, Tail)
 349	end;
 350do_AUTH_each(Socket, Username, Password, ["LOGIN" | Tail]) ->
 351	socket:send(Socket, "AUTH LOGIN\r\n"),
 352	case read_possible_multiline_reply(Socket) of
 353		{ok, <<"334 VXNlcm5hbWU6\r\n">>} ->
 354			%io:format("username prompt~n"),
 355			U = base64:encode(Username),
 356			socket:send(Socket, [U,"\r\n"]),
 357			case read_possible_multiline_reply(Socket) of
 358				{ok, <<"334 UGFzc3dvcmQ6\r\n">>} ->
 359					%io:format("password prompt~n"),
 360					P = base64:encode(Password),
 361					socket:send(Socket, [P,"\r\n"]),
 362					case read_possible_multiline_reply(Socket) of
 363						{ok, <<"235 ", _Rest/binary>>} ->
 364							%io:format("authentication accepted~n"),
 365							true;
 366						{ok, _Msg} ->
 367							%io:format("password rejected: ~s", [Msg]),
 368							do_AUTH_each(Socket, Username, Password, Tail)
 369					end;
 370				{ok, _Msg2} ->
 371					%io:format("username rejected: ~s", [Msg2]),
 372					do_AUTH_each(Socket, Username, Password, Tail)
 373			end;
 374		{ok, _Something} ->
 375			%io:format("got ~s~n", [Something]),
 376			do_AUTH_each(Socket, Username, Password, Tail)
 377	end;
 378do_AUTH_each(Socket, Username, Password, ["PLAIN" | Tail]) ->
 379	AuthString = base64:encode("\0"++Username++"\0"++Password),
 380	socket:send(Socket, ["AUTH PLAIN ", AuthString, "\r\n"]),
 381	case read_possible_multiline_reply(Socket) of
 382		{ok, <<"235", _Rest/binary>>} ->
 383			%io:format("authentication accepted~n"),
 384			true;
 385		_Else ->
 386			% TODO do we need to bother trying the multi-step PLAIN?
 387			%io:format("authentication rejected~n"),
 388			%io:format("~p~n", [Else]),
 389			do_AUTH_each(Socket, Username, Password, Tail)
 390	end;
 391do_AUTH_each(Socket, Username, Password, [_Type | Tail]) ->
 392	%io:format("unsupported AUTH type ~s~n", [Type]),
 393	do_AUTH_each(Socket, Username, Password, Tail).
 394
 395-spec try_EHLO(Socket :: socket:socket(), Options :: list()) -> {ok, list()}.
 396try_EHLO(Socket, Options) ->
 397	ok = socket:send(Socket, ["EHLO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
 398	case read_possible_multiline_reply(Socket) of
 399		{ok, <<"500", _Rest/binary>>} ->
 400			% Unrecognized command, fall back to HELO
 401			try_HELO(Socket, Options);
 402		{ok, <<"4", _Rest/binary>> = Msg} ->
 403			quit(Socket),
 404			throw({temporary_failure, Msg});
 405		{ok, Reply} ->
 406			{ok, parse_extensions(Reply)}
 407	end.
 408
 409-spec try_HELO(Socket :: socket:socket(), Options :: list()) -> {ok, list()}.
 410try_HELO(Socket, Options) ->
 411	ok = socket:send(Socket, ["HELO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
 412	case read_possible_multiline_reply(Socket) of
 413		{ok, <<"250", _Rest/binary>>} ->
 414			{ok, []};
 415		{ok, <<"4", _Rest/binary>> = Msg} ->
 416			quit(Socket),
 417			throw({temporary_failure, Msg});
 418		{ok, Msg} ->
 419			quit(Socket),
 420			throw({permanent_failure, Msg})
 421	end.
 422
 423% check if we should try to do TLS
 424-spec try_STARTTLS(Socket :: socket:socket(), Options :: list(), Extensions :: list()) -> {socket:socket(), list()}.
 425try_STARTTLS(Socket, Options, Extensions) ->
 426		case {proplists:get_value(tls, Options),
 427				proplists:get_value(<<"STARTTLS">>, Extensions)} of
 428			{Atom, true} when Atom =:= always; Atom =:= if_available ->
 429			%io:format("Starting TLS~n"),
 430			case {do_STARTTLS(Socket, Options), Atom} of
 431				{false, always} ->
 432					%io:format("TLS failed~n"),
 433					quit(Socket),
 434					erlang:throw({temporary_failure, tls_failed});
 435				{false, if_available} ->
 436					%io:format("TLS failed~n"),
 437					{Socket, Extensions};
 438				{{S, E}, _} ->
 439					%io:format("TLS started~n"),
 440					{S, E}
 441			end;
 442		{always, _} ->
 443			quit(Socket),
 444			erlang:throw({missing_requirement, tls});
 445		_ ->
 446			{Socket, Extensions}
 447	end.
 448
 449%% attempt to upgrade socket to TLS
 450-spec do_STARTTLS(Socket :: socket:socket(), Options :: list()) -> {socket:socket(), list()} | false.
 451do_STARTTLS(Socket, Options) ->
 452	socket:send(Socket, "STARTTLS\r\n"),
 453	case read_possible_multiline_reply(Socket) of
 454		{ok, <<"220", _Rest/binary>>} ->
 455			application:start(crypto),
 456			application:start(public_key),
 457			application:start(ssl),
 458			case socket:to_ssl_client(Socket, [], 5000) of
 459				{ok, NewSocket} ->
 460					%NewSocket;
 461					{ok, Extensions} = try_EHLO(NewSocket, Options),
 462					{NewSocket, Extensions};
 463				_Else ->
 464					%io:format("~p~n", [Else]),
 465					false
 466			end;
 467		{ok, <<"4", _Rest/binary>> = Msg} ->
 468			quit(Socket),
 469			throw({temporary_failure, Msg});
 470		{ok, Msg} ->
 471			quit(Socket),
 472			throw({permanent_failure, Msg})
 473	end.
 474
 475%% try connecting to a host
 476connect(Host, Options) when is_binary(Host) ->
 477	connect(binary_to_list(Host), Options);
 478connect(Host, Options) ->
 479	SockOpts = [binary, {packet, line}, {keepalive, true}, {active, false}],
 480	Proto = case proplists:get_value(ssl, Options) of
 481		true ->
 482			application:start(crypto),
 483			application:start(public_key),
 484			application:start(ssl),
 485			ssl;
 486		_ ->
 487			tcp
 488	end,
 489	Port = case proplists:get_value(port, Options) of
 490		undefined when Proto =:= ssl ->
 491			465;
 492		OPort when is_integer(OPort) ->
 493			OPort;
 494		_ ->
 495			25
 496	end,
 497	case socket:connect(Proto, Host, Port, SockOpts, 5000) of
 498		{ok, Socket} ->
 499			case read_possible_multiline_reply(Socket) of
 500				{ok, <<"220", Banner/binary>>} ->
 501					{ok, Socket, Host, Banner};
 502				{ok, <<"4", _Rest/binary>> = Msg} ->
 503					quit(Socket),
 504					throw({temporary_failure, Msg});
 505				{ok, Msg} ->
 506					quit(Socket),
 507					throw({permanent_failure, Msg})
 508			end;
 509		{error, Reason} ->
 510			throw({network_failure, {error, Reason}})
 511	end.
 512
 513%% read a multiline reply (eg. EHLO reply)
 514-spec read_possible_multiline_reply(Socket :: socket:socket()) -> {ok, binary()}.
 515read_possible_multiline_reply(Socket) ->
 516	case socket:recv(Socket, 0, ?TIMEOUT) of
 517		{ok, Packet} ->
 518			case binstr:substr(Packet, 4, 1) of
 519				<<"-">> ->
 520					Code = binstr:substr(Packet, 1, 3),
 521					read_multiline_reply(Socket, Code, [Packet]);
 522				<<" ">> ->
 523					{ok, Packet}
 524			end;
 525		Error ->
 526			throw({network_failure, Error})
 527	end.
 528
 529-spec read_multiline_reply(Socket :: socket:socket(), Code :: binary(), Acc :: [binary()]) -> {ok, binary()}.
 530read_multiline_reply(Socket, Code, Acc) ->
 531	case socket:recv(Socket, 0, ?TIMEOUT) of
 532		{ok, Packet} ->
 533			case {binstr:substr(Packet, 1, 3), binstr:substr(Packet, 4, 1)} of
 534				{Code, <<" ">>} ->
 535					{ok, list_to_binary(lists:reverse([Packet | Acc]))};
 536				{Code, <<"-">>} ->
 537					read_multiline_reply(Socket, Code, [Packet | Acc]);
 538				_ ->
 539					quit(Socket),
 540					throw({unexpected_response, lists:reverse([Packet | Acc])})
 541			end;
 542		Error ->
 543			throw({network_failure, Error})
 544	end.
 545
 546quit(Socket) ->
 547	socket:send(Socket, "QUIT\r\n"),
 548	socket:close(Socket),
 549	ok.
 550
 551% TODO - more checking
 552check_options(Options) ->
 553	case proplists:get_value(relay, Options) of
 554		undefined ->
 555			{error, no_relay};
 556		_ ->
 557			case proplists:get_value(auth, Options) of
 558				Atom when Atom =:= always ->
 559					case proplists:is_defined(username, Options) and
 560						proplists:is_defined(password, Options) of
 561						false ->
 562							{error, no_credentials};
 563						true ->
 564							ok
 565					end;
 566				_ ->
 567					ok
 568			end
 569	end.
 570
 571-spec parse_extensions(Reply :: binary()) -> [{binary(), binary()}].
 572parse_extensions(Reply) ->
 573	[_ | Reply2] = re:split(Reply, "\r\n", [{return, binary}, trim]),
 574	[
 575		begin
 576				Body = binstr:substr(Entry, 5),
 577				case re:split(Body, " ",  [{return, binary}, trim, {parts, 2}]) of
 578					[Verb, Parameters] ->
 579						{binstr:to_upper(Verb), Parameters};
 580					[Body] ->
 581						case binstr:strchr(Body, $=) of
 582							0 ->
 583								{binstr:to_upper(Body), true};
 584							_ ->
 585								%io:format("discarding option ~p~n", [Body]),
 586								[]
 587						end
 588				end
 589		end  || Entry <- Reply2].
 590
 591-ifdef(TEST).
 592
 593session_start_test_() ->
 594	{foreach,
 595		local,
 596		fun() ->
 597				{ok, ListenSock} = socket:listen(tcp, 9876),
 598				{ListenSock}
 599		end,
 600		fun({ListenSock}) ->
 601				socket:close(ListenSock)
 602		end,
 603		[fun({ListenSock}) ->
 604					{"simple session initiation",
 605						fun() ->
 606								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 607								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 608								{ok, X} = socket:accept(ListenSock, 1000),
 609								socket:send(X, "220 Some banner\r\n"),
 610								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 611								ok
 612						end
 613					}
 614			end,
 615			fun({ListenSock}) ->
 616					{"retry on crashed EHLO twice if requested",
 617						fun() ->
 618								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {retries, 2}],
 619								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 620								{ok, X} = socket:accept(ListenSock, 1000),
 621								socket:send(X, "220 Some banner\r\n"),
 622								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 623								socket:close(X),
 624								{ok, Y} = socket:accept(ListenSock, 1000),
 625								socket:send(Y, "220 Some banner\r\n"),
 626								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 627								socket:close(Y),
 628								{ok, Z} = socket:accept(ListenSock, 1000),
 629								socket:send(Z, "220 Some banner\r\n"),
 630								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Z, 0, 1000)),
 631								ok
 632						end
 633					}
 634			end,
 635			fun({ListenSock}) ->
 636					{"retry on crashed EHLO",
 637						fun() ->
 638								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 639								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 640								unlink(Pid),
 641								Monitor = erlang:monitor(process, Pid),
 642								{ok, X} = socket:accept(ListenSock, 1000),
 643								socket:send(X, "220 Some banner\r\n"),
 644								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 645								socket:close(X),
 646								{ok, Y} = socket:accept(ListenSock, 1000),
 647								socket:send(Y, "220 Some banner\r\n"),
 648								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 649								socket:close(Y),
 650								?assertEqual({error, timeout}, socket:accept(ListenSock, 1000)),
 651								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end,
 652								ok
 653						end
 654					}
 655			end,
 656			fun({ListenSock}) ->
 657					{"abort on 554 greeting",
 658						fun() ->
 659								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 660								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 661								unlink(Pid),
 662								Monitor = erlang:monitor(process, Pid),
 663								{ok, X} = socket:accept(ListenSock, 1000),
 664								socket:send(X, "554 get lost, kid\r\n"),
 665								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 666								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, no_more_hosts, _}, Error) end,
 667								ok
 668						end
 669					}
 670			end,
 671			fun({ListenSock}) ->
 672					{"retry on 421 greeting",
 673						fun() ->
 674								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 675								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 676								{ok, X} = socket:accept(ListenSock, 1000),
 677								socket:send(X, "421 can't you see I'm busy?\r\n"),
 678								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 679								{ok, Y} = socket:accept(ListenSock, 1000),
 680								socket:send(Y, "220 Some banner\r\n"),
 681								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 682								ok
 683						end
 684					}
 685			end,
 686			fun({ListenSock}) ->
 687					{"retry on messed up EHLO response",
 688						fun() ->
 689								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 690								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 691								unlink(Pid),
 692								Monitor = erlang:monitor(process, Pid),
 693								{ok, X} = socket:accept(ListenSock, 1000),
 694								socket:send(X, "220 Some banner\r\n"),
 695								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 696								socket:send(X, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"),
 697								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 698								
 699								{ok, Y} = socket:accept(ListenSock, 1000),
 700								socket:send(Y, "220 Some banner\r\n"),
 701								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 702								socket:send(Y, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"),
 703								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
 704								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end,
 705								ok
 706						end
 707					}
 708			end,
 709			fun({ListenSock}) ->
 710					{"retry with HELO when EHLO not accepted",
 711						fun() ->
 712								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 713								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 714								{ok, X} = socket:accept(ListenSock, 1000),
 715								socket:send(X, "220 \r\n"),
 716								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 717								socket:send(X, "500 5.3.3 Unrecognized command\r\n"),
 718								?assertMatch({ok, "HELO testing\r\n"}, socket:recv(X, 0, 1000)),
 719								socket:send(X, "250 Some banner\r\n"),
 720								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 721								socket:send(X, "250 ok\r\n"),
 722								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
 723								socket:send(X, "250 ok\r\n"),
 724								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
 725								socket:send(X, "354 ok\r\n"),
 726								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
 727								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
 728								socket:send(X, "250 ok\r\n"),
 729								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 730								ok
 731						end
 732					}
 733			end,
 734			fun({ListenSock}) ->
 735					{"a valid complete transaction without TLS advertised should succeed",
 736						fun() ->
 737								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 738								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 739								{ok, X} = socket:accept(ListenSock, 1000),
 740								socket:send(X, "220 Some banner\r\n"),
 741								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 742								socket:send(X, "250 hostname\r\n"),
 743								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 744								socket:send(X, "250 ok\r\n"),
 745								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
 746								socket:send(X, "250 ok\r\n"),
 747								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
 748								socket:send(X, "354 ok\r\n"),
 749								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
 750								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
 751								socket:send(X, "250 ok\r\n"),
 752								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 753								ok
 754						end
 755					}
 756			end,
 757			fun({ListenSock}) ->
 758					{"a valid complete transaction with binary arguments shoyld succeed",
 759						fun() ->
 760								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
 761								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options),
 762								{ok, X} = socket:accept(ListenSock, 1000),
 763								socket:send(X, "220 Some banner\r\n"),
 764								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 765								socket:send(X, "250 hostname\r\n"),
 766								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 767								socket:send(X, "250 ok\r\n"),
 768								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
 769								socket:send(X, "250 ok\r\n"),
 770								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
 771								socket:send(X, "354 ok\r\n"),
 772								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
 773								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
 774								socket:send(X, "250 ok\r\n"),
 775								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 776								ok
 777						end
 778					}
 779			end,
 780			fun({ListenSock}) ->
 781					{"a valid complete transaction with TLS advertised should succeed",
 782						fun() ->
 783								Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
 784								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 785								{ok, X} = socket:accept(ListenSock, 1000),
 786								socket:send(X, "220 Some banner\r\n"),
 787								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 788								socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
 789								?assertMatch({ok, "STARTTLS\r\n"}, socket:recv(X, 0, 1000)),
 790								application:start(crypto),
 791								application:start(public_key),
 792								application:start(ssl),
 793								socket:send(X, "220 ok\r\n"),
 794								{ok, Y} = socket:to_ssl_server(X, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}], 5000),
 795								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 796								socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
 797								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(Y, 0, 1000)),
 798								socket:send(Y, "250 ok\r\n"),
 799								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(Y, 0, 1000)),
 800								socket:send(Y, "250 ok\r\n"),
 801								?assertMatch({ok, "DATA\r\n"}, socket:recv(Y, 0, 1000)),
 802								socket:send(Y, "354 ok\r\n"),
 803								?assertMatch({ok, "hello world\r\n"}, socket:recv(Y, 0, 1000)),
 804								?assertMatch({ok, ".\r\n"}, socket:recv(Y, 0, 1000)),
 805								socket:send(Y, "250 ok\r\n"),
 806								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
 807								ok
 808						end
 809					}
 810			end,
 811			fun({ListenSock}) ->
 812					{"a valid complete transaction with TLS advertised and binary arguments should succeed",
 813						fun() ->
 814								Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
 815								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options),
 816								{ok, X} = socket:accept(ListenSock, 1000),
 817								socket:send(X, "220 Some banner\r\n"),
 818								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 819								socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
 820								?assertMatch({ok, "STARTTLS\r\n"}, socket:recv(X, 0, 1000)),
 821								application:start(crypto),
 822								application:start(public_key),
 823								application:start(ssl),
 824								socket:send(X, "220 ok\r\n"),
 825								{ok, Y} = socket:to_ssl_server(X, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}], 5000),
 826								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
 827								socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
 828								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(Y, 0, 1000)),
 829								socket:send(Y, "250 ok\r\n"),
 830								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(Y, 0, 1000)),
 831								socket:send(Y, "250 ok\r\n"),
 832								?assertMatch({ok, "DATA\r\n"}, socket:recv(Y, 0, 1000)),
 833								socket:send(Y, "354 ok\r\n"),
 834								?assertMatch({ok, "hello world\r\n"}, socket:recv(Y, 0, 1000)),
 835								?assertMatch({ok, ".\r\n"}, socket:recv(Y, 0, 1000)),
 836								socket:send(Y, "250 ok\r\n"),
 837								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
 838								ok
 839						end
 840					}
 841			end,
 842
 843			fun({ListenSock}) ->
 844					{"AUTH PLAIN should work",
 845						fun() ->
 846								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
 847								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 848								{ok, X} = socket:accept(ListenSock, 1000),
 849								socket:send(X, "220 Some banner\r\n"),
 850								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 851								socket:send(X, "250-hostname\r\n250 AUTH PLAIN\r\n"),
 852								AuthString = binary_to_list(base64:encode("\0user\0pass")),
 853								AuthPacket = "AUTH PLAIN "++AuthString++"\r\n",
 854								?assertEqual({ok, AuthPacket}, socket:recv(X, 0, 1000)),
 855								socket:send(X, "235 ok\r\n"),
 856								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 857								ok
 858						end
 859					}
 860			end,
 861			fun({ListenSock}) ->
 862					{"AUTH LOGIN should work",
 863						fun() ->
 864								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
 865								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 866								{ok, X} = socket:accept(ListenSock, 1000),
 867								socket:send(X, "220 Some banner\r\n"),
 868								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 869								socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"),
 870								?assertEqual({ok, "AUTH LOGIN\r\n"}, socket:recv(X, 0, 1000)),
 871								socket:send(X, "334 VXNlcm5hbWU6\r\n"),
 872								UserString = binary_to_list(base64:encode("user")),
 873								?assertEqual({ok, UserString++"\r\n"}, socket:recv(X, 0, 1000)),
 874								socket:send(X, "334 UGFzc3dvcmQ6\r\n"),
 875								PassString = binary_to_list(base64:encode("pass")),
 876								?assertEqual({ok, PassString++"\r\n"}, socket:recv(X, 0, 1000)),
 877								socket:send(X, "235 ok\r\n"),
 878								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 879								ok
 880						end
 881					}
 882			end,
 883			fun({ListenSock}) ->
 884					{"AUTH CRAM-MD5 should work",
 885						fun() ->
 886								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
 887								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
 888								{ok, X} = socket:accept(ListenSock, 1000),
 889								socket:send(X, "220 Some banner\r\n"),
 890								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 891								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
 892								?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, socket:recv(X, 0, 1000)),
 893								Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
 894								DecodedSeed = base64:decode_to_string(Seed),
 895								Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
 896								String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
 897								socket:send(X, "334 "++Seed++"\r\n"),
 898								{ok, Packet} = socket:recv(X, 0, 1000),
 899								CramDigest = smtp_util:trim_crlf(Packet),
 900								?assertEqual(String, CramDigest),
 901								socket:send(X, "235 ok\r\n"),
 902								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 903								ok
 904						end
 905					}
 906			end,
 907			fun({ListenSock}) ->
 908					{"AUTH CRAM-MD5 should work",
 909						fun() ->
 910								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {username, <<"user">>}, {password, <<"pass">>}],
 911								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
 912								{ok, X} = socket:accept(ListenSock, 1000),
 913								socket:send(X, "220 Some banner\r\n"),
 914								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 915								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
 916								?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, socket:recv(X, 0, 1000)),
 917								Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
 918								DecodedSeed = base64:decode_to_string(Seed),
 919								Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
 920								String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
 921								socket:send(X, "334 "++Seed++"\r\n"),
 922								{ok, Packet} = socket:recv(X, 0, 1000),
 923								CramDigest = smtp_util:trim_crlf(Packet),
 924								?assertEqual(String, CramDigest),
 925								socket:send(X, "235 ok\r\n"),
 926								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 927								ok
 928						end
 929					}
 930			end,
 931			fun({ListenSock}) ->
 932					{"should bail when AUTH is required but not provided",
 933						fun() ->
 934								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>}],
 935								{ok, Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
 936								unlink(Pid),
 937								Monitor = erlang:monitor(process, Pid),
 938								{ok, X} = socket:accept(ListenSock, 1000),
 939								socket:send(X, "220 Some banner\r\n"),
 940								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 941								socket:send(X, "250-hostname\r\n250 8BITMIME\r\n"),
 942								?assertEqual({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 943								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, {missing_requirement, _, auth}}, Error) end,
 944								ok
 945						end
 946					}
 947			end,
 948			fun({ListenSock}) ->
 949					{"should bail when AUTH is required but of an unsupported type",
 950						fun() ->
 951								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>}],
 952								{ok, Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
 953								unlink(Pid),
 954								Monitor = erlang:monitor(process, Pid),
 955								{ok, X} = socket:accept(ListenSock, 1000),
 956								socket:send(X, "220 Some banner\r\n"),
 957								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 958								socket:send(X, "250-hostname\r\n250-AUTH GSSAPI\r\n250 8BITMIME\r\n"),
 959								?assertEqual({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 960								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, no_more_hosts, {permanent_failure, _, auth_failed}}, Error) end,
 961								ok
 962						end
 963					}
 964			end,
 965			fun({_ListenSock}) ->
 966					{"Connecting to a SSL socket directly should work",
 967						fun() ->
 968								application:start(crypto),
 969								application:start(public_key),
 970								application:start(ssl),
 971								{ok, ListenSock} = socket:listen(ssl, 9877, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}]),
 972								Options = [{relay, <<"localhost">>}, {port, 9877}, {hostname, <<"testing">>}, {ssl, true}],
 973								{ok, _Pid} = send({<<"test@foo.com">>, [<<"<foo@bar.com>">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
 974								{ok, X} = socket:accept(ListenSock, 1000),
 975								socket:send(X, "220 Some banner\r\n"),
 976								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
 977								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
 978								?assertEqual({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
 979								socket:send(X, "250 ok\r\n"),
 980								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
 981								socket:send(X, "250 ok\r\n"),
 982								?assertMatch({ok, "RCPT TO: <baz@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
 983								socket:send(X, "250 ok\r\n"),
 984								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
 985								socket:send(X, "354 ok\r\n"),
 986								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
 987								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
 988								socket:send(X, "250 ok\r\n"),
 989								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
 990								socket:close(ListenSock),
 991								ok
 992						end
 993					}
 994			end
 995
 996		]
 997	}.
 998
 999extension_parse_test_() ->
1000	[
1001		{"parse extensions",
1002			fun() ->
1003					Res = parse_extensions(<<"250-smtp.example.com\r\n250-PIPELINING\r\n250-SIZE 20971520\r\n250-VRFY\r\n250-ETRN\r\n250-STARTTLS\r\n250-AUTH CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-AUTH=CRAM-MD5 PLAIN DIGEST-MD5 LOGIN\r\n250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250 DSN">>),
1004					?assertEqual(true, proplists:get_value(<<"PIPELINING">>, Res)),
1005					?assertEqual(<<"20971520">>, proplists:get_value(<<"SIZE">>, Res)),
1006					?assertEqual(true, proplists:get_value(<<"VRFY">>, Res)),
1007					?assertEqual(true, proplists:get_value(<<"ETRN">>, Res)),
1008					?assertEqual(true, proplists:get_value(<<"STARTTLS">>, Res)),
1009					?assertEqual(<<"CRAM-MD5 PLAIN DIGEST-MD5 LOGIN">>, proplists:get_value(<<"AUTH">>, Res)),
1010					?assertEqual(true, proplists:get_value(<<"ENHANCEDSTATUSCODES">>, Res)),
1011					?assertEqual(true, proplists:get_value(<<"8BITMIME">>, Res)),
1012					?assertEqual(true, proplists:get_value(<<"DSN">>, Res)),
1013					?assertEqual(10, length(Res)),
1014					ok
1015			end
1016		}
1017	].
1018
1019-endif.