forked from gen-smtp/gen_smtp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsmtp_server_example.erl
354 lines (329 loc) · 15.1 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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
%% @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,
handle_info/2,
handle_error/3,
code_change/3,
terminate/2
]).
-include_lib("kernel/include/logger.hrl").
-define(LOGGER_META, #{domain => [gen_smtp, example_handler]}).
-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 banner),
%% 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 :: inet:hostname(),
SessionCount :: non_neg_integer(),
Address :: inet:ip_address(),
Options :: list()
) -> {'ok', iodata(), #state{}} | {'stop', any(), iodata()}.
init(Hostname, SessionCount, Address, Options) ->
?LOG_INFO("peer: ~p", [Address], ?LOGGER_META),
case SessionCount > 20 of
false ->
Banner = [Hostname, " ESMTP smtp_server_example"],
State = #state{options = Options},
{ok, Banner, State};
true ->
?LOG_WARNING("Connection limit exceeded", ?LOGGER_META),
{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) ->
%% no size limit because we trust them.
{ok, State};
handle_HELO(Hostname, State) ->
?LOG_INFO("HELO from ~s", [Hostname], ?LOGGER_META),
% 640kb of HELO should be enough for anyone.
MaxSize = proplists:get_value(size, State#state.options, 655360),
{ok, MaxSize, State}.
%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) ->
?LOG_INFO("EHLO from ~s", [Hostname], ?LOGGER_META),
% You can advertise additional extensions, or remove some defaults
MyExtensions1 =
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,
MyExtensions2 =
case proplists:get_value(size, State#state.options) of
undefined ->
MyExtensions1;
infinity ->
[{"SIZE", "0"} | lists:keydelete("SIZE", 1, MyExtensions1)];
Size when is_integer(Size), Size > 0 ->
[{"SIZE", integer_to_list(Size)} | lists:keydelete("SIZE", 1, MyExtensions1)]
end,
{ok, MyExtensions2, 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) ->
?LOG_INFO("Mail from ~s", [From], ?LOGGER_META),
% 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) ->
?LOG_INFO("Mail from extension ~s", [Extension], ?LOGGER_META),
% any MAIL extensions can be handled here
{ok, State};
handle_MAIL_extension(Extension, _State) ->
?LOG_WARNING("Unknown MAIL FROM extension ~s", [Extension], ?LOGGER_META),
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) ->
?LOG_INFO("Mail to ~s", [To], ?LOGGER_META),
% you can accept or reject RCPT TO addresses 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
?LOG_INFO("Mail to extension ~s", [Extension], ?LOGGER_META),
{ok, State};
handle_RCPT_extension(Extension, _State) ->
?LOG_WARNING("Unknown RCPT TO extension ~s", [Extension], ?LOGGER_META),
error.
%% @doc Handle the DATA verb from the client, which corresponds to the body of
%% the message. After receiving the body, a SMTP server can put the email in
%% a queue for later delivering while a LMTP server can handle the delivery
%% directly (LMTP servers are supposed to be simpler and handle emails to
%% local users directly without the need for a queue). Relaying the email to
%% another server is also an option.
%%
%% When using the SMTP protocol, `handle_DATA' should return a single "aggregate" delivery status
%% in the form of a `{ok, SuccessMsg, State}' tuple or `{error, ErrorMsg, State}'.
%% At this point, if `ok' is returned, we have accepted the full responsibility
%% of delivering the email.
%%
%% When using the LMTP protocol, `handle_DATA' should return a status for
%% each accepted address in `handle_RCPT' in the form of a `{multiple, StatusList, State}' tuple
%% where `StatusList' is a list of `{ok, SuccessMsg}' or `{error, ErrorMsg}' tuples
%% (the statuses should be presented in the same order as the recipient addresses were accepted).
%% For each `ok' in the `StatusList', we have accepted full responsibility for
%% delivering the email to that specific recipient. When a single recipient is
%% specified the returned value can also follow the SMTP format.
%%
%% `ErrorMsg' should always start with the SMTP error code, while `SuccessMsg'
%% should not (the `250' code is automatically prepended).
%%
%% According to the SMTP specification the, responsibility of delivering an
%% email must be taken seriously and the servers MUST NOT loose the message.
-spec handle_DATA(
From :: binary(),
To :: [binary(), ...],
Data :: binary(),
State :: #state{}
) ->
{ok | error, string(), #state{}}
| {multiple, [{ok | error, string()}], #state{}}.
handle_DATA(_From, _To, <<>>, State) ->
{error, "552 Message too small", State};
handle_DATA(From, To, Data, State) ->
% 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 ->
% some kind of unique id
Reference = lists:flatten([
io_lib:format("~2.16.0b", [X])
|| <<X>> <= erlang:md5(term_to_binary(unique_id()))
]),
case proplists:get_value(parse, State#state.options, false) of
false ->
ok;
true ->
% In this example we try to decode the email
try mimemail:decode(Data) of
_Result ->
?LOG_INFO("Message decoded successfully!", ?LOGGER_META)
catch
What:Why ->
?LOG_WARNING("Message decode FAILED with ~p:~p", [What, Why], ?LOGGER_META),
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,
queue_or_deliver(From, To, Data, Reference, State)
end.
-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) ->
?LOG_INFO("TLS Started", ?LOGGER_META),
State.
-spec handle_info(Info :: term(), State :: term()) ->
{noreply, NewState :: term()}
| {noreply, NewState :: term(), timeout() | hibernate}
| {stop, Reason :: term(), NewState :: term()}.
handle_info(_Info, State) ->
?LOG_INFO("handle_info(~p, ~p)", [_Info, State], ?LOGGER_META),
{noreply, State}.
%% This optional callback is called when different kinds of protocol errors happen.
%% Return {ok, State} to let gen_smtp decide how to act or {stop, Reason, #state{}}
%% to stop the process with reason Reason immediately.
-spec handle_error(gen_smtp_server_session:error_class(), any(), #state{}) ->
{ok, #state{}} | {stop, any(), #state{}}.
handle_error(Class, Details, State) ->
?LOG_INFO("handle_error(~p, ~p, ~p)", [Class, Details, State], ?LOGGER_META),
{ok, 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 %%%
unique_id() ->
erlang:unique_integer().
-spec relay(binary(), [binary()], binary()) -> ok.
relay(_, [], _) ->
ok;
relay(From, [To | Rest], Data) ->
% relay message to email address
[_User, Host] = string:tokens(binary_to_list(To), "@"),
gen_smtp_client:send({From, [To], erlang:binary_to_list(Data)}, [{relay, Host}]),
relay(From, Rest, Data).
%% @doc Helps `handle_DATA' to deal with the received email.
%% This function is not directly required by the behaviour.
-spec queue_or_deliver(
From :: binary(),
To :: [binary(), ...],
Data :: binary(),
Reference :: string(),
State :: #state{}
) ->
{ok | error, string(), #state{}}
| {multiple, [{ok | error, string()}], #state{}}.
queue_or_deliver(From, To, Data, Reference, State) ->
% At this point, if we return ok, we've accepted responsibility for the emaill
Length = byte_size(Data),
case proplists:get_value(protocol, State#state.options, smtp) of
smtp ->
?LOG_INFO(
"message from ~s to ~p queued as ~s, body length ~p",
[
From, To, Reference, Length
],
?LOGGER_META
),
% ... should actually handle the email,
% if `ok` is returned we are taking the responsibility of the delivery.
{ok, ["queued as ~s", Reference], State};
lmtp ->
?LOG_INFO("message from ~s delivered to ~p, body length ~p", [From, To, Length], ?LOGGER_META),
Multiple = [{ok, ["delivered to ", Recipient]} || Recipient <- To],
% ... should actually handle the email for each recipient for each `ok`
{multiple, Multiple, State}
end.