PageRenderTime 54ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/src/gen_smtp_server_session.erl

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

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

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

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