PageRenderTime 15ms CodeModel.GetById 4ms app.highlight 113ms RepoModel.GetById 1ms app.codeStats 0ms

/src/gen_smtp_client.erl

https://github.com/JackDanger/gen_smtp
Erlang | 974 lines | 859 code | 37 blank | 78 comment | 7 complexity | df41e10bacbb70be9187762010d6ff8e 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:{permanant_failure, Message} ->
147			% permanant failure means no retries, and don't even continue with other hosts
148			{error, no_more_hosts, {permanant_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({permanant_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({permanant_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({permanant_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({permanant_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({permanant_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	socket:send(Socket, ["EHLO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
398	%% TODO handle fallback to HELO!
399	{ok, Reply} = read_possible_multiline_reply(Socket),
400	Extensions = parse_extensions(Reply),
401	{ok, Extensions}.
402
403% check if we should try to do TLS
404-spec try_STARTTLS(Socket :: socket:socket(), Options :: list(), Extensions :: list()) -> {socket:socket(), list()}.
405try_STARTTLS(Socket, Options, Extensions) ->
406		case {proplists:get_value(tls, Options),
407				proplists:get_value(<<"STARTTLS">>, Extensions)} of
408			{Atom, true} when Atom =:= always; Atom =:= if_available ->
409			%io:format("Starting TLS~n"),
410			case {do_STARTTLS(Socket, Options), Atom} of
411				{false, always} ->
412					%io:format("TLS failed~n"),
413					quit(Socket),
414					erlang:throw({temporary_failure, tls_failed});
415				{false, if_available} ->
416					%io:format("TLS failed~n"),
417					{Socket, Extensions};
418				{{S, E}, _} ->
419					%io:format("TLS started~n"),
420					{S, E}
421			end;
422		{always, _} ->
423			quit(Socket),
424			erlang:throw({missing_requirement, tls});
425		_ ->
426			{Socket, Extensions}
427	end.
428
429%% attempt to upgrade socket to TLS
430-spec do_STARTTLS(Socket :: socket:socket(), Options :: list()) -> {socket:socket(), list()} | false.
431do_STARTTLS(Socket, Options) ->
432	socket:send(Socket, "STARTTLS\r\n"),
433	case read_possible_multiline_reply(Socket) of
434		{ok, <<"220", _Rest/binary>>} ->
435			application:start(crypto),
436			application:start(public_key),
437			application:start(ssl),
438			case socket:to_ssl_client(Socket, [], 5000) of
439				{ok, NewSocket} ->
440					%NewSocket;
441					{ok, Extensions} = try_EHLO(NewSocket, Options),
442					{NewSocket, Extensions};
443				_Else ->
444					%io:format("~p~n", [Else]),
445					false
446			end;
447		{ok, <<"4", _Rest/binary>> = Msg} ->
448			quit(Socket),
449			throw({temporary_failure, Msg});
450		{ok, Msg} ->
451			quit(Socket),
452			throw({permanant_failure, Msg})
453	end.
454
455%% try connecting to a host
456connect(Host, Options) when is_binary(Host) ->
457	connect(binary_to_list(Host), Options);
458connect(Host, Options) ->
459	SockOpts = [binary, {packet, line}, {keepalive, true}, {active, false}],
460	Proto = case proplists:get_value(ssl, Options) of
461		true ->
462			application:start(crypto),
463			application:start(public_key),
464			application:start(ssl),
465			ssl;
466		_ ->
467			tcp
468	end,
469	Port = case proplists:get_value(port, Options) of
470		undefined when Proto =:= ssl ->
471			465;
472		OPort when is_integer(OPort) ->
473			OPort;
474		_ ->
475			25
476	end,
477	case socket:connect(Proto, Host, Port, SockOpts, 5000) of
478		{ok, Socket} ->
479			case read_possible_multiline_reply(Socket) of
480				{ok, <<"220", Banner/binary>>} ->
481					{ok, Socket, Host, Banner};
482				{ok, <<"4", _Rest/binary>> = Msg} ->
483					quit(Socket),
484					throw({temporary_failure, Msg});
485				{ok, Msg} ->
486					quit(Socket),
487					throw({permanant_failure, Msg})
488			end;
489		{error, Reason} ->
490			throw({network_failure, {error, Reason}})
491	end.
492
493%% read a multiline reply (eg. EHLO reply)
494-spec read_possible_multiline_reply(Socket :: socket:socket()) -> {ok, binary()}.
495read_possible_multiline_reply(Socket) ->
496	case socket:recv(Socket, 0, ?TIMEOUT) of
497		{ok, Packet} ->
498			case binstr:substr(Packet, 4, 1) of
499				<<"-">> ->
500					Code = binstr:substr(Packet, 1, 3),
501					read_multiline_reply(Socket, Code, [Packet]);
502				<<" ">> ->
503					{ok, Packet}
504			end;
505		Error ->
506			throw({network_failure, Error})
507	end.
508
509-spec read_multiline_reply(Socket :: socket:socket(), Code :: binary(), Acc :: [binary()]) -> {ok, binary()}.
510read_multiline_reply(Socket, Code, Acc) ->
511	case socket:recv(Socket, 0, ?TIMEOUT) of
512		{ok, Packet} ->
513			case {binstr:substr(Packet, 1, 3), binstr:substr(Packet, 4, 1)} of
514				{Code, <<" ">>} ->
515					{ok, list_to_binary(lists:reverse([Packet | Acc]))};
516				{Code, <<"-">>} ->
517					read_multiline_reply(Socket, Code, [Packet | Acc]);
518				_ ->
519					quit(Socket),
520					throw({unexpected_response, lists:reverse([Packet | Acc])})
521			end;
522		Error ->
523			throw({network_failure, Error})
524	end.
525
526quit(Socket) ->
527	socket:send(Socket, "QUIT\r\n"),
528	socket:close(Socket),
529	ok.
530
531% TODO - more checking
532check_options(Options) ->
533	case proplists:get_value(relay, Options) of
534		undefined ->
535			{error, no_relay};
536		_ ->
537			case proplists:get_value(auth, Options) of
538				Atom when Atom =:= always ->
539					case proplists:is_defined(username, Options) and
540						proplists:is_defined(password, Options) of
541						false ->
542							{error, no_credentials};
543						true ->
544							ok
545					end;
546				_ ->
547					ok
548			end
549	end.
550
551-spec parse_extensions(Reply :: binary()) -> [{binary(), binary()}].
552parse_extensions(Reply) ->
553	[_ | Reply2] = re:split(Reply, "\r\n", [{return, binary}, trim]),
554	[
555		begin
556				Body = binstr:substr(Entry, 5),
557				case re:split(Body, " ",  [{return, binary}, trim, {parts, 2}]) of
558					[Verb, Parameters] ->
559						{binstr:to_upper(Verb), Parameters};
560					[Body] ->
561						case binstr:strchr(Body, $=) of
562							0 ->
563								{binstr:to_upper(Body), true};
564							_ ->
565								%io:format("discarding option ~p~n", [Body]),
566								[]
567						end
568				end
569		end  || Entry <- Reply2].
570
571-ifdef(TEST).
572
573session_start_test_() ->
574	{foreach,
575		local,
576		fun() ->
577				{ok, ListenSock} = socket:listen(tcp, 9876),
578				{ListenSock}
579		end,
580		fun({ListenSock}) ->
581				socket:close(ListenSock)
582		end,
583		[fun({ListenSock}) ->
584					{"simple session initiation",
585						fun() ->
586								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
587								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
588								{ok, X} = socket:accept(ListenSock, 1000),
589								socket:send(X, "220 Some banner\r\n"),
590								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
591								ok
592						end
593					}
594			end,
595			fun({ListenSock}) ->
596					{"retry on crashed EHLO twice if requested",
597						fun() ->
598								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {retries, 2}],
599								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
600								{ok, X} = socket:accept(ListenSock, 1000),
601								socket:send(X, "220 Some banner\r\n"),
602								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
603								socket:close(X),
604								{ok, Y} = socket:accept(ListenSock, 1000),
605								socket:send(Y, "220 Some banner\r\n"),
606								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
607								socket:close(Y),
608								{ok, Z} = socket:accept(ListenSock, 1000),
609								socket:send(Z, "220 Some banner\r\n"),
610								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Z, 0, 1000)),
611								ok
612						end
613					}
614			end,
615			fun({ListenSock}) ->
616					{"retry on crashed EHLO",
617						fun() ->
618								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
619								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
620								unlink(Pid),
621								Monitor = erlang:monitor(process, Pid),
622								{ok, X} = socket:accept(ListenSock, 1000),
623								socket:send(X, "220 Some banner\r\n"),
624								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
625								socket:close(X),
626								{ok, Y} = socket:accept(ListenSock, 1000),
627								socket:send(Y, "220 Some banner\r\n"),
628								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
629								socket:close(Y),
630								?assertEqual({error, timeout}, socket:accept(ListenSock, 1000)),
631								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end,
632								ok
633						end
634					}
635			end,
636			fun({ListenSock}) ->
637					{"abort on 554 greeting",
638						fun() ->
639								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
640								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
641								unlink(Pid),
642								Monitor = erlang:monitor(process, Pid),
643								{ok, X} = socket:accept(ListenSock, 1000),
644								socket:send(X, "554 get lost, kid\r\n"),
645								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
646								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, no_more_hosts, _}, Error) end,
647								ok
648						end
649					}
650			end,
651			fun({ListenSock}) ->
652					{"retry on 421 greeting",
653						fun() ->
654								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
655								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
656								{ok, X} = socket:accept(ListenSock, 1000),
657								socket:send(X, "421 can't you see I'm busy?\r\n"),
658								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
659								{ok, Y} = socket:accept(ListenSock, 1000),
660								socket:send(Y, "220 Some banner\r\n"),
661								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
662								ok
663						end
664					}
665			end,
666			fun({ListenSock}) ->
667					{"retry on messed up EHLO response",
668						fun() ->
669								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
670								{ok, Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
671								unlink(Pid),
672								Monitor = erlang:monitor(process, Pid),
673								{ok, X} = socket:accept(ListenSock, 1000),
674								socket:send(X, "220 Some banner\r\n"),
675								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
676								socket:send(X, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"),
677								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
678								
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								socket:send(Y, "250-server.example.com EHLO\r\n250-AUTH LOGIN PLAIN\r\n421 too busy\r\n"),
683								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
684								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, _}, Error) end,
685								ok
686						end
687					}
688			end,
689			fun({ListenSock}) ->
690					{"a valid complete transaction without TLS advertised should succeed",
691						fun() ->
692								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
693								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
694								{ok, X} = socket:accept(ListenSock, 1000),
695								socket:send(X, "220 Some banner\r\n"),
696								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
697								socket:send(X, "250 hostname\r\n"),
698								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
699								socket:send(X, "250 ok\r\n"),
700								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
701								socket:send(X, "250 ok\r\n"),
702								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
703								socket:send(X, "354 ok\r\n"),
704								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
705								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
706								socket:send(X, "250 ok\r\n"),
707								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
708								ok
709						end
710					}
711			end,
712			fun({ListenSock}) ->
713					{"a valid complete transaction with binary arguments shoyld succeed",
714						fun() ->
715								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}],
716								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options),
717								{ok, X} = socket:accept(ListenSock, 1000),
718								socket:send(X, "220 Some banner\r\n"),
719								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
720								socket:send(X, "250 hostname\r\n"),
721								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
722								socket:send(X, "250 ok\r\n"),
723								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
724								socket:send(X, "250 ok\r\n"),
725								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
726								socket:send(X, "354 ok\r\n"),
727								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
728								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
729								socket:send(X, "250 ok\r\n"),
730								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
731								ok
732						end
733					}
734			end,
735			fun({ListenSock}) ->
736					{"a valid complete transaction with TLS advertised should succeed",
737						fun() ->
738								Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
739								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
740								{ok, X} = socket:accept(ListenSock, 1000),
741								socket:send(X, "220 Some banner\r\n"),
742								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
743								socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
744								?assertMatch({ok, "STARTTLS\r\n"}, socket:recv(X, 0, 1000)),
745								application:start(crypto),
746								application:start(public_key),
747								application:start(ssl),
748								socket:send(X, "220 ok\r\n"),
749								{ok, Y} = socket:to_ssl_server(X, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}], 5000),
750								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
751								socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
752								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(Y, 0, 1000)),
753								socket:send(Y, "250 ok\r\n"),
754								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(Y, 0, 1000)),
755								socket:send(Y, "250 ok\r\n"),
756								?assertMatch({ok, "DATA\r\n"}, socket:recv(Y, 0, 1000)),
757								socket:send(Y, "354 ok\r\n"),
758								?assertMatch({ok, "hello world\r\n"}, socket:recv(Y, 0, 1000)),
759								?assertMatch({ok, ".\r\n"}, socket:recv(Y, 0, 1000)),
760								socket:send(Y, "250 ok\r\n"),
761								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
762								ok
763						end
764					}
765			end,
766			fun({ListenSock}) ->
767					{"a valid complete transaction with TLS advertised and binary arguments should succeed",
768						fun() ->
769								Options = [{relay, "localhost"}, {port, 9876}, {hostname, <<"testing">>}],
770								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>], <<"hello world">>}, Options),
771								{ok, X} = socket:accept(ListenSock, 1000),
772								socket:send(X, "220 Some banner\r\n"),
773								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
774								socket:send(X, "250-hostname\r\n250 STARTTLS\r\n"),
775								?assertMatch({ok, "STARTTLS\r\n"}, socket:recv(X, 0, 1000)),
776								application:start(crypto),
777								application:start(public_key),
778								application:start(ssl),
779								socket:send(X, "220 ok\r\n"),
780								{ok, Y} = socket:to_ssl_server(X, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}], 5000),
781								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(Y, 0, 1000)),
782								socket:send(Y, "250-hostname\r\n250 STARTTLS\r\n"),
783								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(Y, 0, 1000)),
784								socket:send(Y, "250 ok\r\n"),
785								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(Y, 0, 1000)),
786								socket:send(Y, "250 ok\r\n"),
787								?assertMatch({ok, "DATA\r\n"}, socket:recv(Y, 0, 1000)),
788								socket:send(Y, "354 ok\r\n"),
789								?assertMatch({ok, "hello world\r\n"}, socket:recv(Y, 0, 1000)),
790								?assertMatch({ok, ".\r\n"}, socket:recv(Y, 0, 1000)),
791								socket:send(Y, "250 ok\r\n"),
792								?assertMatch({ok, "QUIT\r\n"}, socket:recv(Y, 0, 1000)),
793								ok
794						end
795					}
796			end,
797
798			fun({ListenSock}) ->
799					{"AUTH PLAIN should work",
800						fun() ->
801								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
802								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
803								{ok, X} = socket:accept(ListenSock, 1000),
804								socket:send(X, "220 Some banner\r\n"),
805								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
806								socket:send(X, "250-hostname\r\n250 AUTH PLAIN\r\n"),
807								AuthString = binary_to_list(base64:encode("\0user\0pass")),
808								AuthPacket = "AUTH PLAIN "++AuthString++"\r\n",
809								?assertEqual({ok, AuthPacket}, socket:recv(X, 0, 1000)),
810								socket:send(X, "235 ok\r\n"),
811								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
812								ok
813						end
814					}
815			end,
816			fun({ListenSock}) ->
817					{"AUTH LOGIN should work",
818						fun() ->
819								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
820								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
821								{ok, X} = socket:accept(ListenSock, 1000),
822								socket:send(X, "220 Some banner\r\n"),
823								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
824								socket:send(X, "250-hostname\r\n250 AUTH LOGIN\r\n"),
825								?assertEqual({ok, "AUTH LOGIN\r\n"}, socket:recv(X, 0, 1000)),
826								socket:send(X, "334 VXNlcm5hbWU6\r\n"),
827								UserString = binary_to_list(base64:encode("user")),
828								?assertEqual({ok, UserString++"\r\n"}, socket:recv(X, 0, 1000)),
829								socket:send(X, "334 UGFzc3dvcmQ6\r\n"),
830								PassString = binary_to_list(base64:encode("pass")),
831								?assertEqual({ok, PassString++"\r\n"}, socket:recv(X, 0, 1000)),
832								socket:send(X, "235 ok\r\n"),
833								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
834								ok
835						end
836					}
837			end,
838			fun({ListenSock}) ->
839					{"AUTH CRAM-MD5 should work",
840						fun() ->
841								Options = [{relay, "localhost"}, {port, 9876}, {hostname, "testing"}, {username, "user"}, {password, "pass"}],
842								{ok, _Pid} = send({"test@foo.com", ["foo@bar.com"], "hello world"}, Options),
843								{ok, X} = socket:accept(ListenSock, 1000),
844								socket:send(X, "220 Some banner\r\n"),
845								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
846								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
847								?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, socket:recv(X, 0, 1000)),
848								Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
849								DecodedSeed = base64:decode_to_string(Seed),
850								Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
851								String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
852								socket:send(X, "334 "++Seed++"\r\n"),
853								{ok, Packet} = socket:recv(X, 0, 1000),
854								CramDigest = smtp_util:trim_crlf(Packet),
855								?assertEqual(String, CramDigest),
856								socket:send(X, "235 ok\r\n"),
857								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
858								ok
859						end
860					}
861			end,
862			fun({ListenSock}) ->
863					{"AUTH CRAM-MD5 should work",
864						fun() ->
865								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {username, <<"user">>}, {password, <<"pass">>}],
866								{ok, _Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
867								{ok, X} = socket:accept(ListenSock, 1000),
868								socket:send(X, "220 Some banner\r\n"),
869								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
870								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
871								?assertEqual({ok, "AUTH CRAM-MD5\r\n"}, socket:recv(X, 0, 1000)),
872								Seed = smtp_util:get_cram_string(smtp_util:guess_FQDN()),
873								DecodedSeed = base64:decode_to_string(Seed),
874								Digest = smtp_util:compute_cram_digest("pass", DecodedSeed),
875								String = binary_to_list(base64:encode(list_to_binary(["user ", Digest]))),
876								socket:send(X, "334 "++Seed++"\r\n"),
877								{ok, Packet} = socket:recv(X, 0, 1000),
878								CramDigest = smtp_util:trim_crlf(Packet),
879								?assertEqual(String, CramDigest),
880								socket:send(X, "235 ok\r\n"),
881								?assertMatch({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
882								ok
883						end
884					}
885			end,
886			fun({ListenSock}) ->
887					{"should bail when AUTH is required but not provided",
888						fun() ->
889								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>}],
890								{ok, Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
891								unlink(Pid),
892								Monitor = erlang:monitor(process, Pid),
893								{ok, X} = socket:accept(ListenSock, 1000),
894								socket:send(X, "220 Some banner\r\n"),
895								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
896								socket:send(X, "250-hostname\r\n250 8BITMIME\r\n"),
897								?assertEqual({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
898								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, retries_exceeded, {missing_requirement, _, auth}}, Error) end,
899								ok
900						end
901					}
902			end,
903			fun({ListenSock}) ->
904					{"should bail when AUTH is required but of an unsupported type",
905						fun() ->
906								Options = [{relay, <<"localhost">>}, {port, 9876}, {hostname, <<"testing">>}, {auth, always}, {username, <<"user">>}, {retries, 0}, {password, <<"pass">>}],
907								{ok, Pid} = send({<<"test@foo.com">>, [<<"foo@bar.com">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
908								unlink(Pid),
909								Monitor = erlang:monitor(process, Pid),
910								{ok, X} = socket:accept(ListenSock, 1000),
911								socket:send(X, "220 Some banner\r\n"),
912								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
913								socket:send(X, "250-hostname\r\n250-AUTH GSSAPI\r\n250 8BITMIME\r\n"),
914								?assertEqual({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
915								receive {'DOWN', Monitor, _, _, Error} -> ?assertMatch({error, no_more_hosts, {permanant_failure, _, auth_failed}}, Error) end,
916								ok
917						end
918					}
919			end,
920			fun({_ListenSock}) ->
921					{"Connecting to a SSL socket directly should work",
922						fun() ->
923								application:start(crypto),
924								application:start(public_key),
925								application:start(ssl),
926								{ok, ListenSock} = socket:listen(ssl, 9877, [{certfile, "../testdata/server.crt"}, {keyfile, "../testdata/server.key"}]),
927								Options = [{relay, <<"localhost">>}, {port, 9877}, {hostname, <<"testing">>}, {ssl, true}],
928								{ok, _Pid} = send({<<"test@foo.com">>, [<<"<foo@bar.com>">>, <<"baz@bar.com">>], <<"hello world">>}, Options),
929								{ok, X} = socket:accept(ListenSock, 1000),
930								socket:send(X, "220 Some banner\r\n"),
931								?assertMatch({ok, "EHLO testing\r\n"}, socket:recv(X, 0, 1000)),
932								socket:send(X, "250-hostname\r\n250 AUTH CRAM-MD5\r\n"),
933								?assertEqual({ok, "MAIL FROM: <test@foo.com>\r\n"}, socket:recv(X, 0, 1000)),
934								socket:send(X, "250 ok\r\n"),
935								?assertMatch({ok, "RCPT TO: <foo@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
936								socket:send(X, "250 ok\r\n"),
937								?assertMatch({ok, "RCPT TO: <baz@bar.com>\r\n"}, socket:recv(X, 0, 1000)),
938								socket:send(X, "250 ok\r\n"),
939								?assertMatch({ok, "DATA\r\n"}, socket:recv(X, 0, 1000)),
940								socket:send(X, "354 ok\r\n"),
941								?assertMatch({ok, "hello world\r\n"}, socket:recv(X, 0, 1000)),
942								?assertMatch({ok, ".\r\n"}, socket:recv(X, 0, 1000)),
943								socket:send(X, "250 ok\r\n"),
944								?assertMatch({ok, "QUIT\r\n"}, socket:recv(X, 0, 1000)),
945								socket:close(ListenSock),
946								ok
947						end
948					}
949			end
950
951		]
952	}.
953
954extension_parse_test_() ->
955	[
956		{"parse extensions",
957			fun() ->
958					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">>),
959					?assertEqual(true, proplists:get_value(<<"PIPELINING">>, Res)),
960					?assertEqual(<<"20971520">>, proplists:get_value(<<"SIZE">>, Res)),
961					?assertEqual(true, proplists:get_value(<<"VRFY">>, Res)),
962					?assertEqual(true, proplists:get_value(<<"ETRN">>, Res)),
963					?assertEqual(true, proplists:get_value(<<"STARTTLS">>, Res)),
964					?assertEqual(<<"CRAM-MD5 PLAIN DIGEST-MD5 LOGIN">>, proplists:get_value(<<"AUTH">>, Res)),
965					?assertEqual(true, proplists:get_value(<<"ENHANCEDSTATUSCODES">>, Res)),
966					?assertEqual(true, proplists:get_value(<<"8BITMIME">>, Res)),
967					?assertEqual(true, proplists:get_value(<<"DSN">>, Res)),
968					?assertEqual(10, length(Res)),
969					ok
970			end
971		}
972	].
973
974-endif.