/src/gen_smtp_server_session.erl
Erlang | 2251 lines | 1983 code | 59 blank | 209 comment | 9 complexity | afab91831c9db5e9e76df0b75a6885e7 MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- %%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
- %%%
- %%% Redistribution and use in source and binary forms, with or without
- %%% modification, are permitted provided that the following conditions are met:
- %%%
- %%% 1. Redistributions of source code must retain the above copyright notice,
- %%% this list of conditions and the following disclaimer.
- %%% 2. Redistributions in binary form must reproduce the above copyright
- %%% notice, this list of conditions and the following disclaimer in the
- %%% documentation and/or other materials provided with the distribution.
- %%%
- %%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
- %%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- %%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
- %%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
- %%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- %%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- %%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
- %%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- %%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- %%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- %% @doc A Per-connection SMTP server, extensible via a callback module.
- -module(gen_smtp_server_session).
- -behaviour(gen_server).
- -ifdef(TEST).
- -import(smtp_util, [compute_cram_digest/2]).
- -include_lib("eunit/include/eunit.hrl").
- -endif.
- -define(MAXIMUMSIZE, 10485760). %10mb
- -define(BUILTIN_EXTENSIONS, [{"SIZE", "10485670"}, {"8BITMIME", true}, {"PIPELINING", true}]).
- -define(TIMEOUT, 180000). % 3 minutes
- %% External API
- -export([start_link/3, start/3]).
- %% gen_server callbacks
- -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
- code_change/3]).
- -export([behaviour_info/1]).
- -record(envelope,
- {
- from :: string() | 'undefined',
- to = [] :: [string()],
- data = <<>> :: binary(),
- expectedsize = 0 :: pos_integer() | 0,
- auth = {[], []} :: {string() | [], string() | []} % {"username", "password"}
- }
- ).
- -record(state,
- {
- socket = erlang:error({undefined, socket}) :: port() | tuple(),
- module = erlang:error({undefined, module}) :: atom(),
- envelope = undefined :: 'undefined' | #envelope{},
- extensions = [] :: [{string(), string()}],
- waitingauth = false :: 'false' | 'plain' | 'login' | 'cram-md5',
- authdata :: 'undefined' | string(),
- readmessage = false :: boolean(),
- tls = false :: boolean(),
- callbackstate :: any(),
- options = [] :: [tuple()]
- }
- ).
- behaviour_info(callbacks) ->
- [{init,4},
- {terminate,2},
- {code_change,3},
- {handle_HELO,2},
- {handle_EHLO,3},
- {handle_MAIL,2},
- {handle_MAIL_extension,2},
- {handle_RCPT,2},
- {handle_RCPT_extension,2},
- {handle_DATA,4},
- {handle_RSET,1},
- {handle_VRFY,2},
- {handle_other,3}];
- behaviour_info(_Other) ->
- undefined.
- -spec(start_link/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
- start_link(Socket, Module, Options) ->
- gen_server:start_link(?MODULE, [Socket, Module, Options], []).
- -spec(start/3 :: (Socket :: port(), Module :: atom(), Options :: [tuple()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
- start(Socket, Module, Options) ->
- gen_server:start(?MODULE, [Socket, Module, Options], []).
- -spec(init/1 :: (Args :: list()) -> {'ok', #state{}} | {'stop', any()} | 'ignore').
- init([Socket, Module, Options]) ->
- {ok, {PeerName, _Port}} = socket:peername(Socket),
- 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
- {ok, Banner, CallbackState} ->
- socket:send(Socket, io_lib:format("220 ~s\r\n", [Banner])),
- socket:active_once(Socket),
- {ok, #state{socket = Socket, module = Module, options = Options, callbackstate = CallbackState}, ?TIMEOUT};
- {stop, Reason, Message} ->
- socket:send(Socket, Message ++ "\r\n"),
- socket:close(Socket),
- {stop, Reason};
- ignore ->
- socket:close(Socket),
- ignore
- end.
- %% @hidden
- handle_call(stop, _From, State) ->
- {stop, normal, ok, State};
- handle_call(Request, _From, State) ->
- {reply, {unknown_call, Request}, State}.
- %% @hidden
- handle_cast(_Msg, State) ->
- {noreply, State}.
- handle_info({receive_data, {error, size_exceeded}}, #state{socket = Socket, readmessage = true} = State) ->
- socket:send(Socket, "552 Message too large\r\n"),
- socket:active_once(Socket),
- {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
- handle_info({receive_data, {error, bare_newline}}, #state{socket = Socket, readmessage = true} = State) ->
- socket:send(Socket, "451 Bare newline detected\r\n"),
- io:format("bare newline detected: ~p~n", [self()]),
- socket:active_once(Socket),
- {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT};
- handle_info({receive_data, Body, Rest}, #state{socket = Socket, readmessage = true, envelope = Env, module=Module,
- callbackstate = OldCallbackState, extensions = Extensions} = State) ->
- % send the remainder of the data...
- case Rest of
- <<>> -> ok; % no remaining data
- _ -> self() ! {socket:get_proto(Socket), Socket, Rest}
- end,
- socket:setopts(Socket, [{packet, line}]),
- Envelope = Env#envelope{data = Body},% size = length(Body)},
- %io:format("received body from child process, remainder was ~p (~p)~n", [Rest, self()]),
- %handle_info({_Proto, Socket, <<".\r\n">>}, #state{readmessage = true, envelope = Env, module = Module} = State) ->
- %io:format("done reading message~n"),
- %io:format("entire message~n~s~n", [Envelope#envelope.data]),
- %Envelope = Env#envelope{data = list_to_binary(lists:reverse(Env#envelope.data))},
- Valid = case has_extension(Extensions, "SIZE") of
- {true, Value} ->
- case byte_size(Envelope#envelope.data) > list_to_integer(Value) of
- true ->
- socket:send(Socket, "552 Message too large\r\n"),
- socket:active_once(Socket),
- false;
- false ->
- true
- end;
- false ->
- true
- end,
- case Valid of
- true ->
- case Module:handle_DATA(Envelope#envelope.from, Envelope#envelope.to, Envelope#envelope.data, OldCallbackState) of
- {ok, Reference, CallbackState} ->
- socket:send(Socket, io_lib:format("250 queued as ~s\r\n", [Reference])),
- socket:active_once(Socket),
- {noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message++"\r\n"),
- socket:active_once(Socket),
- {noreply, State#state{readmessage = false, envelope = #envelope{}, callbackstate = CallbackState}, ?TIMEOUT}
- end;
- false ->
- % might not even be able to get here anymore...
- {noreply, State#state{readmessage = false, envelope = #envelope{}}, ?TIMEOUT}
- end;
- handle_info({_SocketType, Socket, Packet}, State) ->
- case handle_request(parse_request(Packet), State) of
- {ok, #state{envelope = Envelope, extensions = Extensions, options = Options, readmessage = true} = NewState} ->
- MaxSize = case has_extension(Extensions, "SIZE") of
- {true, Value} ->
- list_to_integer(Value);
- false ->
- ?MAXIMUMSIZE
- end,
- Session = self(),
- Size = 0,
- socket:setopts(Socket, [{packet, raw}]),
- spawn_opt(fun() -> receive_data([],
- Socket, {0, Envelope#envelope.expectedsize div 2}, Size, MaxSize, Session, Options) end,
- [link, {fullsweep_after, 0}]),
- {noreply, NewState, ?TIMEOUT};
- {ok, NewState} ->
- socket:active_once(NewState#state.socket),
- {noreply, NewState, ?TIMEOUT};
- {stop, Reason, NewState} ->
- {stop, Reason, NewState}
- end;
- handle_info({tcp_closed, _Socket}, State) ->
- {stop, normal, State};
- handle_info({ssl_closed, _Socket}, State) ->
- {stop, normal, State};
- handle_info(timeout, #state{socket = Socket} = State) ->
- socket:send(Socket, "421 Error: timeout exceeded\r\n"),
- socket:close(Socket),
- {stop, normal, State};
- handle_info(Info, State) ->
- io:format("unhandled info message ~p~n", [Info]),
- {noreply, State}.
- %% @hidden
- -spec(terminate/2 :: (Reason :: any(), State :: #state{}) -> 'ok').
- terminate(Reason, State) ->
- socket:close(State#state.socket),
- (State#state.module):terminate(Reason, State#state.callbackstate).
- %% @hidden
- code_change(OldVsn, #state{module = Module} = State, Extra) ->
- % TODO - this should probably be the callback module's version or its checksum
- CallbackState =
- case catch Module:code_change(OldVsn, State#state.callbackstate, Extra) of
- {ok, NewCallbackState} -> NewCallbackState;
- _ -> State#state.callbackstate
- end,
- {ok, State#state{callbackstate = CallbackState}}.
- -spec(parse_request/1 :: (Packet :: binary()) -> {string(), list()}).
- parse_request(Packet) ->
- Request = binstr:strip(binstr:strip(binstr:strip(binstr:strip(Packet, right, $\n), right, $\r), right, $\s), left, $\s),
- case binstr:strchr(Request, $\s) of
- 0 ->
- % io:format("got a ~s request~n", [Request]),
- case binstr:to_upper(Request) of
- <<"QUIT">> = Res -> {Res, <<>>};
- <<"DATA">> = Res -> {Res, <<>>};
- % likely a base64-encoded client reply
- _ -> {Request, <<>>}
- end;
- Index ->
- Verb = binstr:substr(Request, 1, Index - 1),
- Parameters = binstr:strip(binstr:substr(Request, Index + 1), left, $\s),
- %io:format("got a ~s request with parameters ~s~n", [Verb, Parameters]),
- {binstr:to_upper(Verb), Parameters}
- end.
- -spec(handle_request/2 :: ({Verb :: string(), Args :: string()}, State :: #state{}) -> {'ok', #state{}} | {'stop', any(), #state{}}).
- handle_request({<<>>, _Any}, #state{socket = Socket} = State) ->
- socket:send(Socket, "500 Error: bad syntax\r\n"),
- {ok, State};
- handle_request({<<"HELO">>, <<>>}, #state{socket = Socket} = State) ->
- socket:send(Socket, "501 Syntax: HELO hostname\r\n"),
- {ok, State};
- handle_request({<<"HELO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState} = State) ->
- case Module:handle_HELO(Hostname, OldCallbackState) of
- {ok, CallbackState} ->
- socket:send(Socket, io_lib:format("250 ~s\r\n", [proplists:get_value(hostname, Options, smtp_util:guess_FQDN())])),
- {ok, State#state{envelope = #envelope{}, callbackstate = CallbackState}};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message ++ "\r\n"),
- {ok, State#state{callbackstate = CallbackState}}
- end;
- handle_request({<<"EHLO">>, <<>>}, #state{socket = Socket} = State) ->
- socket:send(Socket, "501 Syntax: EHLO hostname\r\n"),
- {ok, State};
- handle_request({<<"EHLO">>, Hostname}, #state{socket = Socket, options = Options, module = Module, callbackstate = OldCallbackState, tls = Tls} = State) ->
- case Module:handle_EHLO(Hostname, ?BUILTIN_EXTENSIONS, OldCallbackState) of
- {ok, Extensions, CallbackState} ->
- case Extensions of
- [] ->
- socket:send(Socket, io_lib:format("250 ~s\r\n", [proplists:get_value(hostname, Options, smtp_util:guess_FQDN())])),
- {ok, State#state{extensions = Extensions, callbackstate = CallbackState}};
- _Else ->
- F =
- fun({E, true}, {Pos, Len, Acc}) when Pos =:= Len ->
- {Pos, Len, string:concat(string:concat(string:concat(Acc, "250 "), E), "\r\n")};
- ({E, Value}, {Pos, Len, Acc}) when Pos =:= Len ->
- {Pos, Len, string:concat(Acc, io_lib:format("250 ~s ~s\r\n", [E, Value]))};
- ({E, true}, {Pos, Len, Acc}) ->
- {Pos+1, Len, string:concat(string:concat(string:concat(Acc, "250-"), E), "\r\n")};
- ({E, Value}, {Pos, Len, Acc}) ->
- {Pos+1, Len, string:concat(Acc, io_lib:format("250-~s ~s\r\n", [E, Value]))}
- end,
- Extensions2 = case Tls of
- true ->
- Extensions -- [{"STARTTLS", true}];
- false ->
- Extensions
- end,
- {_, _, Response} = lists:foldl(F, {1, length(Extensions2), string:concat(string:concat("250-", proplists:get_value(hostname, Options, smtp_util:guess_FQDN())), "\r\n")}, Extensions2),
- socket:send(Socket, Response),
- {ok, State#state{extensions = Extensions2, envelope = #envelope{}, callbackstate = CallbackState}}
- end;
- {error, Message, CallbackState} ->
- socket:send(Socket, Message++"\r\n"),
- {ok, State#state{callbackstate = CallbackState}}
- end;
- handle_request({<<"AUTH">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
- socket:send(Socket, "503 Error: send EHLO first\r\n"),
- {ok, State};
- handle_request({<<"AUTH">>, Args}, #state{socket = Socket, extensions = Extensions, envelope = Envelope, options = Options} = State) ->
- case binstr:strchr(Args, $\s) of
- 0 ->
- AuthType = Args,
- Parameters = false;
- Index ->
- AuthType = binstr:substr(Args, 1, Index - 1),
- Parameters = binstr:strip(binstr:substr(Args, Index + 1), left, $\s)
- end,
- case has_extension(Extensions, "AUTH") of
- false ->
- socket:send(Socket, "502 Error: AUTH not implemented\r\n"),
- {ok, State};
- {true, AvailableTypes} ->
- case lists:member(string:to_upper(binary_to_list(AuthType)), string:tokens(AvailableTypes, " ")) of
- false ->
- socket:send(Socket, "504 Unrecognized authentication type\r\n"),
- {ok, State};
- true ->
- case binstr:to_upper(AuthType) of
- <<"LOGIN">> ->
- % socket:send(Socket, "334 " ++ base64:encode_to_string("Username:")),
- socket:send(Socket, "334 VXNlcm5hbWU6\r\n"),
- {ok, State#state{waitingauth = 'login', envelope = Envelope#envelope{auth = {[], []}}}};
- <<"PLAIN">> when Parameters =/= false ->
- % TODO - duplicated below in handle_request waitingauth PLAIN
- case string:tokens(base64:decode_to_string(Parameters), [0]) of
- [_Identity, Username, Password] ->
- try_auth('plain', Username, Password, State);
- [Username, Password] ->
- try_auth('plain', Username, Password, State);
- _ ->
- % TODO error
- {ok, State}
- end;
- <<"PLAIN">> ->
- socket:send(Socket, "334\r\n"),
- {ok, State#state{waitingauth = 'plain', envelope = Envelope#envelope{auth = {[], []}}}};
- <<"CRAM-MD5">> ->
- crypto:start(), % ensure crypto is started, we're gonna need it
- String = smtp_util:get_cram_string(proplists:get_value(hostname, Options, smtp_util:guess_FQDN())),
- socket:send(Socket, "334 "++String++"\r\n"),
- {ok, State#state{waitingauth = 'cram-md5', authdata=base64:decode_to_string(String), envelope = Envelope#envelope{auth = {[], []}}}}
- %"DIGEST-MD5" -> % TODO finish this? (see rfc 2831)
- %crypto:start(), % ensure crypto is started, we're gonna need it
- %Nonce = get_digest_nonce(),
- %Response = io_lib:format("nonce=\"~s\",realm=\"~s\",qop=\"auth\",algorithm=md5-sess,charset=utf-8", Nonce, State#state.hostname),
- %socket:send(Socket, "334 "++Response++"\r\n"),
- %{ok, State#state{waitingauth = "DIGEST-MD5", authdata=base64:decode_to_string(Nonce), envelope = Envelope#envelope{auth = {[], []}}}}
- end
- end
- end;
- % the client sends a response to auth-cram-md5
- handle_request({Username64, <<>>}, #state{waitingauth = 'cram-md5', envelope = #envelope{auth = {[],[]}}, authdata = AuthData} = State) ->
- case string:tokens(base64:decode_to_string(Username64), " ") of
- [Username, Digest] ->
- try_auth('cram-md5', Username, {Digest, AuthData}, State#state{authdata=undefined});
- _ ->
- % TODO error
- {ok, State#state{waitingauth=false, authdata=undefined}}
- end;
- % the client sends a \0username\0password response to auth-plain
- handle_request({Username64, <<>>}, #state{waitingauth = 'plain', envelope = #envelope{auth = {[],[]}}} = State) ->
- case string:tokens(base64:decode_to_string(Username64), [0]) of
- [_Identity, Username, Password] ->
- try_auth('plain', Username, Password, State);
- [Username, Password] ->
- try_auth('plain', Username, Password, State);
- _ ->
- % TODO error
- {ok, State#state{waitingauth=false}}
- end;
- % the client sends a username response to auth-login
- handle_request({Username64, <<>>}, #state{socket = Socket, waitingauth = 'login', envelope = #envelope{auth = {[],[]}}} = State) ->
- Envelope = State#state.envelope,
- Username = base64:decode_to_string(Username64),
- % socket:send(Socket, "334 " ++ base64:encode_to_string("Password:")),
- socket:send(Socket, "334 UGFzc3dvcmQ6\r\n"),
- % store the provided username in envelope.auth
- NewState = State#state{envelope = Envelope#envelope{auth = {Username, []}}},
- {ok, NewState};
- % the client sends a password response to auth-login
- handle_request({Password64, <<>>}, #state{waitingauth = 'login', envelope = #envelope{auth = {Username,[]}}} = State) ->
- Password = base64:decode_to_string(Password64),
- try_auth('login', Username, Password, State);
- handle_request({<<"MAIL">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
- socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
- {ok, State};
- handle_request({<<"MAIL">>, Args}, #state{socket = Socket, module = Module, envelope = Envelope, callbackstate = OldCallbackState, extensions = Extensions} = State) ->
- case Envelope#envelope.from of
- undefined ->
- case binstr:strpos(binstr:to_upper(Args), "FROM:") of
- 1 ->
- Address = binstr:strip(binstr:substr(Args, 6), left, $\s),
- case parse_encoded_address(binary_to_list(Address)) of
- error ->
- socket:send(Socket, "501 Bad sender address syntax\r\n"),
- {ok, State};
- {ParsedAddress, []} ->
- %io:format("From address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
- case Module:handle_MAIL(ParsedAddress, OldCallbackState) of
- {ok, CallbackState} ->
- socket:send(Socket, "250 sender Ok\r\n"),
- {ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message ++ "\r\n"),
- {ok, State#state{callbackstate = CallbackState}}
- end;
- {ParsedAddress, ExtraInfo} ->
- %io:format("From address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
- Options = [binstr:to_upper(X) || X <- binstr:split(list_to_binary(ExtraInfo), <<" ">>)],
- %io:format("options are ~p~n", [Options]),
- F = fun(_, {error, Message}) ->
- {error, Message};
- (<<"SIZE=", Size/binary>>, InnerState) ->
- case has_extension(Extensions, "SIZE") of
- {true, Value} ->
- case list_to_integer(binary_to_list(Size)) > list_to_integer(Value) of
- true ->
- {error, io_lib:format("552 Estimated message length ~s exceeds limit of ~s\r\n", [Size, Value])};
- false ->
- InnerState#state{envelope = Envelope#envelope{expectedsize = list_to_integer(binary_to_list(Size))}}
- end;
- false ->
- {error, "555 Unsupported option SIZE\r\n"}
- end;
- (<<"BODY=", _BodyType/binary>>, InnerState) ->
- case has_extension(Extensions, "8BITMIME") of
- {true, _} ->
- InnerState;
- false ->
- {error, "555 Unsupported option BODY\r\n"}
- end;
- (X, InnerState) ->
- case Module:handle_MAIL_extension(X, OldCallbackState) of
- {ok, CallbackState} ->
- InnerState#state{callbackstate = CallbackState};
- error ->
- {error, io_lib:format("555 Unsupported option: ~s\r\n", [ExtraInfo])}
- end
- end,
- case lists:foldl(F, State, Options) of
- {error, Message} ->
- %io:format("error: ~s~n", [Message]),
- socket:send(Socket, Message),
- {ok, State};
- NewState ->
- %io:format("OK~n"),
- case Module:handle_MAIL(ParsedAddress, State#state.callbackstate) of
- {ok, CallbackState} ->
- socket:send(Socket, "250 sender Ok\r\n"),
- {ok, State#state{envelope = Envelope#envelope{from = ParsedAddress}, callbackstate = CallbackState}};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message ++ "\r\n"),
- {ok, NewState#state{callbackstate = CallbackState}}
- end
- end
- end;
- _Else ->
- socket:send(Socket, "501 Syntax: MAIL FROM:<address>\r\n"),
- {ok, State}
- end;
- _Other ->
- socket:send(Socket, "503 Error: Nested MAIL command\r\n"),
- {ok, State}
- end;
- handle_request({<<"RCPT">>, _Args}, #state{envelope = undefined, socket = Socket} = State) ->
- socket:send(Socket, "503 Error: need MAIL command\r\n"),
- {ok, State};
- handle_request({<<"RCPT">>, Args}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) ->
- case binstr:strpos(binstr:to_upper(Args), "TO:") of
- 1 ->
- Address = binstr:strip(binstr:substr(Args, 4), left, $\s),
- case parse_encoded_address(binary_to_list(Address)) of
- error ->
- socket:send(Socket, "501 Bad recipient address syntax\r\n"),
- {ok, State};
- {[], _} ->
- % empty rcpt to addresses aren't cool
- socket:send(Socket, "501 Bad recipient address syntax\r\n"),
- {ok, State};
- {ParsedAddress, []} ->
- %io:format("To address ~s (parsed as ~s)~n", [Address, ParsedAddress]),
- case Module:handle_RCPT(ParsedAddress, OldCallbackState) of
- {ok, CallbackState} ->
- socket:send(Socket, "250 recipient Ok\r\n"),
- {ok, State#state{envelope = Envelope#envelope{to = Envelope#envelope.to ++ [ParsedAddress]}, callbackstate = CallbackState}};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message++"\r\n"),
- {ok, State#state{callbackstate = CallbackState}}
- end;
- {ParsedAddress, ExtraInfo} ->
- % TODO - are there even any RCPT extensions?
- io:format("To address ~s (parsed as ~s) with extra info ~s~n", [Address, ParsedAddress, ExtraInfo]),
- socket:send(Socket, io_lib:format("555 Unsupported option: ~s\r\n", [ExtraInfo])),
- {ok, State}
- end;
- _Else ->
- socket:send(Socket, "501 Syntax: RCPT TO:<address>\r\n"),
- {ok, State}
- end;
- handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = undefined} = State) ->
- socket:send(Socket, "503 Error: send HELO/EHLO first\r\n"),
- {ok, State};
- handle_request({<<"DATA">>, <<>>}, #state{socket = Socket, envelope = Envelope} = State) ->
- case {Envelope#envelope.from, Envelope#envelope.to} of
- {undefined, _} ->
- socket:send(Socket, "503 Error: need MAIL command\r\n"),
- {ok, State};
- {_, []} ->
- socket:send(Socket, "503 Error: need RCPT command\r\n"),
- {ok, State};
- _Else ->
- socket:send(Socket, "354 enter mail, end with line containing only '.'\r\n"),
- %io:format("switching to data read mode~n", []),
- {ok, State#state{readmessage = true}}
- end;
- handle_request({<<"RSET">>, _Any}, #state{socket = Socket, envelope = Envelope, module = Module, callbackstate = OldCallbackState} = State) ->
- socket:send(Socket, "250 Ok\r\n"),
- % if the client sends a RSET before a HELO/EHLO don't give them a valid envelope
- NewEnvelope = case Envelope of
- undefined -> undefined;
- _Something -> #envelope{}
- end,
- {ok, State#state{envelope = NewEnvelope, callbackstate = Module:handle_RSET(OldCallbackState)}};
- handle_request({<<"NOOP">>, _Any}, #state{socket = Socket} = State) ->
- socket:send(Socket, "250 Ok\r\n"),
- {ok, State};
- handle_request({<<"QUIT">>, _Any}, #state{socket = Socket} = State) ->
- socket:send(Socket, "221 Bye\r\n"),
- {stop, normal, State};
- handle_request({<<"VRFY">>, Address}, #state{module= Module, socket = Socket, callbackstate = OldCallbackState} = State) ->
- case parse_encoded_address(binary_to_list(Address)) of
- {ParsedAddress, []} ->
- case Module:handle_VRFY(ParsedAddress, OldCallbackState) of
- {ok, Reply, CallbackState} ->
- socket:send(Socket, io_lib:format("250 ~s\r\n", [Reply])),
- {ok, State#state{callbackstate = CallbackState}};
- {error, Message, CallbackState} ->
- socket:send(Socket, Message++"\r\n"),
- {ok, State#state{callbackstate = CallbackState}}
- end;
- _Other ->
- socket:send(Socket, "501 Syntax: VRFY username/address\r\n"),
- {ok, State}
- end;
- handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket, tls=false, extensions = Extensions} = State) ->
- case has_extension(Extensions, "STARTTLS") of
- {true, _} ->
- socket:send(Socket, "220 OK\r\n"),
- crypto:start(),
- application:start(public_key),
- application:start(ssl),
- % TODO: certfile and keyfile should be at configurable locations
- case socket:to_ssl_server(Socket, [], 5000) of
- {ok, NewSocket} ->
- %io:format("SSL negotiation sucessful~n"),
- {ok, State#state{socket = NewSocket, envelope=undefined,
- authdata=undefined, waitingauth=false, readmessage=false,
- tls=true}};
- {error, Reason} ->
- io:format("SSL handshake failed : ~p~n", [Reason]),
- socket:send(Socket, "454 TLS negotiation failed\r\n"),
- {ok, State}
- end;
- false ->
- socket:send(Socket, "500 Command unrecognized\r\n"),
- {ok, State}
- end;
- handle_request({<<"STARTTLS">>, <<>>}, #state{socket = Socket} = State) ->
- socket:send(Socket, "500 TLS already negotiated\r\n"),
- {ok, State};
- handle_request({<<"STARTTLS">>, _Args}, #state{socket = Socket} = State) ->
- socket:send(Socket, "501 Syntax error (no parameters allowed)\r\n"),
- {ok, State};
- handle_request({Verb, Args}, #state{socket = Socket, module = Module, callbackstate = OldCallbackState} = State) ->
- {Message, CallbackState} = Module:handle_other(Verb, Args, OldCallbackState),
- socket:send(Socket, Message++"\r\n"),
- {ok, State#state{callbackstate = CallbackState}}.
- -spec(parse_encoded_address/1 :: (Address :: string()) -> {string(), string()} | 'error').
- parse_encoded_address([]) ->
- error; % empty
- parse_encoded_address("<@" ++ Address) ->
- case string:str(Address, ":") of
- 0 ->
- error; % invalid address
- Index ->
- parse_encoded_address(string:substr(Address, Index + 1), "", {false, true})
- end;
- parse_encoded_address("<" ++ Address) ->
- parse_encoded_address(Address, "", {false, true});
- parse_encoded_address(" " ++ Address) ->
- parse_encoded_address(Address);
- parse_encoded_address(Address) ->
- parse_encoded_address(Address, "", {false, false}).
- -spec(parse_encoded_address/3 :: (Address :: string(), Acc :: string(), Flags :: {boolean(), boolean()}) -> {string(), string()}).
- parse_encoded_address([], Acc, {_Quotes, false}) ->
- {lists:reverse(Acc), []};
- parse_encoded_address([], _Acc, {_Quotes, true}) ->
- error; % began with angle brackets but didn't end with them
- parse_encoded_address(_, Acc, _) when length(Acc) > 129 ->
- error; % too long
- parse_encoded_address("\\" ++ Tail, Acc, Flags) ->
- [H | NewTail] = Tail,
- parse_encoded_address(NewTail, [H | Acc], Flags);
- parse_encoded_address("\"" ++ Tail, Acc, {false, AB}) ->
- parse_encoded_address(Tail, Acc, {true, AB});
- parse_encoded_address("\"" ++ Tail, Acc, {true, AB}) ->
- parse_encoded_address(Tail, Acc, {false, AB});
- parse_encoded_address(">" ++ Tail, Acc, {false, true}) ->
- {lists:reverse(Acc), string:strip(Tail, left, $\s)};
- parse_encoded_address(">" ++ _Tail, _Acc, {false, false}) ->
- error; % ended with angle brackets but didn't begin with them
- parse_encoded_address(" " ++ Tail, Acc, {false, false}) ->
- {lists:reverse(Acc), string:strip(Tail, left, $\s)};
- parse_encoded_address(" " ++ _Tail, _Acc, {false, true}) ->
- error; % began with angle brackets but didn't end with them
- parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $0, H =< $9 ->
- parse_encoded_address(Tail, [H | Acc], {false, AB}); % digits
- parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $@, H =< $Z ->
- parse_encoded_address(Tail, [H | Acc], {false, AB}); % @ symbol and uppercase letters
- parse_encoded_address([H | Tail], Acc, {false, AB}) when H >= $a, H =< $z ->
- parse_encoded_address(Tail, [H | Acc], {false, AB}); % lowercase letters
- parse_encoded_address([H | Tail], Acc, {false, AB}) when H =:= $-; H =:= $.; H =:= $_ ->
- parse_encoded_address(Tail, [H | Acc], {false, AB}); % dash, dot, underscore
- parse_encoded_address([_H | _Tail], _Acc, {false, _AB}) ->
- error;
- parse_encoded_address([H | Tail], Acc, Quotes) ->
- parse_encoded_address(Tail, [H | Acc], Quotes).
- -spec(has_extension/2 :: (Extensions :: [{string(), string()}], Extension :: string()) -> {'true', string()} | 'false').
- has_extension(Exts, Ext) ->
- Extension = string:to_upper(Ext),
- Extensions = [{string:to_upper(X), Y} || {X, Y} <- Exts],
- %io:format("extensions ~p~n", [Extensions]),
- case proplists:get_value(Extension, Extensions) of
- undefined ->
- false;
- Value ->
- {true, Value}
- end.
- -spec(try_auth/4 :: (AuthType :: 'login' | 'plain' | 'cram-md5', Username :: string(), Credential :: string() | {string(), string()}, State :: #state{}) -> {'ok', #state{}}).
- try_auth(AuthType, Username, Credential, #state{module = Module, socket = Socket, envelope = Envelope, callbackstate = OldCallbackState} = State) ->
- % clear out waiting auth
- NewState = State#state{waitingauth = false, envelope = Envelope#envelope{auth = {[], []}}},
- case erlang:function_exported(Module, handle_AUTH, 4) of
- true ->
- case Module:handle_AUTH(AuthType, Username, Credential, OldCallbackState) of
- {ok, CallbackState} ->
- socket:send(Socket, "235 Authentication successful.\r\n"),
- {ok, NewState#state{callbackstate = CallbackState,
- envelope = Envelope#envelope{auth = {Username, Credential}}}};
- _Other ->
- socket:send(Socket, "535 Authentication failed.\r\n"),
- {ok, NewState}
- end;
- false ->
- io:format("Please define handle_AUTH/4 in your server module or remove AUTH from your module extensions~n"),
- socket:send(Socket, "535 authentication failed (#5.7.1)\r\n"),
- {ok, NewState}
- end.
- %get_digest_nonce() ->
- %A = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))],
- %B = [io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(integer_to_list(crypto:rand_uniform(0, 4294967295)))],
- %binary_to_list(base64:encode(lists:flatten(A ++ B))).
- %% @doc a tight loop to receive the message body
- receive_data(_Acc, _Socket, _, Size, MaxSize, Session, _Options) when Size > MaxSize ->
- io:format("message body size ~B exceeded maximum allowed ~B~n", [Size, MaxSize]),
- Session ! {receive_data, {error, size_exceeded}};
- receive_data(Acc, Socket, {OldCount, OldRecvSize}, Size, MaxSize, Session, Options) ->
- {Count, RecvSize} = case Size of
- Size when OldCount > 2, OldRecvSize =:= 262144 ->
- %io:format("increasing receive size to ~B~n", [1048576]),
- {0, 1048576};% 1m
- Size when OldCount > 5, OldRecvSize =:= 65536 ->
- %io:format("increasing receive size to ~B~n", [262144]),
- {0, 262144};% 256k
- Size when OldCount > 5, OldRecvSize =:= 8192 ->
- %io:format("increasing receive size to ~B~n", [65536]),
- {0, 65536};% 64k
- Size when OldCount > 2, Size > 8192, OldRecvSize =:= 0 ->
- %io:format("increasing receive size to ~B~n", [8192]),
- {0, 8192}; % 8k
- _ ->
- {OldCount + 1, OldRecvSize} % don't change anything
- end,
- %socket:setopts(Socket, [{packet, raw}]),
- case socket:recv(Socket, RecvSize, 1000) of
- {ok, Packet} when Acc == [] ->
- case check_bare_crlf(Packet, <<>>, proplists:get_value(allow_bare_newlines, Options, false), 0) of
- error ->
- Session ! {receive_data, {error, bare_newline}};
- FixedPacket ->
- case binstr:strpos(FixedPacket, "\r\n.\r\n") of
- 0 ->
- %io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
- %io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
- receive_data([FixedPacket | Acc], Socket, {Count, RecvSize}, Size + byte_size(FixedPacket), MaxSize, Session, Options);
- Index ->
- String = binstr:substr(FixedPacket, 1, Index - 1),
- Rest = binstr:substr(FixedPacket, Index+5),
- %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Result = list_to_binary(lists:reverse([String | Acc])),
- %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Session ! {receive_data, Result, Rest}
- end
- end;
- {ok, Packet} ->
- [Last | _] = Acc,
- case check_bare_crlf(Packet, Last, proplists:get_value(allow_bare_newlines, Options, false), 0) of
- error ->
- Session ! {receive_data, {error, bare_newline}};
- FixedPacket ->
- case binstr:strpos(FixedPacket, "\r\n.\r\n") of
- 0 ->
- %io:format("received ~B bytes; size is now ~p~n", [RecvSize, Size + size(Packet)]),
- %io:format("memory usage: ~p~n", [erlang:process_info(self(), memory)]),
- receive_data([FixedPacket | Acc], Socket, {Count, RecvSize}, Size + byte_size(FixedPacket), MaxSize, Session, Options);
- Index ->
- String = binstr:substr(FixedPacket, 1, Index - 1),
- Rest = binstr:substr(FixedPacket, Index+5),
- %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Result = list_to_binary(lists:reverse([String | Acc])),
- %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Session ! {receive_data, Result, Rest}
- end
- end;
- {error, timeout} when RecvSize =:= 0, length(Acc) > 1 ->
- % check that we didn't accidentally receive a \r\n.\r\n split across 2 receives
- [A, B | Acc2] = Acc,
- Packet = list_to_binary([B, A]),
- case binstr:strpos(Packet, "\r\n.\r\n") of
- 0 ->
- % uh-oh
- %io:format("no data on socket, and no DATA terminator, retrying ~p~n", [Session]),
- % eventually we'll either get data or a different error, just keep retrying
- receive_data(Acc, Socket, {Count - 1, RecvSize}, Size, MaxSize, Session, Options);
- Index ->
- String = binstr:substr(Packet, 1, Index - 1),
- Rest = binstr:substr(Packet, Index+5),
- %io:format("memory usage before flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Result = list_to_binary(lists:reverse([String | Acc2])),
- %io:format("memory usage after flattening: ~p~n", [erlang:process_info(self(), memory)]),
- Session ! {receive_data, Result, Rest}
- end;
- {error, timeout} ->
- NewRecvSize = adjust_receive_size_down(Size, RecvSize),
- %io:format("timeout when trying to read ~B bytes, lowering receive size to ~B~n", [RecvSize, NewRecvSize]),
- receive_data(Acc, Socket, {-5, NewRecvSize}, Size, MaxSize, Session, Options);
- {error, Reason} ->
- io:format("receive error: ~p~n", [Reason]),
- exit(receive_error)
- end.
- adjust_receive_size_down(_Size, RecvSize) when RecvSize > 262144 ->
- 262144;
- adjust_receive_size_down(_Size, RecvSize) when RecvSize > 65536 ->
- 65536;
- adjust_receive_size_down(_Size, RecvSize) when RecvSize > 8192 ->
- 8192;
- adjust_receive_size_down(_Size, _RecvSize) ->
- 0.
- check_for_bare_crlf(Bin, Offset) ->
- case {re:run(Bin, "(?<!\r)\n", [{capture, none}, {offset, Offset}]), re:run(Bin, "\r(?!\n)", [{capture, none}, {offset, Offset}])} of
- {match, _} -> true;
- {_, match} -> true;
- _ -> false
- end.
- fix_bare_crlf(Bin, Offset) ->
- Options = [{offset, Offset}, {return, binary}, global],
- re:replace(re:replace(Bin, "(?<!\r)\n", "\r\n", Options), "\r(?!\n)", "\r\n", Options).
- strip_bare_crlf(Bin, Offset) ->
- Options = [{offset, Offset}, {return, binary}, global],
- re:replace(re:replace(Bin, "(?<!\r)\n", "", Options), "\r(?!\n)", "", Options).
- check_bare_crlf(Binary, _, ignore, _) ->
- Binary;
- check_bare_crlf(<<$\n, _Rest/binary>> = Bin, Prev, Op, Offset) when byte_size(Prev) > 0, Offset == 0 ->
- % check if last character of previous was a CR
- Lastchar = binstr:substr(Prev, -1),
- case Lastchar of
- <<"\r">> ->
- % okay, check again for the rest
- check_bare_crlf(Bin, <<>>, Op, 1);
- _ when Op == false -> % not fixing or ignoring them
- error;
- _ ->
- % no dice
- check_bare_crlf(Bin, <<>>, Op, 0)
- end;
- check_bare_crlf(Binary, _Prev, Op, Offset) ->
- Last = binstr:substr(Binary, -1),
- % is the last character a CR?
- case Last of
- <<"\r">> ->
- % okay, the last character is a CR, we have to assume the next packet contains the corresponding LF
- NewBin = binstr:substr(Binary, 1, byte_size(Binary) -1),
- case check_for_bare_crlf(NewBin, Offset) of
- true when Op == fix ->
- list_to_binary([fix_bare_crlf(NewBin, Offset), "\r"]);
- true when Op == strip ->
- list_to_binary([strip_bare_crlf(NewBin, Offset), "\r"]);
- true ->
- error;
- false ->
- Binary
- end;
- _ ->
- case check_for_bare_crlf(Binary, Offset) of
- true when Op == fix ->
- fix_bare_crlf(Binary, Offset);
- true when Op == strip ->
- strip_bare_crlf(Binary, Offset);
- true ->
- error;
- false ->
- Binary
- end
- end.
- -ifdef(TEST).
- parse_encoded_address_test_() ->
- [
- {"Valid addresses should parse",
- fun() ->
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<God@heaven.af.mil>")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<\\God@heaven.af.mil>")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<\"God\"@heaven.af.mil>")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("<@gateway.af.mil,@uucp.local:\"\\G\\o\\d\"@heaven.af.mil>")),
- ?assertEqual({"God2@heaven.af.mil", []}, parse_encoded_address("<God2@heaven.af.mil>"))
- end
- },
- {"Addresses that are sorta valid should parse",
- fun() ->
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("God@heaven.af.mil")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address("God@heaven.af.mil ")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address(" God@heaven.af.mil ")),
- ?assertEqual({"God@heaven.af.mil", []}, parse_encoded_address(" <God@heaven.af.mil> "))
- end
- },
- {"Addresses containing unescaped <> that aren't at start/end should fail",
- fun() ->
- ?assertEqual(error, parse_encoded_address("<<")),
- ?assertEqual(error, parse_encoded_address("<God<@heaven.af.mil>"))
- end
- },
- {"Address that begins with < but doesn't end with a > should fail",
- fun() ->
- ?assertEqual(error, parse_encoded_address("<God@heaven.af.mil")),
- ?assertEqual(error, parse_encoded_address("<God@heaven.af.mil "))
- end
- },
- {"Address that begins without < but ends with a > should fail",
- fun() ->
- ?assertEqual(error, parse_encoded_address("God@heaven.af.mil>"))
- end
- },
- {"Address longer than 129 character should fail",
- fun() ->
- MegaAddress = lists:seq(97, 122) ++ lists:seq(97, 122) ++ lists:seq(97, 122) ++ "@" ++ lists:seq(97, 122) ++ lists:seq(97, 122),
- ?assertEqual(error, parse_encoded_address(MegaAddress))
- end
- },
- {"Address with an invalid route should fail",
- fun() ->
- ?assertEqual(error, parse_encoded_address("<@gateway.af.mil God@heaven.af.mil>"))
- end
- },
- {"Empty addresses should parse OK",
- fun() ->
- ?assertEqual({[], []}, parse_encoded_address("<>")),
- ?assertEqual({[], []}, parse_encoded_address(" <> "))
- end
- },
- {"Completely empty addresses are an error",
- fun() ->
- ?assertEqual(error, parse_encoded_address("")),
- ?assertEqual(error, parse_encoded_address(" "))
- end
- },
- {"addresses with trailing parameters should return the trailing parameters",
- fun() ->
- ?assertEqual({"God@heaven.af.mil", "SIZE=100 BODY=8BITMIME"}, parse_encoded_address("<God@heaven.af.mil> SIZE=100 BODY=8BITMIME"))
- end
- }
- ].
- parse_request_test_() ->
- [
- {"Parsing normal SMTP requests",
- fun() ->
- ?assertEqual({<<"HELO">>, <<>>}, parse_request(<<"HELO\r\n">>)),
- ?assertEqual({<<"EHLO">>, <<"hell.af.mil">>}, parse_request(<<"EHLO hell.af.mil\r\n">>)),
- ?assertEqual({<<"MAIL">>, <<"FROM:God@heaven.af.mil">>}, parse_request(<<"MAIL FROM:God@heaven.af.mil">>))
- end
- },
- {"Verbs should be uppercased",
- fun() ->
- ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<"helo hell.af.mil">>))
- end
- },
- {"Leading and trailing spaces are removed",
- fun() ->
- ?assertEqual({<<"HELO">>, <<"hell.af.mil">>}, parse_request(<<" helo hell.af.mil ">>))
- end
- },
- {"Blank lines are blank",
- fun() ->
- ?assertEqual({<<>>, <<>>}, parse_request(<<"">>))
- end
- }
- ].
- smtp_session_test_() ->
- {foreach,
- local,
- fun() ->
- Self = self(),
- spawn(fun() ->
- {ok, ListenSock} = socket:listen(tcp, 9876, [binary]),
- {ok, X} = socket:accept(ListenSock),
- socket:controlling_process(X, Self),
- Self ! X
- end),
- {ok, CSock} = socket:connect(tcp, "localhost", 9876),
- receive
- SSock when is_port(SSock) ->
- ?debugFmt("Got server side of the socket ~p, client is ~p~n", [SSock, CSock])
- end,
- {ok, Pid} = gen_smtp_server_session:start(SSock, smtp_server_example, [{hostname, "localhost"}, {sessioncount, 1}]),
- socket:controlling_process(SSock, Pid),
- {CSock, Pid}
- end,
- fun({CSock, _Pid}) ->
- socket:close(CSock)
- end,
- [fun({CSock, _Pid}) ->
- {"A new connection should get a banner",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> ok end,
- ?assertMatch("220 localhost"++_Stuff, Packet)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"A correct response to HELO",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "HELO somehost.com\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?debugFmt("~nHere 5", []),
- ?assertMatch("250 localhost\r\n", Packet2)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"An error in response to an invalid HELO",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "HELO\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("501 Syntax: HELO hostname\r\n", Packet2)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"A rejected HELO",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "HELO invalid\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("554 invalid hostname\r\n", Packet2)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"A rejected EHLO",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "EHLO invalid\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("554 invalid hostname\r\n", Packet2)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"EHLO response",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "EHLO somehost.com\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("250-localhost\r\n", Packet2),
- Foo = fun(F) ->
- receive
- {tcp, CSock, "250-"++Packet3} ->
- socket:active_once(CSock),
- F(F);
- {tcp, CSock, "250 "++Packet3} ->
- socket:active_once(CSock),
- ok;
- R ->
- socket:active_once(CSock),
- error
- end
- end,
- ?assertEqual(ok, Foo(Foo))
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"Unsupported AUTH PLAIN",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "EHLO somehost.com\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("250-localhost\r\n", Packet2),
- Foo = fun(F) ->
- receive
- {tcp, CSock, "250-"++Packet3} ->
- socket:active_once(CSock),
- F(F);
- {tcp, CSock, "250"++Packet3} ->
- socket:active_once(CSock),
- ok;
- R ->
- socket:active_once(CSock),
- error
- end
- end,
- ?assertEqual(ok, Foo(Foo)),
- socket:send(CSock, "AUTH PLAIN\r\n"),
- receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
- ?assertMatch("502 Error: AUTH not implemented\r\n", Packet4)
- end
- }
- end,
- fun({CSock, _Pid}) ->
- {"Sending DATA",
- fun() ->
- socket:active_once(CSock),
- receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- ?assertMatch("220 localhost"++_Stuff, Packet),
- socket:send(CSock, "HELO somehost.com\r\n"),
- receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- ?assertMatch("250 localhost\r\n", Packet2),
- socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
- receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
- ?assertMatch("250 "++_, Packet3),
- socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
- receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
- ?assertMatch("250 "++_, Packet4),
- socket:send(CSock, "DATA\r\n"),
- receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
- ?assertMatch("354 "++_, Packet5),
- socket:send(CSock, "Subject: tls message\r\n"),
- socket:send(CSock, "To: <user@otherhost>\r\n"),
- socket:send(CSock, "From: <user@somehost.com>\r\n"),
- socket:send(CSock, "\r\n"),
- socket:send(CSock, "message body"),
- socket:send(CSock, "\r\n.\r\n"),
- receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
- ?assertMatch("250 queued as"++_, Packet6),
- ?debugFmt("Message send, received: ~p~n", [Packet6])
- end
- }
- end,
- % fun({CSock, _Pid}) ->
- % {"Sending DATA with a bare newline",
- % fun() ->
- % socket:active_once(CSock),
- % receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- % ?assertMatch("220 localhost"++_Stuff, Packet),
- % socket:send(CSock, "HELO somehost.com\r\n"),
- % receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- % ?assertMatch("250 localhost\r\n", Packet2),
- % socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
- % receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
- % ?assertMatch("250 "++_, Packet3),
- % socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
- % receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
- % ?assertMatch("250 "++_, Packet4),
- % socket:send(CSock, "DATA\r\n"),
- % receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
- % ?assertMatch("354 "++_, Packet5),
- % socket:send(CSock, "Subject: tls message\r\n"),
- % socket:send(CSock, "To: <user@otherhost>\r\n"),
- % socket:send(CSock, "From: <user@somehost.com>\r\n"),
- % socket:send(CSock, "\r\n"),
- % socket:send(CSock, "this\r\n"),
- % socket:send(CSock, "body\r\n"),
- % socket:send(CSock, "has\r\n"),
- % socket:send(CSock, "a\r\n"),
- % socket:send(CSock, "bare\n"),
- % socket:send(CSock, "newline\r\n"),
- % socket:send(CSock, "\r\n.\r\n"),
- % receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
- % ?assertMatch("451 "++_, Packet6),
- % ?debugFmt("Message send, received: ~p~n", [Packet6])
- % end
- % }
- % end,
- %fun({CSock, _Pid}) ->
- % {"Sending DATA with a bare CR",
- % fun() ->
- % socket:active_once(CSock),
- % receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- % ?assertMatch("220 localhost"++_Stuff, Packet),
- % socket:send(CSock, "HELO somehost.com\r\n"),
- % receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- % ?assertMatch("250 localhost\r\n", Packet2),
- % socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
- % receive {tcp, CSock, Packet3} -> socket:active_once(CSock) end,
- % ?assertMatch("250 "++_, Packet3),
- % socket:send(CSock, "RCPT TO: <user@otherhost.com>\r\n"),
- % receive {tcp, CSock, Packet4} -> socket:active_once(CSock) end,
- % ?assertMatch("250 "++_, Packet4),
- % socket:send(CSock, "DATA\r\n"),
- % receive {tcp, CSock, Packet5} -> socket:active_once(CSock) end,
- % ?assertMatch("354 "++_, Packet5),
- % socket:send(CSock, "Subject: tls message\r\n"),
- % socket:send(CSock, "To: <user@otherhost>\r\n"),
- % socket:send(CSock, "From: <user@somehost.com>\r\n"),
- % socket:send(CSock, "\r\n"),
- % socket:send(CSock, "this\r\n"),
- % socket:send(CSock, "\rbody\r\n"),
- % socket:send(CSock, "has\r\n"),
- % socket:send(CSock, "a\r\n"),
- % socket:send(CSock, "bare\r"),
- % socket:send(CSock, "CR\r\n"),
- % socket:send(CSock, "\r\n.\r\n"),
- % receive {tcp, CSock, Packet6} -> socket:active_once(CSock) end,
- % ?assertMatch("451 "++_, Packet6),
- % ?debugFmt("Message send, received: ~p~n", [Packet6])
- % end
- % }
- % end,
- % fun({CSock, _Pid}) ->
- % {"Sending DATA with a bare newline in the headers",
- % fun() ->
- % socket:active_once(CSock),
- % receive {tcp, CSock, Packet} -> socket:active_once(CSock) end,
- % ?assertMatch("220 localhost"++_Stuff, Packet),
- % socket:send(CSock, "HELO somehost.com\r\n"),
- % receive {tcp, CSock, Packet2} -> socket:active_once(CSock) end,
- % ?assertMatch("250 localhost\r\n", Packet2),
- % socket:send(CSock, "MAIL FROM: <user@somehost.com>\r\n"),
- % receive {tcp, CSo…
Large files files are truncated, but you can click here to view the full file