Packages

An elixir wrapper around gen_smtp that makes handler and sender registration easier.

Current section

Files

Jump to
pique lib smtp.ex
Raw

lib/smtp.ex

defmodule Pique.Smtp do
require Logger
@behaviour :gen_smtp_server_session
@doc """
Init callback for new SMTP sessions. Checks if the new initiated session
exceeds the allowed session count. If it does, responds with an error.
Otherwise returns the expected banner message.
"""
@spec init(any, any, any, any) :: {:ok, [...], %{}} | {:stop, :normal, [...]}
def init(hostname, session_count, _address, _options) do
if session_count > Application.get_env(:pique, :session_limit, 40) do
Logger.warn("SMTP server connection limit exceeded")
{:stop, :normal, ["421", hostname, " is too busy to accept mail right now"]}
else
banner = [hostname, " ESMTP"]
state = %{}
{:ok, banner, state}
end
end
@doc """
Handles incoming DATA request. Matches if the message is empty and
returns an error.
"""
@spec handle_DATA(any, any, any, map) :: {:error, charlist(), map}
def handle_DATA(_from, _to, "", state) do
{:error, '552 Message too small', state};
end
@doc """
Handles incoming DATA request and passes it off to the defined DATA
handler. If the DATA handler returns an `{:ok, state}` then
passes the state to the defined send handler. Otherwise returns
relevant error messages.
"""
@spec handle_DATA(any, any, String.t, map) :: {:ok, String.t, any} | {:error, charlist(), map}
def handle_DATA(_from, _to, data, state) do
Logger.info "Received DATA"
state = Map.put(state, :body, data)
case Kernel.apply(
Application.get_env(:pique, :data_handler, Pique.Handlers.DATA),
:handle,
[state]) do
{:ok, state} ->
Kernel.apply(
Application.get_env(:pique, :sender, Pique.Senders.Logger),
:send,
[state]
)
{:error, msg} ->
{:error, '552 #{String.to_charlist(msg)}', state}
end
end
@doc """
Handles incoming EHLO request and returns a list of extensions.
If the `auth` config is set to true, it automatically adds in
`AUTH` and `STARTTLS` extensions.
"""
@spec handle_EHLO(any, any, map) :: {:ok, [any], map}
def handle_EHLO(hostname, extensions, state) do
Logger.info("EHLO from #{hostname}")
case Application.get_env(:pique, :auth, false) do
true -> {
:ok,
extensions ++ [{'AUTH', 'PLAIN LOGIN'}, {'STARTTLS', true}],
state
}
_ -> {:ok, extensions, state}
end
end
@doc """
Handles incoming HELO request and returns a limit of 640Kb.
"""
@spec handle_HELO(any, map) :: {:ok, 655_360, map}
def handle_HELO(hostname, state) do
Logger.info("HELO from #{hostname}")
{:ok, 655360, state}
end
@doc """
Handles incoming MAIL request and passes the from address to
the defined MAIL handler. If the handler passes then adds the
from address to the state.
"""
@spec handle_MAIL(any, map) :: {:ok, %{from: map}} | {:error, charlist(), map}
def handle_MAIL(from, state) do
Logger.info("MAIL from #{from}")
case Kernel.apply(
Application.get_env(:pique, :mail_handler, Pique.Handlers.MAIL),
:handle,
[from]) do
{:ok, from} ->
{:ok, Map.put(state, :from, from)}
{:error, msg} ->
{:error, '550 #{String.to_charlist(msg)}', state}
end
end
@doc """
Handles MAIL extension requests. Does nothing.
"""
@spec handle_MAIL_extension(any, map) :: {:ok, map}
def handle_MAIL_extension(extension, state) do
Logger.info(extension)
{:ok, state}
end
@doc """
Handles incoming EHLO request and returns a list of extensions.
If the `auth` config is set to true, it automatically adds in
`AUTH` and `STARTTLS` extensions.
"""
@spec handle_RCPT(any, map) ::
{:ok, %{rcpt: nonempty_maybe_improper_list}} | {:error, charlist(), map}
def handle_RCPT(to, state) do
Logger.info("RCPT to #{to}")
case Kernel.apply(
Application.get_env(:pique, :rcpt_handler, Pique.Handlers.RCPT),
:handle,
[to]) do
{:ok, to} ->
{:ok, Map.put(state, :rcpt, [to] ++ Map.get(state, :rcpt, []))}
{:error, msg} ->
{:error, '550 #{String.to_charlist(msg)}', state}
end
end
@doc """
Handles RCPT extension requests. Does nothing.
"""
@spec handle_RCPT_extension(any, map) :: {:ok, map}
def handle_RCPT_extension(extension, state) do
Logger.info(extension)
{:ok, state}
end
@doc """
Handles RSET requests by removing the existing envelope
information from the state.
"""
@spec handle_RSET(map) :: {:ok, map}
def handle_RSET(state) do
state =
state
|> Map.delete(:rcpt)
|> Map.delete(:from)
|> Map.delete(:body)
{:ok, state}
end
@doc """
Handles VRFY requests by telling people to go away.
"""
@spec handle_VRFY(any, any) ::
{:error, [32 | 50 | 53 | 78 | 101 | 111 | 114 | 115 | 116 | 117, ...], any}
def handle_VRFY(address, state) do
Logger.info("VRFY for #{address}")
{:error, '252 Not sure', state}
end
@doc """
Handles incoming AUTH request and passes it off to the defined AUTH
handler. If the AUTH handler returns an `{:ok, state}`. Otherwise
returns relevant error messages.
"""
@spec handle_AUTH(any, any, any, any) :: {:ok, any} | {:error, charlist(), any}
def handle_AUTH(type, username, password, state) when type == :login or type == :plain do
Logger.info("AUTH request")
case Kernel.apply(
Application.get_env(:pique, :auth_handler, Pique.Handlers.AUTH),
:handle,
[{username, password}]) do
{:ok, _} ->
{:ok, state}
{:error, msg} ->
{:error, '530 #{String.to_charlist(msg)}', state}
end
end
@doc """
Handles incoming AUTH request that do not use the PLAIN or
LOGIN type - looking at you CRAM-MD5. Telling client to use
PLAIN or LOGIN.
"""
def handle_AUTH(_type, _username, _password, state) do
{:error, '530 Use PLAIN or LOGIN', state}
end
@doc """
Handles incoming unkown request. Telling client that
it does not understand.
"""
@spec handle_other(any, any, any) :: {charlist(), any}
def handle_other(command, _args, state) do
Logger.info(command)
{'500 Error: command not recognized : #{command}', state}
end
@doc """
Handles hot swap code change (in theory). Does nothing in
practice.
"""
@spec code_change(any, any, any) :: {:ok, any}
def code_change(_old, state, _extra) do
{:ok, state}
end
@doc """
Handles session termination. Does nothing.
"""
@spec terminate(String.t, map) :: {:ok, map}
def terminate(reason, state) do
Logger.info("Terminating Session: #{reason}")
{:ok, state}
end
end