-
Notifications
You must be signed in to change notification settings - Fork 266
/
smtp_server_example.erl
222 lines (198 loc) · 9.47 KB
/
smtp_server_example.erl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
%% @doc A simple example callback module for `gen_smtp_server_session' that also serves as
%% documentation for the required callback API.
-module(smtp_server_example).
-behaviour(gen_smtp_server_session).
-export([init/4, handle_HELO/2, handle_EHLO/3, handle_MAIL/2, handle_MAIL_extension/2,
handle_RCPT/2, handle_RCPT_extension/2, handle_DATA/4, handle_RSET/1, handle_VRFY/2,
handle_other/3, handle_AUTH/4, handle_STARTTLS/1, code_change/3, terminate/2]).
-define(RELAY, true).
-record(state,
{
options = [] :: list()
}).
-type(error_message() :: {'error', string(), #state{}}).
%% @doc Initialize the callback module's state for a new session.
%% The arguments to the function are the SMTP server's hostname (for use in the SMTP anner),
%% The number of current sessions (eg. so you can do session limiting), the IP address of the
%% connecting client, and a freeform list of options for the module. The Options are extracted
%% from the `callbackoptions' parameter passed into the `gen_smtp_server_session' when it was
%% started.
%%
%% If you want to continue the session, return `{ok, Banner, State}' where Banner is the SMTP
%% banner to send to the client and State is the callback module's state. The State will be passed
%% to ALL subsequent calls to the callback module, so it can be used to keep track of the SMTP
%% session. You can also return `{stop, Reason, Message}' where the session will exit with Reason
%% and send Message to the client.
-spec init(Hostname :: binary(), SessionCount :: non_neg_integer(), Address :: tuple(), Options :: list()) -> {'ok', string(), #state{}} | {'stop', any(), string()}.
init(Hostname, SessionCount, Address, Options) ->
io:format("peer: ~p~n", [Address]),
case SessionCount > 20 of
false ->
Banner = [Hostname, " ESMTP smtp_server_example"],
State = #state{options = Options},
{ok, Banner, State};
true ->
io:format("Connection limit exceeded~n"),
{stop, normal, ["421 ", Hostname, " is too busy to accept mail right now"]}
end.
%% @doc Handle the HELO verb from the client. Arguments are the Hostname sent by the client as
%% part of the HELO and the callback State.
%%
%% Return values are `{ok, State}' to simply continue with a new state, `{ok, MessageSize, State}'
%% to continue with the SMTP session but to impose a maximum message size (which you can determine
%% , for example, by looking at the IP address passed in to the init function) and the new callback
%% state. You can reject the HELO by returning `{error, Message, State}' and the Message will be
%% sent back to the client. The reject message MUST contain the SMTP status code, eg. 554.
-spec handle_HELO(Hostname :: binary(), State :: #state{}) -> {'ok', pos_integer(), #state{}} | {'ok', #state{}} | error_message().
handle_HELO(<<"invalid">>, State) ->
% contrived example
{error, "554 invalid hostname", State};
handle_HELO(<<"trusted_host">>, State) ->
{ok, State}; %% no size limit because we trust them.
handle_HELO(Hostname, State) ->
io:format("HELO from ~s~n", [Hostname]),
{ok, 655360, State}. % 640kb of HELO should be enough for anyone.
%If {ok, State} was returned here, we'd use the default 10mb limit
%% @doc Handle the EHLO verb from the client. As with EHLO the hostname is provided as an argument,
%% but in addition to that the list of ESMTP Extensions enabled in the session is passed. This list
%% of extensions can be modified by the callback module to add/remove extensions.
%%
%% The return values are `{ok, Extensions, State}' where Extensions is the new list of extensions
%% to use for this session or `{error, Message, State}' where Message is the reject message as
%% with handle_HELO.
-spec handle_EHLO(Hostname :: binary(), Extensions :: list(), State :: #state{}) -> {'ok', list(), #state{}} | error_message().
handle_EHLO(<<"invalid">>, _Extensions, State) ->
% contrived example
{error, "554 invalid hostname", State};
handle_EHLO(Hostname, Extensions, State) ->
io:format("EHLO from ~s~n", [Hostname]),
% You can advertise additional extensions, or remove some defaults
MyExtensions = case proplists:get_value(auth, State#state.options, false) of
true ->
% auth is enabled, so advertise it
Extensions ++ [{"AUTH", "PLAIN LOGIN CRAM-MD5"}, {"STARTTLS", true}];
false ->
Extensions
end,
{ok, MyExtensions, State}.
%% @doc Handle the MAIL FROM verb. The From argument is the email address specified by the
%% MAIL FROM command. Extensions to the MAIL verb are handled by the `handle_MAIL_extension'
%% function.
%%
%% Return values are either `{ok, State}' or `{error, Message, State}' as before.
-spec handle_MAIL(From :: binary(), State :: #state{}) -> {'ok', #state{}} | error_message().
handle_MAIL(<<"badguy@blacklist.com">>, State) ->
{error, "552 go away", State};
handle_MAIL(From, State) ->
io:format("Mail from ~s~n", [From]),
% you can accept or reject the FROM address here
{ok, State}.
%% @doc Handle an extension to the MAIL verb. Return either `{ok, State}' or `error' to reject
%% the option.
-spec handle_MAIL_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'.
handle_MAIL_extension(<<"X-SomeExtension">> = Extension, State) ->
io:format("Mail from extension ~s~n", [Extension]),
% any MAIL extensions can be handled here
{ok, State};
handle_MAIL_extension(Extension, _State) ->
io:format("Unknown MAIL FROM extension ~s~n", [Extension]),
error.
-spec handle_RCPT(To :: binary(), State :: #state{}) -> {'ok', #state{}} | {'error', string(), #state{}}.
handle_RCPT(<<"nobody@example.com">>, State) ->
{error, "550 No such recipient", State};
handle_RCPT(To, State) ->
io:format("Mail to ~s~n", [To]),
% you can accept or reject RCPT TO addesses here, one per call
{ok, State}.
-spec handle_RCPT_extension(Extension :: binary(), State :: #state{}) -> {'ok', #state{}} | 'error'.
handle_RCPT_extension(<<"X-SomeExtension">> = Extension, State) ->
% any RCPT TO extensions can be handled here
io:format("Mail to extension ~s~n", [Extension]),
{ok, State};
handle_RCPT_extension(Extension, _State) ->
io:format("Unknown RCPT TO extension ~s~n", [Extension]),
error.
-spec handle_DATA(From :: binary(), To :: [binary(),...], Data :: binary(), State :: #state{}) -> {'ok', string(), #state{}} | {'error', string(), #state{}}.
handle_DATA(_From, _To, <<>>, State) ->
{error, "552 Message too small", State};
handle_DATA(From, To, Data, State) ->
% some kind of unique id
Reference = lists:flatten([io_lib:format("~2.16.0b", [X]) || <<X>> <= erlang:md5(term_to_binary(erlang:now()))]),
% if RELAY is true, then relay email to email address, else send email data to console
case proplists:get_value(relay, State#state.options, false) of
true -> relay(From, To, Data);
false ->
io:format("message from ~s to ~p queued as ~s, body length ~p~n", [From, To, Reference, byte_size(Data)]),
case proplists:get_value(parse, State#state.options, false) of
false -> ok;
true ->
try mimemail:decode(Data) of
_Result ->
io:format("Message decoded successfully!~n")
catch
What:Why ->
io:format("Message decode FAILED with ~p:~p~n", [What, Why]),
case proplists:get_value(dump, State#state.options, false) of
false -> ok;
true ->
%% optionally dump the failed email somewhere for analysis
File = "dump/"++Reference,
case filelib:ensure_dir(File) of
ok ->
file:write_file(File, Data);
_ ->
ok
end
end
end
end
end,
% At this point, if we return ok, we've accepted responsibility for the email
{ok, Reference, State}.
-spec handle_RSET(State :: #state{}) -> #state{}.
handle_RSET(State) ->
% reset any relevant internal state
State.
-spec handle_VRFY(Address :: binary(), State :: #state{}) -> {'ok', string(), #state{}} | {'error', string(), #state{}}.
handle_VRFY(<<"someuser">>, State) ->
{ok, "someuser@"++smtp_util:guess_FQDN(), State};
handle_VRFY(_Address, State) ->
{error, "252 VRFY disabled by policy, just send some mail", State}.
-spec handle_other(Verb :: binary(), Args :: binary(), #state{}) -> {string(), #state{}}.
handle_other(Verb, _Args, State) ->
% You can implement other SMTP verbs here, if you need to
{["500 Error: command not recognized : '", Verb, "'"], State}.
%% this callback is OPTIONAL
%% it only gets called if you add AUTH to your ESMTP extensions
-spec handle_AUTH(Type :: 'login' | 'plain' | 'cram-md5', Username :: binary(), Password :: binary() | {binary(), binary()}, #state{}) -> {'ok', #state{}} | 'error'.
handle_AUTH(Type, <<"username">>, <<"PaSSw0rd">>, State) when Type =:= login; Type =:= plain ->
{ok, State};
handle_AUTH('cram-md5', <<"username">>, {Digest, Seed}, State) ->
case smtp_util:compute_cram_digest(<<"PaSSw0rd">>, Seed) of
Digest ->
{ok, State};
_ ->
error
end;
handle_AUTH(_Type, _Username, _Password, _State) ->
error.
%% this callback is OPTIONAL
%% it only gets called if you add STARTTLS to your ESMTP extensions
-spec handle_STARTTLS(#state{}) -> #state{}.
handle_STARTTLS(State) ->
io:format("TLS Started~n"),
State.
-spec code_change(OldVsn :: any(), State :: #state{}, Extra :: any()) -> {ok, #state{}}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
-spec terminate(Reason :: any(), State :: #state{}) -> {'ok', any(), #state{}}.
terminate(Reason, State) ->
{ok, Reason, State}.
%%% Internal Functions %%%
relay(_, [], _) ->
ok;
relay(From, [To|Rest], Data) ->
% relay message to email address
[_User, Host] = string:tokens(To, "@"),
gen_smtp_client:send({From, [To], erlang:binary_to_list(Data)}, [{relay, Host}]),
relay(From, Rest, Data).