%%%------------------------------------------------------------------------
%%% @doc SMTP mail client. This module can sent emails to one or more
%%% recipients, using primary/backup SMTP servers. Messages can
%%% contain attachments.
%%%
%%% ```
%%% Example:
%%% % Send a message to two recipients with a file attachment using
%%% % SSL protocol at mail server "mail.bevemyr.com":
%%% smtp:send(ssl, "Alex <jb@bevemyr.com>",
%%% ["katrin@bevemyr.com","jb@bevemyr.com"],
%%% "Test Subject", "My Message",
%%% [{server, "mail.bevemyr.com"},
%%% {username, "alex"}, {password, "secret"},
%%% {attachments, ["file1.txt"]}]).
%%%
%%% % Send a message to a recipient with a file attachment given custom
%%% % MIME type using localhost mail server
%%% smtp:send(tcp, "jb@bevemyr.com",
%%% ["katrin@bevemyr.com"], "Test Subject", "My Message",
%%% [{server, "mail.bevemyr.com"},
%%% {username, "alex"}, {password, "secret"},
%%% {attachments, [{"file1.bin","application/custom_MIME"}]}]).
%%%
%%% % Send a message to two recipients with an attachment given as list
%%% smtp:send(tcp, "jb@bevemyr.com",
%%% ["katrin@bevemyr.com","jb@bevemyr.com"],
%%% "Test Subject", "My Message",
%%% [{"file1.txt","text/plain","Attachment past as list"}]).
%%% '''
%%%
%%% @author Johan Bevemyr, Serge Aleynikov <saleyn@gmail.com>
%%% @end
%%%------------------------------------------------------------------------
%%% Created 02/24/2004 Johan Bevemyr
%%%------------------------------------------------------------------------
-module(smtp).
-author('jb@son.bevemyr.com').
-author('saleyn@gmail.com').
-export([send/5, send/6, domain/0]).
-include_lib("kernel/include/inet.hrl").
-type proto() :: tcp | ssl.
%% Protocol type.
-type smtp_options() :: [
{server, Server::string()}
| {relay, Relay::string()}
| {port, Port::integer()}
| {auth, Auth :: always | never}
| {username, Username::string()}
| {password, Password::string()}
| {tls, Tls :: always | if_available}
| {domain, Domain::string()}
| {timeout, Millisec::integer()}
| {verbose, debug}
| {ssl, SSLOpts::list()}
| {attachments, [
Filename::string() |
{Filename::string(), ContentType::string()} |
{Filename::string(), ContentType::string(), Data::list()}]}].
%% SNMP Options
%% <ul>
%% <li>Server - server to connect to (no MX lookup)</li>
%% <li>Relay - domain to do MX lookup of list of servers</li>
%% <li>Port - optional port number (ssl def: 465; tcp def: 25)</li>
%% <li>Auth - controls mandatory / optional authentication</li>
%% <li>Tls - controls enabling of TLS protocol</li>
%% <li>Domain - name of the domain to include in the HELO handshake</li>
%% <li>Timeout - timeout to use (default 10000)</li>
%% <li>Verbose - controls debugging printout</li>
%% <li>Attachments - list of files to attach</li>
%% <li>SSLOpts - additional SSL options if using SSL protocol</li>
%% </ul>
%%-------------------------------------------------------------------------
%% @doc Send a message to a list of `To' receipients using `localhost'.
%% Error is thrown if unable to send a message.
%% Use inet:format_error/1 to decode the Reason if it is an atom.
%% @end
%%-------------------------------------------------------------------------
-spec send(Proto :: proto(), From :: string() | binary(),
To :: string() | binary(), Subj :: string() | binary(),
Msg :: string() | binary()) -> ok.
send(Proto, From, To, Subject, Message) ->
send(Proto, From, To, Subject, Message, []).
%%-------------------------------------------------------------------------
%% @doc Send a message to a list of recipients by connecting to an SMTP
%% server Server. The message can contain attachments in the
%% Attachments list. See examples on the top of this page.
%% Error is thrown if unable to send a message.
%% @end
%%-------------------------------------------------------------------------
-spec send(Proto :: proto(), From :: string() | binary(),
To :: string() | binary(), Subj :: string() | binary(),
Msg :: string() | binary(), Opts :: smtp_options()) -> ok.
send(Proto, From, To, Subj, Msg, Opts)
when Proto =:= tcp; Proto =:= ssl ->
Module = proto_module(Proto),
case proplists:get_value(server, Opts) of
undefined ->
case proplists:get_value(relay, Opts) of
undefined ->
try_send(Module, From, To, Subj, Msg, "localhost", Opts);
Domain ->
Servers = mxlookup(Domain),
send_mail(Module, Servers, {From, To, Subj, Msg},
no_servers_provided, Opts)
end;
[I | _] = Server when is_integer(I) ->
try_send(Module, From, To, Subj, Msg, Server, Opts);
Servers when is_list(Servers) ->
send_mail(Module, Servers, {From, To, Subj, Msg},
no_servers_provided, Opts)
end.
%%-------------------------------------------------------------------------
%% @doc Get domain that this host belongs to.
%% @end
%%-------------------------------------------------------------------------
-spec domain() -> binary().
domain() ->
case lists:keyfind(domain, 1, inet:get_rc()) of
{domain, D} when is_binary(D) -> D;
{domain, D} when is_list(D) -> list_to_binary(D);
false ->
{ok, Hostname} = inet:gethostname(),
{ok, #hostent{h_name = FQDN}} = inet:gethostbyname(Hostname),
list_to_binary(FQDN)
end.
%%%------------------------------------------------------------------------
%%% Internal functions
%%%------------------------------------------------------------------------
proto_module(tcp) -> gen_tcp;
proto_module(ssl) -> ssl.
mxlookup(Domain) ->
case whereis(inet_db) of
P when is_pid(P) -> ok;
_ -> inet_db:start()
end,
case lists:keyfind(nameserver, 1, inet_db:get_rc()) of
false ->
% we got no nameservers configured, suck in resolv.conf
inet_config:do_load_resolv(os:type(), longnames);
_ ->
ok
end,
case inet_res:lookup(Domain, in, mx) of
[] -> [];
L -> [H || {_, H} <- lists:sort(L)]
end.
try_send(Module, From, To, Subj, Msg, Server, Opts) ->
Verbose = proplists:get_value(verbose, Opts),
Attachments = proplists:get_value(attachments, Opts, []),
Port = smtp_init(Module, Server, From, To, Verbose, Opts),
Boundary=boundary_bin(Attachments),
smtp_send_headers(Module, Port, From, To, Subj, Boundary),
smtp_send_message(Module, Port, Msg, Boundary),
smtp_send_attachments(Module, Port, Attachments, Boundary),
smtp_close(Module, Port).
send_mail(_Mod, [], _What, LastReason, _Options) ->
throw(LastReason);
send_mail(Mod, [S | Rest], {From, To, Subj, Msg} = What, _LastReason, Options) ->
try
ok = try_send(Mod, From, To, Subj, Msg, S, Options)
catch
_:Reason when is_atom(Reason) ->
% This is likely a connection error
send_mail(Mod, Rest, What, Reason, Options);
C:R:Stack ->
% This is the case when a server couldn't send the message due to
% other than networking reasons. Don't retry.
erlang:raise(C,R,Stack)
end.
smtp_send_headers(Mod, Port, From, To, Subject, Boundary) ->
CommonHeaders = [mail_headers(<<"To: ">>, [list_to_binary(T) || T <- To]),
mail_header (<<"From: ">>, list_to_binary(From)),
mail_header (<<"Subject: ">>, list_to_binary(Subject))],
Headers =
case Boundary of
undefined ->
[mail_header(<<"Content-Type: ">>, <<"text/plain">>),
mail_header(<<"Content-Transfer-Encoding: ">>, <<"8bit">>)];
_ ->
[mail_header(<<"Mime-Version: ">>, <<"1.0">>),
mail_header(<<"Content-Type: ">>, [<<"Multipart/Mixed; boundary=\"">>,
Boundary, <<"\"">>]),
mail_header(<<"Content-Transfer-Encoding: ">>, <<"8bit">>)]
end,
Mod:send(Port, [CommonHeaders, Headers, <<"\r\n">>]).
smtp_send_message(Mod, Port, Data, Boundary) ->
case Boundary of
undefined ->
ok;
_ ->
Mod:send(Port,
[<<"--">>,Boundary,<<"\r\n">>,
mail_header(<<"Content-Type: ">>, <<"Text/Plain; charset=us-ascii">>),
mail_header(<<"Content-Transfer-Encoding: ">>, <<"8bit">>),
<<"\r\n">>])
end,
{_LastNL, Escaped} = dot_escape(Data, true),
Mod:send(Port, Escaped).
smtp_send_attachments(Mod, Port, [], _Boundary) ->
Mod:send(Port, <<"\r\n.\r\n">>);
smtp_send_attachments(Mod, Port, Attachments, Boundary) ->
send_attachments(Mod, Port, Boundary, Attachments),
Mod:send(Port, <<"\r\n.\r\n">>).
send_attachments(Mod, Port, Boundary, []) ->
Mod:send(Port, <<"\r\n--",(list_to_binary(Boundary))/binary,"--\r\n">>);
send_attachments(Mod, Port, Boundary, [{FileName,ContentType}|Rest]) ->
Data =
case file:read_file(FileName) of
{ok, Bin} ->
binary_to_list(Bin);
{error, Reason} ->
throw(lists:flatten(
io_lib:format("File ~s: ~s", [FileName, file:format_error(Reason)])))
end,
send_attachment(Mod, Port, Boundary, FileName, ContentType, Data),
send_attachments(Mod, Port, Boundary, Rest);
send_attachments(Mod, Port, Boundary, [{FileName,ContentType,Data}|Rest]) ->
send_attachment(Mod, Port, Boundary, FileName, ContentType, Data),
send_attachments(Mod, Port, Boundary, Rest);
send_attachments(Mod, Port, Boundary, [FileName | Rest]) ->
send_attachments(Mod, Port, Boundary, [{FileName, undefined} | Rest]).
send_attachment(Mod, Port, Boundary, FileName, ContentType, Data) ->
File = filename:basename(FileName,""),
CT = case {ContentType, io_lib:printable_list(Data)} of
{undefined, true} -> "plain/text";
{undefined, false} -> "application/octet-stream; name=\"" ++ File ++"\"";
{_, _} -> ContentType
end,
Mod:send(Port,
[<<"\r\n--">>,Boundary,<<"\r\n">>,
mail_header(<<"Content-Type: ">>, CT),
mail_header(<<"Content-Transfer-Encoding: ">>, <<"base64">>),
mail_header(<<"Content-Disposition: ">>,
[<<"attachment; filename=\"">>, list_to_binary(File), <<"\"">>]),
<<"\r\n">>
]),
B64 = sting2base64(Data),
Mod:send(Port, B64).
def_port_and_opts(gen_tcp, _Opts) ->
{25, [{active, false}, {reuseaddr,true}, {packet, line}, binary]};
def_port_and_opts(ssl, Opts) ->
SSLOpts = proplists:get_value(ssl, Opts, []),
{465, SSLOpts ++ [{active, false}, {depth, 3}, {packet, line}, {ssl_imp, new}, binary]}.
connect(gen_tcp, Server, _Verbose, _Options) when is_port(Server) ->
Server;
connect(Mod, Server, Verbose, Options) ->
{DefPort, SockOpts} = def_port_and_opts(Mod, Options),
Timeout = proplists:get_value(timeout, Options, 10000),
% For ssl make sure applications crypto, public_key and ssl are started
if is_port(Server) ->
case Mod:connect(Server, SockOpts, Timeout) of
{ok, Sock} -> Sock;
{error, Why} -> throw(Why)
end;
true ->
Port = proplists:get_value(port, Options, DefPort),
print(Verbose, "Connecting to: ~w://~s:~w\n", [Mod, Server, Port]),
case Mod:connect(Server, Port, SockOpts, Timeout) of
{ok, Sock} -> Sock;
{error, Why} -> throw(Why)
end
end.
domain(Options) ->
case proplists:get_value(domain, Options) of
undefined ->
domain();
Domain when is_binary(Domain) ->
Domain;
Domain when is_list(Domain) ->
list_to_binary(Domain)
end.
smtp_STARTTLS(ssl, Port, _Options, Extensions, _Domain, _Verbose) ->
{Port, Extensions};
smtp_STARTTLS(Mod, Port, Options, Extensions, Domain, Verbose) ->
case {proplists:get_value(tls, Options), proplists:get_value(<<"STARTTLS">>, Extensions)} of
{always, true} ->
do_STARTTLS(Mod, Port, Extensions, Verbose, Options, Domain);
{if_available, true} ->
do_STARTTLS(Mod, Port, Extensions, Verbose, Options, Domain);
{always, _} ->
smtp_close(Mod, Port),
throw({missing_requirement, tls});
_ ->
{Port, Extensions}
end.
do_STARTTLS(Mod, Port, Extensions, Verbose, Options, Domain) ->
smtp_put(Mod, <<"STARTTLS">>, Port),
smtp_expect(Mod, <<"220">>, Port, undefined),
case Mod of
ssl ->
{Port, Extensions};
gen_tcp ->
Sock = connect(ssl, Port, Verbose, Options),
{ok, Extensions2} = try_HELO(Mod, Port, Domain),
{Sock, Extensions2}
end.
try_AUTH(Mod, Port, Options, Ext, _Verbose) when Ext =:= undefined; Ext =:= [] ->
case proplists:get_value(auth, Options) of
always ->
smtp_close(Mod, Port),
throw({missing_requirement, auth});
_ ->
{false, proplists:get_value(username, Options)}
end;
try_AUTH(Mod, Port, Options, AuthTypes, Verbose) ->
Username = proplists:get_value(username, Options),
Password = proplists:get_value(password, Options),
Auth = proplists:get_value(auth, Options),
case Auth of
never ->
{false, Username};
_ when Username =:= undefined ->
throw({missing_auth, username});
_ when Password =:= undefined ->
throw({missing_auth, password});
_ ->
Types = [decode_auth(X) || X <- re:split(AuthTypes, " ", [{return, binary}, trim])],
AllowedTypes = [X || X <- Types, is_atom(X)],
case do_AUTH_each(Mod, Port, Username, Password, AllowedTypes, Verbose) of
false when Auth =:= always ->
smtp_close(Mod, Port),
erlang:throw({permanent_failure, auth_failed});
false ->
{false, Username};
true ->
{true, Username}
end
end.
decode_auth(<<"CRAM-MD5">>) -> cram_md5;
decode_auth(<<"cram-md5">>) -> cram_md5;
decode_auth(<<"LOGIN">>) -> login;
decode_auth(<<"login">>) -> login;
decode_auth(<<"PLAIN">>) -> plain;
decode_auth(<<"plain">>) -> plain;
decode_auth(Other) -> Other.
to_hex(Prefix, Bin) -> list_to_binary(Prefix ++ [to_hex_int(I) || <<I>> <= Bin]).
to_hex_int(I) when I < 10 -> $0 + I;
to_hex_int(I) when I < 16 -> $A + (I - 10);
to_hex_int(I) when I < 256 -> J = I div 256, [to_hex_int(J), to_hex_int(I - 256*J)].
do_AUTH_each(_Mod, _Port, _Username, _Password, [], _Verbose) ->
false;
do_AUTH_each(Mod, Port, Username, Password, [cram_md5 | Tail], Verbose) ->
smtp_put(Mod, <<"AUTH CRAM-MD5">>, Port),
try
{ok, Seed64} = smtp_expect(Mod, <<"334">>, Port, undefined),
Seed = base64:decode_to_string(Seed64),
Bin = crypto:mac(hmac, md5, Password, Seed),
Digest = to_hex([Username, " "], Bin),
smtp_put(Mod, base64:encode(Digest), Port),
smtp_expect(Mod, <<"235">>, Port, undefined),
print(Verbose, "Authenticated using crom_md5\n", []),
true
catch _:_ ->
do_AUTH_each(Mod, Port, Username, Password, Tail, Verbose)
end;
do_AUTH_each(Mod, Port, Username, Password, [login | Tail], Verbose) ->
smtp_put(Mod, <<"AUTH LOGIN">>, Port),
try
{ok, <<"VXNlcm5hbWU6", _/binary>>} = smtp_expect(Mod, <<"334">>, Port, undefined),
U = base64:encode(Username),
smtp_put(Mod, U, Port),
{ok, <<"UGFzc3dvcmQ6", _/binary>>} = smtp_expect(Mod, <<"334">>, Port, undefined),
P = base64:encode(Password),
smtp_put(Mod, P, Port),
smtp_expect(Mod, <<"235">>, Port, undefined),
print(Verbose, "Authenticated using login\n", []),
true
catch _:_ ->
do_AUTH_each(Mod, Port, Username, Password, Tail, Verbose)
end;
do_AUTH_each(Mod, Port, Username, Password, [plain | Tail], Verbose) ->
AuthString = base64:encode("\0"++Username++"\0"++Password),
smtp_put(Mod, [<<"AUTH PLAIN ">>, AuthString], Port),
try
smtp_expect(Mod, <<"235">>, Port, undefined),
print(Verbose, "Authenticated using plain\n", []),
true
catch _:_ ->
do_AUTH_each(Mod, Port, Username, Password, Tail, Verbose)
end.
try_HELO(Mod, Port, Domain) ->
smtp_put(Mod, [<<"EHLO ">>, Domain], Port),
try
smtp_expect(Mod, <<"250">>, Port, undefined)
catch throw:<<"500", _/binary>> ->
smtp_put(Mod, [<<"HELO ">>, Domain], Port),
smtp_expect(Mod, <<"250">>, Port, undefined)
end.
smtp_init(Mod, Server, From, Recipients, Verbose, Options) ->
Port = connect(Mod, Server, Verbose, Options),
smtp_expect(Mod, <<"220">>, Port, "SMTP server does not respond"),
Domain = domain(Options),
{ok, Extensions} = try_HELO(Mod, Port, Domain),
print(Verbose, "Extensions: ~p\n", [Extensions]),
{Port2, Extensions2} = smtp_STARTTLS(Mod, Port, Options, Extensions, Domain, Verbose),
{_Auth, Username} =
try_AUTH(Mod, Port, Options, proplists:get_value(<<"AUTH">>, Extensions2), Verbose),
FromEmail = format_email(Username, From),
print(Verbose, "From email: ~p\n", [FromEmail]),
smtp_put(Mod, [<<"MAIL FROM: ">>, FromEmail], Port2),
smtp_expect(Mod, <<"250">>, Port2, undefined),
send_recipients(Mod, Recipients, Port2),
smtp_put(Mod, <<"DATA">>, Port2),
smtp_expect(Mod, <<"354">>, Port2, "Message not accepted by mail server."),
Port2.
smtp_close(Mod, Port) ->
smtp_put(Mod, <<".">>, Port),
smtp_expect(Mod, <<"250">>, Port, "Message not accepted by mail server."),
Mod:close(Port),
ok.
format_email(undefined, Default) -> format_email(Default);
format_email(Other, _Default) -> format_email(Other).
format_email(Addr) when is_list(Addr) ->
case lists:splitwith(fun(I) -> I =/= $< end, Addr) of
{_, []} -> [list_to_binary([$< | Addr]), $>];
{_, A} -> list_to_binary(A)
end;
format_email(Addr) when is_binary(Addr) ->
case binary:match(Addr, <<"<">>) of
{0, _} -> Addr;
{I, _} -> binary:part(Addr, {I, byte_size(Addr) - I});
nomatch -> [$<, Addr, $>]
end.
send_recipients(Mod, To, Port) when is_binary(To) ->
send_recipients2(Mod, [<<"RCPT TO: ">>, format_email(To)], Port);
send_recipients(Mod, [R|_] = Addr, Port) when is_integer(R) ->
send_recipients2(Mod, [<<"RCPT TO: ">>, format_email(Addr)], Port);
send_recipients(Mod, List, Port) when is_list(List) ->
[send_recipients2(Mod, [<<"RCPT TO: ">>, format_email(A)], Port) || A <- List],
ok.
send_recipients2(Mod, Data, Port) ->
smtp_put(Mod, Data, Port),
smtp_expect(Mod, <<"250">>, Port, undefined).
smtp_put(Mod, Message, Port) ->
Mod:send(Port, [Message,<<"\r\n">>]).
smtp_expect(Mod, Code, Port, ErrorMsg) ->
smtp_expect(Mod, Code, Port, ErrorMsg, 0, []).
smtp_expect(Mod, Code, Port, ErrorMsg, N, Acc) when is_binary(Code) ->
case Mod:recv(Port, 0, 15000) of
{ok, <<RespCode:3/binary, C, Rest/binary>> = Bin} when RespCode =:= Code ->
case C of
$ when Acc =:= [] ->
{ok, trim_nl(Rest)};
$ ->
{ok, Acc};
$- when N =:= 0 ->
smtp_expect(Mod, Code, Port, ErrorMsg, N+1, Acc);
$- ->
ExtensionAcc = parse_extension(Rest, Acc),
smtp_expect(Mod, Code, Port, ErrorMsg, N+1, ExtensionAcc);
_ when ErrorMsg =:= undefined ->
throw(Bin);
_ ->
throw(ErrorMsg)
end;
{ok, Other} when ErrorMsg =:= undefined ->
throw(Other);
{ok, _Other} ->
throw(ErrorMsg);
{error, closed} ->
throw("Socket closed unexpectedly");
{error, Reason} ->
throw({Mod, recv, Reason, lists:flatten(inet:format_error(Reason))})
end.
parse_extension(Bin, Acc) ->
case binary:match(Bin, <<" ">>) of
{I, _} ->
<<E:I/binary, $ , Args/binary>> = Bin,
[{to_upper(E), trim_nl(Args)} | Acc];
nomatch ->
case binary:match(Bin, <<"=">>) of
nomatch ->
[{to_upper(trim_nl(Bin)), true} | Acc];
_ ->
Acc
end
end.
to_upper(Bin) -> to_upper(byte_size(Bin)-1, Bin).
to_upper(-1, Bin) -> Bin;
to_upper(I, Bin) ->
case binary:at(Bin, I) of
C when C >= $a, $z =< C ->
list_to_binary(string:to_upper(binary_to_list(Bin)));
_ ->
to_upper(I-1, Bin)
end.
trim_nl(Bin) ->
case binary:match(Bin, [<<"\r">>, <<"\n">>]) of
{I, _} ->
binary:part(Bin, {0, I});
nomatch ->
Bin
end.
print(debug, Fmt, Args) ->
io:format("SMTP: " ++ Fmt, Args);
print(_, _Fmt, _Args) ->
ok.
%% Add an . at all lines starting with a dot.
dot_escape(Data, NL) ->
dot_escape(Data, NL, []).
dot_escape([], NL, Acc) ->
{NL, lists:reverse(Acc)};
dot_escape([$.|Rest], true, Acc) ->
dot_escape(Rest, false, [$.,$.|Acc]);
dot_escape([$\n|Rest], _, Acc) ->
dot_escape(Rest, true, [$\n|Acc]);
dot_escape([C|Rest], _, Acc) ->
dot_escape(Rest, false, [C|Acc]).
%%
sting2base64(String) ->
sting2base64(String, []).
sting2base64([], Acc) ->
lists:reverse(Acc);
sting2base64(String, Acc) ->
case str2b64_line(String, []) of
{ok, Line, Rest} ->
sting2base64(Rest, ["\n",Line|Acc]);
{more, Cont} ->
lists:reverse(["\n",str2b64_end(Cont)|Acc])
end.
%%
str2b64_line(S, []) -> str2b64_line(S, [], 0);
str2b64_line(S, {Rest,Acc,N}) -> str2b64_line(Rest ++ S, Acc, N).
str2b64_line(S, Out, 76) -> {ok,lists:reverse(Out),S};
str2b64_line([C1,C2,C3|S], Out, N) ->
O1 = e(C1 bsr 2),
O2 = e(((C1 band 16#03) bsl 4) bor (C2 bsr 4)),
O3 = e(((C2 band 16#0f) bsl 2) bor (C3 bsr 6)),
O4 = e(C3 band 16#3f),
str2b64_line(S, [O4,O3,O2,O1|Out], N+4);
str2b64_line(S, Out, N) ->
{more,{S,Out,N}}.
%%
str2b64_end({[C1,C2],Out,_N}) ->
O1 = e(C1 bsr 2),
O2 = e(((C1 band 16#03) bsl 4) bor (C2 bsr 4)),
O3 = e( (C2 band 16#0f) bsl 2 ),
lists:reverse(Out, [O1,O2,O3,$=]);
str2b64_end({[C1],Out,_N}) ->
O1 = e(C1 bsr 2),
O2 = e((C1 band 16#03) bsl 4),
lists:reverse(Out, [O1,O2,$=,$=]);
str2b64_end({[],Out,_N}) -> lists:reverse(Out);
str2b64_end([]) -> [].
%%
boundary_bin([]) ->
undefined;
boundary_bin(_) ->
rand:seed(exs64),
<<"Boundary_(", (list_to_binary(random_list(10)))/binary, ")">>.
random_list(0) -> [];
random_list(N) -> [64+rand:uniform(25), 96+rand:uniform(25) | random_list(N-1)].
%%
e(X) when X >= 0, X < 26 -> X + $A;
e(X) when X >= 26, X < 52 -> X + $a - 26;
e(X) when X >= 52, X < 62 -> X + $0 - 52;
e(62) -> $+;
e(63) -> $/;
e(X) -> erlang:error({badchar,X}).
%%
mail_headers(_Key, []) -> [];
mail_headers(Key, [H|T]) -> [mail_header(Key, H) | mail_headers(Key, T)].
mail_header(_Key, []) -> [];
mail_header(Key, Val) -> [Key, Val, <<"\r\n">>].