diff --git a/.gitignore b/.gitignore index a41afba..58a60f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ *.beam .rebar erl_crash.dump ebin deps db log .*.swp Mnesia* test.spec +cover.spec test_logs rel/kolab_guam diff --git a/Makefile b/Makefile index 67f2f8a..c16fb38 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,28 @@ REBAR = ./rebar #$(shell which rebar || echo ./rebar) ENABLE_STATIC = no all: deps-up guam deps: $(REBAR) get-deps deps-up: deps $(REBAR) update-deps guam: ENABLE_STATIC=$(ENABLE_STATIC) $(REBAR) compile run: erl -pa apps/*/ebin deps/*/ebin -config app -s kolab_guam release: $(REBAR) compile generate -test.spec: test.spec.in +test.spec: test.spec.in cover.spec.in cat test.spec.in | sed -e "s,@PATH@,$(PWD)," > $(PWD)/test.spec + cat cover.spec.in | sed -e "s,@PATH@,$(PWD)," > $(PWD)/cover.spec test: test.spec guam mkdir -p test_logs ct_run -pa $(PWD)/apps/*/ebin -pa $(PWD)/deps/*/ebin -spec test.spec -erl_args -config $(PWD)/apps/kolab_guam/test/test.config -s kolab_guam diff --git a/app.config b/app.config index 3b7f6bd..07e68e5 100644 --- a/app.config +++ b/app.config @@ -1,11 +1,73 @@ [ %% SASL config {sasl, [ {sasl_error_logger, {file, "log/sasl-error.log"}}, {errlog_type, error}, {error_logger_mf_dir, "log/sasl"}, % Log directory {error_logger_mf_maxbytes, 10485760}, % 10 MB max file size {error_logger_mf_maxfiles, 5} % 5 files max - ]} + ]}, +{ kolab_guam, + [ + { imap_servers, [ + { default, [ + { host, "192.168.56.101" }, + { port, 143 }, + { tls, no } + ] + }, + { kolabsys, [ + { host, "imap.kolabsys.com" }, + { port, 143 }, + { tls, starttls } + ] + } + ] + }, + { listeners, [ + { default, [ + { port, 1143 }, + { imap_server, default }, + { rules, [ + { filter_groupware, [] } + ] + }, + { tls_config, [ { certfile, "/etc/ssl/sample.pem" } ] } + ] + }, + { default_tls, [ + { port, 1993 }, + { imap_server, default }, + { rules, [ + { filter_groupware, [] } + ] + }, + { implicit_tls, true }, + { tls_config, [ { certfile, "/etc/ssl/sample.cert" }, { keyfile, "/etc/ssl/sample.key" } ] } + ] + }, + { kolabsys, [ + { port, 1994 }, + { imap_server, kolabsys }, + { rules, [ + { filter_groupware, [] } + ] + } + ] + } + ] + } + ] +}, +{ lager, + [ + { + handlers, + [ + { lager_console_backend, debug }, + { lager_file_backend, [ { file, "log/error.log"}, { level, error } ] }, + { lager_file_backend, [ { file, "log/console.log"}, { level, debug } ] } + ] } + ] } ]. diff --git a/apps/kolab_guam/src/kolab_guam_session.erl b/apps/kolab_guam/src/kolab_guam_session.erl index 06b4187..60a28cb 100644 --- a/apps/kolab_guam/src/kolab_guam_session.erl +++ b/apps/kolab_guam/src/kolab_guam_session.erl @@ -1,314 +1,314 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% 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 . -module(kolab_guam_session). -behaviour(gen_server). --include_lib("eimap/src/eimap.hrl"). %% API -export([ start_link/6 ]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). %% state record definition --record(state, { socket, super_pid, tls_config = [], client_implicit_tls = false, client_tls_active = false, server_config = #eimap_server_config{}, +-record(state, { socket, super_pid, tls_config = [], client_implicit_tls = false, client_tls_active = false, server_config = [], rules_active = [], rules_deciding = [], imap_session, inflator, deflator }). %% public API start_link(SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules) -> gen_server:start_link(?MODULE, [SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules], []). %% gen_server API -init([SupervisorPID, ListenSocket, ImapConfig, ImplicitTLS, TLSConfig, Rules]) -> +init([SupervisorPID, ListenSocket, ServerConfig, ImplicitTLS, TLSConfig, Rules]) -> %% accepting a connection is blocking .. so do it async %% lager:debug("Starting a session handler on socket ~p for listener ~p", [ListenSocket, SupervisorPID]), process_flag(trap_exit, true), ActiveRules = init_rules(Rules), gen_server:cast(self(), accept), %% lager:debug("Rules are ~p from ~p", [ActiveRules, Rules]), - { ok, #state{ socket = ListenSocket, super_pid = SupervisorPID, client_implicit_tls = ImplicitTLS, tls_config = TLSConfig, server_config = ImapConfig, rules_deciding = ActiveRules } }. + { ok, #state{ socket = ListenSocket, super_pid = SupervisorPID, client_implicit_tls = ImplicitTLS, tls_config = TLSConfig, server_config = ServerConfig, rules_deciding = ActiveRules } }. handle_call(_Request, _From, State) -> { reply, ok, State }. handle_cast(accept, State = #state{ socket = ListenSocket, server_config = ServerConfig }) -> { ok, AcceptSocket, TLSActive } = accept_client(ListenSocket, State), { ok, ImapSession } = eimap:start_link(ServerConfig), eimap:connect(ImapSession, self(), server_hello), { noreply, State#state{ socket = AcceptSocket, imap_session = ImapSession, client_tls_active = TLSActive } }; handle_cast(_Msg, State) -> { noreply, State }. handle_info({ tcp_closed, _Socket }, State) -> %lager:debug("Client closed socket"), { stop, normal, State }; handle_info({ tcp_error, _Socket, _Error }, State) -> %lager:debug("Socket error"), { stop, normal, State }; handle_info({ ssl_closed, _Socket }, State) -> %lager:debug("Client closed socket"), { stop, normal, State }; handle_info({ ssl_error, _Socket, _Error }, State) -> %lager:debug("Socket error"), { stop, normal, State }; handle_info({ tcp, Socket, Data }, #state{ client_tls_active = false } = State) -> %lager:debug("Data coming in from client over TCP ~s", [Data]), process_client_data(Socket, Data, State); handle_info({ ssl, Socket, Data }, State) -> %lager:debug("Data coming in from client over SSL, ~p", [Data]), process_client_data(Socket, Data, State); handle_info({ server_hello, ServerHello }, #state{ imap_session = ImapSession, tls_config = TLSConfig, socket = Socket, client_tls_active = TLSActive, deflator = Deflator } = State) -> CorrectedHello = correct_hello(TLSActive, TLSConfig, ServerHello), eimap:start_passthrough(ImapSession, self()), - relay_response(Socket, postprocess_server_data(Deflator, CorrectedHello), TLSActive), + relay_response(Socket, postprocess_server_data(Deflator, <>), TLSActive), { noreply, State }; handle_info({ { rule_data, Module, ResponseToken }, Data }, #state{ rules_active = ActiveRules } = State) -> %lager:debug("Got back data requested by rule ~p: ~p", [Module, Data]), NewActiveRules = case proplists:get_value(Module, ActiveRules) of undefined -> ActiveRules; ModuleState -> NewModuleState = Module:imap_data(ResponseToken, Data, ModuleState), lists:reverse(lists:foldl(fun({ Rule, RuleState }, Acc) -> case Rule =:= Module of true -> [{ Rule, NewModuleState }|Acc]; _ -> [{ Rule, RuleState }|Acc] end end, [], ActiveRules)) end, %TODO: should we also support non-active rules doing imapy things here? { noreply, State#state{ rules_active = NewActiveRules } }; handle_info({ imap_server_response, Data }, #state{ socket = Socket, imap_session = ImapSession, client_tls_active = TLS, deflator = Deflator, rules_active = ActiveRules } = State) -> %lager:debug("FROM SERVER: ~s", [Data]), { ModifiedData, CurrentlyActiveRules } = apply_ruleset_serverside(ImapSession, Data, ActiveRules), relay_response(Socket, postprocess_server_data(Deflator, ModifiedData), TLS), { noreply, State#state{ rules_active = CurrentlyActiveRules } }; handle_info({ 'EXIT', PID, _Reason }, #state { imap_session = PID } = State) -> { stop, normal, State }; handle_info(Info, State) -> lager:debug("Received unexpected info... ~p", [Info]), { noreply, State }. terminate(_Reason, #state{ inflator = Inflator, deflator = Deflator, socket = Socket, client_tls_active = TLS }) -> %lager:debug("Termination!~p", [self()]), close_zlib_handle(Inflator), close_zlib_handle(Deflator), close_socket(TLS, Socket), ok. code_change(_OldVsn, State, _Extra) -> { ok, State }. %% private API accept_client(ListenSocket, #state{ client_implicit_tls = true, super_pid = SupervisorPID }) -> { ok, AcceptSocket } = ssl:transport_accept(ListenSocket), ok = ssl:ssl_accept(AcceptSocket), ok = ssl:setopts(AcceptSocket, [{ active, once }, { mode, binary }]), %% start a new accepting process to replace this one, which is now i use supervisor:start_child(SupervisorPID, []), ok = ssl:setopts(ListenSocket, [{ active, once }, { mode, binary }]), % lager:info("~p All done!", [self()]), { ok, AcceptSocket, true }; accept_client(ListenSocket, #state{ super_pid = SupervisorPID }) -> { ok, AcceptSocket } = gen_tcp:accept(ListenSocket), ok = inet:setopts(AcceptSocket, [{ active, once }, { mode, binary }]), %% start a new accepting process to replace this one, which is now i use supervisor:start_child(SupervisorPID, []), ok = inet:setopts(ListenSocket, [{ active, once }]), { ok, AcceptSocket, false }. close_zlib_handle(undefined) -> ok; close_zlib_handle(Z) -> zlib:close(Z). close_socket(_TLS, undefined) -> ok; close_socket(true, Socket) -> ssl:close(Socket); close_socket(_TLS, Socket) -> gen_tcp:close(Socket). -process_client_data(Socket, Data, #state{ rules_deciding = UndecidedRules, tls_config = TLSConfig, client_tls_active = TLS, rules_active = ActiveRules, socket = Socket, imap_session = ImapSession, inflator = Inflator, deflator = Deflator, server_config = #eimap_server_config{ tls = BackendTls } } = State) -> +process_client_data(Socket, Data, #state{ rules_deciding = UndecidedRules, tls_config = TLSConfig, client_tls_active = TLS, rules_active = ActiveRules, socket = Socket, imap_session = ImapSession, inflator = Inflator, deflator = Deflator, server_config = ServerConfig } = State) -> %%TODO: multipacket input from clients % TODO: refactor so starttls and compress commands can be made into rules PreprocessData = preprocess_client_data(Inflator, Data), %lager:info("FROM CLIENT: ~s", [PreprocessData]), { TLSActive, CurrentSocket, CurrentInflator, CurrentDeflator, CurrentUndecidedRules, CurrentActiveRules } = case check_for_transmission_change_commands(TLS, TLSConfig, PreprocessData, Deflator, Socket) of { socket_upgraded, SSLSocket } -> %% if we have upgraded our socket, then do so to the backend if that hasn't happened auomatically - case BackendTls of + case proplists:get_value(implicit_tls, ServerConfig, false) of false -> eimap:starttls(ImapSession, undefined, undefined); _ -> ok end, { true, SSLSocket, Inflator, Deflator, UndecidedRules, ActiveRules }; { compression, NewInflator, NewDeflator } -> eimap:compress(ImapSession), % TODO: make optional { TLS, Socket, NewInflator, NewDeflator, UndecidedRules, ActiveRules }; nochange -> %%lager:debug("... now applying rules"), { ModifiedData, NewUndecidedRules, NewActiveRules } = apply_ruleset_clientside(ImapSession, Socket, PreprocessData, UndecidedRules, ActiveRules), %%lager:info("The modified data is: ~s", [ModifiedData]), %lager:info("The post-processed data is: ~s", [PostProcessed]), eimap:passthrough_data(ImapSession, ModifiedData), { TLS, Socket, Inflator, Deflator, NewUndecidedRules, NewActiveRules} end, set_socket_active(TLSActive, CurrentSocket), { noreply, State#state{ rules_deciding = CurrentUndecidedRules, rules_active = CurrentActiveRules, socket = CurrentSocket, client_tls_active = TLSActive, inflator = CurrentInflator, deflator = CurrentDeflator } }. preprocess_client_data(undefined, Data) -> Data; preprocess_client_data(Z, Data) -> joined(zlib:inflate(Z, Data), <<>>). postprocess_server_data(undefined, Data) -> %% we aren't compressing so there is nothing to do Data; postprocess_server_data(Z, Data) -> joined(zlib:deflate(Z, Data, sync), <<>>). joined([], Binary) -> Binary; joined([H|Rest], Binary) -> joined(Rest, <>). init_rules(RuleConfig) -> init_rule(RuleConfig, []). init_rule([], Acc) -> Acc; init_rule([{ RuleName, Config }|RuleConfig], Acc) -> Module = full_rule_name(RuleName), %% we try to new the module, but if something goes wrong, e.g. it does not exist, %% then we skip this config block because it is BROKEN try Module:new(Config) of ModuleState -> init_rule(RuleConfig, [{ Module, ModuleState }|Acc]) catch Type:Error -> lager:warning("Could not create rule for ~p due to failure: ~p ~p", [RuleName, Type, Error]), init_rule(RuleConfig, Acc) end; init_rule([_|RuleConfig], Acc) -> init_rule(RuleConfig, Acc). full_rule_name(Module) when is_atom(Module) -> list_to_atom("kolab_guam_rule_" ++ atom_to_list(Module)). apply_ruleset_serverside(ImapSession, ServerData, CurrentlyActiveRules) -> %TODO: allow undecided rules to opt-in here as well apply_next_rule_serverside(ImapSession, ServerData, [], CurrentlyActiveRules). apply_next_rule_serverside(_ImapSession, ServerData, ActiveRulesAcc, []) -> { ServerData, lists:reverse(ActiveRulesAcc) }; apply_next_rule_serverside(ImapSession, ServerData, ActiveRulesAcc, [{ Module, RuleState } | ActiveRules]) -> %TODO: allow rules to remove themselves from the action during serverside processing? { ModifiedData, ModifiedRuleState } = Module:apply_to_server_message(ImapSession, ServerData, RuleState), apply_next_rule_serverside(ImapSession, ModifiedData, [{ Module, ModifiedRuleState } | ActiveRulesAcc], ActiveRules). apply_ruleset_clientside(ImapSession, Socket, ClientData, UndecidedRules, CurrentlyActiveRules) -> { StillUndecided, NewlyActive } = check_undecided(Socket, ClientData, UndecidedRules), ActiveRules = CurrentlyActiveRules ++ NewlyActive, { ModifiedData, ActiveRulesRun } = apply_next_rule_clientside(ImapSession, ClientData, [], ActiveRules), { ModifiedData, StillUndecided, ActiveRulesRun }. check_undecided(Socket, ClientData, Rules) -> check_next_undecided_rule(Socket, ClientData, Rules, { [], [] }). check_next_undecided_rule(_Socket, _ClientData, [], Accs) -> Accs; check_next_undecided_rule(Socket, ClientData, [Rule|Rules], { UndecidedAcc, NewActiveAcc }) -> { Module, RuleState } = Rule, %%lager:debug("Does ~p apply with state ~p? let's find out!", [Module, RuleState]), check_next_undecided_rule(Socket, ClientData, Rules, applies(Module, Module:applies(Socket, ClientData, RuleState), UndecidedAcc, NewActiveAcc)). applies(Module, { true, RuleState }, UndecidedAcc, NewActiveAcc) -> { UndecidedAcc, [{ Module, RuleState }|NewActiveAcc] }; applies(_Module, { false, _RuleState }, UndecidedAcc, NewActiveAcc) -> { UndecidedAcc, NewActiveAcc }; applies(Module, { notyet, RuleState }, UndecidedAcc, NewActiveAcc) -> { [{ Module, RuleState }|UndecidedAcc], NewActiveAcc }. apply_next_rule_clientside(_ImapSession, ClientData, ActiveRulesAcc, []) -> { ClientData, lists:reverse(ActiveRulesAcc) }; apply_next_rule_clientside(ImapSession, ClientData, ActiveRulesAcc, [{ Module, RuleState }|Rules]) -> { Data, NewState } = Module:apply_to_client_message(ImapSession, ClientData, RuleState), apply_next_rule_clientside(ImapSession, Data, [{ Module, NewState } | ActiveRulesAcc], Rules). relay_response(Socket, Data, false) -> %lager:debug("Sending over non-secure socket ..."), gen_tcp:send(Socket, Data); relay_response(Socket, Data, _TLS) -> %lager:debug("Sending over TLS!"), ssl:send(Socket, Data). check_for_transmission_change_commands(TLS, TLSConfig, Buffer, Deflator, Socket) -> {Tag, Command, _Data } = eimap_utils:split_command_into_components(Buffer), case check_tls_state(TLS, TLSConfig, Command, Deflator, Socket, Tag) of nochange -> check_compress_request(Deflator, Command, Socket, TLS, Tag); Response -> Response end. check_tls_state(false, TLSConfig, <<"STARTTLS">>, Deflator, Socket, Tag) -> start_client_tls(TLSConfig, Deflator, Socket, Tag); check_tls_state(false, TLSConfig, <<"starttls">>, Deflator, Socket, Tag) -> start_client_tls(TLSConfig, Deflator, Socket, Tag); check_tls_state(_TLS, _TLSConfig, _Buffer, _Deflator, _Socket, _Tag) -> nochange. start_client_tls(TLSConfig, Deflator, Socket, Tag) -> Response = <>, relay_response(Socket, postprocess_server_data(Deflator, Response), false), { ok, SSLSocket } = ssl:ssl_accept(Socket, TLSConfig), { socket_upgraded, SSLSocket }. check_compress_request(undefined, <<"COMPRESS">>, Socket, TLS, Tag) -> start_client_compression(Socket, TLS, Tag); check_compress_request(undefined, <<"compress">>, Socket, TLS, Tag) -> start_client_compression(Socket, TLS, Tag); check_compress_request(_Deflator, _Command, _Socket, _TLS, _Tag) -> nochange. start_client_compression(Socket, TLS, Tag) -> Response = <>, relay_response(Socket, postprocess_server_data(undefined, Response), TLS), %% create an inflate/deflate pair for use with the client Inflator = zlib:open(), ok = zlib:inflateInit(Inflator, -15), Deflator = zlib:open(), ok = zlib:deflateInit(Deflator, 1, deflated, -15, 8, default), { compression, Inflator, Deflator }. set_socket_active(true, Socket) -> ssl:setopts(Socket, [{ active, once }]); set_socket_active(_, Socket) -> inet:setopts(Socket, [{ active, once }]). -spec correct_hello(TLSActive :: true | false, TlSConfig :: [] | list(), ServerHello :: binary()) -> CorrectedHello :: binary(). correct_hello(true, _TLSConfig, ServerHello) -> % the connection is already secured, so don't advertise starttls to the client ensure_hello_does_not_have_starttls(ServerHello); correct_hello(_TLSActive, [], ServerHello) -> % guam does not have a TLS config and so can not provide TLS to the client ensure_hello_does_not_have_starttls(ServerHello); correct_hello(_TLSAcive, _TLSConfig, ServerHello) -> % guam has a TLS config, and it is not currently active, so make sure to include % STARTTLS in our response regardless of what the backend says ensure_hello_has_starttls(ServerHello). -ensure_hello_has_starttls(ServerHello) -> +ensure_hello_has_starttls(ServerResponse) -> + ServerHello = proplists:get_value(capabilities, ServerResponse, <<>>), case binary:match(ServerHello, <<"STARTTLS">>) of nomatch -> add_starttls_to_capabilities(ServerHello); _ -> ServerHello end. add_starttls_to_capabilities(ServerHello) -> case binary:match(ServerHello, <<"CAPABILITY ">>) of nomatch -> ServerHello; { Start, End } -> Prefix = binary:part(ServerHello, 0, Start + End), Suffix = binary:part(ServerHello, Start + End, size(ServerHello) - Start - End), <> end. ensure_hello_does_not_have_starttls(ServerHello) -> case binary:match(ServerHello, <<"STARTTLS">>) of nomatch -> ServerHello; { Start, End } -> Prefix = binary:part(ServerHello, 0, Start), Suffix = binary:part(ServerHello, Start + End, size(ServerHello) - Start - End), <> end. diff --git a/apps/kolab_guam/src/kolab_guam_sup.erl b/apps/kolab_guam/src/kolab_guam_sup.erl index 95cea1d..79622d5 100644 --- a/apps/kolab_guam/src/kolab_guam_sup.erl +++ b/apps/kolab_guam/src/kolab_guam_sup.erl @@ -1,66 +1,54 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% 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 . -module(kolab_guam_sup). -behaviour(supervisor). --include_lib("eimap/src/eimap.hrl"). %% API --export([start_link/0, default_imap_server_config/0, imap_server_config/1, imap_server_settings_to_config/1]). +-export([start_link/0, default_imap_server_config/0, imap_server_config/1]). %% Supervisor callbacks -export([init/1]). %% =================================================================== %% API functions %% =================================================================== start_link() -> supervisor:start_link({local, ?MODULE}, ?MODULE, []). default_imap_server_config() -> imap_server_config(default). imap_server_config(DefinitionName) when is_atom(DefinitionName) -> ServerDefinitions = application:get_env(kolab_guam, imap_servers, []), - case proplists:get_value(DefinitionName, ServerDefinitions, none) of - none -> default_imap_server_config(); - RawConfig -> imap_server_settings_to_config(RawConfig) - end. - -imap_server_settings_to_config(Config) when is_list(Config) -> - Defaults = #eimap_server_config{ }, - #eimap_server_config{ - host = proplists:get_value(host, Config, Defaults#eimap_server_config.host), - port = proplists:get_value(port, Config, Defaults#eimap_server_config.port), - tls = proplists:get_value(tls, Config, Defaults#eimap_server_config.tls) - }. + proplists:get_value(DefinitionName, ServerDefinitions, []). %% =================================================================== %% Supervisor callbacks %% =================================================================== init([]) -> lager:debug("Creating listeners ..."), ListenersConfig = application:get_env(kolab_guam, listeners, []), Children = [ create_child(Name, ListenerConfig) || { Name, ListenerConfig } <- ListenersConfig ], lager:debug("We have ~p~n", [Children]), { ok, { { one_for_one, 5, 10}, Children } }. create_child(Name, ListenerConfig) -> lager:debug("Making listener \"~p\" with ~p", [Name, ListenerConfig]), { Name, { kolab_guam_listener, start_link, [Name, ListenerConfig] }, permanent, 5000, supervisor, [Name] }. diff --git a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl index 7c0c15a..a383ad9 100644 --- a/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl +++ b/apps/kolab_guam/src/rules/kolab_guam_rule_filter_groupware.erl @@ -1,150 +1,137 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% 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 . -module(kolab_guam_rule_filter_groupware). -export([new/1, applies/3, imap_data/3, apply_to_client_message/3, apply_to_server_message/3]). -behavior(kolab_guam_rule). --record(state, { blacklist = [], tag = <<>>, active = false, last_chunk = <<>> }). +-record(state, { blacklist = [], tag = <<>>, active = false, last_chunk = <<>>, + trigger_commands = [<<"LIST">>, <<"list">>, <<"XLIST">>, <<"xlist">>, <<"LSUB">>, <<"lsub">>]}). new(_Config) -> #state { blacklist = undefined }. applies(_ConnectionDetails, Buffer, State) -> { _Tag, Command, Data } = eimap_utils:split_command_into_components(Buffer), %lager:debug("********** Checking ...~n Command: ~s ~s", [Command, Data]), - { apply_if_id_matches(Command, Data), State }. + { apply_if_id_matches(Command, Data, State#state.trigger_commands), State }. apply_to_client_message(ImapSession, Buffer, State) -> { Tag, Command, Data } = eimap_utils:split_command_into_components(Buffer), { Active, StateTag }= - if Command =:= <<"LIST">>; - Command =:= <<"list">>; - Command =:= <<"XLIST">>; - Command =:= <<"xlist">>; - Command =:= <<"LSUB">>; - Command =:= <<"lsub">> -> - case binary:match(Data, <<"*">>) of - nomatch -> { false, <<>> }; - _ -> - fetch_metadata(ImapSession, State), - { true, Tag } - end; - true -> { false, <<>> } + case lists:any(fun(T) -> (Command =:= T) andalso + ((binary:match(Data, <<"*">>) =/= nomatch) orelse (binary:match(Data, <<"%">>) =/= nomatch)) end, + State#state.trigger_commands) of + true -> fetch_metadata(ImapSession, State), { true, Tag }; + _ -> { false, <<>> } end, %lager:info("Client sent: ~s ~s ~p", [Command, Data, Active]), { Buffer, State#state{ active = Active, tag = StateTag }}. apply_to_server_message(_ImapSession, Buffer, #state{ active = true } = State) -> filter_folders(Buffer, State); apply_to_server_message(_ImapSession, Buffer, State) -> { Buffer, State }. imap_data(blacklist, { error, _Reason }, State) -> State; imap_data(blacklist, Response, State) -> %TODO: we don't need Foo/Bar if we already have Foo, so filter folders-of-groupwarefolders Blacklist = lists:foldl(fun({ _Folder, [ { _Property, null } ]}, Acc) -> Acc; ({ _Folder, [ { _Property, <<"mail", _Rest/binary>> } ]}, Acc) -> Acc; ({ Folder, _ }, Acc) -> [{ Folder, <> }|Acc] end, [], Response), State#state{ blacklist = Blacklist }. %%PRIVATE +fetch_metadata(none, #state{ blacklist = undefined }) -> ok; fetch_metadata(ImapSession, #state{ blacklist = undefined }) -> eimap:get_folder_metadata(ImapSession, self(), { rule_data, ?MODULE, blacklist }, "*", ["/shared/vendor/kolab/folder-type"]); fetch_metadata(_ImapSession, _State) -> ok. -apply_if_id_matches(<<"LIST">>, _Data) -> - true; -apply_if_id_matches(<<"XLIST">>, _Data) -> - true; -apply_if_id_matches(<<"LSUB">>, _Data) -> - true; -apply_if_id_matches(<<"list">>, _Data) -> - true; -apply_if_id_matches(<<"xlist">>, _Data) -> - true; -apply_if_id_matches(<<"lsub">>, _Data) -> - true; -apply_if_id_matches(<<"ID">>, Data) -> +apply_if_id_matches(<<"ID">>, Data, _TriggerCommands) -> apply_if_found_kolab(binary:match(Data, <<"/Kolab">>)); -apply_if_id_matches(_Command, _Data) -> notyet. +apply_if_id_matches(Command, _Data, TriggerCommands) -> + case lists:any(fun(T) -> Command =:= T end, TriggerCommands) of + true -> true; + _ -> notyet + end. apply_if_found_kolab(nomatch) -> true; apply_if_found_kolab(_) -> false. filter_folders(<<>>, State) -> { <<>>, State#state{ active = true } }; filter_folders(Buffer, #state{ last_chunk = LeftOvers } = State) -> FullBuffer = <>, { FullLinesBuffer, LastChunk } = eimap_utils:only_full_lines(FullBuffer), ListResponses = binary:split(FullLinesBuffer, <<"\r\n">>, [ global ]), { Response, More } = filter_folders(State, ListResponses, { <<>>, true }), %io:format("Filtered ... ~p~n", [Response]), { <>, State#state { active = More, last_chunk = LastChunk } }. filter_folders(_State, [], Return) -> Return; filter_folders(_State, _Folders, { Acc, false }) -> { Acc, false }; filter_folders(State, [Unfiltered|Folders], { Acc, _More }) -> filter_folders(State, Folders, filter_folder(State, Unfiltered, Acc)). filter_folder(_State, <<>>, Acc) -> { Acc, true }; filter_folder(State, <<"* LIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(State, <<"* XLIST ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(State, <<"* LSUB ", Details/binary>> = Response, Acc) -> { filter_on_details(State, Response, Acc, Details), true }; filter_folder(#state{ tag = Tag }, Response, Acc) -> HasMore = case byte_size(Tag) =< byte_size(Response) of true -> case binary:match(Response, Tag, [{ scope, { 0, byte_size(Tag) } }]) of nomatch -> true; _ -> false % we have found our closing tag! end; false -> true end, { add_response(Response, Acc), HasMore }. filter_on_details(#state{ blacklist = Blacklist }, Response, Acc, Details) -> %% first determine if we have a quoted item or a non-quoted item and start from there DetailsSize = byte_size(Details), { Quoted, Start } = case binary:at(Details, DetailsSize - 1) of $" -> { quoted, DetailsSize - 2 }; _ -> { unquoted, DetailsSize - 1 } end, Folder = find_folder_name(Details, Quoted, Start, Start, binary:at(Details, Start)), %io:format("COMPARING ~p ??? ~p~n", [Folder, in_blacklist(Folder, Blacklist)]), case in_blacklist(Folder, Blacklist) of true -> Acc; _ -> add_response(Response, Acc) end. find_folder_name(Details, quoted, End, Start, $") -> binary:part(Details, Start + 1, End - Start); find_folder_name(Details, unquoted, End, Start, $ ) -> binary:part(Details, Start + 1, End - Start); find_folder_name(Details, _Quoted, _End, 0, _) -> Details; find_folder_name(Details, Quoted, End, Start, _) -> find_folder_name(Details, Quoted, End, Start - 1, binary:at(Details, Start - 1)). add_response(Response, <<>>) -> Response; add_response(Response, Acc) -> <>. +in_blacklist(_Folder, undefined) -> false; in_blacklist(_Folder, []) -> false; in_blacklist(Folder, [{ Literal, Prefix }|List]) -> case Literal =:= Folder of true -> true; _ -> case binary:match(Folder, Prefix) of { 0, _ } -> true; _ -> in_blacklist(Folder, List) end end. diff --git a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl index 15b0de4..b97d180 100644 --- a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl @@ -1,87 +1,89 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% 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 . -module(kolab_guam_rules_SUITE). % easier than exporting by name -compile(export_all). % required for common_test to work -include("ct.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% common test callbacks %% %%%%%%%%%%%%%%%%%%%%%%%%%%% % Specify a list of all unit test functions all() -> [ kolab_guam_rule_filter_groupware_responsefiltering_test, kolab_guam_rule_filter_groupware_responsefiltering_multipacket_test ]. % required, but can just return Config. this is a suite level setup function. init_per_suite(Config) -> Config. % required, but can just return Config. this is a suite level tear down function. end_per_suite(Config) -> Config. % optional, can do function level setup for all functions, % or for individual functions by matching on TestCase. init_per_testcase(_TestCase, Config) -> Config. % optional, can do function level tear down for all functions, % or for individual functions by matching on TestCase. end_per_testcase(_TestCase, Config) -> Config. % c("apps/kolab_guam/test/kolab_guam_sup_tests.erl"). eunit:test(kolab_guam_sup_tests). kolab_guam_rule_filter_groupware_responsefiltering_test(_TestConfig) -> Data = [ { <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n* LIST (\\Subscribed) \"/\" \"Calendar/Personal Calendar\"\r\n* LIST (\\Subscribed) \"/\" Configuration\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Contacts\r\n* LIST (\\Subscribed) \"/\" \"Contacts/Personal Contacts\"\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Files\r\n* LIST (\\Subscribed) \"/\" Journal\r\n* LIST (\\Subscribed) \"/\" Notes\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Tasks\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">>, <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> } ], Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), - { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(<<"7 list (subscribed) \"\" \"*\" return (special-use)">>, State), - lists:foreach(fun({ Input, Filtered }) -> { Filtered, NewState } = kolab_guam_rule_filter_groupware:apply_to_server_message(Input, ReadyState) end, Data). + ServerConfig = kolab_guam_sup:default_imap_server_config(), + { ok, ImapSession } = eimap:start_link(ServerConfig), + { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, State), + lists:foreach(fun({ Input, Filtered }) -> { Filtered, NewState } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, ReadyState) end, Data). kolab_guam_rule_filter_groupware_responsefiltering_multipacket_test(_TestConfig) -> Data = [ { [ <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n* LIST (\\Subscribed) \"/\" \"Calendar/Personal Calendar\"\r\n* LIST (\\Subscribed) \"/\" Configuration\r\n* LIST (\\Subscribed \\HasChildren) \"/\" Contacts\r\n* LIST (\\Subscribed) \"/\" \"Contacts/Personal Contacts\"\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Files\r\n* LIST (\\Subscribed) \"/\" Journal\r\n* LIST (\\Subscribed)">>, <<"\"/\" Notes\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Tasks\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> ], <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* LIST (\\Subscribed) \"/\" Archive\r\n* LIST (\\Subscribed) \"/\" Drafts\r\n* LIST (\\Subscribed) \"/\" Sent\r\n* LIST (\\Subscribed) \"/\" Spam\r\n* LIST (\\Subscribed) \"/\" Trash\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> } ], Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(<<"7 list (subscribed) \"\" \"*\" return (special-use)">>, State), lists:foreach(fun({ Input, Filtered }) -> Filtered = filter_groupware_packets(ReadyState, Input, <<>>) end, Data). filter_groupware_packets(_ReadyState, [], Buffer) -> Buffer; filter_groupware_packets(ReadyState, [Input|More], Buffer) -> { Processed, State } = kolab_guam_rule_filter_groupware:apply_to_server_message(Input, ReadyState), filter_groupware_packets(State, More, <>). diff --git a/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl b/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl index b9e2b80..14a1c5a 100644 --- a/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_sup_SUITE.erl @@ -1,83 +1,82 @@ %% Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% 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 . -module(kolab_guam_sup_SUITE). % easier than exporting by name -compile(export_all). % required for common_test to work --include_lib("eimap/src/eimap.hrl"). -include("ct.hrl"). %%%%%%%%%%%%%%%%%%%%%%%%%%% %% common test callbacks %% %%%%%%%%%%%%%%%%%%%%%%%%%%% % Specify a list of all unit test functions all() -> [imap_server_config_test, imap_server_settings_to_config_test]. % required, but can just return Config. this is a suite level setup function. init_per_suite(Config) -> Config. % required, but can just return Config. this is a suite level tear down function. end_per_suite(Config) -> Config. % optional, can do function level setup for all functions, % or for individual functions by matching on TestCase. init_per_testcase(_TestCase, Config) -> Config. % optional, can do function level tear down for all functions, % or for individual functions by matching on TestCase. end_per_testcase(_TestCase, Config) -> Config. % c("apps/kolab_guam/test/kolab_guam_sup_tests.erl"). eunit:test(kolab_guam_sup_tests). imap_server_settings_to_config_test(_TestConfig) -> Configs = [ { [], #eimap_server_config{} }, { [ { tls, false } ], #eimap_server_config{ tls = false } }, { [ { host, "192.168.56.101" }, { port, 993 }, { tls, true } ], #eimap_server_config{ host = "192.168.56.101", port = 993, tls = true } } ], lists:foreach(fun({ Config, Record }) -> Record = kolab_guam_sup:imap_server_settings_to_config(Config) end, Configs). default_imap_server_config_test(_TestConfig) -> Expected = #eimap_server_config{ host = "192.168.56.102", port = 994, tls = true }, Expected = kolab_guam_sup:default_imap_server_config(). imap_server_config_test(_TestConfig) -> Configs = [ { test_default, #eimap_server_config{ host = "192.168.56.101", port = 993, tls = false } } ], lists:foreach(fun({ Config, Record }) -> Record = kolab_guam_sup:imap_server_config(Config) end, Configs). diff --git a/guam.service b/contrib/guam.service similarity index 100% rename from guam.service rename to contrib/guam.service diff --git a/contrib/guam.sysvinit b/contrib/guam.sysvinit new file mode 100644 index 0000000..3003a23 --- /dev/null +++ b/contrib/guam.sysvinit @@ -0,0 +1,103 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start daemon at boot time +# Description: Enable service provided by daemon. +### END INIT INFO + +name=`basename $0` + +start_cmd="$name start" +restart_cmd="$name restart" +stop_cmd="$name stop" +ping_cmd="$name ping" +user="" + + +get_pid() { + cat "$pid_file" +} + +is_running() { + $ping_cmd > /dev/null 2>&1 +} + +case "$1" in + start) + if is_running; then + echo "Already started" + else + echo "Starting $name" + if [ -z "$user" ]; then + $start_cmd + else + sudo -u "$user" $start_cmd + fi + sleep 2 + + for i in {1..10} + do + if is_running; then + break; + fi + + echo -n "." + sleep 1 + done + + if ! is_running; then + echo "Unable to start" + exit 1 + fi + fi + ;; + stop) + if is_running; then + echo -n "Stopping $name.." + $stop_cmd + for i in {1..10} + do + if ! is_running; then + break + fi + + echo -n "." + sleep 1 + done + echo + + if is_running; then + echo "Not stopped; may still be shutting down or shutdown may have failed" + exit 1 + else + echo "Stopped" + if [ -f "$pid_file" ]; then + rm "$pid_file" + fi + fi + else + echo "Not running" + fi + ;; + restart) + $restart_cmd + ;; + status) + if is_running; then + echo "Running" + else + echo "Stopped" + exit 1 + fi + ;; + *) + echo "Usage: $0 {start|stop|restart|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/cover.spec b/cover.spec deleted file mode 100644 index e745c16..0000000 --- a/cover.spec +++ /dev/null @@ -1,2 +0,0 @@ -{level, details}. -{incl_dirs_r, ["/home/aseigo/src/pim/guam/apps/kolab_guam/ebin"]}. diff --git a/cover.spec.in b/cover.spec.in new file mode 100644 index 0000000..5da3359 --- /dev/null +++ b/cover.spec.in @@ -0,0 +1,2 @@ +{level, details}. +{incl_dirs_r, ["@PATH@/apps/kolab_guam/ebin"]}. diff --git a/rebar.config b/rebar.config index f0df955..bd55f4e 100644 --- a/rebar.config +++ b/rebar.config @@ -1,30 +1,30 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et { deps_dir, "deps" }. { deps, [ - { lager, ".*", { git, "https://github.com/basho/lager.git", { tag, "2.1.0" } } }, - { lager_syslog, "2.0.*", { git, "https://github.com/basho/lager_syslog.git", { tag, "2.0.3" } } }, - { eimap, ".*", { git, "https://git.kolab.org/diffusion/EI/eimap.git", { branch, "master" } } } + { lager, "(2.0|2.1|2.2).*", { git, "git://github.com/basho/lager.git", { tag, "2.2.0" } } }, + { lager_syslog, "2.*", { git, "git://github.com/basho/lager_syslog.git", { tag, "2.1.3" } } }, + { eimap, ".*", { git, "https://git.kolab.org/diffusion/EI/eimap.git", { tag, "0.2.2" } } } %% pull in the proper version of meck before jobs 0.3 gets around to pulling in the wrong version ] }. { erl_opts, [ %%no_debug_info, {parse_transform, lager_transform}, {platform_define, "(linux|solaris|freebsd|darwin)", 'HAVE_SENDFILE'} ] }. { sub_dirs, [ "apps/kolab_guam", "rel" ]}. { erl_first_files, ["apps/kolab_guam/src/kolab_guam_rule.erl"] }. { erl_opts, [debug_info, fail_on_warning] }. { eunit_opts, [verbose, {skip_deps, true }] }. { eunit_exclude_deps, true }. { cover_enabled, true }. %%{require_otp_vsn, "17"}. { pre_hooks, [ { clean, "rm -fr ebin priv erl_crash.dump" } ] }. diff --git a/rel/reltool.config b/rel/reltool.config index 298f1ac..1ece54f 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -1,51 +1,51 @@ %% -*- mode: erlang -*- %% ex: ft=erlang {sys, [ {lib_dirs, ["../deps"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "kolab_guam", "0.8", + {rel, "kolab_guam", "0.9", [ kernel, stdlib, sasl, compiler, syntax_tools, goldrush, lager, lager_syslog, crypto, kolab_guam ]}, {rel, "start_clean", "", [ kernel, stdlib ]}, {boot_rel, "kolab_guam"}, {profile, embedded}, {incl_cond, derived}, {excl_archive_filters, [".*"]}, %% Do not archive built libs {excl_sys_filters, ["^bin/(?!start_clean.boot)", "^erts.*/bin/(dialyzer|typer)", "^erts.*/(doc|info|include|lib|man|src)"]}, {excl_app_filters, ["\.gitignore"]}, {app, kolab_guam, [{mod_cond, app}, {incl_cond, include}, {lib_dir, "../apps/kolab_guam"}]} ]}. {target_dir, "kolab_guam"}. {overlay, [ {mkdir, "log/sasl"}, {copy, "files/erl", "\{\{erts_vsn\}\}/bin/erl"}, {copy, "files/nodetool", "releases/\{\{rel_vsn\}\}/nodetool"}, {copy, "kolab_guam/bin/start_clean.boot", "\{\{erts_vsn\}\}/bin/start_clean.boot"}, {copy, "files/kolab_guam", "bin/kolab_guam"}, {copy, "files/kolab_guam.cmd", "bin/kolab_guam.cmd"}, {copy, "files/start_erl.cmd", "bin/start_erl.cmd"}, %% Following line may be safely removed in new projects {copy, "files/install_upgrade.escript", "bin/install_upgrade.escript"}, {copy, "files/sys.config", "releases/\{\{rel_vsn\}\}/sys.config"}, {copy, "files/vm.args", "releases/\{\{rel_vsn\}\}/vm.args"} ]}.