diff --git a/.gitignore b/.gitignore index 85b83bc..1e34c8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ *.beam .rebar +rebar.lock erl_crash.dump ebin deps db log .*.swp Mnesia* .eunit +_build diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5cbe4d7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# 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.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/Makefile b/Makefile index 175ae8a..7333de0 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,13 @@ -REBAR = $(shell which rebar || echo ./rebar) +REBAR = $(shell which rebar3 || echo ./rebar3) ENABLE_STATIC = no -all: deps-up eimap - -deps: - rebar get-deps - -deps-up: deps - rebar update-deps - -eimap: - ENABLE_STATIC=no rebar compile +all: + ENABLE_STATIC=$(ENABLE_STATIC) $(REBAR) compile tests: - rebar eunit + $(REBAR) eunit run: - erl -pa ebin deps/*/ebin + $(REBAR) shell --apps eimap diff --git a/README.md b/README.md index eddaa3b..e73c0d0 100644 --- a/README.md +++ b/README.md @@ -1,200 +1,202 @@ This is a basic IMAP client implementation written in Erlang. The API is not finalized, and is currently limited to the eimap module. Its current and only recognized use case is software intended to interact with a Kolab server environment which are written in Erlang. Expansion of that scope is welcome through community participation. However, currently the following disclaimer should be taken with seriousness: USE AT YOUR OWN RISK. THINGS WILL CHANGE. The following is equally true, however: CONTRIBUTIONS WELCOME. USAGE WELCOME. Usage ===== -To use eimap from your imap application add the following line to your rebar -config: +To add eimap from your application, add the following line to the deps of rebar.config: + + * rebar 2.x: { eimap, "*", {git, "git://git.kolab.org/diffusion/EI/eimap.git" } } + * rebar3: eimap + - { eimap, "*", {git, "git://git.kolab.org/diffusion/EI/eimap.git" } } There is no need to start the eimap application as it does not have any process or startup routines related to its usage. eimap does rely on lager being avilable, however. eimap Module ============ The eimap erlang module is the home of the primary API for the eimap library. It is a gen_fsm and should be started as a process in the normal Erlang/OTP manner for use. An eimap instance represents a single IMAP connection to a single IMAP server and is stateful: commands that are started may change the selected folder, for instance, and commands that are sent may be put into a command queue for subsequent execution depeding on the current state of the connection. Once started, an eimap process may be directed to connect to an imap server and then start with functions such as fetching path tokens: ImapServerArgs = [ { host, "imap.acme.com" }, { port, 143 }, { tls, starttls } ] { ok, Imap } = eimap:start_link(ImapServerArgs), eimap_imap:starttls(), eimap_imap:login(Imap, self(), undefined, "username", "password"), eimap_imap:connect(Imap), eimap_imap:get_path_tokens(Imap, self(), get_path_tokens) The Imap server args is a simple proplist which allows one to set host, port and TLS settings. For TLS, the following values are supported: * true: start a TLS session when opening the socket ("implicit TLS") * starttls: start TLS via the STARTTLS IMAP command auomagically * false: just open a regular connection; this is what you usually want, and the client will call eimap:starttls/3 when it wishes to switch to an encrypted connection The starttls, login and even get_path_tokens commands will be queued and sent to the IMAP server only when the connection has been established. This prevents having to wait for connection signals and lets you write what you intend to be executed with as few issues as possible. Commands are executed in the order they are queued, and they follow a consistent parametic pattern: * the first parameter is the eimap PID returned by eimap:start_link/1 * the second parameter is the PID the response should be sent to as a message * the third parameter is a token to send back with the response, allowing users of eimap to track responses to specific commands; undefined is allowed and will surpress the use of a response token * .. additional parameters specific to the command, such as username and password in the case of login Responses are sent as a normal message with the form: Response { Token, Resposne } The Token is the resposne token provided as the third parameter and may be anything. The Response depends on the given command, but will be a properly formed Erlang term representing the content of the response. Passthrough Mode ================ eimap supports a mode of operation which simply passes data between the user and the imap server blindly. This is known as "passthrough" mode and can be started and stopped with the start_passthrough/2 and stop_passthrough/1 functions. Data is queued up for sending with the passthrough_data/2 function, and will be sent to the server as soon as possible. Responses are similarly sent back to the initiator of the passthrough mode for dispatch in the form of { imap_server_response, Data } messages. The receiver is the PID passed to start_passthrough/2 as the second parameter. As the user is entirely responsible for the traffic and thereby the state of the IMAP conenction during passthrough, exercise caution while using this mode. Any commands which are queued using eimap's command functions (logini/5, logout/3, etc) will interupt passthrough to run those commands. Once the queued commands have been cleared passthrough will restart auomatically. Commands ======== Individual commands are implemented in individual modules in the src/commands/ directory, and this is the prefered mechanism for adding features to eimap. The API for commands is defined in src/eimap_command.erl as a behavior. Commands are expected to provide at least two functions: new_command(Args) -> { Command, ResponseType } create a command bitstring to be passed to the imap server and defines the type of response for this command. Response types include single_line_response, multiline_response, all_multiline_response and blob_response. Args is specific to the command, and some commands ignore this parameter Single line response -------------------- Commands which the IMAP server will respond to with a single line in return should use single_line_response and must implement formulate_response/2 which is passed the Data and the command Tag. This usually returns one of { fini, Result } or { error, Reason }, though some special commands may return atoms which the eimap module responds to such as starttls. Multiline Response ------------------ This is the most common response type and is used when the IMAP server may respond with zero or more untagged responses and a final tagged response. Such commands must implement: process_line/2 which is passed one line at a time (minus newlines) and a list accumulator to add responses to formulate_response/2 which is passed ok on success or an error tuple of { [no|bad], Reason }. This should return either { fini, Response } or { error, Reason } in most cases; and for that there is eimap_command:formulate_response. Which means that for most commands formulate_response/2 is implemented as: formulate_response(Result, Response) -> eimap_command:formulate(Result, Response). Commands which are all_multiline_responses also must implement process_tagged_line/2 which behaves exactly like process_line but which accepts the tagged response line from the IMAP server. This is useful for commands where useful information is also passed in the tagged response line. An example of this is the select/exmine commands. Unstructured (blob) responses ----------------------------- Responses that do not follow the usual untagged/tagged line response pattern may use the blog_response type and implement parse/2 which will be passed the data as it arrives. The command is responsible for all buffer stitching across network packets, etc. In the case of partial responses, parse/2 may return { more, fun/3, State }. The State object allows preserving the parsing state and will be passed back to fun/3 in addition to the Data and Tag parameters. parse/2 (or the continuation fun/3) should return { fini, Result } when successfully completed, or { error, Reason } when it fails. Only once either a fini or error tuple are returned will the eimap process move on to the next command queued. IMAP Utils ========== eimap_utils provides a set of handy helpers for use when IMAP'ing your way across the network. These include folder path and UID set extractors, IMAP server response manipulators and misc utilities. Testing ======= Tests can be run with `make tests` or `rebar eunit`. All new commands must be accompanied by tests in the test/ directory. Contributing ============ Maintainer: Aaron Seigo Mailing list: devel@lists.kolab.com Project page: https://git.kolab.org/tag/eimap/ This project uses the git flow workflow as described here: http://nvie.com/posts/a-successful-git-branching-model/ You can installed git flow from here: https://github.com/nvie/gitflow Then initialize your local clone with `git flow init -d`. You can find the list of open tasks on the project page's workboard. Anything in the backlog is open to be worked on. diff --git a/rebar b/rebar deleted file mode 100755 index dacc520..0000000 Binary files a/rebar and /dev/null differ diff --git a/rebar.config b/rebar.config index 879d214..e8bac2a 100644 --- a/rebar.config +++ b/rebar.config @@ -1,19 +1,17 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et -{deps, [ - { lager, ".*", { git, "git://github.com/basho/lager.git" } } - ]}. - {erl_opts, [warnings_as_errors, {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"}]}. diff --git a/rebar3 b/rebar3 new file mode 100755 index 0000000..5f7ed75 Binary files /dev/null and b/rebar3 differ diff --git a/src/commands/eimap_command_annotation.erl b/src/commands/eimap_command_annotation.erl index 0a7b205..12dd234 100644 --- a/src/commands/eimap_command_annotation.erl +++ b/src/commands/eimap_command_annotation.erl @@ -1,49 +1,49 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_annotation). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% Public API new_command(Folder) when is_list(Folder) -> new_command(list_to_binary(Folder)); new_command(Folder) -> { <<"GETANNOTATION \"", Folder/binary, "\" \"*\" \"value.shared\"">>, multiline_response }. process_line(<<" ", Data/binary>>, Acc) -> process_line(Data, Acc); process_line(<<"* ANNOTATION ", Data/binary>>, Acc) -> Pieces = binary:split(Data, <<"\"">>, [global]), process_pieces(Pieces, Acc); process_line(<<>>, Acc) -> Acc. formulate_response(Result, Data) -> eimap_command:formulate_response(Result, Data). %% Private API process_pieces([MBox, Key, _, _, _, Value, _], Acc) when MBox =/= <<>> -> [ { Key, translate(Value) } | Acc ]; process_pieces([_, _MBox, _, Key, _, _, _, Value, _], Acc) -> [ { Key, translate(Value) } | Acc ]; process_pieces(_, Acc) -> Acc. translate(<<"false">>) -> false; translate(<<"true">>) -> true; translate(Value) -> try list_to_integer(binary_to_list(Value)) of Number -> Number catch _:_ -> Value end. diff --git a/src/commands/eimap_command_capability.erl b/src/commands/eimap_command_capability.erl index c0d9b30..45156a2 100644 --- a/src/commands/eimap_command_capability.erl +++ b/src/commands/eimap_command_capability.erl @@ -1,50 +1,50 @@ %% 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 +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_capability). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command(parse_serverid) -> { <<"CAPABILITY">>, single_line_response }; new_command(_Args) -> { <<"CAPABILITY">>, multiline_response }. process_line(<<"* CAPABILITY ", Data/binary>>, _Acc) -> [Data]; process_line(_Data, Acc) -> Acc. formulate_response(ok, Response) -> { fini, Response }; formulate_response({ _, Reason }, _Data) -> { error, Reason }; formulate_response(Data, Tag) -> parse_oneliner(eimap_utils:check_response_for_failure(Data, Tag), eimap_utils:remove_tag_from_response(Data, Tag, check)). %% Private API % TODO: probably way too cyrus imap specific on the responses (capitalization, etc) % make generic with a nicer parser parse_oneliner(ok, <<"* OK [CAPABILITY ", Data/binary>>) -> % this is a server response on connect { End, _ } = binary:match(Data, <<"]">>), { TextEnd, _ } = binary:match(Data, <<"\r\n">>), Capabilities = binary:part(Data, { 0, End }), ServerID = binary:part(Data, { End + 2, TextEnd - End - 2}), { fini, { Capabilities, ServerID } }; parse_oneliner({ _, Reason }, _Data) -> { error, Reason }; -parse_oneliner(_, Data) -> { fini, Data }. +parse_oneliner(_, Data) -> { fini, { Data, <<>> } }. diff --git a/src/commands/eimap_command_compress.erl b/src/commands/eimap_command_compress.erl index 3e32a2a..c23e789 100644 --- a/src/commands/eimap_command_compress.erl +++ b/src/commands/eimap_command_compress.erl @@ -1,32 +1,32 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_compress). -behavior(eimap_command). -export([new_command/1, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command(_Args) -> { <<"COMPRESS DEFLATE">>, single_line_response }. formulate_response(Data, Tag) -> case eimap_utils:check_response_for_failure(Data, Tag) of ok -> compression_active; { _, Reason } -> { error, Reason } end. diff --git a/src/commands/eimap_command_getmetadata.erl b/src/commands/eimap_command_getmetadata.erl index cdfb83d..c9ed920 100644 --- a/src/commands/eimap_command_getmetadata.erl +++ b/src/commands/eimap_command_getmetadata.erl @@ -1,117 +1,117 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_getmetadata). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command({ Folder }) -> new_command({ Folder, [] }); new_command({ Folder, Attributes }) -> new_command({ Folder, Attributes, infinity, nomax }); %% Depth and MaxSize are both optional arguments, so we have a 4-tuple version of new_command to accomodate this %% "Depth, Maxsize" could be replaced with a proplist of the form [ { depth, Depth }, { maxsize, MaxSize } ] but %% as this is an internal (to eimap) API there is little benefit for the performance cost new_command({ Folder, Attributes, Depth, MaxSize }) when is_list(Folder) -> new_command({ list_to_binary(Folder), Attributes, Depth, MaxSize }); new_command({ Folder, Attributes, Depth, MaxSize }) -> AttributesString = format_attributes(Attributes, <<>>), DepthString = depth_param(Depth), MaxSizeString = maxsize_param(MaxSize), Command = metadata_command(DepthString, MaxSizeString, Folder, AttributesString), { Command, multiline_response }. process_line(<<"* METADATA ", Details/binary>>, Acc) -> Results = parse_folder(Details), [Results|Acc]; process_line(_Line, Acc) -> Acc. formulate_response(Result, Data) -> eimap_command:formulate_response(Result, Data). %% Private API depth_param(infinity) -> <<"DEPTH infinity">>; depth_param(Depth) when is_integer(Depth) -> Bin = integer_to_binary(Depth), <<"DEPTH ", Bin/binary>>; depth_param(_) -> <<>>. maxsize_param(Size) when is_integer(Size) -> Bin = integer_to_binary(Size), <<"MAXSIZE ", Bin/binary>>; maxsize_param(_) -> <<>>. metadata_command(<<>>, <<>>, Folder, Attributes) -> <<"GETMETADATA \"", Folder/binary, "\"", Attributes/binary>>; metadata_command(Depth, <<>>, Folder, Attributes) -> <<"GETMETADATA (", Depth/binary, ") \"", Folder/binary, "\"", Attributes/binary>>; metadata_command(<<>>, MaxSize, Folder, Attributes) -> <<"GETMETADATA (", MaxSize/binary, ") \"", Folder/binary, "\"", Attributes/binary>>; metadata_command(Depth, MaxSize, Folder, Attributes) -> <<"GETMETADATA (", Depth/binary, " ", MaxSize/binary, ") \"", Folder/binary, "\"", Attributes/binary>>. format_attributes([], <<>>) -> <<>>; format_attributes([], String) -> <<" (", String/binary, ")">>; format_attributes([Attribute|Attributes], String) -> AttrBin = case is_list(Attribute) of true -> list_to_binary(Attribute); false -> Attribute end, case String of <<>> -> format_attributes(Attributes, AttrBin); _ -> format_attributes(Attributes, <>) end; format_attributes(_, _String) -> <<>>. parse_folder(<<"\"", Rest/binary>>) -> { Folder, RemainingBuffer } = until_closing_quote(Rest), Properties = parse_properties(RemainingBuffer), { Folder, Properties }; parse_folder(Buffer) -> { FolderEnd, _ } = binary:match(Buffer, <<" ">>), Folder = binary:part(Buffer, 0, FolderEnd), Properties = parse_properties(binary:part(Buffer, FolderEnd, size(Buffer) - FolderEnd)), { Folder, Properties }. parse_properties(Buffer) -> { Start, _ } = binary:match(Buffer, <<"(">>), { End, _ } = binary:match(Buffer, <<")">>), Properties = binary:part(Buffer, Start + 1, End - Start - 1), next_property(Properties, []). next_property(<<>>, Acc) -> Acc; next_property(Buffer, Acc) -> { KeyEnd, _ } = binary:match(Buffer, <<" ">>), Key = binary:part(Buffer, 0, KeyEnd), { Value, RemainingBuffer } = case next_value(binary:part(Buffer, KeyEnd + 1, size(Buffer) - KeyEnd - 1)) of { <<"NIL">>, RBuffer } -> { null, RBuffer }; Rv -> Rv end, next_property(RemainingBuffer, [{ Key, Value }|Acc]). next_value(<<"\"", Rest/binary>>) -> until_closing_quote(Rest); next_value(Buffer) -> case binary:match(Buffer, <<" ">>) of nomatch -> { Buffer, <<>> }; { ValueEnd , _ } -> { binary:part(Buffer, 0, ValueEnd - 1), binary:part(Buffer, ValueEnd, size(Buffer) - ValueEnd) } end. until_closing_quote(Buffer) -> until_closing_quote(Buffer, 0, binary:at(Buffer, 0), 0, <<>>). until_closing_quote(Buffer, Start, $\\, Pos, Acc) -> Escaped = binary:at(Buffer, Pos + 1 ), until_closing_quote(Buffer, Start, binary:at(Buffer, Pos + 2), Pos + 2, <>); until_closing_quote(Buffer, Start, $", Pos, Acc) -> { Acc, binary:part(Buffer, Start + Pos + 1, size(Buffer) - Start - Pos - 1) }; until_closing_quote(Buffer, _Start, Char, Pos, Acc) when Pos =:= size(Buffer) - 1 -> { <>, <<>> }; until_closing_quote(Buffer, Start, Char, Pos, Acc) -> until_closing_quote(Buffer, Start, binary:at(Buffer, Pos + 1), Pos + 1, <>). diff --git a/src/commands/eimap_command_login.erl b/src/commands/eimap_command_login.erl index d4a8f11..358d233 100644 --- a/src/commands/eimap_command_login.erl +++ b/src/commands/eimap_command_login.erl @@ -1,35 +1,35 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_login). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command({ User, Password }) -> UserBin = eimap_utils:ensure_binary(User), PasswordBin = eimap_utils:ensure_binary(Password), { <<"LOGIN ", UserBin/binary, " ", PasswordBin/binary>>, multiline_response }. process_line(_Data, Acc) -> Acc. %% Private API formulate_response(ok, _Acc) -> { fini, authed }; formulate_response({ _, Reason }, _Acc) -> { error, Reason }. diff --git a/src/commands/eimap_command_logout.erl b/src/commands/eimap_command_logout.erl index ef806ee..9bfb3e2 100644 --- a/src/commands/eimap_command_logout.erl +++ b/src/commands/eimap_command_logout.erl @@ -1,29 +1,29 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_logout). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% Public API new_command(_Args) -> { <<"LOGOUT">>, multiline_response }. process_line(_Data, Acc) -> Acc. formulate_response(ok, _Data) -> { close_socket, ok }; formulate_response({ _, Reason }, _Data) -> { close_socket, { error, Reason } }. diff --git a/src/commands/eimap_command_namespace.erl b/src/commands/eimap_command_namespace.erl index cf59915..74bf4a6 100644 --- a/src/commands/eimap_command_namespace.erl +++ b/src/commands/eimap_command_namespace.erl @@ -1,74 +1,74 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_namespace). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command(_Args) -> { <<"NAMESPACE">>, multiline_response }. process_line(<<"* NAMESPACE ", Data/binary>>, Acc) -> [process_shared_prefix_parts(Data, 1)|Acc]; process_line(_Line, Acc) -> Acc. formulate_response(ok, [Acc]) -> { fini, relevant_shared_prefix_parts(Acc) }; formulate_response({ _, Reason }, _Acc) -> { error, Reason }. % Private API relevant_shared_prefix_parts([]) -> { none, none}; relevant_shared_prefix_parts([[], [], _]) -> { none, none }; relevant_shared_prefix_parts([[], Delim]) -> { none, Delim }; relevant_shared_prefix_parts([SharedPrefix, [], _]) -> { SharedPrefix, "/" }; relevant_shared_prefix_parts([SharedPrefix, Delim]) -> { SharedPrefix, Delim }. process_shared_prefix_parts(<<"NIL ", Data/binary>>, PartNumber) -> process_shared_prefix_parts(Data, PartNumber + 1); process_shared_prefix_parts(<<"NIL", Data/binary>>, PartNumber) -> process_shared_prefix_parts(Data, PartNumber + 1); process_shared_prefix_parts(<<"((", Data/binary>>, PartNumber) -> process_shared_prefix_parts_inner(Data, [], PartNumber); process_shared_prefix_parts(<<>>, _PartNumber) -> []. process_shared_prefix_parts_inner(<<"))", _/binary>>, Acc, 3) -> parse_shared_prefix_fields(lists:reverse(Acc), []); process_shared_prefix_parts_inner(<<")) ", Data/binary>>, _Acc, N) -> process_shared_prefix_parts(Data, N + 1); process_shared_prefix_parts_inner(<>, Acc, 3) -> process_shared_prefix_parts_inner(Data, [Char|Acc], 3); process_shared_prefix_parts_inner(<<_Char, Data/binary>>, Acc, N) -> process_shared_prefix_parts_inner(Data, Acc, N); process_shared_prefix_parts_inner(<<>>, _Acc, _N) -> []. %%FIXME: naively assumes no "s in the values are allowed. is this correct? parse_shared_prefix_fields([$"|Tail], Acc) -> parse_shared_prefix_field_inner(Tail, [], Acc); parse_shared_prefix_fields([_|Tail], Acc) -> parse_shared_prefix_fields(Tail, Acc); parse_shared_prefix_fields([], Acc) -> lists:reverse(Acc). parse_shared_prefix_field_inner([$"|Tail], PartAcc, Acc) -> parse_shared_prefix_fields(Tail, [lists:reverse(PartAcc)|Acc]); parse_shared_prefix_field_inner([Char|Tail], PartAcc, Acc) -> parse_shared_prefix_field_inner(Tail, [Char|PartAcc], Acc); parse_shared_prefix_field_inner([], _PartAcc, Acc) -> Acc. diff --git a/src/commands/eimap_command_noop.erl b/src/commands/eimap_command_noop.erl index 50dfd05..31be58a 100644 --- a/src/commands/eimap_command_noop.erl +++ b/src/commands/eimap_command_noop.erl @@ -1,30 +1,30 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_noop). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). %% https://tools.ietf.org/html/rfc3501#section-6.3.2 %% Public API new_command(_) -> { <<"NOOP">>, multiline_response }. process_line(Data, Acc) -> eimap_command:process_status_line(Data, Acc). formulate_response(Response, Acc) -> eimap_command:formulate_response(Response, Acc). diff --git a/src/commands/eimap_command_peek_message.erl b/src/commands/eimap_command_peek_message.erl index c32d247..d4f164c 100644 --- a/src/commands/eimap_command_peek_message.erl +++ b/src/commands/eimap_command_peek_message.erl @@ -1,137 +1,137 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_peek_message). -behavior(eimap_command). -export([new_command/1, parse/2, continue_parse/3]). -record(parse_state, { body_size, parts, data }). -record(parts, { headers = <<"">>, body = <<"">>, flags = <<"">> }). %% https://tools.ietf.org/html/rfc3501#section-6.4.5 %% Public API new_command(MessageID) when is_integer(MessageID) -> new_command(integer_to_binary(MessageID)); new_command(MessageID) when is_binary(MessageID) -> { <<"UID FETCH ", MessageID/binary, " (FLAGS BODY.PEEK[HEADER] BODY.PEEK[TEXT])">>, blob_response }. continue_parse(Data, _Tag, #parse_state{ body_size = Size, parts = Parts, data = PrevData }) -> try_body_parse(<>, Size, Parts). parse(Data, Tag) when is_binary(Data) -> case eimap_utils:check_response_for_failure(Data, Tag) of ok -> process_parts(get_past_headers(Data)); { _, Reason } -> { error, Reason } end. %% Private API process_parts(#parts{ headers = Headers, flags = Flags, body = Body }) -> { fini, [ { flags, binary:split(Flags, <<" ">>, [global]) }, { headers, filter_headers(binary:split(Headers, <<"\r\n">>, [global])) }, { message, <> } ] }; process_parts(Result) -> Result. get_past_headers(<<" OK ", _Data/binary>>) -> #parts{}; get_past_headers(<<" FETCH ", Data/binary>>) -> find_open_parens(Data); get_past_headers(<<_, Data/binary>>) -> get_past_headers(Data); get_past_headers(<<>>) -> { error, <<"Unparsable">> }. find_open_parens(<<$(, Data/binary>>) -> parse_next_component(Data, #parts{}); find_open_parens(<<_, Data/binary>>) -> find_open_parens(Data). parse_next_component(<<"FLAGS (", Data/binary>>, Parts) -> parse_flags(Data, Parts); parse_next_component(<<"BODY[HEADER] {", Data/binary>>, Parts) -> parse_header(Data, Parts); parse_next_component(<<"BODY[TEXT] {", Data/binary>>, Parts) -> parse_body(Data, Parts); parse_next_component(<<_, Data/binary>>, Parts) -> parse_next_component(Data, Parts); parse_next_component(<<"OK Completed", _/binary>>, Parts) -> Parts; parse_next_component(<<>>, Parts) -> Parts. parse_flags(Data, Parts) -> parse_flags(Data, Parts, Data, 0). parse_flags(OrigData, Parts, <<$), Rest/binary>>, Length) -> FlagsString = binary:part(OrigData, 0, Length), parse_next_component(Rest, Parts#parts{ flags = FlagsString }); parse_flags(OrigData, Parts, <<_, Data/binary>>, Length) -> parse_flags(OrigData, Parts, Data, Length + 1); parse_flags(_OrigData, Parts, <<>>, _Length) -> Parts. parse_header(Data, Parts) -> parse_header(Data, Parts, Data, 0). parse_header(_OrigData, Parts, <<$}, Rest/binary>>, 0) -> parse_next_component(Rest, Parts); parse_header(OrigData, Parts, <<$}, Rest/binary>>, Length) -> ByteSizeString = binary:part(OrigData, 0, Length), Size = binary_to_integer(ByteSizeString), HeaderString = binary:part(Rest, 2, Size), %% the 2 is for \r\n %%FIXME: make sure we have enough data loaded, otherwise continue Remainder = binary:part(Rest, Size, byte_size(Rest) - Size), parse_next_component(Remainder, Parts#parts{ headers = HeaderString }); parse_header(OrigData, Parts, <<_, Rest/binary>>, Length) -> parse_header(OrigData, Parts, Rest, Length + 1); parse_header(_OrigData, Parts, <<>>, _Length) -> Parts. parse_body(Data, Parts) -> parse_body(Data, Parts, Data, 0). parse_body(_OrigData, Parts, <<$}, Rest/binary>>, 0) -> parse_next_component(Rest, Parts); parse_body(OrigData, Parts, <<$}, Rest/binary>>, Length) -> ByteSizeString = binary:part(OrigData, 0, Length), Size = binary_to_integer(ByteSizeString), %%lager:info("We have ... ~p ~p ~p", [ByteSizeString, Size, byte_size(Rest)]), try_body_parse(Rest, Size, Parts); parse_body(OrigData, Parts, <<_, Rest/binary>>, Length) -> parse_body(OrigData, Parts, Rest, Length + 1); parse_body(_OrigData, Parts, <<>>, _Length) -> Parts. try_body_parse(Data, Size, Parts) -> case Size > byte_size(Data) of true -> { more, fun ?MODULE:continue_parse/3, #parse_state{ body_size = Size, parts = Parts, data = Data } }; false -> Body = binary:part(Data, 2, Size), %% the 2 is for \r\n Remainder = binary:part(Data, Size, byte_size(Data) - Size), process_parts(parse_next_component(Remainder, Parts#parts{ body = Body })) end. filter_headers(RawHeaders) -> filter_headers(RawHeaders, none, none, []). filter_headers([], CurrentKey, CurrentValue, Acc) -> filter_header_add(CurrentKey, CurrentValue, Acc); filter_headers([<<>>|Headers], CurrentKey, CurrentValue, Acc) -> filter_headers(Headers, CurrentKey, CurrentValue, Acc); filter_headers([Header|Headers], CurrentKey, CurrentValue, Acc) -> filter_header(Headers, CurrentKey, CurrentValue, Acc, binary:split(Header, <<": ">>)). filter_header(Headers, CurrentKey, CurrentValue, Acc, [Key, Value]) -> Added = filter_header_add(CurrentKey, CurrentValue, Acc), filter_headers(Headers, Key, Value, Added); filter_header(Headers, none, _CurrentValue, Acc, _Value) -> filter_headers(Headers, none, none, Acc); filter_header(Headers, CurrentKey, CurrentValue, Acc, [Value]) -> %%TODO: get rid of tab, ensure "proper" whitespace NewValue = <>, filter_headers(Headers, CurrentKey, NewValue, Acc). filter_header_add(CurrentKey, CurrentValue, Acc) when CurrentKey =/= none, CurrentValue =/= none -> %%TODO: split CurrentValue on ',' [ { CurrentKey, CurrentValue } | Acc ]; filter_header_add(_, _, Acc) -> Acc. diff --git a/src/commands/eimap_command_starttls.erl b/src/commands/eimap_command_starttls.erl index 4d8aabf..770d1b4 100644 --- a/src/commands/eimap_command_starttls.erl +++ b/src/commands/eimap_command_starttls.erl @@ -1,32 +1,32 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_starttls). -behavior(eimap_command). -export([new_command/1, formulate_response/2]). %% http://tools.ietf.org/html/rfc2342 %% Public API new_command(_Args) -> { <<"STARTTLS">>, single_line_response }. formulate_response(Data, Tag) -> case eimap_utils:check_response_for_failure(Data, Tag) of ok -> starttls; { _, Reason } -> { error, Reason } end. diff --git a/src/commands/eimap_command_status.erl b/src/commands/eimap_command_status.erl index 3c64669..633d028 100644 --- a/src/commands/eimap_command_status.erl +++ b/src/commands/eimap_command_status.erl @@ -1,78 +1,78 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_status). -behavior(eimap_command). -export([new_command/1, process_line/2, formulate_response/2]). % https://tools.ietf.org/html/rfc5464 % supported attributes: messages, recent, uidnext, uidvalidity, unseen, annotate % Public API new_command({ Folder, Attributes }) when is_list(Folder) -> new_command({ list_to_binary(Folder), Attributes }); new_command({ <<>>, Attributes}) -> new_command({ <<"INBOX">>, Attributes }); new_command({ Folder, []}) -> new_command({ Folder, [messages] }); new_command({ Folder, Attributes }) when is_list(Attributes) -> AttributesString = attribute_string(Attributes, <<>>), { <<"STATUS ", Folder/binary, " (", AttributesString/binary, ")">>, multiline_response }. process_line(<<"* STATUS ", Data/binary>>, Acc) -> process_status_items(binary:match(Data, <<"(">>), binary:match(Data, <<")">>), Data, Acc); process_line(_, Acc) -> Acc. formulate_response(Result, Data) -> eimap_command:formulate_response(Result, Data). %% Private API attribute_string([], <<>>) -> attribute_string(messages); attribute_string([], String) -> String; attribute_string([Attribute|Attributes], <<>>) -> Attr = attribute_string(Attribute), attribute_string(Attributes, Attr); attribute_string([Attribute|Attributes], String) -> case attribute_string(Attribute) of <<>> -> attribute_string(Attributes, String); Attr -> attribute_string(Attributes, <>) end. % Private API process_status_items(nomatch, _, _Data, Acc) -> Acc; process_status_items(_, nomatch, _Data, Acc) -> Acc; process_status_items({ Start, _ }, { End, _ }, Data, Acc) -> Parts = binary:split(binary:part(Data, Start + 1, End - Start - 1), <<" ">>, [global]), process_next_status_item(Parts, Acc). process_next_status_item([], Acc) -> Acc; process_next_status_item([_], Acc) -> Acc; process_next_status_item([Token, Value|Parts], Acc) -> Tuple = { attribute_atom(Token), binary_to_integer(Value) }, process_next_status_item(Parts, [Tuple|Acc]). attribute_string(messages) -> <<"MESSAGES">>; attribute_string(recent) -> <<"RECENT">>; attribute_string(uidnext) -> <<"UIDNEXT">>; attribute_string(uidvalidity) -> <<"UIDVALIDITY">>; attribute_string(unseen) -> <<"UNSEEN">>; attribute_string(_) -> <<>>. attribute_atom(<<"MESSAGES">>) -> messages; attribute_atom(<<"RECENT">>) -> recent; attribute_atom(<<"UIDNEXT">>) -> uidnext; attribute_atom(<<"UIDVALIDITY">>) -> uidvalidity; attribute_atom(<<"UNSEEN">>) -> unseen. diff --git a/src/commands/eimap_command_switch_folder.erl b/src/commands/eimap_command_switch_folder.erl index 52608ce..fa62a1e 100644 --- a/src/commands/eimap_command_switch_folder.erl +++ b/src/commands/eimap_command_switch_folder.erl @@ -1,65 +1,65 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_switch_folder). -behavior(eimap_command). -export([new_command/1, process_line/2, process_tagged_line/2, formulate_response/2]). %% https://tools.ietf.org/html/rfc3501#section-6.3.2 %% Public API new_command({ MBox, Mechanism }) when is_list(MBox) -> new_command({ list_to_binary(MBox), Mechanism }); new_command({ MBox, select }) -> new_command(MBox); new_command({ MBox, examine }) -> { <<"EXAMINE \"", MBox/binary, "\"">>, all_multiline_response }; new_command(MBox) when is_list(MBox) -> new_command(list_to_binary(MBox)); new_command(MBox) when is_binary(MBox) -> { <<"SELECT \"", MBox/binary, "\"">>, all_multiline_response }. %TODO: parse: % REQUIRED untagged responses: FLAGS, EXISTS, RECENT % REQUIRED OK untagged responses: UNSEEN, PERMANENTFLAGS, UIDNEXT, UIDVALIDITY process_line(<<"* OK [", Info/binary>>, Acc) -> case binary:match(Info, <<"]">>) of nomatch -> Acc; { ClosingBracket, _ } -> process_ok_response(binary:part(Info, 0, ClosingBracket), Acc) end; process_line(<<"* FLAGS ", FlagString/binary>>, Acc) -> Flags = eimap_utils:parse_flags(FlagString), [{ flags, Flags }|Acc]; process_line(<<"* ", Info/binary>>, Acc) -> case binary:split(Info, <<" ">>) of [ Value, Key ] -> [{ eimap_utils:binary_to_atom(Key), binary_to_integer(Value) }|Acc]; _ ->Acc end; process_line(_Data, Acc) -> Acc. process_tagged_line(Data, Acc) -> case binary:match(Data, <<"[READ-WRITE]">>) of nomatch -> [{ writeable, false }|Acc]; _ -> [{ writeable, true}|Acc] end. formulate_response(Response, Acc) -> eimap_command:formulate_response(Response, Acc). %PRIVATE process_ok_response(<<"PERMANENTFLAGS ", FlagString/binary>>, Acc) -> [{ permanent_flags, eimap_utils:parse_flags(FlagString) }|Acc]; process_ok_response(<<"UIDVALIDITY ", String/binary>>, Acc) -> [{ uid_validity, binary_to_integer(String) }|Acc]; process_ok_response(<<"UIDNEXT ", String/binary>>, Acc) -> [{ uid_next, binary_to_integer(String) }|Acc]; process_ok_response(<<"HIGHESTMODSEQ ", String/binary>>, Acc) -> [{ highest_mod_seq, binary_to_integer(String) }|Acc]; process_ok_response(<<"URLMECH ", String/binary>>, Acc) -> [{ url_mech, eimap_utils:binary_to_atom(String) }|Acc]; %TODO: this is very ugly process_ok_response(<<"ANNOTATIONS ", String/binary>>, Acc) -> [{ annotations, binary_to_integer(String) }|Acc]; process_ok_response(String, Acc) -> lager:warning("eimap_command_select_folder: Unknown untagged OK response ~s~n", [String]), Acc. diff --git a/src/eimap.app.src b/src/eimap.app.src index d3abcd5..1e30a65 100644 --- a/src/eimap.app.src +++ b/src/eimap.app.src @@ -1,19 +1,22 @@ %% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*- {application, eimap, [ { description, "IMAP client implementation" }, - { vsn, "0.2.5" }, + { vsn, "0.3.0" }, { registered, [] }, { applications, [ kernel, stdlib, crypto, ssl, compiler, syntax_tools, goldrush, lager ] }, - { env, [ ]} + { env, [ ]}, + { maintainers, ["Aaron Seigo"] }, + { licenses, ["LGPLv3+"] }, + { links, ["https://git.kolab.org/diffusion/EI/"] } ]}. diff --git a/src/eimap.erl b/src/eimap.erl index 2434cfe..b331d1d 100644 --- a/src/eimap.erl +++ b/src/eimap.erl @@ -1,481 +1,481 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% 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 }). -record(command, { tag, mbox, message, from, response_type, response_token, parse_state }). -define(SSL_UPGRADE_TIMEOUT, 5000). -define(TCP_CONNECT_TIMEOUT, 5000). %% 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), State = #state { host = Host, port = Port, tls = TLS }, { 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) -> { next_state, idle, ensure_process_command_queue(State) }; 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)]), next_command_after_response(Response, State); 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(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 }]. 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)), State#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), <<>>). 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, <>). diff --git a/src/eimap_uidset.erl b/src/eimap_uidset.erl index 1124c99..d48f79f 100644 --- a/src/eimap_uidset.erl +++ b/src/eimap_uidset.erl @@ -1,78 +1,78 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_uidset). -export([uid_list_to_binary/1, parse/1, next_uid/1]). % for internal use only -export([add_uid_as_binary_to_list/2, stitch_bin_list/2]). %% tokens have the form: integer | { integer, integer } -record(uidset, { tokens = [], current = none }). uid_list_to_binary(UidList) when is_list(UidList) -> ListOfBins = lists:foldl(fun ?MODULE:add_uid_as_binary_to_list/2, [], UidList), lists:foldl(fun ?MODULE:stitch_bin_list/2, <<>>, ListOfBins); uid_list_to_binary(_UidList) -> <<>>. parse(UidSet) when is_list(UidSet) -> parse(list_to_binary(UidSet)); parse(<<>>) -> badarg; parse(UidSet) when is_binary(UidSet) -> Components = binary:split(UidSet, <<",">>, [global]), try lists:foldl(fun(Component, Acc) -> add_component(binary:split(Component, <<":">>), Acc) end, [], Components) of Parsed -> #uidset{ tokens = lists:reverse(Parsed) } catch _:_ -> badarg end. next_uid(#uidset{ tokens = Tokens, current = Current }) -> next_uid(Tokens, Current). next_uid([], _Current) -> { none, #uidset {} }; next_uid([{ First, Second }|_] = FullList, Current) -> next_in_range(FullList, First, Second, Current); next_uid([Uid|UidSet], _Current) -> { Uid, #uidset{ tokens = UidSet } }. % PRIVATE API add_uid_as_binary_to_list(Uid, Acc) when is_integer(Uid), Uid >= 0 -> [integer_to_binary(Uid)|Acc]; add_uid_as_binary_to_list({ StartUid, EndUid }, Acc) when is_integer(StartUid), is_integer(EndUid) -> case add_uid_range_as_binary_to_list(StartUid, EndUid) of error -> Acc; { Start, End } -> [<>|Acc] end; add_uid_as_binary_to_list(_, Acc) -> Acc. add_uid_range_as_binary_to_list(Start, End) when Start < 0; End < 0 -> error; add_uid_range_as_binary_to_list(Start, End) when Start < End -> { integer_to_binary(Start), integer_to_binary(End) }; add_uid_range_as_binary_to_list(Start, End) -> { integer_to_binary(End), integer_to_binary(Start) }. stitch_bin_list(Uid, <<>>) -> Uid; stitch_bin_list(Uid, Acc) -> <>. add_component(Uid, Acc) when is_integer(Uid), Uid >= 0 -> [Uid|Acc]; add_component([First, Second], Acc) -> add_component(binary_to_integer(First), binary_to_integer(Second), Acc); add_component([<<"">>], Acc) -> Acc; add_component([Single], Acc) when is_binary(Single) -> add_component(binary_to_integer(Single), Acc); add_component(_, _Acc) -> throw(badarg). add_component(First, Second, Acc) when is_integer(First), First >= 0, is_integer(Second), Second >= 0 -> add_range(First, Second, Acc); add_component(_, _, _Acc) -> throw(badarg). add_range(First, Second, Acc) when First == Second -> [First|Acc]; add_range(First, Second, Acc) -> [{ First, Second }|Acc]. next_in_range(Tokens, First, _Second, none) -> { First, #uidset{ tokens = Tokens, current = First } }; next_in_range(Tokens, First, Second, Current) when First < Second, Current < Second -> Next = Current + 1, { Next, #uidset{ tokens = Tokens, current = Next } }; next_in_range(Tokens, First, Second, Current) when First > Second, Current > Second -> Next = Current - 1, { Next, #uidset{ tokens = Tokens, current = Next } }; next_in_range([_|Tokens], _First, _Second, _Current) -> next_uid(#uidset{ tokens = Tokens }). diff --git a/src/eimap_utils.erl b/src/eimap_utils.erl index 163229b..84b5947 100644 --- a/src/eimap_utils.erl +++ b/src/eimap_utils.erl @@ -1,246 +1,246 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% 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">>)), { 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 }. eol_found(Buffer, nomatch) -> size(Buffer); eol_found(_Buffer, { MatchStart, _MatchLength }) -> 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_command_annotation_tests.erl b/test/eimap_command_annotation_tests.erl index b7d5b7a..32158a2 100644 --- a/test/eimap_command_annotation_tests.erl +++ b/test/eimap_command_annotation_tests.erl @@ -1,71 +1,71 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_annotation_tests). -include_lib("eunit/include/eunit.hrl"). % c("test/eimap_command_annotation_tests.erl"). eunit:test(eimap_command_annotation). parse_test_() -> Data = [ %% { tag, server_response, expected_parsed_results } { <<"EG0002">>, <<"* ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/color\" (\"value.shared\" \"32CD32\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/folder-type\" (\"value.shared\" \"event\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/x-toltec/test\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/horde/share-params\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/h-share-attr-desc\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/uniqueid\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/pxfb-readable-for\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/incidences-for\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/folder-test\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/displayname\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/kolab/activesync\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/uniqueid\" (\"value.shared\" \"b1a5bb95-2bd3-4628-a0d2-49e9bd10735e\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/squat\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/size\" (\"value.shared\" \"5726\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/sieve\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/sharedseen\" (\"value.shared\" \"false\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/pop3showafter\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/pop3newuidl\" (\"value.shared\" \"true\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/partition\" (\"value.shared\" \"default\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/news2mail\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/lastupdate\" (\"value.shared\" \"20-Mar-2015 05:17:51 +0100\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/lastpop\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/expire\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/vendor/cmu/cyrus-imapd/duplicatedeliver\" (\"value.shared\" \"false\")\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/thread\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/specialuse\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/sort\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/comment\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/checkperiod\" (\"value.shared\" NIL)\r\n * ANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"/check\" (\"value.shared\" NIL)\r\nEG0002 OK Completed\r\n">>, { fini, [ {<<"/vendor/cmu/cyrus-imapd/duplicatedeliver">>, false}, {<<"/vendor/cmu/cyrus-imapd/lastupdate">>, <<"20-Mar-2015 05:17:51 +0100">>}, {<<"/vendor/cmu/cyrus-imapd/partition">>,<<"default">>}, {<<"/vendor/cmu/cyrus-imapd/pop3newuidl">>,true}, {<<"/vendor/cmu/cyrus-imapd/sharedseen">>,false}, {<<"/vendor/cmu/cyrus-imapd/size">>,5726}, {<<"/vendor/cmu/cyrus-imapd/uniqueid">>, <<"b1a5bb95-2bd3-4628-a0d2-49e9bd10735e">>}, {<<"/vendor/kolab/folder-type">>,<<"event">>}, {<<"/vendor/kolab/color">>,<<"32CD32">>} ] } }, { <<"EG0002">>, <<"* ANNOTATION user/john.doe/Sent@example.org \"/vendor/x-toltec/test\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/horde/share-params\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/h-share-attr-desc\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/uniqueid\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/pxfb-readable-for\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/incidences-for\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/folder-type\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/folder-test\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/displayname\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/color\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/kolab/activesync\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/uniqueid\" (\"value.shared\" \"237357ec-7610-422e-9e55-0bae83caf58a\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/squat\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/size\" (\"value.shared\" \"401\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/sieve\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/sharedseen\" (\"value.shared\" \"false\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/pop3showafter\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/pop3newuidl\" (\"value.shared\" \"true\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/partition\" (\"value.shared\" \"default\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/news2mail\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/lastupdate\" (\"value.shared\" \"23-Mar-2015 15:19:29 +0100\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/lastpop\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/expire\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/vendor/cmu/cyrus-imapd/duplicatedeliver\" (\"value.shared\" \"false\")\r\n * ANNOTATION user/john.doe/Sent@example.org \"/thread\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/specialuse\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/sort\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/comment\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/checkperiod\" (\"value.shared\" NIL)\r\n * ANNOTATION user/john.doe/Sent@example.org \"/check\" (\"value.shared\" NIL)\r\nEG0002 OK Completed\r\n">>, { fini, [ {<<"/vendor/cmu/cyrus-imapd/duplicatedeliver">>,false}, {<<"/vendor/cmu/cyrus-imapd/lastupdate">>,<<"23-Mar-2015 15:19:29 +0100">>}, {<<"/vendor/cmu/cyrus-imapd/partition">>,<<"default">>}, {<<"/vendor/cmu/cyrus-imapd/pop3newuidl">>,true}, {<<"/vendor/cmu/cyrus-imapd/sharedseen">>,false}, {<<"/vendor/cmu/cyrus-imapd/size">>,401}, {<<"/vendor/cmu/cyrus-imapd/uniqueid">>,<<"237357ec-7610-422e-9e55-0bae83caf58a">>} ] } } ], { _Command, ResponseType } = eimap_command_annotation:new_command(<<"/my/folder">>), lists:foldl(fun({ Tag, ServerData, Expected }, Acc) -> [?_assertEqual(Expected, eimap_command:parse_response(ResponseType, ServerData, Tag, eimap_command_annotation))|Acc] end, [], Data). new_test_() -> Data = [ %% mailbox, command { <<"user/john.doe/Calendar/Personal Calendar@example.org">>, { <<"GETANNOTATION \"user/john.doe/Calendar/Personal Calendar@example.org\" \"*\" \"value.shared\"">>, multiline_response } } ], lists:foldl(fun({ Mailbox, Command }, Acc) -> [?_assertEqual(Command, eimap_command_annotation:new_command(Mailbox))|Acc] end, [], Data). diff --git a/test/eimap_command_capability_tests.erl b/test/eimap_command_capability_tests.erl index 745a13c..a4de267 100644 --- a/test/eimap_command_capability_tests.erl +++ b/test/eimap_command_capability_tests.erl @@ -1,70 +1,70 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_capability_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { parse_serverid, <<"* OK [CAPABILITY IMAP4rev1 LITERAL+ ID ENABLE STARTTLS LOGINDISABLED] acme.com Cyrus IMAP 2.5.5.5-Kolab-2.5.5-5.1.el6.kolab_14 server ready\r\n">>, <<>>, { fini, { <<"IMAP4rev1 LITERAL+ ID ENABLE STARTTLS LOGINDISABLED">>, <<"acme.com Cyrus IMAP 2.5.5.5-Kolab-2.5.5-5.1.el6.kolab_14 server ready">> } } }, { ok, <<"* CAPABILITY IMAP4rev1 LITERAL+ ID ENABLE STARTTLS AUTH=PLAIN AUTH=LOGIN SASL-IR\r\nabcd OK CAPABILITY COMPLETED\r\n">>, <<"abcd">>, { fini, [<<"IMAP4rev1 LITERAL+ ID ENABLE STARTTLS AUTH=PLAIN AUTH=LOGIN SASL-IR">>] } }, { ok, <<"other stuff\r\n">>, <<"abcd">>, { more, { <<>>, [], eimap_command_capability } } }, { ok, <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { ok, <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ InitArgs, Response, Tag, Parsed }, Acc) -> { _Command, ResponseType } = eimap_command_capability:new_command(InitArgs), [?_assertEqual(Parsed, eimap_command:parse_response(ResponseType, Response, Tag, eimap_command_capability))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { parse_serverid, { <<"CAPABILITY">>, single_line_response } }, { <<>>, { <<"CAPABILITY">>, multiline_response } }, { true, { <<"CAPABILITY">>, multiline_response } }, { [], { <<"CAPABILITY">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_capability:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_compress_tests.erl b/test/eimap_command_compress_tests.erl index b58c3f6..da551a9 100644 --- a/test/eimap_command_compress_tests.erl +++ b/test/eimap_command_compress_tests.erl @@ -1,55 +1,55 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_compress_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"abcd OK DEFLATE active\r\n">>, <<"abcd">>, compression_active }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], InitArgs = ok, { _Command, ResponseType } = eimap_command_compress:new_command(InitArgs), lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(ResponseType, Response, Tag, eimap_command_compress))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { ok, { <<"COMPRESS DEFLATE">>, single_line_response } }, { <<>>, { <<"COMPRESS DEFLATE">>, single_line_response } }, { true, { <<"COMPRESS DEFLATE">>, single_line_response } }, { [], { <<"COMPRESS DEFLATE">>, single_line_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_compress:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_getmetadata_tests.erl b/test/eimap_command_getmetadata_tests.erl index 8c482d2..83c23db 100644 --- a/test/eimap_command_getmetadata_tests.erl +++ b/test/eimap_command_getmetadata_tests.erl @@ -1,78 +1,78 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_getmetadata_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"* METADATA Tasks (/shared/vendor/kolab/folder-type \"task\")\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { <<"Tasks">>, [ { <<"/shared/vendor/kolab/folder-type">>, <<"task">> } ] } ] } }, { <<"* METADATA \"Tasks Tasks\" (/shared/vendor/kolab/folder-type \"task\")\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { <<"Tasks Tasks">>, [ { <<"/shared/vendor/kolab/folder-type">>, <<"task">> } ] } ] } }, { <<"* METADATA Tasks (/shared/vendor/kolab/folder-type \"task \\\"sigh\\\"\")\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { <<"Tasks">>, [ { <<"/shared/vendor/kolab/folder-type">>, <<"task \"sigh\"">> } ] } ] } }, { <<"* METADATA Tasks (/shared/vendor/kolab/folder-type \"task \\\"sigh\\\"\")\r\n* METADATA Archive (/shared/vendor/kolab/folder-type NIL)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { <<"Archive">>, [ {<<"/shared/vendor/kolab/folder-type">>, null } ] }, { <<"Tasks">>, [ { <<"/shared/vendor/kolab/folder-type">>, <<"task \"sigh\"">> } ] } ] } }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(multiline_response, Response, Tag, eimap_command_getmetadata))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { { <<>> }, { <<"GETMETADATA (DEPTH infinity) \"\"">>, multiline_response } }, { { <<>>, [<<"/shared/comment">>, "/private/comment"] }, { <<"GETMETADATA (DEPTH infinity) \"\" (/shared/comment /private/comment)">>, multiline_response } }, { { <<"/my/folder">>, [<<"/shared/comment">>, "/private/comment"] }, { <<"GETMETADATA (DEPTH infinity) \"/my/folder\" (/shared/comment /private/comment)">>, multiline_response } }, { { "/my/folder", [<<"/shared/comment">>, "/private/comment"] }, { <<"GETMETADATA (DEPTH infinity) \"/my/folder\" (/shared/comment /private/comment)">>, multiline_response } }, { { <<"/my/folder">> }, { <<"GETMETADATA (DEPTH infinity) \"/my/folder\"">>, multiline_response } }, { { "/my/folder", [], 10, 100 }, { <<"GETMETADATA (DEPTH 10 MAXSIZE 100) \"/my/folder\"">>, multiline_response } }, { { <<"/my/folder">>, [], 10, 100 }, { <<"GETMETADATA (DEPTH 10 MAXSIZE 100) \"/my/folder\"">>, multiline_response } }, { { <<"/my/folder">>, [], 10, none}, { <<"GETMETADATA (DEPTH 10) \"/my/folder\"">>, multiline_response } }, { { <<"/my/folder">>, [], none, 100 }, { <<"GETMETADATA (MAXSIZE 100) \"/my/folder\"">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_getmetadata:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_login_tests.erl b/test/eimap_command_login_tests.erl index 7407f88..fbba14d 100644 --- a/test/eimap_command_login_tests.erl +++ b/test/eimap_command_login_tests.erl @@ -1,51 +1,51 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_login_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"abcd OK LOGIN completed\r\n">>, <<"abcd">>, { fini, authed } }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(multiline_response, Response, Tag, eimap_command_login))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { { <<"aseigo">>, <<"oohyeah">> }, { <<"LOGIN aseigo oohyeah">>, multiline_response } }, { { "aseigo", "oohyeah" }, { <<"LOGIN aseigo oohyeah">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_login:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_logout_tests.erl b/test/eimap_command_logout_tests.erl index 15cc903..e948148 100644 --- a/test/eimap_command_logout_tests.erl +++ b/test/eimap_command_logout_tests.erl @@ -1,52 +1,52 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_logout_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"abcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { close_socket, ok } }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { close_socket, { error, <<"Uh uh uh">> } } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { close_socket, { error, <<"Uh uh uh">> } } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(multiline_response, Response, Tag, eimap_command_logout))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { <<>>, { <<"LOGOUT">>, multiline_response } }, { true, { <<"LOGOUT">>, multiline_response } }, { [], { <<"LOGOUT">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_logout:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_namespace_tests.erl b/test/eimap_command_namespace_tests.erl index 0271d8c..0b2cf81 100644 --- a/test/eimap_command_namespace_tests.erl +++ b/test/eimap_command_namespace_tests.erl @@ -1,72 +1,72 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_namespace_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"* NAMESPACE ((\"\" \"/\")) NIL NIL\r\nabcd OK NAMESPACE command completed\r\n">>, <<"abcd">>, { fini, { none, none } } }, % { % <<"* NAMESPACE NIL NIL ((\"\" \".\"))\r\nabcd OK NAMESPACE command completed\r\n">>, % <<"abcd">>, % namespace % }, % { % <<"* NAMESPACE ((\"\" \"/\")) NIL ((\"Public Folders/\" \"/\"))\r\nabcd OK NAMESPACE command completed\r\n">>, % <<"abcd">>, % namespace % }, % { % <<"* NAMESPACE ((\"\" \"/\")) ((\"~\" \"/\")) ((\"#shared/\" \"/\")\r\n(\"#public/\" \"/\")(\"#ftp/\" \"/\")(\"#news.\" \".\"))\r\nabcd OK NAMESPACE command completed\r\n">>, % <<"abcd">>, % namespace % }, % { % <<"* NAMESPACE ((\"INBOX.\" \".\")) NIL NIL\r\nabcd OK NAMESPACE command completed\r\n">>, % <<"abcd">>, % namespace % }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(multiline_response, Response, Tag, eimap_command_namespace))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { <<>>, { <<"NAMESPACE">>, multiline_response } }, { true, { <<"NAMESPACE">>, multiline_response } }, { [], { <<"NAMESPACE">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_namespace:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_noop_tests.erl b/test/eimap_command_noop_tests.erl index 6c0553a..097e89c 100644 --- a/test/eimap_command_noop_tests.erl +++ b/test/eimap_command_noop_tests.erl @@ -1,69 +1,69 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_noop_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { ok, <<"* 100 EXISTS\r\n* 22 EXPUNGE\r\n* 14 FETCH (FLAGS (\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen))\r\n* 3 Recent\r\nabcd OK Completed\r\n">>, <<"abcd">>, { fini, [ { recent, 3 }, { fetch, 14 }, { expunge, 22 }, { exists, 100 } ] } }, { ok, <<"abcd OK Completed\r\n">>, <<"abcd">>, { fini, [] } }, { ok, <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { ok, <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ InitArgs, ServerResponse, Tag, Parsed }, Acc) -> { _Command, ResponseType } = eimap_command_noop:new_command(InitArgs), [?_assertEqual(Parsed, eimap_command:parse_response(ResponseType, ServerResponse, Tag, eimap_command_noop))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { ok, { <<"NOOP">>, multiline_response } }, { <<>>, { <<"NOOP">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_noop:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_starttls_tests.erl b/test/eimap_command_starttls_tests.erl index c52ee16..a0a1faa 100644 --- a/test/eimap_command_starttls_tests.erl +++ b/test/eimap_command_starttls_tests.erl @@ -1,52 +1,52 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_starttls_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"abcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, starttls }, { <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(single_line_response, Response, Tag, eimap_command_starttls))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { <<>>, { <<"STARTTLS">>, single_line_response } }, { true, { <<"STARTTLS">>, single_line_response } }, { [], { <<"STARTTLS">>, single_line_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_starttls:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_status_tests.erl b/test/eimap_command_status_tests.erl index 7718e54..e05d324 100644 --- a/test/eimap_command_status_tests.erl +++ b/test/eimap_command_status_tests.erl @@ -1,65 +1,65 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_status_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [{ uidnext, 44292 }, { messages, 231 }] } }, { <<"* STATUS blurdybloop (MESSAGES 231 UIDNEXT 44292 RECENT 0 UIDVALIDITY 10110 UNSEEN 10)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [{ unseen, 10 }, { uidvalidity, 10110 }, { recent, 0 }, { uidnext, 44292 }, { messages, 231 }] } }, { <<"* STATUS blurdybloop (RECENT 1 UIDVALIDITY 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [{ uidvalidity, 44292 }, { recent, 1 }] } } ], lists:foldl(fun({ Response, Tag, Parsed }, Acc) -> [?_assertEqual(Parsed, eimap_command:parse_response(multiline_response, Response, Tag, eimap_command_status))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { { "INBOX", [] }, { <<"STATUS INBOX (MESSAGES)">>, multiline_response } }, { { <<"INBOX">>, [] }, { <<"STATUS INBOX (MESSAGES)">>, multiline_response } }, { { <<>>, [messages] }, { <<"STATUS INBOX (MESSAGES)">>, multiline_response } }, { { <<"">>, [messages] }, { <<"STATUS INBOX (MESSAGES)">>, multiline_response } }, { { <<"/my/folder">>, [messages, recent, uidnext, uidvalidity, unseen] }, { <<"STATUS /my/folder (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)">>, multiline_response } }, { { <<"/my/folder">>, [uidnext, recent, uidvalidity, unseen] }, { <<"STATUS /my/folder (UIDNEXT RECENT UIDVALIDITY UNSEEN)">>, multiline_response } }, { { <<"/my/folder">>, [garbage, unseen] }, { <<"STATUS /my/folder (UNSEEN)">>, multiline_response } }, { { <<"/my/folder">>, [garbage] }, { <<"STATUS /my/folder (MESSAGES)">>, multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_status:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_switch_folder_tests.erl b/test/eimap_command_switch_folder_tests.erl index 5ce537d..d50d34f 100644 --- a/test/eimap_command_switch_folder_tests.erl +++ b/test/eimap_command_switch_folder_tests.erl @@ -1,72 +1,72 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_switch_folder_tests). -include_lib("eunit/include/eunit.hrl"). parse_test_() -> Data = [ % { Binary Response, Binary Tag, Parsed Results } { <<"Trash">>, <<"* 0 EXISTS\r\n* 0 RECENT\r\n* FLAGS (\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen)\r\n* OK [PERMANENTFLAGS ()] Ok\r\n* OK [UIDVALIDITY 1447439786] Ok\r\n* OK [UIDNEXT 1] Ok\r\n* OK [HIGHESTMODSEQ 1] Ok\r\n* OK [URLMECH INTERNAL] Ok>\r\n* OK [ANNOTATIONS 65536] Ok\r\nabcd OK [READ-WRITE] SELECT completed\r\n">>, <<"abcd">>, { fini, [ { writeable, true }, { annotations, 65536 }, { url_mech, internal }, { highest_mod_seq, 1 }, { uid_next, 1 }, { uid_validity, 1447439786 }, { permanent_flags, [] }, { flags, [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">>] }, { recent, 0 }, { exists, 0 } ] } }, { <<"Nope">>, <<"abcd BAD Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } }, { <<"ALsoWrong">>, <<"abcd NO Uh uh uh\r\n">>, <<"abcd">>, { error, <<"Uh uh uh">> } } ], lists:foldl(fun({ InitArgs, ServerResponse, Tag, Parsed }, Acc) -> { _Command, ResponseType } = eimap_command_switch_folder:new_command(InitArgs), [?_assertEqual(Parsed, eimap_command:parse_response(ResponseType, ServerResponse, Tag, eimap_command_switch_folder))|Acc] end, [], Data). new_test_() -> Data = [ % input, output { "Trash", { <<"SELECT \"Trash\"">>, all_multiline_response } }, { <<"Trash">>, { <<"SELECT \"Trash\"">>, all_multiline_response } }, { { <<"Trash">>, select }, { <<"SELECT \"Trash\"">>, all_multiline_response } }, { { <<"Trash">>, examine }, { <<"EXAMINE \"Trash\"">>, all_multiline_response } }, { { "Trash", examine }, { <<"EXAMINE \"Trash\"">>, all_multiline_response } } ], lists:foldl(fun({ Params, Command }, Acc) -> [?_assertEqual(Command, eimap_command_switch_folder:new_command(Params))|Acc] end, [], Data). diff --git a/test/eimap_command_tests.erl b/test/eimap_command_tests.erl index 0a347d0..a678e4e 100644 --- a/test/eimap_command_tests.erl +++ b/test/eimap_command_tests.erl @@ -1,65 +1,65 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_command_tests). -include_lib("eunit/include/eunit.hrl"). -export([process_line/2, formulate_response/2]). % c("test/eimap_command_tests.erl"). eunit:test(eimap_command). process_line(Data, Acc) -> [{ marked, Data } | Acc]. formulate_response(Result, Data) -> eimap_command:formulate_response(Result, lists:reverse(Data)). literal_continuations_test_() -> Data = [ % input, output % { Binary Response, Binary Tag, Parsed Results } { <<"* STATUS 1 (MESSAGES 231 {14}\r\nUIDNEXT 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { marked, <<"* STATUS 1 (MESSAGES 231 UIDNEXT 44292)">> } ] } }, { <<"* STATUS 1a (MESSAGES 231 {14}\r\nUIDNEXT 44292)\r\nabcd OK Begin TLS negotiation">>, <<"abcd">>, { more, { <<"abcd OK Begin TLS negotiation">>, [ { marked, <<"* STATUS 1a (MESSAGES 231 UIDNEXT 44292)">> } ], ?MODULE } } }, { <<"* STATUS 2 (MESSAGES 231 {14}\r\n">>, <<"abcd">>, { more, { <<"* STATUS 2 (MESSAGES 231 {14}">>, [], ?MODULE } } }, { <<"* STATUS 3 (MESSAGES 231 {h14}\r\nUIDNEXT 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { marked, <<"* STATUS 3 (MESSAGES 231 {h14}">> }, { marked, <<"UIDNEXT 44292)">> } ] } }, { <<"* STATUS 4 (MESSAGES 231 \r\nUIDNEXT 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { marked, <<"* STATUS 4 (MESSAGES 231 ">> }, { marked, <<"UIDNEXT 44292)">> } ] } }, { <<"* STATUS 5 (MESSAGES 231 UIDNEXT 44292)\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { marked, <<"* STATUS 5 (MESSAGES 231 UIDNEXT 44292)">> } ] } } ], lists:foldl(fun({ Binary, Tag, Result }, Acc) -> [?_assertEqual(Result, eimap_command:parse_response(multiline_response, Binary, Tag, ?MODULE))|Acc] end, [], Data). diff --git a/test/eimap_uidset_tests.erl b/test/eimap_uidset_tests.erl index 901d88d..4422894 100644 --- a/test/eimap_uidset_tests.erl +++ b/test/eimap_uidset_tests.erl @@ -1,103 +1,103 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_uidset_tests). -include_lib("eunit/include/eunit.hrl"). % c("test/eimap_uidset_tests.erl"). eunit:test(eimap_uidset). new_test_() -> Data = [ % input, output { [4], <<"4">> }, { [1, 2, 3, 4], <<"1,2,3,4">> }, { [-1, 1, 2, 3, 4], <<"1,2,3,4">> }, { [1, {100, 10}, 2, 3, 4], <<"1,10:100,2,3,4">> }, { [1, {10, 100}, 2, 3, 4], <<"1,10:100,2,3,4">> }, { [1, {-10, 100}, 2, 3, 4], <<"1,2,3,4">> }, { [1, {-100, 10}, 2, 3, 4], <<"1,2,3,4">> }, { [1, {-10, 100}, 2, 3, 4], <<"1,2,3,4">> }, { [1, {"alpha", 100}, <<"random_binary">>, some_atom, 2, 3, 4], <<"1,2,3,4">> }, { <<"1,2,3,4">>, <<>> }, { other, <<>> }, { [], <<>> } ], lists:foldl(fun({ List, Binary }, Acc) -> [?_assertEqual(Binary, eimap_uidset:uid_list_to_binary(List))|Acc] end, [], Data). single_value_test_() -> [ ?_assertEqual([1], iterate_uidset(eimap_uidset:parse(<<"1,">>))), ?_assertEqual([1], iterate_uidset(eimap_uidset:parse(<<"1">>))), ?_assertEqual([20], iterate_uidset(eimap_uidset:parse(<<"20">>))), ?_assertEqual([147], iterate_uidset(eimap_uidset:parse(<<"147">>))) ]. multiple_single_value_test_() -> [ ?_assertEqual([1, 2], iterate_uidset(eimap_uidset:parse(<<"1,2">>))) ]. range_test_() -> [ ?_assertEqual([1, 2, 3], iterate_uidset(eimap_uidset:parse(<<"1:3">>))), ?_assertEqual([3, 2, 1], iterate_uidset(eimap_uidset:parse(<<"3:1">>))), ?_assertEqual([1], iterate_uidset(eimap_uidset:parse(<<"1:1">>))) ]. multiple_range_test_() -> [ ?_assertEqual([1, 2, 3, 10, 11, 12, 13, 14, 15], iterate_uidset(eimap_uidset:parse(<<"1:3,10:15">>))) ]. mix_single_and_range_test_() -> [ ?_assertEqual([1, 3, 4, 5], iterate_uidset(eimap_uidset:parse(<<"1,3:5">>))), ?_assertEqual([1, 3, 4, 5, 10], iterate_uidset(eimap_uidset:parse(<<"1,3:5,10">>))), ?_assertEqual([1, 3, 4, 5, 10, 20, 21, 22, 23, 30], iterate_uidset(eimap_uidset:parse(<<"1,3:5,10,20:23,30">>))) ]. mix_single_and_range_with_whitespace_test_() -> [ ?_assertEqual(badarg, eimap_uidset:parse(<<"1, 3:5">>)), ?_assertEqual(badarg, eimap_uidset:parse(<<"1,3:5 ,10">>)), ?_assertEqual(badarg, eimap_uidset:parse(<<"1,3 :5,10,20: 23,30">>)) ]. bad_uidsets_test_() -> [ ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<>>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"-1,3:5">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"alpha">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"1,a,3:5">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"1,3:5,a">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"1;2">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"1, 2">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"1:3:5">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"-11,3:5">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"11,-3:5">>))), ?_assertEqual(badarg, iterate_uidset(eimap_uidset:parse(<<"11,3:-5">>))) ]. iterate_uidset(badarg) -> badarg; iterate_uidset(UidSet) -> %%io:fwrite("we are going to iterate over ~p~n", [UidSet]), lists:reverse(iterate_uidset(eimap_uidset:next_uid(UidSet), [])). iterate_uidset({ none, _UidSet }, Acc) -> Acc; iterate_uidset({ Uid, UidSet }, Acc) -> iterate_uidset(eimap_uidset:next_uid(UidSet), [Uid|Acc]). diff --git a/test/eimap_utils_tests.erl b/test/eimap_utils_tests.erl index 389be6f..dd111d4 100644 --- a/test/eimap_utils_tests.erl +++ b/test/eimap_utils_tests.erl @@ -1,198 +1,198 @@ %% 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 General Public License as published by +%% 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 General Public License for more details. +%% GNU Library General Public License for more details. %% -%% You should have received a copy of the GNU General Public License +%% 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 = [ { { <<>>, <<>>, <<>> }, <<>> }, { { <<".">>, <<"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).