Skip to content

Instantly share code, notes, and snippets.

@arjan
Created February 7, 2023 09:57
Show Gist options
  • Save arjan/cc6989c71a71697beb728d68c54a0b93 to your computer and use it in GitHub Desktop.
Save arjan/cc6989c71a71697beb728d68c54a0b93 to your computer and use it in GitHub Desktop.
Example on how to receive email in Elixir
defmodule MyApp.ReceiveServer do
@behaviour :gen_smtp_server_session
require Logger
# how many simultaneous SMTP sessions
@max_sessions 20
# max email message size
@maxsize 200 * 1024 * 1024
def child_spec(_opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, []}
}
end
def start_link() do
args = [port: 2525, address: {0, 0, 0, 0}]
Logger.info("Accepting e-mail on port #{args[:port]}")
:gen_smtp_server.start({:local, __MODULE__}, __MODULE__, args)
end
defstruct [:hostname, :from, :peername, :options, :helo, :bns_entry]
alias __MODULE__, as: State
@impl true
def init(_hostname, session_count, _peername, _options) when session_count > @max_sessions do
{:stop, :normal, '421 server too busy to accept mail right now'}
end
def init(_hostname, _session_count, peername, _options) when not is_tuple(peername) do
{:stop, :normal, '421 invalid peername'}
end
def init(hostname, _session_count, peername, options) do
banner = 'ESMTP example 1.0'
{:ok, banner, %State{hostname: hostname, peername: peername, options: options}}
end
@impl true
# credo:disable-for-next-line
def handle_HELO(hostname, state) do
{:ok, @maxsize, %State{state | helo: hostname}}
end
@impl true
# credo:disable-for-next-line
def handle_EHLO(hostname, extensions, state) do
extensions = [{'SIZE', '#{@maxsize}'} | :lists.keydelete('SIZE', 1, extensions)]
{:ok, extensions, %State{state | helo: hostname}}
end
@impl true
# credo:disable-for-next-line
def handle_STARTTLS(state), do: state
@impl true
# credo:disable-for-next-line
def handle_MAIL(from, state) do
state = %State{state | from: from}
# this function comes from from z_stdlib
case :z_email_dnsbl.status(state.peername) do
{:ok, {:blocked, service}} ->
{:error, '451 peer blocked by DNSBL service #{inspect(service)}', state}
{:ok, _} ->
{:ok, state}
end
end
@impl true
# credo:disable-for-next-line
def handle_MAIL_extension(_from, state) do
{:ok, state}
end
@impl true
# credo:disable-for-next-line
def handle_RCPT(to, state) do
# TODO check if we accept mail for this address (in `to`)
case do_we_accept_this_addres?(to) do
false ->
{:error, '550 No such recipient', state}
true ->
{:ok, state}
end
end
@impl true
# credo:disable-for-next-line
def handle_RCPT_extension(_from, state) do
{:ok, state}
end
@impl true
# credo:disable-for-next-line
def handle_DATA(_from, _to, data, state) do
with {:ok, parsed_mime} <- decode_mime(data) do
# TODO do something with `parsed_mime` here
msg_id = process_message()
{:ok, msg_id, %State{}}
else
{:error, message} ->
{:error, '552 #{message}', state}
end
end
@impl true
# credo:disable-for-next-line
def handle_RSET(_state) do
%State{}
end
@impl true
# credo:disable-for-next-line
def handle_VRFY(_address, state) do
{:error, '252 VRFY disabled by policy, just send some mail', state}
end
@impl true
def handle_other(verb, _args, state) do
{'500 error: verb not recognized: ' ++ verb, state}
end
@impl true
def code_change(_oldvsn, state, _extra), do: {:ok, state}
@impl true
def terminate(reason, state), do: {:ok, reason, state}
defp decode_mime(mime) do
mime = mime |> String.replace("\r", "") |> String.replace("\n", "\r\n")
try do
{:ok, :mimemail.decode(mime)}
catch
:error, r ->
{:error, "MIME decoding error: " <> String.replace(to_string(r), "_", " ")}
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment