diff --git a/CHANGELOG.md b/CHANGELOG.md index bd8ae46..62d825d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,79 +1,85 @@ # Changelog All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ### Added ### Changed ### Deprecated ### Removed ### Fixed ### Security +## [0.9.5] - 2019-11-18 +### Added +- Support for LIST-STATUS and LIST-EXTENDED LIST responses +### Fixed +- Client buffer handling via guam-0.9.2-stalling-client-buffer-and-split-command-handling patch + ## [0.9.4] - 2018-02-19 ### Fixed - Support empty lines from the client ## [0.9.3] - 2017-05-03 ### Fixed - With a properly crafted folder list, clients could receive empty lines in filtered folder listings ## [0.9.2] - 2017-03-21 ### Fixed - Fix client message processing when there are no active rules ## [0.9.1] - 2016-02-20 ### Fixed - Improve SSL connection accepts (prevent timing errors on the socket) - Support fragmentary messages from clients (e.g. tag in one packet, cmd in another) ## [0.9.0] - 2016-07-29 ### Added - bind to a network interface (rather than an IP/host) with net_iface ### Changed - handle the implicit ssl upgrade a bit more manually, allowing faster replenish of the listener pool and simplifying socket setup code - upgraded build to rebar3 - Upgraded eimap to 0.3.0 ### Fixed - fix CAPABILITY response (was CAPABILITIES) ## [0.8.3] - 2016-08-08 ### Fixed - always close ssl sockets as an ssl socket - keep listen_socket separate from socket - not required to make the socket active to accept connections - ensure eimap processes are always terminated in all cases ## [0.8.2] - 2016-07-08 ### Added - listener_pool_size configuration option for listeners ### Changed - Default size of listener pool drops to 10 from 20 - Rate limit (by introducing a short wait) connection accept()s ### Fixed - Prevent starvation of the session pool due to clients dropping connections pre-accept() ## [0.8.1] - 2016-07-06 ### Added - ipv6 connections ### Changed - update to eimap 0.2.5 ### Fixed - Ignore non-listing LIST commands (e.g. requests for the root/separator) - Tidy up the server greetings ## [0.8.0] - 2016-06-08 ### Added - systemd service module - sysv init script ### Changed - Upgraded eimap to 0.2.4 ### Fixed - Support more variations of the LIST command args in the filter_groupware rule diff --git a/Makefile b/Makefile index 63cde4b..6500629 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,22 @@ REBAR = $(shell which rebar3 || echo ./rebar3) ENABLE_STATIC = no all: ENABLE_STATIC=$(ENABLE_STATIC) $(REBAR) compile clean: $(REBAR) clean run: $(REBAR) shell --config app.config release: $(REBAR) release 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 all - $(REBAR) ct --cover --readable --allow_user_terms + $(REBAR) ct --cover --readable=true --allow_user_terms diff --git a/apps/kolab_guam/src/kolab_guam.app.src b/apps/kolab_guam/src/kolab_guam.app.src index eb352d0..8700999 100644 --- a/apps/kolab_guam/src/kolab_guam.app.src +++ b/apps/kolab_guam/src/kolab_guam.app.src @@ -1,20 +1,20 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- {application, kolab_guam, [ {description, "IMAP session proxy"}, - {vsn, "0.9.4"}, + {vsn, "0.9.5"}, {registered, []}, {applications, [ kernel, stdlib, compiler, syntax_tools, goldrush, lager, crypto, ssl ]}, {mod, { kolab_guam, []}}, {env, [ ]} ]}. diff --git a/apps/kolab_guam/src/kolab_guam_session.erl b/apps/kolab_guam/src/kolab_guam_session.erl index ca4e633..e547c55 100644 --- a/apps/kolab_guam/src/kolab_guam_session.erl +++ b/apps/kolab_guam/src/kolab_guam_session.erl @@ -1,427 +1,445 @@ %% 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). %% 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, { listen_socket, socket = undefined, super_pid, tls_config = [], client_implicit_tls = false, client_tls_active = false, server_config = [], rules_active = [], rules_deciding = [], imap_session = undefined, inflator, deflator, buffered_client_data = <<>>, current_command_split = undefined, command_split_reset_trigger = reset_for_next_client_command }). %% 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, 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{ listen_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{ server_config = ServerConfig } = State) -> %% try to rate limit our responses a bit here so that hammering the socket with connections is survivable timer:sleep(3), { ok, AcceptSocket, TLSActive } = accept_client(State), { ok, ImapSession } = eimap:start_link(ServerConfig), eimap:connect(ImapSession, self(), server_hello), { noreply, State#state{ listen_socket = undefined, 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_implicit_tls = ImplicitTLS, client_tls_active = TLSActive, deflator = Deflator } = State) -> CorrectedHello = correct_hello(TLSActive, ImplicitTLS, TLSConfig, ServerHello), ServerIdent = proplists:get_value(server_id, ServerHello, <<>>), FullGreeting = <<"* OK [CAPABILITY ", CorrectedHello/binary, "] ", ServerIdent/binary, "\r\n">>, eimap:start_passthrough(ImapSession, self()), relay_response(Socket, postprocess_server_data(Deflator, FullGreeting), 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), NewSplitCommand = update_split_command_state(ModifiedData, State), { noreply, State#state{ rules_active = CurrentlyActiveRules, current_command_split = NewSplitCommand } }; handle_info({ 'EXIT', PID, _Reason }, #state { imap_session = PID } = State) -> { stop, normal, State#state{ imap_session = undefined } }; handle_info(Info, State) -> lager:debug("Received unexpected info... ~p", [Info]), { noreply, State }. terminate(_Reason, #state{ inflator = Inflator, imap_session = ImapSession, deflator = Deflator, socket = Socket, client_implicit_tls = ImplicitTLS, client_tls_active = TLS }) -> close_zlib_handle(Inflator), close_zlib_handle(Deflator), close_socket(ImplicitTLS, TLS, Socket), case ImapSession of undefined -> ok; _ -> exit(ImapSession, kill) end, ok. code_change(_OldVsn, State, _Extra) -> { ok, State }. %% private API %% update_split_command_state updates the split_command being held on to when we get a server response %% in the case of "transactional" messages (such as authentication) where the client and server enter a bidirectional conversation %% that is goverened by rules outside the the usual IMAP call/response pattern, we need to wait for the end of the server response %% since this is relatively expensive due to having to scan the data for the tagged server response, and is not necessary for all other %% IMAP commands, we shortcircuit when the command does not trigger a "transactional" interaction between client and server, and instead %% just always reset the split data state at that point update_split_command_state(Data, #state{ command_split_reset_trigger = reset_on_server_response, current_command_split = CurrentCommandSplit }) -> case CurrentCommandSplit of undefined -> undefined; { Tag, _Command, _Data } -> case binary:match(Data, <>) of nomatch -> CurrentCommandSplit; { 0, _ } -> undefined; { Start, _ } -> case binary:at(Data, Start - 1) of $\n -> undefined; _ -> CurrentCommandSplit end end end; update_split_command_state(_Data, _State) -> undefined. accept_client(#state{ client_implicit_tls = true, tls_config = TLSConfig, listen_socket = ListenSocket, super_pid = SupervisorPID }) -> AcceptSocket = accept_socket(ListenSocket, SupervisorPID), %% prep for the next listen { ok, SSLSocket } = ssl:ssl_accept(AcceptSocket, TLSConfig), ok = ssl:setopts(SSLSocket, [{ active, once }, { mode, binary }]), % lager:info("~p All done!", [self()]), { ok, SSLSocket, true }; accept_client(#state{ listen_socket = ListenSocket, super_pid = SupervisorPID }) -> AcceptSocket = accept_socket(ListenSocket, SupervisorPID), ok = inet:setopts(AcceptSocket, [{ active, once }, { mode, binary }]), { ok, AcceptSocket, false }. accept_socket(ListenSocket, SupervisorPID) -> AcceptResult = gen_tcp:accept(ListenSocket), %% start a new accepting process to replace this one, which is now in use supervisor:start_child(SupervisorPID, []), %% assert that the accept worked { ok, AcceptSocket } = AcceptResult, AcceptSocket. close_zlib_handle(undefined) -> ok; close_zlib_handle(Z) -> zlib:close(Z). close_socket(_ImplicitTLS, _TLS, undefined) -> ok; close_socket(_ImplicitTLS, true, Socket) -> ssl:close(Socket); close_socket(true, _TLS, Socket) -> ssl:close(Socket); close_socket(_ImplicitTLS, _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 = ServerConfig, current_command_split = CurrentCommandSplit } = State) -> %%TODO: multipacket input from clients % TODO: refactor so starttls and compress commands can be made into rules PreprocessData = preprocess_client_data(Inflator, Data, State), %lager:info("FROM CLIENT: ~s", [PreprocessData]), { TLSActive, CurrentSocket, CurrentInflator, CurrentDeflator, CurrentUndecidedRules, CurrentActiveRules, DataToBuffer, SplitCommand, SplitResetTrigger } = 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 proplists:get_value(implicit_tls, ServerConfig, false) of false -> eimap:starttls(ImapSession, undefined, undefined); _ -> ok end, { true, SSLSocket, Inflator, Deflator, UndecidedRules, ActiveRules, <<>>, undefined, undefined }; { compression, NewInflator, NewDeflator } -> eimap:compress(ImapSession), % TODO: make optional { TLS, Socket, NewInflator, NewDeflator, UndecidedRules, ActiveRules, <<>>, undefined, undefined }; nochange -> %%lager:debug("... now applying rules"), { ModifiedData, NewSplitCommand, NewSplitResetTrigger, NewUndecidedRules, NewActiveRules, PostAction } = apply_ruleset_clientside(ImapSession, Socket, PreprocessData, CurrentCommandSplit, UndecidedRules, ActiveRules), %%lager:info("The modified data is: ~s", [ModifiedData]), %lager:info("The post-processed data is: ~s", [PostProcessed]), BufferThisData = case PostAction of perform_passthrough -> + %lager:info("sending (no buffer): ~s", [ModifiedData]), eimap:passthrough_data(ImapSession, ModifiedData), <<>>; buffer_data -> - Data + % Originally Aaron uses Data here, but later on this buffer is assumed to be + % already decoded, so we do have to use PreprocessData here, I think. + case binary:matches(PreprocessData, <<"\r\n">>) of + [] -> + %lager:info("buffering: ~s", [PreprocessData]), + PreprocessData; + List -> + {FoundPos, _} = lists:last(List), + % I would like to have some binary:match for the last instead of the + % first occurrence; but I'm really inexperienced in erlang so I don't + % know how to solve this efficiently, so I'm using binary:matches with + % using the last element only + SplitPos = FoundPos + 2, + eimap:passthrough_data(ImapSession, binary:part(PreprocessData, 0, SplitPos)), + %lager:info("sending first part: ~s", [binary:part(PreprocessData, 0, SplitPos)] ), + %lager:info("buffering second part: ~s", [binary:part(PreprocessData, SplitPos, size(PreprocessData)-SplitPos)]), + binary:part(PreprocessData, SplitPos, size(PreprocessData)-SplitPos) + end end, { TLS, Socket, Inflator, Deflator, NewUndecidedRules, NewActiveRules, BufferThisData, NewSplitCommand, NewSplitResetTrigger } end, set_socket_active(TLSActive, CurrentSocket), - PrevBuffered = State#state.buffered_client_data, + %buffered_client_data is already in DataToBuffer via preprocess_client_data { noreply, State#state{ rules_deciding = CurrentUndecidedRules, rules_active = CurrentActiveRules, socket = CurrentSocket, client_tls_active = TLSActive, inflator = CurrentInflator, deflator = CurrentDeflator, - buffered_client_data = <>, + buffered_client_data = <>, current_command_split = SplitCommand, command_split_reset_trigger = SplitResetTrigger } }. preprocess_client_data(undefined, Data, #state{ buffered_client_data = Buffered }) -> <>; preprocess_client_data(Z, Data, #state{ buffered_client_data = Buffered }) -> Inflated = 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, _CurrentCommandSplit, [], []) -> - { ClientData, [], [], [], [], perform_passthrough }; + { ClientData, undefined, [], [], [], perform_passthrough }; apply_ruleset_clientside(ImapSession, Socket, ClientData, CurrentCommandSplit, UndecidedRules, CurrentlyActiveRules) -> { PostAction, SplitCommand, SplitResetTrigger } = case CurrentCommandSplit of undefined -> %We first have to check whether the command is an empty line. In such a case split_command_into_components would return an empty command, %even though the command is complete. case ClientData of <<"\r\n">> -> { perform_passthrough, CurrentCommandSplit, reset_for_next_client_command }; _ -> case eimap_utils:split_command_into_components(ClientData) of { _Tag, <<>>, <<>> } -> { buffer_data, undefined, reset_for_next_client_command }; { _Tag, Command, _Data } = Split -> { perform_passthrough, Split, when_to_reset_split(Command) } end end; _ -> { perform_passthrough, CurrentCommandSplit, reset_for_next_client_command } end, { StillUndecided, NewlyActive } = check_undecided(Socket, ClientData, SplitCommand, UndecidedRules), ActiveRules = CurrentlyActiveRules ++ NewlyActive, %lager:info("Active Rules: ~p", [ActiveRules]), { ModifiedData, ActiveRulesRun } = apply_next_rule_clientside(ImapSession, ClientData, SplitCommand, [], ActiveRules), { ModifiedData, SplitCommand, SplitResetTrigger, StillUndecided, ActiveRulesRun, PostAction }. when_to_reset_split(<<"AUTHENTICATE">>) -> reset_on_server_response; when_to_reset_split(<<"authenticate">>) -> reset_on_server_response; when_to_reset_split(_) -> reset_for_next_client_command. check_undecided(_Socket, _ClientData, undefined, Rules) -> %% if we do not have a properly split command ... do nothing! { Rules, [] }; check_undecided(Socket, ClientData, SplitCommand, Rules) -> check_next_undecided_rule(Socket, ClientData, SplitCommand, Rules, { [], [] }). check_next_undecided_rule(_Socket, _ClientData, _SplitCommand, [], Accs) -> Accs; check_next_undecided_rule(Socket, ClientData, SplitCommand, [Rule|Rules], { UndecidedAcc, NewActiveAcc }) -> { Module, RuleState } = Rule, %%lager:debug("Does ~p apply with state ~p? let's find out!", [Module, RuleState]), Application = Module:applies(Socket, ClientData, SplitCommand, RuleState), check_next_undecided_rule(Socket, ClientData, SplitCommand, Rules, applies(Module, Application, 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, _SplitCommand, ActiveRulesAcc, []) -> { ClientData, lists:reverse(ActiveRulesAcc) }; apply_next_rule_clientside(ImapSession, ClientData, SplitCommand, ActiveRulesAcc, [{ Module, RuleState }|Rules]) -> { Data, NewState } = Module:apply_to_client_message(ImapSession, ClientData, SplitCommand, RuleState), apply_next_rule_clientside(ImapSession, Data, SplitCommand, [{ 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), inet:setopts(Socket, [{ active, false }]), %% must be set to active false, otherwise can fail depending on timing { 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, ImplicitTLS :: true | false, TlSConfig :: [] | list(), ServerHello :: binary()) -> CorrectedHello :: binary(). correct_hello(true, true, _TLSConfig, ServerResponse) -> % the connection is already secured, so don't advertise starttls to the client ensure_hello_does_not_have_starttls(ServerResponse); correct_hello(true, _ImplicitTLS, _TLSConfig, ServerResponse) -> % the connection is already secured, so don't advertise starttls to the client ensure_hello_does_not_have_starttls(ServerResponse); correct_hello(_TLSActive, _ImplicitTLS, [], ServerResponse) -> % guam does not have a TLS config and so can not provide TLS to the client ensure_hello_does_not_have_starttls(ServerResponse); correct_hello(_TLSActive, _ImplicitTLS, _TLSConfig, ServerResponse) -> % 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(ServerResponse). 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 -> add_starttls_after_imap4_atom(ServerHello); { Start, End } -> Prefix = binary:part(ServerHello, 0, Start + End), Suffix = binary:part(ServerHello, Start + End, size(ServerHello) - Start - End), CorrectHello = <>, remove_auth_offers(CorrectHello) end. add_starttls_after_imap4_atom(ServerHello) -> case binary:match(ServerHello, <<"IMAP4rev1 ">>) of nomatch -> <<"STARTTLS ", ServerHello/binary>>; { Start, End } -> Prefix = binary:part(ServerHello, 0, Start + End), Suffix = binary:part(ServerHello, Start + End, size(ServerHello) - Start - End), CorrectHello = <>, remove_auth_offers(CorrectHello) end. ensure_hello_does_not_have_starttls(ServerResponse) -> ServerHello = proplists:get_value(capabilities, ServerResponse, <<>>), 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. remove_auth_offers(ServerHello) -> case binary:match(ServerHello, <<"AUTH=">>) of nomatch -> ensure_advertise_login_disabled(ServerHello); { Start, _End } -> Prefix = binary:part(ServerHello, 0, Start), Suffix = case binary:match(ServerHello, <<" ">>, [{ scope, { Start, size(ServerHello) - Start } }]) of nomatch -> %% end of the line, so no suffix <<>>; { SpaceStart, SpaceEnd } -> binary:part(ServerHello, SpaceStart + SpaceEnd, size(ServerHello) - SpaceStart - SpaceEnd) end, remove_auth_offers(<>) end. ensure_advertise_login_disabled(ServerHello) -> case binary:match(ServerHello, <<"LOGINDISABLED">>) of nomatch -> <>; _ -> ServerHello end. 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 decbd9a..7edf7e7 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,149 +1,222 @@ %% 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/4, imap_data/3, apply_to_client_message/4, apply_to_server_message/3]). -behavior(kolab_guam_rule). -include("kolab_guam_rule_filter_groupware.hrl"). new(_Config) -> #state { blacklist = undefined }. applies(_ConnectionDetails, _Buffer, { _Tag, Command, Data }, State) -> Applies = apply_if_id_matches(Command, Data, State), %lager:debug("********** Checking ...~n Command: ~s ~s, Result ~p", [Command, Data, Applies]), { Applies, State }. apply_to_client_message(_ImapSession, Buffer, undefined, State) -> { Buffer, State }; apply_to_client_message(ImapSession, Buffer, { Tag, Command, Data }, State) -> { Active, StateTag } = case is_triggering_command(Command, Data, State) 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 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). 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(<<"ID">>, Data, _State) -> apply_if_found_kolab(string:str(string:to_lower(binary_to_list(Data)), "/kolab")); apply_if_id_matches(Command, Data, State) -> case is_triggering_command(Command, Data, State) of true -> true; _ -> notyet end. apply_if_found_kolab(0) -> true; apply_if_found_kolab(_) -> false. possibly_append_newline(<<>>) -> <<>> ; possibly_append_newline(Response) -> <> . filter_folders(<<>>, State) -> { <<>>, State#state{ active = true } }; filter_folders(Buffer, #state{ last_chunk = LeftOvers } = State) -> % Add the left overs from the previous buffer to the current buffer FullBuffer = <>, % From that buffer, only take the complete lines and save off the remainder. { FullLinesBuffer, LastChunk } = eimap_utils:only_full_lines(FullBuffer), % Create a list so we can filter the individual folders ListResponses = binary:split(FullLinesBuffer, <<"\r\n">>, [ global ]), { Response, More } = filter_folders(State, ListResponses, { <<>>, true }), %io:format("Filtered ... ~p~n", [Response]), %Note that the last list item does not contain \r\n, so that needs to be added unless we filtered the complete content. { possibly_append_newline(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, <<"* STATUS ", 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)]), + %% Remove "*" and extract response command name + { _, Start, _ } = pop_token(Response), %% asterisk + { Cmd, _, _ } = pop_token(Start), %% command + + %% Extract folder name + Suffix = + case Cmd =:= <<"STATUS">> of + true -> Details; + _ -> + { Pos, _Length } = binary:match(Details, [<<")">>], []), + { _Delimiter, Rest, _} = pop_token(binary:part(Details, Pos + 2, byte_size(Details) - Pos - 2)), + Rest + end, + { Folder, _, _ } = pop_token(list_to_binary([Suffix, <<"\r\n">>])), + + %% Check the folder in blacklist + %% 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. + +%% pop_token from https://github.com/MainframeHQ/switchboard/blob/master/src/imap.erl (BSD Lic.) +%% with some small changes by Aleksander Machniak +pop_token(Data) -> + pop_token(Data, none). + +pop_token(<<>>, State) -> + {none, <<>>, State}; + +%% Consume hanging spaces +pop_token(<<" ", Rest/binary>>, none) -> + pop_token(Rest, none); + +%% \r\n +pop_token(<<$\r, $\n, Rest/binary>>, none) -> + {crlf, Rest, none}; + +%% NIL +pop_token(<<"NIL", Rest/binary>>, none) -> + {nil, Rest, none}; + +%% ( | ) | [ | ] +pop_token(<<$(, Rest/binary>>, none) -> + {'(', Rest, none}; +pop_token(<<$), Rest/binary>>, none) -> + {')', Rest, none}; + + +%% Atom +pop_token(<> = Data, {atom, AtomAcc}) when + C =:= 32; C =:= 40; C =:= 41; C =:= $(; C =:= $) -> + {AtomAcc, Data, none}; +pop_token(<<$\r, $\n, _/binary>> = Data, {atom, AtomAcc}) -> + {AtomAcc, Data, none}; +pop_token(<>, none) when C >= 35, C < 123 -> + pop_token(Rest, {atom, <>}); +pop_token(<>, {atom, AtomAcc}) when C >= 35, C < 123 -> + pop_token(Rest, {atom, <>}); + +%% Literal Strings +pop_token(<<${, Rest/binary>>, none) -> + pop_token(Rest, {literal, <<>>}); +pop_token(<<$}, $\r, $\n, Rest/binary>>, {literal, ByteAcc}) -> + pop_token(Rest, {literal, binary_to_integer(ByteAcc), <<>>}); +pop_token(<>, {literal, ByteAcc}) when D >= 48, D < 58 -> + pop_token(Rest, {literal, <>}); +pop_token(Binary, {literal, Bytes, LiteralAcc}) when is_integer(Bytes) -> + case Binary of + <> -> + {<>, Rest, none}; + _ -> + %% If the binary is too short, accumulate it in the state + pop_token(<<>>, {literal, Bytes - size(Binary), <>}) + end; + +%% Quoted Strings +pop_token(<<$", Rest/binary>>, none) -> + pop_token(Rest, {quoted, <<>>}); +pop_token(<<$\\, C, Rest/binary>>, {quoted, Acc}) -> + pop_token(Rest, {quoted, <>}); +pop_token(<<$", Rest/binary>>, {quoted, Acc}) -> + {Acc, Rest, none}; +pop_token(<<$\r, $\n, _>>, {quoted, _}) -> + throw({error, crlf_in_quoted}); +pop_token(<>, {quoted, Acc}) -> + pop_token(Rest, {quoted, <>}); + +pop_token(Binary, _) -> + {none, Binary, none}. diff --git a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl index 0f44cdd..738b5d3 100644 --- a/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl +++ b/apps/kolab_guam/test/kolab_guam_rules_SUITE.erl @@ -1,150 +1,213 @@ %% 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_lib("common_test/include/ct.hrl"). -include("../src/rules/kolab_guam_rule_filter_groupware.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 to be fed into the test, one tuple per iteration %% Tuple format: { [] = folder_blacklist, input_data, correct_output } Data = [ { [ {<<"Calendar">>, <<"Calendar/">>}, {<<"Calendar/Personal Calendar">>, <<"Calendar/Personal Calendar/">>}, {<<"Configuration">>, <<"Configuration/">>}, {<<"Contacts">>, <<"Contacts/">>}, {<<"Contacts/Personal Contacts">>, <<"Contacts/Personal Contacts/">>}, {<<"Files">>, <<"Files/">>}, {<<"Journal">>, <<"Journal/">>}, {<<"Notes">>, <<"Notes/">>}, {<<"Tasks">>, <<"Tasks/">>} ], <<"* 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">> } ], %% setup boilerplate Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), %% create the rule, ready for testing ClientData = <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, Split = eimap_utils:split_command_into_components(ClientData), { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, ClientData, Split, State), %% run the dataset through the rule lists:foreach(fun({ Blacklist, Input, Filtered }) -> BlacklistState = ReadyState#state{ blacklist = Blacklist }, { Filtered, _NewState } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, BlacklistState) end, Data). kolab_guam_rule_filter_groupware_responsefiltering_multipacket_test(_TestConfig) -> %% Data to be fed into the test, one tuple per iteration - %% Tuple format: { [ input_packets ] = complete_message, correct_output } + %% Tuple format: { [] = folder_blacklist, [] = input_data_packets, correct_output } Data = [ { [ {<<"Calendar">>, <<"Calendar/">>}, {<<"Calendar/Personal Calendar">>, <<"Calendar/Personal Calendar/">>}, {<<"Configuration">>, <<"Configuration/">>}, {<<"Contacts">>, <<"Contacts/">>}, {<<"Contacts/Personal Contacts">>, <<"Contacts/Personal Contacts/">>}, {<<"Files">>, <<"Files/">>}, {<<"Journal">>, <<"Journal/">>}, {<<"Notes">>, <<"Notes/">>}, {<<"Tasks">>, <<"Tasks/">>} ], [ <<"* 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">> + <<" \"/\" 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">> }, %Filter complete packet { [ {<<"Calendar">>, <<"Calendar/">>} ], [ <<"* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r\n">>, <<"7 OK Completed (0.000 secs 15 calls)\r\n">> ], <<"7 OK Completed (0.000 secs 15 calls)\r\n">> + }, + %Test folder names with square brackes + { + [ + {<<"[Calendar]">>,<<"[Calendar]/">>} + ], + [ + <<"* LSUB () \".\" INBOX\r\n* LSUB () \".\" \"[Stuff].Sent Mail.Sent\"\r\n* LSUB () \".\" [Calendar]\r\nwgku OK Lsub completed (0.001 + 0.000 secs).\r\n">> + ], + <<"* LSUB () \".\" INBOX\r\n* LSUB () \".\" \"[Stuff].Sent Mail.Sent\"\r\nwgku OK Lsub completed (0.001 + 0.000 secs).\r\n">> + }, + %Split CR and LF + { + [ + {<<"Calendar">>, <<"Calendar/">>} + ], + [ + <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r">>, + <<"\n* LIST (\\Subscribed \\HasChildren) \"/\" Calendar\r">>, + <<"\n7 OK Completed (0.000 secs 15 calls)\r\n">> + ], + <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> + }, + %Numeric folders + { + [ + ], + [ + <<"* LIST () \"/\" 2017\r\n">>, + <<"7 OK Completed (0.000 secs 15 calls)\r\n">> + + ], + <<"* LIST () \"/\" 2017\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> + }, + %LIST-EXTENDED + LIST-STATUS response (as used by evolution) + { + [ + {<<"Calendar">>, <<"Calendar/">>}, + {<<"Calendar/Calendar2/">>, <<"Calendar/Calendar2/">>} + ], + [ + <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n">>, + <<"* STATUS \"INBOX\" (MESSAGES 17 UNSEEN 16)\r\n">>, + <<"* LIST (\\Subscribed \\HasChildren) \"/\" Calendar (CHILDINFO (\"SUBSCRIBED\"))\r\n">>, + <<"* STATUS \"Calendar\" (MESSAGES 17 UNSEEN 16)\r\n">>, + <<"* LIST (\\Subscribed \\HasNoChildren) \"/\" Calendar/Calendar2\r\n">>, + <<"* STATUS \"Calendar/Calendar2\" (MESSAGES 17 UNSEEN 16)\r\n">>, + <<"7 OK Completed (0.000 secs 15 calls)\r\n">> + ], + <<"* LIST (\\Noinferiors \\Subscribed) \"/\" INBOX\r\n* STATUS \"INBOX\" (MESSAGES 17 UNSEEN 16)\r\n7 OK Completed (0.000 secs 15 calls)\r\n">> + }, + %String literals + %Filtering on string literals is broken, it will attempt to filter on {8}Calendar + { + [ + %{<<"Calendar">>, <<"Calendar/">>} + ], + [ + %<<"* LIST (\Subscribed) \"/\" {8}Calendar\r\n">>, + <<"* LIST (\Subscribed) \"/\" {6}\r\n">>, + <<"Folder\r\n">> + ], + <<"* LIST (\Subscribed) \"/\" {6}\r\nFolder\r\n">> } ], %% setup boilerplate Config = {}, %%TODO? State = kolab_guam_rule_filter_groupware:new(Config), ServerConfig = kolab_guam_sup:default_imap_server_config(), { ok, ImapSession } = eimap:start_link(ServerConfig), %% create the rule, ready for testing ClientData = <<"7 list (subscribed) \"\" \"*\" return (special-use)">>, Split = eimap_utils:split_command_into_components(ClientData), { _, ReadyState } = kolab_guam_rule_filter_groupware:apply_to_client_message(ImapSession, ClientData, Split, State), %% run the dataset through the rule lists:foreach(fun({ Blacklist, Input, Filtered }) -> BlacklistState = ReadyState#state{ blacklist = Blacklist }, Filtered = filter_groupware_packets(ImapSession, BlacklistState, Input, <<>>) end, Data). filter_groupware_packets(_ImapSession, _ReadyState, [], Buffer) -> Buffer; filter_groupware_packets(ImapSession, ReadyState, [Input|More], Buffer) -> { Processed, State } = kolab_guam_rule_filter_groupware:apply_to_server_message(ImapSession, Input, ReadyState), filter_groupware_packets(ImapSession, State, More, <>).