PageRenderTime 33ms CodeModel.GetById 10ms app.highlight 19ms RepoModel.GetById 1ms app.codeStats 0ms

/deps/gen_smtp/src/gen_smtp_server.erl

https://code.google.com/p/zotonic/
Erlang | 228 lines | 140 code | 25 blank | 63 comment | 2 complexity | c588f2f4100d5c778b923f4cbc200ebb MD5 | raw file
  1%%% Copyright 2009 Andrew Thompson <andrew@hijacked.us>. All rights reserved.
  2%%%
  3%%% Redistribution and use in source and binary forms, with or without
  4%%% modification, are permitted provided that the following conditions are met:
  5%%%
  6%%%   1. Redistributions of source code must retain the above copyright notice,
  7%%%      this list of conditions and the following disclaimer.
  8%%%   2. Redistributions in binary form must reproduce the above copyright
  9%%%      notice, this list of conditions and the following disclaimer in the
 10%%%      documentation and/or other materials provided with the distribution.
 11%%%
 12%%% THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR
 13%%% IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 14%%% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 15%%% EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 16%%% INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 17%%% (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 18%%% LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 19%%% ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 20%%% (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 21%%% SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 22
 23%% @doc A non-blocking tcp listener for SMTP connections. Based on the tcp_listener module by Serge
 24%% Aleynikov [http://www.trapexit.org/Building_a_Non-blocking_TCP_server_using_OTP_principles]
 25
 26-module(gen_smtp_server).
 27-behaviour(gen_server).
 28
 29-define(PORT, 2525).
 30
 31%% External API
 32-export([start_link/3, start_link/2, start_link/1,
 33    start/3, start/2, start/1,
 34    stop/1, sessions/1]).
 35
 36%% gen_server callbacks
 37-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
 38		code_change/3]).
 39
 40
 41-record(listener, {
 42		hostname :: list(),
 43		port :: port(),
 44		sessionoptions = [] :: [tuple()],
 45		socket :: port() | any(),
 46		listenoptions = [] :: [tuple()]
 47		}).
 48-type(listener() :: #listener{}).
 49
 50-record(state, {
 51		listeners :: [listener()],  % Listening sockets (tcp or ssl)
 52		module :: atom(),
 53		sessions = [] :: [pid()]
 54		}).
 55
 56-type(options() :: [{'domain', string()} | {'address', {pos_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}} |
 57		{'port', pos_integer()} | {'protocol', 'tcp' | 'ssl'} | {'sessionoptions', [any()]}]).
 58
 59%% @doc Start the listener as a registered process with callback module `Module' on with options `Options' linked to the calling process.
 60-spec(start_link/3 :: (ServerName :: {'local', atom()} | {'global', any()}, Module :: atom(), Options :: [options()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 61start_link(ServerName, Module, Options) when is_list(Options) ->
 62	gen_server:start_link(ServerName, ?MODULE, [Module, Options], []).
 63
 64%% @doc Start the listener with callback module `Module' on with options `Options' linked to the calling process.
 65-spec(start_link/2 :: (Module :: atom(), Options :: [options()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 66start_link(Module, Options) when is_list(Options) ->
 67	gen_server:start_link(?MODULE, [Module, Options], []).
 68
 69%% @doc Start the listener with callback module `Module' with default options linked to the calling process.
 70-spec(start_link/1 :: (Module :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 71start_link(Module) ->
 72	start_link(Module, [[]]).
 73
 74%% @doc Start the listener as a registered process with callback module `Module' with options `Options' linked to no process.
 75-spec(start/3 :: (ServerName :: {'local', atom()} | {'global', any()}, Module :: atom(), Options :: [options()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 76start(ServerName, Module, Options) when is_list(Options) ->
 77	gen_server:start(ServerName, ?MODULE, [Module, Options], []).
 78
 79%% @doc Start the listener with callback module `Module' with options `Options' linked to no process.
 80-spec(start/2 :: (Module :: atom(), Options :: [options()]) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 81start(Module, Options) when is_list(Options) ->
 82	gen_server:start(?MODULE, [Module, Options], []).
 83
 84%% @doc Start the listener with callback module `Module' with default options linked to no process.
 85-spec(start/1 :: (Module :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}).
 86start(Module) ->
 87	start(Module, [[]]).
 88
 89%% @doc Stop the listener pid() `Pid' with reason `normal'.
 90-spec(stop/1 :: (Pid :: pid()) -> 'ok').
 91stop(Pid) ->
 92	gen_server:call(Pid, stop).
 93
 94%% @doc Return the list of active SMTP session pids.
 95-spec sessions(Pid :: pid()) -> [pid()].
 96sessions(Pid) ->
 97	gen_server:call(Pid, sessions).
 98
 99%% @doc
100%% The gen_smtp_server is given a list of tcp listener configurations.
101%% You'll typically only want to listen on one port so your options
102%% will be a single-item list containing a proplist. e.g.:
103%%
104%% <pre>  [[{port,25},{protocol,tcp},{domain,"myserver.com"},{address,,{0,0,0,0}}]]</pre>
105%%
106%% By providing additional configurations the server will listen on multiple
107%% ports over either tcp or ssl on any given port. e.g.:
108%% <pre>
109%%  [
110%%    [{port,25},{protocol,tcp},{domain,"myserver.com"},{address,{0,0,0,0}}],
111%%    [{port,465},{protocol,ssl},{domain,"secure.myserver.com"},{address,{0.0.0.0}}],
112%%    [{port, 25},{family, inet6},{domain,"ipv6.myserver.com"},{address,"::"}]
113%%  ]
114%% </pre>
115%% Note that the default port to listen on is `2525' because the regular SMTP
116%% ports are privileged and only bindable by root. The default protocol is
117%% `tcp', the default listen address is `0.0.0.0' and the default address family
118%% is `inet'. Anything passed in the `sessionoptions' option, is passed through
119%% to `gen_server_smtp_session'.
120%% @see gen_smtp_server_session
121-spec(init/1 :: (Args :: list()) -> {'ok', #state{}} | {'stop', any()}).
122init([Module, Configurations]) ->
123	DefaultConfig = [{domain, smtp_util:guess_FQDN()}, {address, {0,0,0,0}},
124		{port, ?PORT}, {protocol, tcp}, {family, inet}],
125	try
126		case Configurations of
127			[FirstConfig|_] when is_list(FirstConfig) -> ok;
128			_ -> exit({init,"Please start gen_smtp_server with an options argument formatted as a list of proplists"})
129		end,
130		Listeners = [
131			begin
132					NewConfig = lists:ukeymerge(1, lists:sort(Config), lists:sort(DefaultConfig)),
133					Port = proplists:get_value(port, NewConfig),
134					IP = proplists:get_value(address, NewConfig),
135					Family = proplists:get_value(family, NewConfig),
136					Hostname = proplists:get_value(domain, NewConfig),
137					Protocol = proplists:get_value(protocol, NewConfig),
138					SessionOptions = proplists:get_value(sessionoptions, NewConfig, []),
139					error_logger:info_msg("~p starting at ~p~n", [?MODULE, node()]),
140					error_logger:info_msg("listening on ~p:~p via ~p~n", [IP, Port, Protocol]),
141					process_flag(trap_exit, true),
142					ListenOptions = [binary, {ip, IP}, Family],
143					case socket:listen(Protocol, Port, ListenOptions) of
144						{ok, ListenSocket} -> %%Create first accepting process
145							socket:begin_inet_async(ListenSocket),
146							#listener{port = socket:extract_port_from_socket(ListenSocket),
147								hostname = Hostname, sessionoptions = SessionOptions,
148								socket = ListenSocket, listenoptions = ListenOptions};
149						{error, Reason} ->
150							exit({init, Reason})
151					end
152			end || Config <- Configurations],
153		{ok, #state{listeners = Listeners, module = Module}}
154	catch exit:Why ->
155		{stop, Why}
156  end.
157
158%% @hidden
159-spec handle_call(Message :: any(), From :: {pid(), reference()}, State :: #state{}) -> {'stop', 'normal', 'ok', #state{}} | {'reply', any(), #state{}}.
160handle_call(stop, _From, State) ->
161	{stop, normal, ok, State};
162
163handle_call(sessions, _From, State) ->
164	{reply, State#state.sessions, State};
165
166handle_call(Request, _From, State) ->
167	{reply, {unknown_call, Request}, State}.
168
169%% @hidden
170-spec handle_cast(Message :: any(), State :: #state{}) -> {'noreply', #state{}}.
171handle_cast(_Msg, State) ->
172	{noreply, State}.
173
174%% @hidden
175-spec handle_info(Message :: any(), State :: #state{}) -> {'noreply', #state{}} | {'stop', any(), #state{}}.
176handle_info({inet_async, ListenPort,_, {ok, ClientAcceptSocket}},
177	#state{module = Module, listeners = Listeners, sessions = CurSessions} = State) ->
178	try
179		% find this ListenPort in our listeners.
180		[Listener] = lists:flatten([case L#listener.port of
181					ListenPort -> L;
182					_ -> []
183				end || L <- Listeners]),
184		{ok, ClientSocket} = socket:handle_inet_async(Listener#listener.socket, ClientAcceptSocket, Listener#listener.listenoptions),
185		%% New client connected
186		% io:format("new client connection.~n", []),
187		Sessions = case gen_smtp_server_session:start(ClientSocket, Module, [{hostname, Listener#listener.hostname}, {sessioncount, length(CurSessions) + 1} | Listener#listener.sessionoptions]) of
188			{ok, Pid} ->
189				link(Pid),
190				socket:controlling_process(ClientSocket, Pid),
191				CurSessions ++[Pid];
192			_Other ->
193				CurSessions
194		end,
195		{noreply, State#state{sessions = Sessions}}
196	catch _:Error ->
197		error_logger:error_msg("Error in socket acceptor: ~p.~n", [Error]),
198		{noreply, State}
199	end;
200handle_info({'EXIT', From, Reason}, State) ->
201	case lists:member(From, State#state.sessions) of
202		true ->
203			{noreply, State#state{sessions = lists:delete(From, State#state.sessions)}};
204		false ->
205			io:format("process ~p exited with reason ~p~n", [From, Reason]),
206			{noreply, State}
207	end;
208handle_info({inet_async, ListenSocket, _, {error, econnaborted}}, State) ->
209	io:format("Client terminated connection with econnaborted~n"),
210	socket:begin_inet_async(ListenSocket),
211	{noreply, State};
212handle_info({inet_async, _ListenSocket,_, Error}, State) ->
213	error_logger:error_msg("Error in socket acceptor: ~p.~n", [Error]),
214	{stop, Error, State};
215handle_info(_Info, State) ->
216	{noreply, State}.
217
218%% @hidden
219-spec terminate(Reason :: any(), State :: #state{}) -> 'ok'.
220terminate(Reason, State) ->
221	io:format("Terminating due to ~p~n", [Reason]),
222	lists:foreach(fun(#listener{socket=S}) -> catch socket:close(S) end, State#state.listeners),
223	ok.
224
225%% @hidden
226-spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {'ok', #state{}}.
227code_change(_OldVsn, State, _Extra) ->
228	{ok, State}.