%% Copyright 2021 Apheleia IT AG (http://www.apheleia.ch)
%%
%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com>
%% Christian Mollekopf (Apheleia IT) <mollekopf a apheleia.ch>
%%
%% This program is free software: you can redistribute it and/or modify
%% it under the terms of the GNU General Public License as published by
%% the Free Software Foundation, either version 3 of the License, or
%% (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%%
%% You should have received a copy of the GNU General Public License
%% along with this program.  If not, see <http://www.gnu.org/licenses/>.

-module(kolab_guam_rule_audit).
-export([new/1, applies/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]).
-behavior(kolab_guam_rule).

-include("kolab_guam_rule_audit.hrl").

new(_Config) -> #state { }.

peername({sslsocket, _, _} = Socket) -> ssl:peername(Socket);
peername(Socket) -> inet:peername(Socket).

applies(Socket, _Buffer, { _Tag, _Command, _Data }, State) ->
    {ok, {Ip, _Port}} = peername(Socket),
    % This command is always immediately active as we expect the LOGIN command at the beginning
    { true, State#state{ ip = Ip }}.


% Unused
imap_data(_ResponseToken, _Response, State) -> State.


apply_to_client_message(_ImapSession, Buffer, undefined, State) ->
    { Buffer, State };

% We just buffer the entire command once activated
apply_to_client_message(_ImapSession, Buffer, { _Tag, _Command, _Data }, #state{ buffer = LeftOvers, active = true } = State) ->
    { Buffer, State#state{ buffer = <<LeftOvers/binary, Buffer/binary>> }};

% Monitor for the trigger command, otherwise do nothing
apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) ->
    case is_triggering_command(Command, Data, State) of
        true -> apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State#state{ active = true, tag = Tag, command = Command });
        _ -> { Buffer, State }
    end.


apply_to_server_message(_ImapSession, Buffer, #state{ active = true, tag = Tag, ip = Ip, buffer = FullBuffer, command = Command } = State) ->
    NewState = case eimap_utils:is_tagged_response(Buffer, Tag) of
        tagged ->
            Username = extract_username(FullBuffer, Command),
            case eimap_utils:check_response_for_failure(Buffer, Tag) of
                ok ->
                    lager:info("login: ~s from ~s, OK", [Username, inet:ntoa(Ip)]),
                    State#state{ active = false, username = Username };
                { no, Reason } ->
                    lager:info("badlogin: ~s from ~s, NO: ~s", [Username, inet:ntoa(Ip), Reason]),
                    State#state{ active = false, username = Username };
                { bad, Reason } ->
                    lager:info("badlogin: ~s from ~s, BAD: ~s", [Username, inet:ntoa(Ip), Reason]),
                    State#state{ active = false, username = Username }
            end;
        untagged -> State
    end,
    { Buffer, NewState };

% Do nothing if not active
apply_to_server_message(_ImapSession, Buffer, State) ->
    { Buffer, State }.

%%PRIVATE
is_triggering_command(Command, Data, #state{ trigger_commands = TriggerCommands }) ->
    %% if the command is in the list of trigger commands and the ending is not "" (which means "send me
    %% the root and separator" according to RFC 3501), then it is treated as a triggering event
    lists:any(fun(T) -> (Command =:= T) andalso (binary:longest_common_suffix([Data, <<"\"\"">>]) =/= 2) end,
              TriggerCommands).


extract_username(FullBuffer, Command) ->
    case Command of
        Command when Command =:= <<"AUTHENTICATE">>; Command =:= <<"authenticate">> ->
            Lines = binary:split(FullBuffer, <<"\r\n">>, [ global ]),
            FirstLine = lists:nth(1, Lines),
            FirstLineParts = binary:split(FirstLine, <<" ">>, [ global ]),
            AuthenticateMethod = lists:last(FirstLineParts),
            case AuthenticateMethod of
                <<"PLAIN">> ->
                    Decoded = base64:decode(lists:nth(2, Lines)),
                    % In the form of \0$username\0$password
                    Split = binary:split(Decoded, <<0>>, [ global ]),
                    lists:nth(2, Split);
                <<"LOGIN">> ->
                    base64:decode(lists:nth(2, Lines));
                _ ->
                    %TODO SASL-IR would go here
                    lager:info("AUTHENTICATE method not implemented ~p", [AuthenticateMethod]),
                    <<"Not implemented">>
            end;
        <<"LOGIN">> ->
            List = binary:split(FullBuffer, <<" ">>, [ global ]),
            lists:nth(3, List)
    end.
