src/gen_smtp_server.erl

%%% 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 Setup ranch socket acceptor for gen_smtp_server_session

-module(gen_smtp_server).

-define(PORT, 2525).

-include_lib("kernel/include/logger.hrl").

%% External API
-export([
    start/3, start/2, start/1,
    stop/1,
    child_spec/3,
    sessions/1
]).
-export_type([options/0]).

-type server_name() :: any().
-type options() ::
    [
        {domain, string()}
        | {address, inet:ip4_address()}
        | {family, inet | inet6}
        | {port, inet:port_number()}
        | {protocol, 'tcp' | 'ssl'}
        | {ranch_opts, ranch:opts()}
        | {sessionoptions, gen_smtp_server_session:options()}
    ].

%% @doc Start the listener as a registered process with callback module `Module' with options `Options' linked to no process.
-spec start(
    ServerName :: server_name(),
    CallbackModule :: module(),
    Options :: options()
) -> {'ok', pid()} | {'error', any()}.
start(ServerName, CallbackModule, Options) when is_list(Options) ->
    case convert_options(CallbackModule, Options) of
        {ok, Transport, TransportOpts, ProtocolOpts} ->
            ranch:start_listener(
                ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts
            );
        {error, Reason} ->
            {error, Reason}
    end.

child_spec(ServerName, CallbackModule, Options) ->
    case convert_options(CallbackModule, Options) of
        {ok, Transport, TransportOpts, ProtocolOpts} ->
            ranch:child_spec(
                ServerName, Transport, TransportOpts, gen_smtp_server_session, ProtocolOpts
            );
        {error, Reason} ->
            % `supervisor:child_spec' is not compatible with ok/error tuples.
            % This error is likely to occur when starting the application,
            % so the user can sort out the configuration parameters and try again.
            erlang:error(Reason)
    end.

convert_options(CallbackModule, Options) ->
    Transport =
        case proplists:get_value(protocol, Options, tcp) of
            tcp -> ranch_tcp;
            ssl -> ranch_ssl
        end,
    Family = proplists:get_value(family, Options, inet),
    Address = proplists:get_value(address, Options, {0, 0, 0, 0}),
    Port = proplists:get_value(port, Options, ?PORT),
    Hostname = proplists:get_value(domain, Options, smtp_util:guess_FQDN()),
    ProtocolOpts = proplists:get_value(sessionoptions, Options, []),
    EmailTransferProtocol = proplists:get_value(protocol, ProtocolOpts, smtp),
    case {EmailTransferProtocol, Port} of
        {lmtp, 25} ->
            ?LOG_ERROR("LMTP is different from SMTP, it MUST NOT be used on the TCP port 25", #{
                domain => [gen_smtp, server]
            }),
            % Error defined in section 5 of https://tools.ietf.org/html/rfc2033
            {error, invalid_lmtp_port};
        _ ->
            ProtocolOpts1 = {CallbackModule, [{hostname, Hostname} | ProtocolOpts]},
            RanchOpts = proplists:get_value(ranch_opts, Options, #{}),
            SocketOpts = maps:get(socket_opts, RanchOpts, []),
            TransportOpts = RanchOpts#{
                socket_opts =>
                    [
                        {port, Port},
                        {ip, Address},
                        {keepalive, true},
                        %% binary, {active, false}, {reuseaddr, true} - ranch defaults
                        Family
                        | SocketOpts
                    ]
            },
            {ok, Transport, TransportOpts, ProtocolOpts1}
    end.

%% @doc Start the listener with callback module `Module' with options `Options' linked to no process.
-spec start(CallbackModule :: module(), Options :: options()) ->
    {'ok', pid()} | 'ignore' | {'error', any()}.
start(CallbackModule, Options) when is_list(Options) ->
    start(?MODULE, CallbackModule, Options).

%% @doc Start the listener with callback module `Module' with default options linked to no process.
-spec start(CallbackModule :: atom()) -> {'ok', pid()} | 'ignore' | {'error', any()}.
start(CallbackModule) ->
    start(CallbackModule, []).

%% @doc Stop the listener pid() `Pid' with reason `normal'.
-spec stop(Name :: server_name()) -> 'ok'.
stop(Name) ->
    ranch:stop_listener(Name).

%% @doc Return the list of active SMTP session pids.
-spec sessions(Name :: server_name()) -> [pid()].
sessions(Name) ->
    ranch:procs(Name, connections).