diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..b9fd1a3 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,3 @@ +{ + "phabricator.uri": "https://git.kolab.org" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 31c5de3..42d3ab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,53 +1,59 @@ # 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.4.1] - 2020-03-28 +### Added +- Enabled tcp keepalive +- Support for untagged commands + ## [0.4.0] - 2016-11-11 ### Added - optional timeout on eimap objects for server delay passing in an integer command_timeout value to eimap:start_link sets the timeout to that number of ms ### Fixed - METADATA command parses multiple key/value returns correctly ## [0.3.0] - 2016-07-29 ### Changed - moved to rebar3 ### Fixed - consistency in capabilities response parsing ## [0.2.5] - 2016-07-04 ### Fixed - improved the capabilities response parsing ## [0.2.4] - 2016-06-08 This section contains the changes from 0.2.0 through 0.2.4 ### Added - NOOP command - support for automated interruption of passthrough state to send structured commands - commands receive server responses for commands they put into the queue ### Changed - centralize core IMAP response handling and utils in eimap_command - support for multi-line, single-line and binary response command types - improved TLS support ### Fixed - crash in GETMETADATA command when the Folder param is a list - Support more variations of the LIST command args in the filter_groupware rule - Prevent crashes (while maintaining simplicity) in session FSM by limiting processcommandqueue messages in the mailbox to one - support for literals continuation - fixes for metadata fetching diff --git a/rebar.config b/rebar.config index e8bac2a..33e4158 100644 --- a/rebar.config +++ b/rebar.config @@ -1,17 +1,20 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et -{erl_opts, [warnings_as_errors, {parse_transform, lager_transform}]}. +{erl_opts, [{parse_transform, lager_transform}]}. {deps, [ lager ]}. { erl_first_files, ["src/eimap_command.erl"] }. {sub_dirs, [ "src", - "rel", "tests" ]}. {cover_enabled, true}. %%{require_otp_vsn, "17"}. {pre_hooks, [{clean, "rm -rf ebin priv erl_crash.dump"}]}. - +{ relx, [ { release, { eimap, "0.4.1" }, [eimap]}, + { dev_mode, false }, + { include_erts, false } + ] +}. diff --git a/rebar3 b/rebar3 index 5f7ed75..c981fbe 100755 Binary files a/rebar3 and b/rebar3 differ diff --git a/src/eimap.app.src b/src/eimap.app.src index 6feb194..d50dc2d 100644 --- a/src/eimap.app.src +++ b/src/eimap.app.src @@ -1,22 +1,21 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- {application, eimap, [ { description, "IMAP client implementation" }, - { vsn, "0.4.0" }, + { vsn, "0.4.1" }, { registered, [] }, { applications, [ kernel, stdlib, crypto, ssl, compiler, syntax_tools, - goldrush, lager ] }, { env, [ ]}, { maintainers, ["Aaron Seigo"] }, { licenses, ["LGPLv3+"] }, { links, ["https://git.kolab.org/diffusion/EI/"] } ]}. diff --git a/src/eimap.erl b/src/eimap.erl index a630bd4..0a50543 100644 --- a/src/eimap.erl +++ b/src/eimap.erl @@ -1,504 +1,501 @@ %% Copyright 2014 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 Library 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 Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap). -behaviour(gen_fsm). %% API -export([start_link/1, %% passthrough mode, where data is just sent to the server blindly and %% responses passed back equally blindly. in this mode the user is on %% their own and better know what they are doing. can only be activated %% when disconnected or idle start_passthrough/2, stop_passthrough/1, passthrough_data/2, %% connection management connect/1, connect/3, disconnect/1, %% commands starttls/3, capabilities/3, login/5, logout/3, compress/1, get_server_metadata/4, get_server_metadata/6, get_folder_status/5, get_folder_metadata/5, get_folder_metadata/7, get_folder_annotations/4, peek_message_headers_and_body/5, get_path_tokens/3, noop/3]). %% gen_fsm callbacks -export([disconnected/2, idle/2, passthrough/2, wait_response/2, startingtls/2]). -export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). %% state record definition -record(state, { host, port, tls, tls_state = false, socket, server_id = <<>>, command_serial = 1, command_queue = queue:new(), current_command, current_mbox, passthrough = false, passthrough_recv, passthrough_send_buffer = <<>>, inflator, deflator, process_command_queue_guard = false, command_timeout = infinity, command_timeout_timer = none }). -record(command, { tag, mbox, message, from, response_type, response_token, parse_state }). -define(SSL_UPGRADE_TIMEOUT, 5000). -define(TCP_CONNECT_TIMEOUT, 5000). -define(DEFAULT_COMMAND_TIMEOUT, infinity). %% public API start_link(Options) when is_list(Options) -> gen_fsm:start_link(?MODULE, Options, []). start_passthrough(PID, Receiver) when is_pid(Receiver) -> gen_fsm:send_all_state_event(PID, { start_passthrough, Receiver } ). stop_passthrough(PID) -> gen_fsm:send_all_state_event(PID, stop_passthrough). passthrough_data(PID, Data) when is_binary(Data) -> gen_fsm:send_all_state_event(PID, { passthrough, Data }). connect(PID) -> connect(PID, undefined, undefined). connect(PID, From, ResponseToken) -> gen_fsm:send_all_state_event(PID, { connect, From, ResponseToken }). disconnect(PID) -> gen_fsm:send_all_state_event(PID, disconnect). -spec compress(EImap :: pid()) -> ok. compress(EImap) when is_pid(EImap) -> send_command_to_queue(EImap, EImap, compress, eimap_command_compress, ok). -spec starttls(EImap :: pid(), From :: pid(), ResponseToken :: any()) -> ok. starttls(EImap, From, ResponseToken) when is_pid(EImap) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_starttls, ok). -spec capabilities(EImap :: pid(), From :: pid(), ResponseToken :: any()) -> ok. capabilities(EImap, From, ResponseToken) when is_pid(EImap) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_capability, ok). -spec login(EImap :: pid(), From :: pid(), ResponseToken :: any(), User :: list() | binary(), Pass :: list() | binary()) -> ok. login(EImap, From, ResponseToken, User, Pass) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_login, { User, Pass }). -spec logout(EImap :: pid(), From :: pid(), ResponseToken :: any()) -> ok. logout(EImap, From, ResponseToken) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_logout, ok). -type status_property() :: messages | recent | uidnext | uidvalidity | unseen. -type status_properties() :: [status_property()]. -spec get_folder_status(EImap :: pid(), From :: pid(), ResponseToken :: any(), Folder :: list() | binary(), Properties:: status_properties()) -> ok. get_folder_status(EImap, From, ResponseToken, Folder, Properties) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_status, { Folder, Properties }). -spec get_folder_metadata(EImap :: pid(), From :: pid(), ResponseToken :: any(), Folder :: list() | binary(), Properties:: [list() | binary()]) -> ok. get_folder_metadata(EImap, From, ResponseToken, Folder, Properties) -> get_folder_metadata(EImap, From, ResponseToken, Folder, Properties, infinity, nomax). -spec get_folder_metadata(EImap :: pid(), From :: pid(), ResponseToken :: any(), Folder :: list() | binary(), Properties:: [list() | binary()], Depth :: infinity | integer(), MaxSize :: nomax | integer()) -> ok. get_folder_metadata(EImap, From, ResponseToken, Folder, Properties, Depth, MaxSize) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_getmetadata, { Folder, Properties, Depth, MaxSize }). -spec get_server_metadata(EImap :: pid(), From :: pid(), ResponseToken :: any(), Properties:: [list() | binary()]) -> ok. get_server_metadata(EImap, From, ResponseToken, Properties) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_getmetadata, { <<>>, Properties, infinity, nomax }). -spec get_server_metadata(EImap :: pid(), From :: pid(), ResponseToken :: any(), Properties:: [list() | binary()], Depth :: infinity | integer(), MaxSize :: nomax | integer()) -> ok. get_server_metadata(EImap, From, ResponseToken, Properties, Depth, MaxSize) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_getmetadata, { <<>>, Properties, Depth, MaxSize }). -spec get_folder_annotations(EImap :: pid(), From :: pid(), ResponseToken :: any(), Folder :: [list() | binary()]) -> ok. get_folder_annotations(EImap, From, ResponseToken, Folder) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_annotation, Folder). -spec peek_message_headers_and_body(EImap :: pid(), From :: pid(), ResponseToken :: any(), Folder :: [list() | binary()], MessageID :: [integer() | binary()]) -> ok. peek_message_headers_and_body(EImap, From, ResponseToken, Folder, MessageID) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_peek_message, MessageID, Folder). -spec get_path_tokens(EImap :: pid(), From :: pid(), ResponseToken :: any()) -> ok. get_path_tokens(EImap, From, ResponseToken) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_namespace, ok). -spec noop(EImap :: pid(), From :: pid(), ResponseToken :: any()) -> ok. noop(EImap, From, ResponseToken) -> send_command_to_queue(EImap, From, ResponseToken, eimap_command_noop, ok). %% gen_server API init(Options) -> Host = proplists:get_value(host, Options, "127.0.0.1"), Port = proplists:get_value(port, Options, 143), TLS = proplists:get_value(tls, Options, false), CommandTimeout = proplists:get_value(command_timeout, Options, ?DEFAULT_COMMAND_TIMEOUT), State = #state { host = Host, port = Port, tls = TLS, command_timeout = CommandTimeout }, { ok, disconnected, State }. disconnected({ connect, Receiver, ResponseToken }, #state{ command_queue = CommandQueue, host = Host, port = Port, tls = TLS, socket = undefined } = State) -> %lager:debug("CONNECTING! ~p ~p", [Receiver, ResponseToken]), { {ok, Socket}, TlsState, SendCapabilitiesTo, NewCommandQueue } = create_socket(Host, Port, TLS, Receiver, ResponseToken, CommandQueue), { Message, ResponseType } = eimap_command_capability:new_command(parse_serverid), Command = #command{ message = Message, response_type = ResponseType, from = SendCapabilitiesTo, response_token = { connected, Receiver, ResponseToken }, parse_state = eimap_command_capability }, { next_state, wait_response, State#state { socket = Socket, tls_state = TlsState, current_command = Command, command_queue = NewCommandQueue } }; disconnected(Command, State) when is_record(Command, command) -> { next_state, disconnected, enque_command(Command, State) }. passthrough(flush_passthrough_buffer, #state{ passthrough_send_buffer = Buffer } = State) -> %lager:info("Passing through ~p", [Buffer]), passthrough({ passthrough, Buffer }, State#state{ passthrough_send_buffer = <<>> }); passthrough({ passthrough, Data }, #state{ socket = Socket, tls_state = true } = State) -> %lager:info("Passing through ssl \"~s\"", [Data]), ssl:send(Socket, deflated(Data, State)), { next_state, passthrough, State }; passthrough({ passthrough, Data }, #state{ socket = Socket } = State) -> %lager:info("Passing through tcp \"~s\"", [Data]), gen_tcp:send(Socket, deflated(Data, State)), { next_state, passthrough, State }; passthrough({ data, Data }, #state{ passthrough_recv = Receiver } = State) -> %lager:info("Passing back ~p", [Data]), Receiver ! { imap_server_response, Data }, { next_state, passthrough, State }; passthrough(Command, State) when is_record(Command, command) -> NewState = ensure_process_command_queue(State), { next_state, idle, enque_command(Command, NewState) }. idle(process_command_queue, #state{ command_queue = Queue } = State) -> UnguardedState = State#state{ process_command_queue_guard = false }, case queue:out(Queue) of { { value, Command }, ModifiedQueue } when is_record(Command, command) -> %lager:info("Clearing queue of ~p", [Command]), NewState = send_command(Command, UnguardedState#state{ command_queue = ModifiedQueue }), { next_state, wait_response, NewState }; { empty, _ModifiedQueue } -> NextState = next_state_after_emptied_queue(State), { next_state, NextState, UnguardedState } end; idle({ data, _Data }, State) -> %%lager:info("Idling, server sent: ~p", [_Data]), { next_state, idle, State }; idle(Command, State) when is_record(Command, command) -> %%lager:info("Idling"), NewState = send_command(Command, State), { next_state, wait_response, NewState }; idle(_Event, State) -> { next_state, idle, ensure_process_command_queue(State) }. next_state_after_emptied_queue(#state{ passthrough = true }) -> gen_fsm:send_event(self(), flush_passthrough_buffer), passthrough; next_state_after_emptied_queue(_State) -> idle. ensure_process_command_queue(State) -> case State#state.process_command_queue_guard of true -> State; _ -> gen_fsm:send_event(self(), process_command_queue), State#state{ process_command_queue_guard = true } end. %%TODO a variant that checks "#command{ from = undefined }" to avoid parsing responses which will go undelivered? wait_response(Command, State) when is_record(Command, command) -> { next_state, wait_response, enque_command(Command, State) }; wait_response({ data, _Data }, #state{ current_command = #command{ parse_state = undefined } } = State) -> NewState = cancel_timeout(State), { next_state, idle, ensure_process_command_queue(NewState) }; wait_response({ data, Data }, #state{ current_command = #command{ response_type = ResponseType, parse_state = CommandState , tag = Tag } } = State) -> Response = eimap_command:parse_response(ResponseType, Data, Tag, CommandState), %%lager:info("Response from parser was ~p ~p, size of queue ~p", [More, Response, queue:len(State#state.command_queue)]), NewState = cancel_timeout(State), next_command_after_response(Response, NewState); wait_response(process_command_queue, State) -> % ignore this one, we'll get to it when the response comes { next_state, wait_response, State }. startingtls({ passthrough, Data }, #state{ passthrough = true, passthrough_send_buffer = Buffer } = State) -> { next_state, startingtls, State#state{ passthrough_send_buffer = <> } }; startingtls(Command, State) when is_record(Command, command) -> { next_state, startingtls, enque_command(Command, State) }; startingtls({ data, Data }, #state{ current_command = #command{ response_type = ResponseType, parse_state = CommandState, tag = Tag } } = State) -> Response = eimap_command:parse_response(ResponseType, Data, Tag, CommandState), %%lager:info("Response from parser was ~p ~p, size of queue ~p", [More, Response, queue:len(State#state.command_queue)]), next_command_after_response(Response, State). handle_event({ connect, _From, _ResponseToken } = Event, disconnected, State) -> gen_fsm:send_event(self(), Event), { next_state, disconnected, State }; handle_event({ connect, _From, _ResponseToken }, _Statename, State) -> %%lager:info("Already connected to IMAP server!"), { next_state, _Statename, State }; handle_event(disconnect, _StateName, State) -> close_socket(State), { next_state, disconnected, reset_state(State) }; handle_event({ ready_command, Command }, StateName, State) when is_record(Command, command) -> ?MODULE:StateName(Command, State); handle_event({ start_passthrough, Receiver }, StateName, State) -> { next_state, StateName, State#state{ passthrough = true, passthrough_recv = Receiver } }; handle_event(stop_passthrough, StateName, State) -> { NextStateName, NewState } = case StateName of passthrough -> { idle, ensure_process_command_queue(State) }; StateName -> { StateName, State } end, { next_state, NextStateName, NewState#state{ passthrough = false } }; handle_event({ passthrough, Data }, passthrough, #state{ passthrough = true } = State) -> ?MODULE:passthrough({ passthrough, Data }, State); handle_event({ passthrough, Data }, StateName, #state{ passthrough = true, passthrough_send_buffer = Buffer } = State) -> NewBuffer = <>, NewState = case StateName of idle -> ensure_process_command_queue(State); _ -> State end, { next_state, StateName, NewState#state{ passthrough_send_buffer = NewBuffer } }; handle_event(_Event, StateName, State) -> { next_state, StateName, State}. handle_sync_event(_Event, _From, StateName, State) -> { next_state, StateName, State}. handle_info({ ssl, Socket, Bin }, StateName, #state{ socket = Socket } = State) -> % Flow control: enable forwarding of next TCP message ssl:setopts(Socket, [{ active, once }]), Data = inflated(Bin, State), %lager:info("Received from server over ssl: ~s", [Data]), ?MODULE:StateName({ data, Data }, State); handle_info({ tcp, Socket, Bin }, StateName, #state{ socket = Socket } = State) -> % Flow control: enable forwarding of next TCP message inet:setopts(Socket, [{ active, once }]), Data = inflated(Bin, State), %lager:info("Received from server plaintext: ~s", [Data]), ?MODULE:StateName({ data, Data }, State); handle_info({ ssl_closed, Socket }, _StateName, #state{ socket = Socket, host = Host, port = Port } = State) -> lager:debug("~p Disconnected from ~p:~p .\n", [self(), Host, Port]), { stop, normal, State }; handle_info({ ssl_error, Socket, _Reason }, _StateName, #state{ socket = Socket, host = Host, port = Port } = State) -> lager:info("~p Disconnected due to socket error from ~p:~p .\n", [self(), Host, Port]), { stop, normal, State }; handle_info({ tcp_closed, Socket }, _StateName, #state{ socket = Socket, host = Host, port = Port } = State) -> lager:debug("~p Disconnected from ~p:~p .\n", [self(), Host, Port]), { stop, normal, State }; handle_info({ tcp_error, Socket, _Reason }, _StateName, #state{ socket = Socket, host = Host, port = Port } = State) -> lager:info("~p Disconnected due to socket error from ~p:~p .\n", [self(), Host, Port]), { stop, normal, State }; handle_info({ { connected, Receiver, ResponseToken }, { Capabilities, ServerID } }, _StateName, #state{ passthrough = Passthrough, passthrough_recv = PassthroughReceiver, tls = TLS } = State) -> case TLS of starttls -> % we do not pass through or notifty when we are going to automatically do a starttls % this allows us to send the post-starttls capabilities triggering client activity % only AFTER we have completely set up the connectiona, as that usually tends to % alter the capabilities % % if the user of eimap does not want this behavior, they can starttls themselves % explicitly ok; _ -> %lager:debug("Connected, capabilities are: ~s; ServerID is ~s", [Capabilities, ServerID]), send_hello_string(Capabilities, ServerID, Receiver, ResponseToken, Passthrough, PassthroughReceiver) end, { next_state, idle, State#state{ current_command = undefined, server_id = ServerID } }; handle_info({ { posttls_capabilities, Receiver, ResponseToken }, [Capabilities] }, _StateName, #state{ server_id = ServerID, passthrough = Passthrough, passthrough_recv = PassthroughReceiver } = State) -> send_hello_string(Capabilities, ServerID, Receiver, ResponseToken, Passthrough, PassthroughReceiver), { next_state, idle, State#state{ current_command = none } }; handle_info({ { selected, MBox }, ok }, StateName, State) -> %%lager:info("~p Selected mbox ~p", [self(), MBox]), { next_state, StateName, State#state{ current_mbox = MBox } }; handle_info({ { selected, MBox }, { error, Reason } }, StateName, State) -> lager:info("Failed to select mbox ~p: ~p", [MBox, Reason]), NewQueue = queue:filter(fun(Command) -> notify_of_mbox_failure_during_filter(Command, Command#command.mbox =:= MBox) end, State#state.command_queue), { next_state, StateName, State#state{ command_queue = NewQueue } }; handle_info(starttls_complete, StateName, State) -> %lager:info("STARTTLS completed successfully"), { next_state, StateName, State }; handle_info(response_timeout, _StateName, State) -> { stop, normal, State }; handle_info(Info, StateName, State) -> lager:debug("handle_info called with unhandled info of ~p", [Info]), { next_state, StateName, State }. terminate(_Reason, _Statename, State) -> close_socket(State), ok. code_change(_OldVsn, Statename, State, _Extra) -> { ok, Statename, State }. %% private API send_command_to_queue(EImap, From, ResponseToken, Module, Args) -> send_command_to_queue(EImap, From, ResponseToken, Module, Args, undefined). send_command_to_queue(EImap, From, ResponseToken, Module, Args, Folder) -> { Message, ResponseType } = Module:new_command(Args), Command = #command{ mbox = Folder, message = Message, response_type = ResponseType, from = From, response_token = ResponseToken, parse_state = Module }, gen_fsm:send_all_state_event(EImap, { ready_command, Command }). send_hello_string(Capabilities, ServerId, Receiver, ResponseToken, Passthrough, PassthroughReceiver) -> notify_of_response([{ capabilities, Capabilities }, { server_id, ServerId } ], Receiver, ResponseToken), passthrough_capabilities(Capabilities, ServerId, Passthrough, PassthroughReceiver). passthrough_capabilities(Capabilities, ServerId, true, Receiver) -> Message = <<"* OK [CAPABILITY ", Capabilities/binary, "] ", ServerId/binary, "\r\n">>, Receiver ! { imap_server_response, Message }; passthrough_capabilities(_Capabilities, _ServerId, _Passthrough, _Receiver) -> ok. notify_of_response(none, _Command) -> ok; notify_of_response(Response, #command { from = From, response_token = Token }) -> notify_of_response(Response, From, Token); notify_of_response(_, _) -> ok. notify_of_response(_Response, undefined, _Token) -> ok; notify_of_response(Response, From, undefined) -> From ! Response; notify_of_response(Response, From, Token) -> From ! { Token, Response }. %% the return is inverted for filtering notify_of_mbox_failure_during_filter(Command, true) -> notify_of_response({ error, mailboxnotfound }, Command), false; notify_of_mbox_failure_during_filter(_Command, false) -> true. next_command_after_response({ more, ParseState }, State) -> { next_state, wait_response, State#state{ current_command = State#state.current_command#command{ parse_state = ParseState } } }; next_command_after_response({ error, _ } = ErrorResponse, State) -> notify_of_response(ErrorResponse, State#state.current_command), NewState = ensure_process_command_queue(State), { next_state, idle, NewState#state{ current_command = undefined } }; next_command_after_response({ fini, Response }, State) -> %lager:info("Notifying with ~p", [State#state.current_command]), notify_of_response(Response, State#state.current_command), NewState = ensure_process_command_queue(State), { next_state, idle, NewState#state{ current_command = undefined } }; next_command_after_response(starttls, State) -> { TLSState, Socket } = upgrade_socket(State), %lager:info("~p Upgraded the socket ...", [self()]), NewState = ensure_process_command_queue(State), { next_state, idle, NewState#state{ current_command = undefined, socket = Socket, tls_state = TLSState } }; next_command_after_response(compression_active, State) -> { Inflator, Deflator } = eimap_utils:new_imap_compressors(), NewState = ensure_process_command_queue(State), { next_state, idle, NewState#state{ current_command = undefined, inflator = Inflator, deflator = Deflator } }; next_command_after_response({ close_socket, Response }, State) -> notify_of_response(Response, State#state.current_command), { stop, normal, State }. tag_field_width(Serial) when Serial < 10000 -> 4; tag_field_width(Serial) -> tag_field_width(Serial / 10000, 5). tag_field_width(Serial, Count) when Serial < 10 -> Count; tag_field_width(Serial, Count) -> tag_field_width(Serial / 10, Count + 1). create_socket(Host, Port, true, _Receiver, _ResponseToken, CommandQueue) -> { ssl:connect(Host, Port, socket_options(), ?SSL_UPGRADE_TIMEOUT), true, self(), CommandQueue }; create_socket(Host, Port, starttls, Receiver, ResponseToken, CommandQueue) -> %lager:debug("Setting up the tls creation with ultimate end point of ~p ~p", [Receiver, ResponseToken]), % we do an implicit TLS by adding a starttls command and then a capability command so we can % pretend to the user that the socket just magically opened up like this. %TODO: some duplicated code here; would be nice to clean this up a bit? { TlsMessage, TlsResponseType } = eimap_command_starttls:new_command(ok), TlsCommand = #command{ message = TlsMessage, response_type = TlsResponseType, from = self(), response_token = undefined, parse_state = eimap_command_starttls }, { CapMessage, CapResponseType } = eimap_command_capability:new_command(ok), CapabilitiesCommand = #command{ message = CapMessage, response_type = CapResponseType, from = self(), response_token = { posttls_capabilities, Receiver, ResponseToken }, parse_state = eimap_command_capability }, % note the use of queue:in_r to _prepend_ the commands so they get run first even if the user % has pre-connection queued up commands NewCommandQueue = queue:in_r(TlsCommand, queue:in_r(CapabilitiesCommand, CommandQueue)), { gen_tcp:connect(Host, Port, socket_options(), ?TCP_CONNECT_TIMEOUT), false, self(), NewCommandQueue }; create_socket(Host, Port, _, _Receiver, _ResponseToken, CommandQueue) -> { gen_tcp:connect(Host, Port, socket_options(), ?TCP_CONNECT_TIMEOUT), false, self(), CommandQueue }. -socket_options() -> [binary, { active, once }, { send_timeout, 5000 }]. +socket_options() -> [binary, { active, once }, { send_timeout, 5000 }, { keepalive, true }]. upgrade_socket(#state{ socket = Socket, tls_state = true, current_command = Command }) -> notify_of_response(starttls_complete, Command), ssl:setopts(Socket, [{ active, once }]), { true, Socket }; upgrade_socket(#state{ socket = Socket, current_command = Command }) -> %lager:debug("~p upgrading the server socket due to starttls"[self()]), case ssl:connect(Socket, socket_options(), ?SSL_UPGRADE_TIMEOUT) of { ok, SSLSocket } -> %lager:info("~p it worked", [self()]), notify_of_response(starttls_complete, Command), ssl:setopts(SSLSocket, [{ active, once }]), { true, SSLSocket }; { error, Reason } -> lager:warning("~p StartTLS failed due to: ~p", [self(), Reason]), notify_of_response(starttls_failed, Command), inet:setopts(Socket, [{ active, once }]), { false, Socket } end. close_socket(#state{ socket = undefined }) -> false; close_socket(#state{ socket = Socket, tls_state = true }) -> ssl:close(Socket); close_socket(#state{ socket = Socket }) -> gen_tcp:close(Socket). reset_state(State) -> State#state{ socket = undefined, command_serial = 1 }. %% sending command code paths: %% 0. not connected, TLS/SSL, unencrypted %% 1. no mbox needed, mbox is already selected, mbox needs selecting send_command(Command, #state{ socket = undefined } = State) -> lager:warning("Not connected, dropping command on floor: ~s", [Command]), State; send_command(Command, #state{ tls_state = true} = State) -> send_command(fun ssl:send/2, Command, State); send_command(Command, State) -> send_command(fun gen_tcp:send/2, Command, State). send_command(SocketFun, #command{ mbox = undefined } = Command, State) -> %%lager:info("~p SELECT_DEBUG issuing command without mbox: ~p", [self(), Command#command.message]), send_command_now(SocketFun, Command, State); send_command(SocketFun, #command{ mbox = MBox } = Command, #state{ current_mbox = CurrentMbox } = State) -> %%lager:info("~p SELECT_DEBUG issuing command with mbox ~p (current: ~p, equal -> ~p): ~p", [self(), MBox, CurrentMbox, (MBox =:= CurrentMbox), Command#command.message]), send_command_or_select_mbox(SocketFun, Command, State, MBox, MBox =:= CurrentMbox). send_command_or_select_mbox(SocketFun, Command, State, _MBox, true) -> send_command_now(SocketFun, Command, State); send_command_or_select_mbox(SocketFun, DelayedCommand, State, MBox, false) -> NextState = reenque_command(DelayedCommand, State), %TODO: this really should be SELECT rather than EXAMINE { SelectMessage, ResponseType } = eimap_command_switch_folder:new_command(MBox), SelectCommand = #command{ message = SelectMessage, response_type = ResponseType, parse_state = eimap_command_switch_folder, from = self(), response_token = { selected, MBox } }, %%lager:info("~p SELECT_DEBUG: Doing a select first ~p", [self(), SelectMessage]), send_command_now(SocketFun, SelectCommand, NextState). send_command_now(SocketFun, #command{ message = Message } = Command, #state{ command_serial = Serial, socket = Socket } = State) -> Tag = list_to_binary(io_lib:format("EG~*..0B", [tag_field_width(Serial), Serial])), Data = <>, %lager:info("Sending command via ~p: ~s", [Fun, Data]), SocketFun(Socket, deflated(Data, State)), StateWithTimeout = reset_timeout(State), StateWithTimeout#state{ command_serial = Serial + 1, current_command = Command#command{ tag = Tag } }. enque_command(Command, State) -> %%lager:info("Enqueuing command ~p", [Command]), State#state { command_queue = queue:in(Command, State#state.command_queue) }. reenque_command(Command, State) -> %%lager:info("Re-queueing command ~p", [Command]), State#state { command_queue = queue:in_r(Command, State#state.command_queue) }. inflated(Data, #state{ inflator = undefined }) -> Data; -inflated(Data, #state{ inflator = Inflator }) -> joined(zlib:inflate(Inflator, Data), <<>>). +inflated(Data, #state{ inflator = Inflator }) -> iolist_to_binary(zlib:inflate(Inflator, Data)). deflated(Data, #state{ deflator = undefined }) -> Data; -deflated(Data, #state{ deflator = Deflator }) -> joined(zlib:deflate(Deflator, Data, sync), <<>>). - -joined([], Binary) -> Binary; -joined([H|Rest], Binary) -> joined(Rest, <>). +deflated(Data, #state{ deflator = Deflator }) -> iolist_to_binary(zlib:deflate(Deflator, Data, sync)). reset_timeout(#state{ command_timeout = Timeout } = State) -> cancel_timeout(State), { ok, TimerRef } = case Timeout of Timeout when is_integer(Timeout), Timeout >= 0 -> timer:send_after(Timeout, response_timeout); _ -> { ok, none } end, State#state{ command_timeout_timer = TimerRef }. cancel_timeout(#state{ command_timeout_timer = none } = State) -> State; cancel_timeout(#state{ command_timeout_timer = TimerRef } = State) -> timer:cancel(TimerRef), State#state{ command_timeout_timer = none }. diff --git a/src/eimap_utils.erl b/src/eimap_utils.erl index 84b5947..b41ef2f 100644 --- a/src/eimap_utils.erl +++ b/src/eimap_utils.erl @@ -1,246 +1,252 @@ %% Copyright 2014 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 Library 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 Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_utils). -export([ extract_path_from_uri/3, extract_uidset_from_uri/1, split_command_into_components/1, is_tagged_response/2, remove_tag_from_response/3, header_name/1, parse_flags/1, check_response_for_failure/2, ensure_binary/1, new_imap_compressors/0, only_full_lines/1, binary_to_atom/1, num_literal_continuation_bytes/1 ]). %% Translate the folder name in to a fully qualified folder path such as it %% would be used by a cyrus administrator. -spec extract_path_from_uri(SharedPrefix :: binary(), HierarchyDelim :: binary, URI :: binary()) -> Path :: binary() | bad_uri. extract_path_from_uri(SharedPrefix, HierarchyDelim, URI) when is_binary(URI) -> extract_path_from_uri(SharedPrefix, HierarchyDelim, binary_to_list(URI)); extract_path_from_uri(SharedPrefix, HierarchyDelim, URI) when is_list(URI) -> %%lager:info("Parsing ~p", [URI]), SchemeDefaults = [{ imap, 143 }, { imaps, 993 }], ParseOpts = [ { scheme_defaults, SchemeDefaults } ], case imap_folder_path(SharedPrefix, HierarchyDelim, http_uri:parse(URI, ParseOpts)) of Path when is_list(Path) -> list_to_binary(Path); Error -> Error end. -spec extract_uidset_from_uri(URI :: binary()) -> UIDSet:: binary(). extract_uidset_from_uri(URI) when is_binary(URI) -> { TagStart, TagEnd } = binary:match(URI, <<";UID=">>), UIDStart = TagStart + TagEnd + 1, UriLength = byte_size(URI), case binary:match(URI, <<";">>, [{ scope, { UIDStart, UriLength - UIDStart } }]) of nomatch -> binary:part(URI, UIDStart - 1, UriLength - UIDStart + 1); { Semicolon, _ } -> binary:part(URI, UIDStart - 1, Semicolon - UIDStart + 1) end. -spec header_name(mailbox_uid | groupware_uid | groupware_uid) -> binary(); (any()) -> unknown. header_name(mailbox_uid) -> <<"/vendor/cmu/cyrus-imapd/uniqueid">>; header_name(groupware_type) -> <<"X-Kolab-Type">>; header_name(groupware_uid) -> <<"Subject">>; header_name(_) -> unknown. -spec parse_flags(FlagString :: binary() | list()) -> Flags :: [binary()]. parse_flags(String) when is_list(String) -> parse_flags(list_to_binary(String)); parse_flags(<<"(", Parened/binary>>) -> case binary:match(Parened, <<")">>) of nomatch -> []; { ClosingParens, _ } -> parse_flags(binary_part(Parened, 0, ClosingParens)) end; parse_flags(<<>>) -> []; parse_flags(FlagString) when is_binary(FlagString) -> binary:split(FlagString, <<" ">>, [global]). -spec check_response_for_failure(Data :: binary(), Tag :: undefined | binary()) -> ok | { error, Reason :: binary() }. check_response_for_failure(Data, undefined) when is_binary(Data) -> check_response_for_failure(Data, <<>>); check_response_for_failure(Data, Tag) when is_binary(Data), is_binary(Tag) -> NoToken = <>, NoTokenLength = byte_size(NoToken), case NoTokenLength > byte_size(Data) of true -> ok; false -> is_no_token_found(Data, Tag, binary:match(Data, NoToken, [ { scope, { 0, NoTokenLength } } ])) end. -spec split_command_into_components(Buffer :: binary()) -> { Tag :: binary(), Command :: binary(), Data :: binary() }. split_command_into_components(Buffer) when is_binary(Buffer) -> split_command(Buffer). -spec is_tagged_response(Buffer :: binary(), Tag :: binary()) -> tagged | untagged. is_tagged_response(Buffer, Tag) -> TagSize = size(Tag) + 1, % The extra char is a space BufferSize = size(Buffer), case case (TagSize =< BufferSize) of true -> <> =:= binary:part(Buffer, 0, TagSize); _ -> false end of true -> tagged; _ -> untagged end. -spec num_literal_continuation_bytes(Buffer :: binary()) -> { BufferSansContinuation :: binary(), NumberBytes :: integer() }. num_literal_continuation_bytes(Buffer) when size(Buffer) < 4 -> { Buffer, 0 }; num_literal_continuation_bytes(Buffer) -> case binary:last(Buffer) =:= $} of true -> number_of_bytes_in_continuation(Buffer); false -> { Buffer, 0 } end. number_of_bytes_in_continuation(Buffer) -> BufferSize = size(Buffer), OpenBracePos = find_continuation_open_brace(Buffer, BufferSize - 3), confirm_continuation(Buffer, OpenBracePos). find_continuation_open_brace(_Buffer, 0) -> -1; find_continuation_open_brace(Buffer, Pos) -> case binary:at(Buffer, Pos) of ${ -> Pos; _ -> find_continuation_open_brace(Buffer, Pos - 1) end. confirm_continuation(Buffer, -1) -> { Buffer, 0 }; confirm_continuation(Buffer, OpenBracePos) -> BufferSize = size(Buffer), try binary_to_integer(binary:part(Buffer, OpenBracePos + 1, BufferSize - OpenBracePos - 2)) of Result -> { binary:part(Buffer, 0, OpenBracePos), Result } catch _:_ -> { Buffer, 0 } end. -spec remove_tag_from_response(Buffer :: binary(), Tag :: undefine | binary(), Check :: check | trust) -> Command :: binary(). remove_tag_from_response(Buffer, undefined, _) -> Buffer; remove_tag_from_response(Buffer, <<>>, _) -> Buffer; remove_tag_from_response(Buffer, Tag, check) -> TagSize = size(Tag) + 1, % The extra char is a space BufferSize = size(Buffer), case TagSize =< BufferSize of true -> case <> =:= binary:part(Buffer, 0, TagSize) of true -> binary:part(Buffer, TagSize, BufferSize - TagSize); false -> Buffer end; false -> Buffer end; remove_tag_from_response(Buffer, Tag, trust) -> TagSize = size(Tag) + 1, % The extra char is a space BufferSize = size(Buffer), case TagSize =< BufferSize of true -> binary:part(Buffer, TagSize, BufferSize - TagSize); false -> Buffer end. %% Private split_command(<<>>) -> { <<>>, <<>>, <<>> }; split_command(Buffer) -> - End = eol_found(Buffer, binary:match(Buffer, <<"\r\n">>)), + {Terminated, End} = eol_found(Buffer, binary:match(Buffer, <<"\r\n">>)), { Tag, CommandStart } = searched_in_buffer(Buffer, 0, End, binary:match(Buffer, <<" ">>, [ { scope, { 0, End } } ])), - { Command, DataStart } = searched_in_buffer(Buffer, CommandStart, End, binary:match(Buffer, <<" ">>, [ { scope, { CommandStart, End - CommandStart } } ])), - Data = binary:part(Buffer, DataStart, End - (DataStart)), - { Tag, Command, Data }. + case Terminated of + true when End == CommandStart -> + %% when we have a newline, and the tag takes the entire line, the "tag" is actually the command + {<<>>, Tag, <<>>}; + _ -> + { Command, DataStart } = searched_in_buffer(Buffer, CommandStart, End, binary:match(Buffer, <<" ">>, [ { scope, { CommandStart, End - CommandStart } } ])), + Data = binary:part(Buffer, DataStart, End - (DataStart)), + { Tag, Command, Data } + end. -eol_found(Buffer, nomatch) -> size(Buffer); -eol_found(_Buffer, { MatchStart, _MatchLength }) -> MatchStart. +eol_found(Buffer, nomatch) -> {false, size(Buffer)}; +eol_found(_Buffer, { MatchStart, _MatchLength }) -> {true, MatchStart}. searched_in_buffer(Buffer, Start, End, nomatch) -> { binary:part(Buffer, Start, End - Start), End }; searched_in_buffer(Buffer, Start, _End, { MatchStart, MatchLength } ) -> { binary:part(Buffer, Start, MatchStart - Start), MatchStart + MatchLength }. is_no_token_found(Data, Tag, nomatch) -> BadToken = <>, BadTokenLength = byte_size(BadToken), Match = binary:match(Data, BadToken, [ { scope, { 0, BadTokenLength } } ]), is_bad_token_found(Data, Tag, Match); is_no_token_found(Data, _Tag, { Start, Length }) -> ReasonStart = Start + Length, Reason = binary:part(Data, ReasonStart, byte_size(Data) - ReasonStart), { no, chop_newlines(Reason) }. is_bad_token_found(_Data, _Tag, nomatch) -> ok; is_bad_token_found(Data, _Tag, { Start, Length }) -> ReasonStart = Start + Length, %% -2 is due to the traling \r\n Reason = binary:part(Data, ReasonStart, byte_size(Data) - ReasonStart), { bad, chop_newlines(Reason) }. chop_newlines(Data) -> Size = size(Data), chop_newline(Data, binary:at(Data, Size - 1), Size - 1). chop_newline(Data, $\r, Size) -> chop_newline(Data, binary:at(Data, Size - 1), Size - 1); chop_newline(Data, $\n, Size) -> chop_newline(Data, binary:at(Data, Size - 1), Size - 1); chop_newline(Data, _, Size) -> binary_part(Data, 0, Size + 1). imap_folder_path_from_parts(none, _HierarchyDelim, [], _Domain, Path) -> Path; imap_folder_path_from_parts(SharedPrefix, _HierarchyDelim, [], _Domain, Path) -> case string:str(Path, SharedPrefix) of 1 -> string:substr(Path, length(SharedPrefix) + 1); _ -> Path end; imap_folder_path_from_parts(_SharedPrefix, HierarchyDelim, User, Domain, "INBOX") -> string:join(["user", string:join([User, Domain], "@")], HierarchyDelim); imap_folder_path_from_parts(_SharedPrefix, HierarchyDelim, User , Domain, Path) -> string:join(["user", User, string:join([Path, Domain], "@")], HierarchyDelim). imap_folder_path(_SharedPrefix, _HierarchyDelim, { error, Reason }) -> lager:info("ERROR! ~p", [Reason]), bad_uri; imap_folder_path(SharedPrefix, HierarchyDelim, { ok, {_Scheme, User, Domain, _Port, FullPath, _Query} }) -> { VDomain, _ImapHost } = split_imap_uri_domain(string:tokens(Domain, "@")), [ [_|Path] | _ ] = string:tokens(FullPath, ";"), %%lager:info("PARSED IMAP URI: ~p ~p ~p", [User, VDomain, Path]), CanonicalPath = imap_folder_path_from_parts(SharedPrefix, HierarchyDelim, User, VDomain, http_uri:decode(Path)), %%lager:info("PUT TOGETHER AS: ~p", [CanonicalPath]), CanonicalPath. split_imap_uri_domain([ ImapHost ]) -> { ImapHost, ImapHost }; split_imap_uri_domain([ VDomain, ImapHost ]) -> { VDomain, ImapHost }. ensure_binary(Arg) when is_list(Arg) -> list_to_binary(Arg); ensure_binary(Arg) when is_binary(Arg) -> Arg; ensure_binary(Arg) when is_atom(Arg) -> atom_to_binary(Arg, latin1); ensure_binary(_Arg) -> <<>>. new_imap_compressors() -> Inflator = zlib:open(), ok = zlib:inflateInit(Inflator, -15), Deflator = zlib:open(), ok = zlib:deflateInit(Deflator, 1, deflated, -15, 8, default), { Inflator, Deflator }. -spec only_full_lines(Buffer :: binary()) -> { BufferOfFullLines :: binary(), TrailingFragmentaryLine :: binary() }. only_full_lines(Buffer) -> BufferLength = size(Buffer), only_full_lines(Buffer, BufferLength, binary:at(Buffer, BufferLength - 1), BufferLength). only_full_lines(Buffer, BufferLength, $\n, Pos) when Pos =:= BufferLength -> { Buffer, <<>> }; only_full_lines(Buffer, BufferLength, $\n, Pos) -> { binary:part(Buffer, 0, Pos + 1), binary:part(Buffer, Pos + 1, BufferLength - Pos - 1) }; only_full_lines(Buffer, _BufferLength, _, 0) -> { <<>>, Buffer }; only_full_lines(Buffer, BufferLength, _, Pos) -> only_full_lines(Buffer, BufferLength, binary:at(Buffer, Pos - 1), Pos - 1). -spec binary_to_atom(Value :: binary()) -> ValueAsAtom :: atom(). binary_to_atom(Value) -> list_to_atom(string:to_lower(binary_to_list(Value))). diff --git a/test/eimap_utils_tests.erl b/test/eimap_utils_tests.erl index dd111d4..838cf57 100644 --- a/test/eimap_utils_tests.erl +++ b/test/eimap_utils_tests.erl @@ -1,198 +1,199 @@ %% Copyright 2014 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 Library 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 Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_utils_tests). -include_lib("eunit/include/eunit.hrl"). % c("test/eimap_utils_tests.erl"). eunit:test(eimap_utils). extract_path_from_uri_test_() -> Data = [ { <<"user/john.doe/Calendar@example.org">>, none, "/", <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"user/john.doe/Personal Calendar@example.org">>, none, "/", <<"imap://john.doe@example.org@kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, none, "/", <<"imap://kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, "Shared/", "/", <<"imap://kolab.example.org/Shared/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, "Shared/", "/", <<"imap://kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"user/john.doe@example.org">>, "Shared/", "/", <<"imap://john.doe@example.org@kolab.example.org/INBOX;UIDVALIDITY=1424683684/;UID=1">> }, { bad_uri, none, "/", <<"merf">> } ], lists:foldl(fun({ Val, SharePrefix, Sep, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:extract_path_from_uri(SharePrefix, Sep, Input))|Acc] end, [], Data). extract_uid_from_uri_test_() -> Data = [ { <<"1">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"12">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=12">> }, { <<"123">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=123">> }, { <<"1">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1;foo">> }, { <<"12">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=12;foo=bar">> }, { <<"123">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=123;foo=bar">> } ], lists:foldl(fun({ Val, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:extract_uidset_from_uri(Input))|Acc] end, [], Data). split_command_into_components_test_() -> Data = [ { { <<>>, <<>>, <<>> }, <<>> }, + { { <<>>, <<"DONE">>, <<>> }, <<"DONE\r\n">> }, { { <<".">>, <<"LIST">>, <<"\"\" \"*\"">> }, <<". LIST \"\" \"*\"">> }, { { <<"1">>, <<"STARTTLS">>, <<>> }, <<"1 STARTTLS">> }, { { <<"1">>, <<"STARTTLS">>, <<>> }, <<"1 STARTTLS\r\n">> }, { { <<"3">>, <<"ID">>, <<"(\"name\" \"Thunderbird\" \"version\" \"38.3.0\")">> }, <<"3 ID (\"name\" \"Thunderbird\" \"version\" \"38.3.0\")">> } ], lists:foldl(fun({ Val, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:split_command_into_components(Input)) | Acc] end, [], Data). check_response_for_failure_test_() -> Tag = <<"abcdef">>, Data = [ { Tag, <>, { no, <<"reasons">> } }, { Tag, <>, { no, <<"reasons">> } }, { Tag, <>, { bad, <<"reasons">> } }, { Tag, <>, { bad, <<"reasons">> } }, { Tag, <>, ok }, { Tag, <<"short">>, ok }, { undefined, <<"* OK reasons">>, ok } ], lists:foldl(fun({ Tag2, Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:check_response_for_failure(Input, Tag2)) | Acc] end, [], Data). is_tagged_response_test_() -> Tag = <<"abcd">>, Data = [ { <>, tagged }, { <>, tagged }, { <<"one">>, untagged }, { <<"* Yeah baby">>, untagged } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:is_tagged_response(Input, Tag)) | Acc] end, [], Data). remove_tag_from_response_test_() -> Tag = <<"abcd">>, Data = [ { Tag, <>, check, <<"Indeed\r\n">> }, { Tag, <>, trust, <<"Indeed\r\n">> }, { Tag, <>, check, <<"Indeed">>}, { Tag, <>, trust, <<"Indeed">>}, { undefined, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { undefined, <<"abcd4 Indeed">>, trust, <<"abcd4 Indeed">>}, { <<>>, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { <<>>, <<"abcd4 Indeed">>, trust, <<"abcd4 Indeed">>}, { Tag, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { Tag, <<"abcd4 Indeed">>, trust, <<" Indeed">>}, { Tag, <<"* Yeah baby">>, check, <<"* Yeah baby">> }, { Tag, <<"">>, check, <<"">> }, { Tag, <<"">>, trust, <<"">> }, { Tag, <<>>, check, <<>> }, { Tag, <<>>, trust, <<>> } ], lists:foldl(fun({ Tag2, Input, Check, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:remove_tag_from_response(Input, Tag2, Check)) | Acc] end, [], Data). header_name_test_() -> Data = [ { mailbox_uid , <<"/vendor/cmu/cyrus-imapd/uniqueid">> }, { groupware_type , <<"X-Kolab-Type">> }, { groupware_uid , <<"Subject">> }, { dunno, unknown }, { "dunno", unknown }, { <<"dunno">>, unknown }, { 134, unknown } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:header_name(Input)) | Acc] end, [], Data). ensure_binary_test_() -> Data = [ { "yep", <<"yep">> }, { <<"yep">>, <<"yep">> }, { [1, 2, 3], <<1, 2, 3>> }, { yep, <<"yep">> }, { 123, <<>> } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:ensure_binary(Input)) | Acc] end, [], Data). only_full_lines_test_() -> Data = [ { <<"yep">>, { <<>>, <<"yep">> } }, { <<"yep\r\nhohoho">>, { <<"yep\r\n">>, <<"hohoho">> } }, { <<"nope\r\nyep\r\nhohoho">>, { <<"nope\r\nyep\r\n">>, <<"hohoho">> } }, { <<"nope\r\nyep\r\nhohoho\r\n">>, { <<"nope\r\nyep\r\nhohoho\r\n">>, <<>> } } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:only_full_lines(Input)) | Acc] end, [], Data). parse_flags_test_() -> Data = [ { <<"()">>, [] }, { <<>>, [] }, { <<"\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen">>, [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] }, { <<"(\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen)">>, [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] }, { "(\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen)", [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:parse_flags(Input)) | Acc] end, [], Data). num_literal_continuation_bytes_test_() -> Data = [ { <<"abcd">>, { <<"abcd">>, 0 } }, { <<"abcd{5}">>, { <<"abcd">>, 5 } }, { <<"abcd{100}">>, { <<"abcd">>, 100 } }, { <<"123abcd{100}">>, { <<"123abcd">>, 100 } }, { <<"ab{123abcd{100}">>, { <<"ab{123abcd">>, 100 } }, { <<"ab{123abcd{1{00}">>, { <<"ab{123abcd{1">>, 0 } }, { <<"abcd{aa0}">>, { <<"abcd{aa0}">>, 0 } }, { <<"abcd{10aa0}">>, { <<"abcd{10aa0}">>, 0 } }, { <<"abcd100}">>, { <<"abcd100}">>, 0 } }, { <<"abcd100}">>, { <<"abcd100}">>, 0 } } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:num_literal_continuation_bytes(Input)) | Acc] end, [], Data).