%    -*- Erlang -*-
%    File:        smtp.erl  (~jb/work/yaws/applications/mail/src/smtp.erl)
%    Author:        Johan Bevemyr
%    Created:        Tue Feb 24 23:15:59 2004
%    Purpose:

-module('smtp').
-author('jb@bevemyr.com').

-export([send/6]).

%% Example

%
% smtp:send("mail.bevemyr.com", "jb@bevemyr.com",
%            ["katrin@bevemyr.com","jb@bevemyr.com"],
%            "Test Subject",
%            "My Message", [{"file1.txt","text/plain","hej hopp igen"}]).
%


send(Server, From, To, Subject, Message, Attached) ->
    {ok, Port} = smtp_init(Server, From, To),
    Boundary="--Next_Part("++boundary_date()++")--",
    CommonHeaders =
        [mail_header("To: ", To),
         mail_header("From: ", From),
         mail_header("Subject: ", Subject)],
    Headers = case Attached of
                  [] ->
                      [mail_header("Content-Type: ", "text/plain"),
                       mail_header("Content-Transfer-Encoding: ", "8bit")];
                  _ ->
                      [mail_header("Mime-Version: ", "1.0"),
                       mail_header("Content-Type: ",
                                   "Multipart/Mixed;\r\n boundary=\""++
                                   Boundary++"\""),
                       mail_header("Content-Transfer-Encoding: ", "8bit")]
              end,
    smtp_send_part(Port, [CommonHeaders, Headers, "\r\n"]),
    case Attached of
        [] ->
            ok;
        _ ->
            smtp_send_part(Port, ["--",Boundary,"\r\n",
                                  mail_header("Content-Type: ",
                                              "Text/Plain; charset=us-ascii"),
                                  mail_header("Content-Transfer-Encoding: ",
                                              "8bit"),
                                  "\r\n"])
    end,
    smtp_send_message(Port, Message),
    case Attached of
        [] ->
            smtp_send_part(Port, ["\r\n.\r\n"]),
            smtp_close(Port);
        Files ->
            smtp_send_attachments(Port, Boundary, Files),
            smtp_send_part(Port, ["\r\n.\r\n"]),
            smtp_close(Port)
    end.


smtp_send_attachments(Port, Boundary, []) ->
    smtp_send_part(Port, ["\r\n--",Boundary,"--\r\n"]);
smtp_send_attachments(Port, Boundary, [{FileName,ContentType,Data}|Rest]) ->
    smtp_send_part(Port, ["\r\n--",Boundary,"\r\n",
                          mail_header("Content-Type: ", ContentType),
                          mail_header("Content-Transfer-Encoding: ",
                                      "base64"),
                          mail_header("Content-Disposition: ",
                                      "attachment; filename=\""++
                                      FileName++"\""),
                          "\r\n"
                         ]),
    smtp_send_b64(Port, Data),
    smtp_send_attachments(Port, Boundary, Rest).


smtp_send_b64(Port, Data) ->
    B64 = str2b64_final(Data),
    gen_tcp:send(Port, B64).

boundary_date() ->
    dat2str_boundary(calendar:local_time()).

dat2str_boundary({{Y, Mo, D}, {H, M, S}}) ->
    lists:flatten(
      io_lib:format("~s_~2.2.0w_~s_~w_~2.2.0w:~2.2.0w:~2.2.0w_~w",
                    [weekday(Y,Mo,D), D, int_to_mt(Mo),
                     Y,H,M,S,bin2int(crypto:strong_rand_bytes(4))])).

bin2int(Bin) ->
    lists:foldl(fun(N, Acc) -> Acc * 256 + N end, 0, binary_to_list(Bin)).


smtp_init(Server, From, Recipients) ->
    {ok, Port} = gen_tcp:connect(Server, 25, [{active, false},
                                              {reuseaddr,true},
                                              binary]),
    smtp_expect(220, Port, "SMTP server does not respond"),
    smtp_put( smtp_from(From), Port ),
    smtp_expect(250, Port, "Sender not accepted by mail server"),
    send_recipients(Recipients, Port),
    smtp_put("DATA", Port),
    smtp_expect(354, Port, "Message not accepted by mail server."),
    {ok, Port}.


smtp_close(Port) ->
    smtp_put(".", Port),
    smtp_expect(250, Port, "Message not accepted by mail server."),
    gen_tcp:close(Port),
    ok.

smtp_send_part(Port, Data) ->
    gen_tcp:send(Port, Data).

smtp_send_message(Port, Data) ->
    {_LastNL, Escaped} = dot_escape(Data, true),
    gen_tcp:send(Port, Escaped).

send_recipients( Recipients, Port ) ->
        Fun = fun (R) ->
                        smtp_put( smtp_recipient(R), Port),
                        smtp_expect(250, Port, io_lib:format("Recipient ~s not accepted.",[R]))
                end,
        lists:foreach( Fun, Recipients ).

smtp_put(Message, Port) ->
    gen_tcp:send(Port, [Message,"\r\n"]).

smtp_expect(Code, Port, ErrorMsg) ->
    smtp_expect(Code, Port, [], ErrorMsg).

smtp_expect(Code, Port, Acc, ErrorMsg) ->
    Res = gen_tcp:recv(Port, 0, 15000),
    case Res of
        {ok, Bin} ->
            NAcc = Acc++binary_to_list(Bin),
            case string:chr(NAcc, $\n) of
                0 ->
                    smtp_expect(Code, Port, NAcc, ErrorMsg);
                _N ->
                    ResponseCode = to_int(NAcc),
                    if
                        ResponseCode == Code -> ok;
                        true -> throw({error, ErrorMsg})
                    end
            end;
        Err ->
            throw({error, Err})
    end.

%% add smtp from prelude. add <> around address (if needed)
smtp_from( Address ) ->
        lists:append( "MAIL FROM: ", add_angle_brackets( Address ) ).

%% add smtp recipients prelude. add <> around address (if needed)
smtp_recipient( Address ) ->
        lists:append( "RCPT TO: ", add_angle_brackets( Address ) ).

%% make sure the address has <> around itself
add_angle_brackets( Address ) ->
        add_angle_bracket_start( add_angle_bracket_close(Address) ).

add_angle_bracket_start( [$<|T] ) -> [$<|T];
add_angle_bracket_start( Address ) -> [$<|Address].

%% add > at the end of address, if it is not present
add_angle_bracket_close( Address ) ->
        case lists:reverse( Address ) of
        [$>|_T] -> Address;
        Reversed -> lists:reverse( [$>|Reversed] )
        end.

%% 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]).

%%

%dot_unescape(Data) ->
%    {_,Dt} = dot_unescape(Data, true, []),
%    Dt.
%
%dot_unescape([], NL, Acc) ->
%    {NL, lists:reverse(Acc)};
%dot_unescape([$.|Rest], true, Acc) ->
%    dot_unescape(Rest, false, Acc);
%dot_unescape([$\n|Rest], _, Acc) ->
%    dot_unescape(Rest, true, [$\n|Acc]);
%dot_unescape([L|Rest], NL, Acc) when list(L) ->
%    {NL2, L2} = dot_unescape(L, NL, []),
%    dot_unescape(Rest, NL2, [L2|Acc]);
%dot_unescape([C|Rest], _, Acc) ->
%    dot_unescape(Rest, false, [C|Acc]).


%%

str2b64_final(String) ->
    str2b64_final(String, []).


str2b64_final([], Acc) ->
    lists:reverse(Acc);
str2b64_final(String, Acc) ->
    case str2b64_line(String, []) of
        {ok, Line, Rest} ->
            str2b64_final(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([]) -> [].

%%

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}).


%%


weekday(Y,Mo,D) ->
    int_to_wd(calendar:day_of_the_week(Y,Mo,D)).

int_to_wd(1) -> "Mon";
int_to_wd(2) -> "Tue";
int_to_wd(3) -> "Wed";
int_to_wd(4) -> "Thu";
int_to_wd(5) -> "Fri";
int_to_wd(6) -> "Sat";
int_to_wd(7) -> "Sun".

int_to_mt(1)  -> "Jan";
int_to_mt(2)  -> "Feb";
int_to_mt(3)  -> "Mar";
int_to_mt(4)  -> "Apr";
int_to_mt(5)  -> "May";
int_to_mt(6)  -> "Jun";
int_to_mt(7)  -> "Jul";
int_to_mt(8)  -> "Aug";
int_to_mt(9)  -> "Sep";
int_to_mt(10) -> "Oct";
int_to_mt(11) -> "Nov";
int_to_mt(12) -> "Dec".

%%

mail_header(_Key, []) -> [];
mail_header(Key, Val) -> Key++Val++"\r\n".

%%

to_int(Str) ->
    to_int(Str, 0).

to_int([D|Ds], Acc) when D >= $0, D =< $9->
    to_int(Ds, Acc*10+D-$0);
to_int(_, Acc) -> Acc.

%%

