Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Fix more dialyzer warnings, add some more docs

  • Loading branch information...
commit e6bef275ea5ef541e37bc282551def5ddaec38de 1 parent 8d3544e
Andrew Thompson authored
55 src/gen_smtp_client.erl
View
@@ -48,6 +48,8 @@
-export([send/2, send/3, send_blocking/2]).
-endif.
+-type email() :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}.
+
-spec send(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> {'ok', pid()} | {'error', any()}.
%% @doc Send an email in a non-blocking fashion via a spawned_linked process.
%% The process will exit abnormally on a send failure.
@@ -55,8 +57,8 @@ send(Email, Options) ->
send(Email, Options, undefined).
%% @doc Send an email nonblocking and invoke a callback with the result of the send.
-%% The callback will receive either `ok', `{error, Type, Message}' or `{exit, ExitReason}'
-%% as the single argument.
+%% The callback will receive either `{ok, Receipt}' where Receipt is the SMTP server's receipt
+%% identifier, `{error, Type, Message}' or `{exit, ExitReason}', as the single argument.
-spec send(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list(), Callback :: function() | 'undefined') -> {'ok', pid()} | {'error', any()}.
send(Email, Options, Callback) ->
NewOptions = lists:ukeymerge(1, lists:sort(Options),
@@ -89,8 +91,9 @@ send(Email, Options, Callback) ->
{error, Reason}
end.
--spec send_blocking(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> binary() | {'error', atom(), any()}.
-%% @doc Send an email and block waiting for the reply.
+-spec send_blocking(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> binary() | {'error', atom(), any()} | {'error', any()}.
+%% @doc Send an email and block waiting for the reply. Returns either a binary that contains
+%% the SMTP server's receipt or `{error, Type, Message}' or `{error, Reason}'.
send_blocking(Email, Options) ->
NewOptions = lists:ukeymerge(1, lists:sort(Options),
lists:sort(?DEFAULT_OPTIONS)),
@@ -101,19 +104,22 @@ send_blocking(Email, Options) ->
{error, Reason}
end.
+-spec send_it_nonblock(Email :: email(), Options :: list(), Callback :: function() | 'undefined') ->{'ok', binary()} | {'error', any(), any()}.
send_it_nonblock(Email, Options, Callback) ->
case send_it(Email, Options) of
{error, Type, Message} when is_function(Callback) ->
- Callback({error, Type, Message});
+ Callback({error, Type, Message}),
+ {error, Type, Message};
{error, Type, Message} ->
erlang:exit({error, Type, Message});
- _ when is_function(Callback) ->
- Callback(ok);
- _ ->
- ok
+ Receipt when is_function(Callback) ->
+ Callback({ok, Receipt}),
+ {ok, Receipt};
+ Receipt ->
+ {ok, Receipt}
end.
--spec send_it(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> 'ok'.
+-spec send_it(Email :: {string() | binary(), [string() | binary(), ...], string() | binary() | function()}, Options :: list()) -> binary() | {'error', any(), any()}.
send_it(Email, Options) ->
RelayDomain = proplists:get_value(relay, Options),
MXRecords = case proplists:get_value(no_mx_lookups, Options) of
@@ -131,9 +137,11 @@ send_it(Email, Options) ->
end,
try_smtp_sessions(Hosts, Email, Options, []).
+-spec try_smtp_sessions(Hosts :: [{non_neg_integer(), string()}, ...], Email :: email(), Options :: list(), RetryList :: list()) -> binary() | {'error', any(), any()}.
try_smtp_sessions([{Distance, Host} | Tail], Email, Options, RetryList) ->
Retries = proplists:get_value(retries, Options),
- try do_smtp_session(Host, Email, Options)
+ try do_smtp_session(Host, Email, Options) of
+ Res -> Res
catch
throw:{permanant_failure, Message} ->
% permanant failure means no retries, and don't even continue with other hosts
@@ -167,13 +175,13 @@ try_smtp_sessions([{Distance, Host} | Tail], Email, Options, RetryList) ->
end
end.
+-spec do_smtp_session(Host :: string(), Email :: email(), Options :: list()) -> binary().
do_smtp_session(Host, Email, Options) ->
{ok, Socket, _Host, _Banner} = connect(Host, Options),
%io:format("connected to ~s; banner was ~s~n", [Host, Banner]),
{ok, Extensions} = try_EHLO(Socket, Options),
%io:format("Extensions are ~p~n", [Extensions]),
- {Socket2, Extensions2} = try_STARTTLS(Socket, Options,
- Extensions),
+ {Socket2, Extensions2} = try_STARTTLS(Socket, Options, Extensions),
%io:format("Extensions are ~p~n", [Extensions2]),
_Authed = try_AUTH(Socket2, Options, proplists:get_value(<<"AUTH">>, Extensions2)),
%io:format("Authentication status is ~p~n", [Authed]),
@@ -182,11 +190,13 @@ do_smtp_session(Host, Email, Options) ->
quit(Socket2),
Receipt.
+-spec try_sending_it(Email :: email(), Socket :: socket:socket(), Extensions :: list()) -> binary().
try_sending_it({From, To, Body}, Socket, Extensions) ->
try_MAIL_FROM(From, Socket, Extensions),
try_RCPT_TO(To, Socket, Extensions),
try_DATA(Body, Socket, Extensions).
+-spec try_MAIL_FROM(From :: string() | binary(), Socket :: socket:socket(), Extensions :: list()) -> true.
try_MAIL_FROM(From, Socket, Extensions) when is_binary(From) ->
try_MAIL_FROM(binary_to_list(From), Socket, Extensions);
try_MAIL_FROM("<" ++ _ = From, Socket, _Extensions) ->
@@ -207,6 +217,7 @@ try_MAIL_FROM(From, Socket, Extensions) ->
% someone was bad and didn't put in the angle brackets
try_MAIL_FROM("<"++From++">", Socket, Extensions).
+-spec try_RCPT_TO(Tos :: [binary() | string()], Socket :: socket:socket(), Extensions :: list()) -> true.
try_RCPT_TO([], _Socket, _Extensions) ->
true;
try_RCPT_TO([To | Tail], Socket, Extensions) when is_binary(To) ->
@@ -229,6 +240,7 @@ try_RCPT_TO([To | Tail], Socket, Extensions) ->
% someone was bad and didn't put in the angle brackets
try_RCPT_TO(["<"++To++">" | Tail], Socket, Extensions).
+-spec try_DATA(Body :: binary() | function(), Socket :: socket:socket(), Extensions :: list()) -> binary().
try_DATA(Body, Socket, Extensions) when is_function(Body) ->
try_DATA(Body(), Socket, Extensions);
try_DATA(Body, Socket, _Extensions) ->
@@ -237,7 +249,7 @@ try_DATA(Body, Socket, _Extensions) ->
{ok, <<"354", _Rest/binary>>} ->
socket:send(Socket, [Body, "\r\n.\r\n"]),
case read_possible_multiline_reply(Socket) of
- {ok, <<"250", Receipt/binary>>} ->
+ {ok, <<"250 ", Receipt/binary>>} ->
Receipt;
{ok, <<"4", _Rest2/binary>> = Msg} ->
quit(Socket),
@@ -254,6 +266,7 @@ try_DATA(Body, Socket, _Extensions) ->
throw({permanant_failure, Msg})
end.
+-spec try_AUTH(Socket :: socket:socket(), Options :: list(), AuthTypes :: [string()]) -> boolean().
try_AUTH(Socket, Options, []) ->
case proplists:get_value(auth, Options) of
always ->
@@ -301,6 +314,7 @@ try_AUTH(Socket, Options, AuthTypes) ->
end
end.
+-spec do_AUTH(Socket :: socket:socket(), Username :: string(), Password :: string(), Types :: [string()]) -> boolean().
do_AUTH(Socket, Username, Password, Types) ->
FixedTypes = [string:to_upper(X) || X <- Types],
%io:format("Fixed types: ~p~n", [FixedTypes]),
@@ -309,6 +323,7 @@ do_AUTH(Socket, Username, Password, Types) ->
% [AllowedTypes]),
do_AUTH_each(Socket, Username, Password, AllowedTypes).
+-spec do_AUTH_each(Socket :: socket:socket(), Username :: string() | binary(), Password :: string() | binary(), AuthTypes :: [string()]) -> boolean().
do_AUTH_each(_Socket, _Username, _Password, []) ->
false;
do_AUTH_each(Socket, Username, Password, ["CRAM-MD5" | Tail]) ->
@@ -377,12 +392,16 @@ do_AUTH_each(Socket, Username, Password, [_Type | Tail]) ->
%io:format("unsupported AUTH type ~s~n", [Type]),
do_AUTH_each(Socket, Username, Password, Tail).
+-spec try_EHLO(Socket :: socket:socket(), Options :: list()) -> {ok, list()}.
try_EHLO(Socket, Options) ->
- ok = socket:send(Socket, ["EHLO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
+ socket:send(Socket, ["EHLO ", proplists:get_value(hostname, Options, smtp_util:guess_FQDN()), "\r\n"]),
+ %% TODO handle fallback to HELO!
{ok, Reply} = read_possible_multiline_reply(Socket),
- {ok, parse_extensions(Reply)}.
+ Extensions = parse_extensions(Reply),
+ {ok, Extensions}.
% check if we should try to do TLS
+-spec try_STARTTLS(Socket :: socket:socket(), Options :: list(), Extensions :: list()) -> {socket:socket(), list()}.
try_STARTTLS(Socket, Options, Extensions) ->
case {proplists:get_value(tls, Options),
proplists:get_value(<<"STARTTLS">>, Extensions)} of
@@ -408,6 +427,7 @@ try_STARTTLS(Socket, Options, Extensions) ->
end.
%% attempt to upgrade socket to TLS
+-spec do_STARTTLS(Socket :: socket:socket(), Options :: list()) -> {socket:socket(), list()} | false.
do_STARTTLS(Socket, Options) ->
socket:send(Socket, "STARTTLS\r\n"),
case read_possible_multiline_reply(Socket) of
@@ -471,6 +491,7 @@ connect(Host, Options) ->
end.
%% read a multiline reply (eg. EHLO reply)
+-spec read_possible_multiline_reply(Socket :: socket:socket()) -> {ok, binary()}.
read_possible_multiline_reply(Socket) ->
case socket:recv(Socket, 0, ?TIMEOUT) of
{ok, Packet} ->
@@ -485,6 +506,7 @@ read_possible_multiline_reply(Socket) ->
throw({network_failure, Error})
end.
+-spec read_multiline_reply(Socket :: socket:socket(), Code :: binary(), Acc :: [binary()]) -> {ok, binary()}.
read_multiline_reply(Socket, Code, Acc) ->
case socket:recv(Socket, 0, ?TIMEOUT) of
{ok, Packet} ->
@@ -526,6 +548,7 @@ check_options(Options) ->
end
end.
+-spec parse_extensions(Reply :: binary()) -> [{binary(), binary()}].
parse_extensions(Reply) ->
[_ | Reply2] = re:split(Reply, "\r\n", [{return, binary}, trim]),
[
2  src/mimemail.erl
View
@@ -70,7 +70,7 @@ decode(All) ->
{Headers, Body} = parse_headers(All),
decode(Headers, Body, ?DEFAULT_OPTIONS).
--spec(decode/2 :: (Headers :: [{binary(), binary()}], Options :: options()) -> mimetuple()).
+-spec(decode/2 :: (Email :: binary(), Options :: options()) -> mimetuple()).
%% @doc Decode with custom options
decode(All, Options) when is_binary(All), is_list(Options) ->
{Headers, Body} = parse_headers(All),
22 src/smtp_server_example.erl
View
@@ -91,6 +91,8 @@ handle_EHLO(Hostname, Extensions, State) ->
%%
%% 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
@@ -99,24 +101,34 @@ handle_MAIL(From, 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(Extension, State) ->
+handle_MAIL_extension(<<"X-SomeExtension">> = Extension, State) ->
io:format("Mail from extension ~s~n", [Extension]),
% any MAIL extensions can be handled here
- {ok, State}.
+ {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(Extension, State) ->
+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}.
+ {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()))]),
@@ -158,6 +170,8 @@ handle_RSET(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}.
16 src/socket.erl
View
@@ -128,7 +128,7 @@ accept(Socket, Timeout) ->
Error -> Error
end.
--spec send(Socket :: socket(), Data :: binary() | string()) -> 'ok' | {'error', any()}.
+-spec send(Socket :: socket(), Data :: binary() | string() | iolist()) -> 'ok' | {'error', any()}.
send(Socket, Data) when is_port(Socket) ->
gen_tcp:send(Socket, Data);
send(Socket, Data) ->
@@ -234,21 +234,21 @@ to_ssl_server(Socket, Options) ->
to_ssl_server(Socket, Options, Timeout) when is_port(Socket) ->
ssl:ssl_accept(Socket, ssl_listen_options(Options), Timeout);
to_ssl_server(_Socket, _Options, _Timeout) ->
- erlang:error(ssl_connected, "Socket is already using SSL").
+ {error, already_ssl}.
--spec to_ssl_client(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', any()}.
+-spec to_ssl_client(Socket :: socket()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}.
to_ssl_client(Socket) ->
to_ssl_client(Socket, []).
--spec to_ssl_client(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', any()}.
+-spec to_ssl_client(Socket :: socket(), Options :: list()) -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}.
to_ssl_client(Socket, Options) ->
to_ssl_client(Socket, Options, infinity).
--spec to_ssl_client(Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity') -> {'ok', ssl:sslsocket()} | {'error', any()}.
+-spec to_ssl_client(Socket :: socket(), Options :: list(), Timeout :: non_neg_integer() | 'infinity') -> {'ok', ssl:sslsocket()} | {'error', 'already_ssl'}.
to_ssl_client(Socket, Options, Timeout) when is_port(Socket) ->
ssl:connect(Socket, ssl_connect_options(Options), Timeout);
to_ssl_client(_Socket, _Options, _Timeout) ->
- erlang:error(ssl_connected, "Socket is already using SSL").
+ {error, already_ssl}.
-spec type(Socket :: socket()) -> protocol().
type(Socket) when is_port(Socket) ->
@@ -657,7 +657,7 @@ ssl_upgrade_test_() ->
spawn(fun() ->
{ok, ListenSocket} = listen(ssl, ?TEST_PORT, [{keyfile, "../testdata/server.key"}, {certfile, "../testdata/server.crt"}]),
{ok, ServerSocket} = accept(ListenSocket),
- ?assertException(error, ssl_connected, to_ssl_server(ServerSocket)),
+ ?assertMatch({error, already_ssl}, to_ssl_server(ServerSocket)),
close(ServerSocket)
end),
{ok, ClientSocket} = connect(tcp, "localhost", ?TEST_PORT),
@@ -677,7 +677,7 @@ ssl_upgrade_test_() ->
end),
{ok, ClientSocket} = connect(ssl, "localhost", ?TEST_PORT),
receive ServerSocket -> ok end,
- ?assertException(error, ssl_connected, to_ssl_client(ClientSocket)),
+ ?assertMatch({error, already_ssl}, to_ssl_client(ClientSocket)),
close(ClientSocket),
close(ServerSocket)
end
Please sign in to comment.
Something went wrong with that request. Please try again.