diff --git a/src/eimap_command.erl b/src/eimap_command.erl index 329e1e2..f351e88 100644 --- a/src/eimap_command.erl +++ b/src/eimap_command.erl @@ -1,78 +1,78 @@ -module(eimap_command). -export([ parse_response/4, formulate_response/2, process_status_line/2 ]). -type more_tuple() :: { more, ParseContinuation :: parse_continuation(), State :: term }. -type finished_tuple() :: { fini, Results :: term }. -type error_tuple() :: { error, Reason :: binary() }. -type parse_continuation() :: fun((Data :: binary(), Tag :: binary(), State :: term()) -> more_tuple() | finished_tuple() | error_tuple()). -callback new_command(Args :: any()) -> { binary(), single_line_response | multiline_reponse | blob_response }. % TODO:bring back when we can depend on OTP 18 which introduced optional_callback % also define parse/2 (for blob_response), formulate_response/2 %-callback process_line(Data :: binary(), Acc :: any()) -> % more_tuple() | finished_tuple() | error_tuple() | starttls. parse_response(multiline_response, Data, Tag, ParseState) -> multiline_parse(false, Data, Tag, ParseState); parse_response(all_multiline_response, Data, Tag, ParseState) -> multiline_parse(parse_tagged, Data, Tag, ParseState); parse_response(single_line_response, Data, Tag, Module) -> Module:formulate_response(Data, Tag); parse_response(blob_response, Data, Tag, { Continuation, ParseState }) -> Continuation(Data, Tag, ParseState); parse_response(blob_response, Data, Tag, Module) -> case Module:parse(Data, Tag) of { more, Continuation, ParseState } -> { more, { Continuation, ParseState } }; Response -> Response end. multiline_parse(ParseTaggedLine, Data, Tag, { LastPartialLine, Acc, Module }) -> FullBuffer = <>, { FullLinesBuffer, NewLastPartialLine } = eimap_utils:only_full_lines(FullBuffer), Lines = binary:split(FullLinesBuffer, <<"\r\n">>, [global]), process_lines(ParseTaggedLine, Tag, NewLastPartialLine, Lines, Acc, Module); multiline_parse(ParseTaggedLine, Data, Tag, Module) -> multiline_parse(ParseTaggedLine, Data, Tag, { <<>>, [], Module }). process_lines(_ParseTaggedLine, _Tag, LastPartialLine, [], Acc, Module) -> { more, { LastPartialLine, Acc, Module } }; process_lines(ParseTaggedLine, Tag, LastPartialLine, [Line|MoreLines], Acc, Module) -> { FirstLine, ContinuationBytes } = eimap_utils:num_literal_continuation_bytes(Line), process_line(ContinuationBytes, ParseTaggedLine, eimap_utils:is_tagged_response(FirstLine, Tag), Tag, LastPartialLine, FirstLine, MoreLines, Acc, Module). process_line(ContinuationBytes, ParseTaggedLine, IsTagged, Tag, LastPartialLine, Line, [<<>>|MoreLines], Acc, Module) -> %% skip empty lines process_line(ContinuationBytes, ParseTaggedLine, IsTagged, Tag, LastPartialLine, Line, MoreLines, Acc, Module); -process_line(0, ParseTaggedLine, tagged, Tag, _LastPartialLine, Line, _MoreLines, Acc, Module) -> +process_line(none, ParseTaggedLine, tagged, Tag, _LastPartialLine, Line, _MoreLines, Acc, Module) -> Checked = eimap_utils:check_response_for_failure(Line, Tag), Module:formulate_response(Checked, parse_tagged(Checked, ParseTaggedLine, Line, Acc, Module)); -process_line(0, ParseTaggedLine, untagged, Tag, LastPartialLine, Line, MoreLines, Acc, Module) -> %io:format("Calling it here with ~p~n~n...", [Line]), +process_line(none, ParseTaggedLine, untagged, Tag, LastPartialLine, Line, MoreLines, Acc, Module) -> process_lines(ParseTaggedLine, Tag, LastPartialLine, MoreLines, Module:process_line(Line, Acc), Module); process_line(ContinuationBytes, ParseTaggedLine, _IsTagged, Tag, LastPartialLine, Line, [], Acc, Module) -> %% the line was continued, but there is no more lines ... so this line is now our last partial line. more must be on its way io:format("Missing lines!~p~n~n", [Acc]), BytesAsBinary = integer_to_binary(ContinuationBytes), process_lines(ParseTaggedLine, Tag, <>, [], Acc, Module); process_line(_ContinuationBytes, ParseTaggedLine, IsTagged, Tag, LastPartialLine, Line, [NextLine|MoreLines], Acc, Module) -> { StrippedNextLine, NextContinuationBytes } = eimap_utils:num_literal_continuation_bytes(NextLine), io:format("Connected up the next line: ~p ~i~n", [StrippedNextLine, NextContinuationBytes]), FullLine = <>, process_line(NextContinuationBytes, ParseTaggedLine, IsTagged, Tag, LastPartialLine, FullLine, MoreLines, Acc, Module). formulate_response(ok, Data) -> { fini, Data }; formulate_response({ _, Reason }, _Data) -> { error, Reason }. parse_tagged(_, false, _Line, Acc, _Module) -> Acc; % we are not passing the tagged line forward (no content, e.g) parse_tagged(ok, _, Line, Acc, Module) -> Module:process_tagged_line(Line, Acc); % success, so pass it forward parse_tagged(_Checked, _ParsedTaggedLine, _Line, Acc, _Module) -> Acc. % error, don't bother passing it forward -spec process_status_line(Line :: binary(), Acc :: list()) -> NewAcc :: list(). process_status_line(<<"* ", Rest/binary>>, Acc) -> process_matched_status_line(binary:split(Rest, <<" ">>, [global]), Acc); process_status_line(_, Acc) -> Acc. process_matched_status_line([Number, Key|_], Acc) -> [{ eimap_utils:binary_to_atom(Key), binary_to_integer(Number) }|Acc]; process_matched_status_line(_, Acc) -> Acc. diff --git a/src/eimap_utils.erl b/src/eimap_utils.erl index ae026a9..a0a716f 100644 --- a/src/eimap_utils.erl +++ b/src/eimap_utils.erl @@ -1,258 +1,258 @@ %% Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU Library General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_utils). -export([ extract_path_from_uri/3, extract_uidset_from_uri/1, split_command_into_components/1, is_tagged_response/2, remove_tag_from_response/3, header_name/1, parse_flags/1, check_response_for_failure/2, ensure_binary/1, new_imap_compressors/0, only_full_lines/1, binary_to_atom/1, num_literal_continuation_bytes/1 ]). %% Translate the folder name in to a fully qualified folder path such as it %% would be used by a cyrus administrator. -spec extract_path_from_uri(SharedPrefix :: binary(), HierarchyDelim :: binary, URI :: binary()) -> Path :: binary() | bad_uri. extract_path_from_uri(SharedPrefix, HierarchyDelim, URI) when is_binary(URI) -> extract_path_from_uri(SharedPrefix, HierarchyDelim, binary_to_list(URI)); extract_path_from_uri(SharedPrefix, HierarchyDelim, URI) when is_list(URI) -> %%lager:info("Parsing ~p", [URI]), SchemeDefaults = [{ imap, 143 }, { imaps, 993 }], ParseOpts = [ { scheme_defaults, SchemeDefaults } ], case imap_folder_path(SharedPrefix, HierarchyDelim, http_uri:parse(URI, ParseOpts)) of Path when is_list(Path) -> list_to_binary(Path); Error -> Error end. -spec extract_uidset_from_uri(URI :: binary()) -> UIDSet:: binary(). extract_uidset_from_uri(URI) when is_binary(URI) -> { TagStart, TagEnd } = binary:match(URI, <<";UID=">>), UIDStart = TagStart + TagEnd + 1, UriLength = byte_size(URI), case binary:match(URI, <<";">>, [{ scope, { UIDStart, UriLength - UIDStart } }]) of nomatch -> binary:part(URI, UIDStart - 1, UriLength - UIDStart + 1); { Semicolon, _ } -> binary:part(URI, UIDStart - 1, Semicolon - UIDStart + 1) end. -spec header_name(mailbox_uid | groupware_uid | groupware_uid) -> binary(); (any()) -> unknown. header_name(mailbox_uid) -> <<"/vendor/cmu/cyrus-imapd/uniqueid">>; header_name(groupware_type) -> <<"X-Kolab-Type">>; header_name(groupware_uid) -> <<"Subject">>; header_name(_) -> unknown. -spec parse_flags(FlagString :: binary() | list()) -> Flags :: [binary()]. parse_flags(String) when is_list(String) -> parse_flags(list_to_binary(String)); parse_flags(<<"(", Parened/binary>>) -> case binary:match(Parened, <<")">>) of nomatch -> []; { ClosingParens, _ } -> parse_flags(binary_part(Parened, 0, ClosingParens)) end; parse_flags(<<>>) -> []; parse_flags(FlagString) when is_binary(FlagString) -> binary:split(FlagString, <<" ">>, [global]). -spec check_response_for_failure(Data :: binary(), Tag :: undefined | binary()) -> ok | { error, Reason :: binary() }. check_response_for_failure(Data, undefined) when is_binary(Data) -> check_response_for_failure(Data, <<>>); check_response_for_failure(Data, Tag) when is_binary(Data), is_binary(Tag) -> NoToken = <>, NoTokenLength = byte_size(NoToken), case NoTokenLength > byte_size(Data) of true -> ok; false -> is_no_token_found(Data, Tag, binary:match(Data, NoToken, [ { scope, { 0, NoTokenLength } } ])) end. -spec split_command_into_components(Buffer :: binary()) -> { Tag :: binary(), Command :: binary(), Data :: binary() }. split_command_into_components(Buffer) when is_binary(Buffer) -> split_command(Buffer). -spec is_tagged_response(Buffer :: binary(), Tag :: binary()) -> tagged | untagged. is_tagged_response(Buffer, Tag) -> TagSize = size(Tag) + 1, % The extra char is a space BufferSize = size(Buffer), case case (TagSize =< BufferSize) of true -> <> =:= binary:part(Buffer, 0, TagSize); _ -> false end of true -> tagged; _ -> untagged end. -spec num_literal_continuation_bytes(Buffer :: binary()) -> { BufferSansContinuation :: binary(), NumberBytes :: integer() }. num_literal_continuation_bytes(Buffer) when size(Buffer) < 4 -> - { Buffer, 0 }; + { Buffer, none }; num_literal_continuation_bytes(Buffer) -> case binary:last(Buffer) =:= $} of true -> number_of_bytes_in_continuation(Buffer); - false -> { Buffer, 0 } + false -> { Buffer, none } 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 }; + { Buffer, none }; confirm_continuation(Buffer, OpenBracePos) -> BufferSize = size(Buffer), % Strip any '+' following the literal size (due to LITERAL+) LiteralSize = case binary:at(Buffer, BufferSize - 2) of $+ -> binary:part(Buffer, OpenBracePos + 1, BufferSize - OpenBracePos - 3); _ -> binary:part(Buffer, OpenBracePos + 1, BufferSize - OpenBracePos - 2) end, try binary_to_integer(LiteralSize) of Result -> { binary:part(Buffer, 0, OpenBracePos), Result } catch - _:_ -> { Buffer, 0 } + _:_ -> { Buffer, none } 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) -> {Terminated, End} = eol_found(Buffer, binary:match(Buffer, <<"\r\n">>)), { Tag, CommandStart } = searched_in_buffer(Buffer, 0, End, binary:match(Buffer, <<" ">>, [ { scope, { 0, End } } ])), case Terminated of true when End == CommandStart -> %% when we have a newline, and the tag takes the entire line, the "tag" is actually the command {<<>>, Tag, <<>>}; _ -> { Command, DataStart } = searched_in_buffer(Buffer, CommandStart, End, binary:match(Buffer, <<" ">>, [ { scope, { CommandStart, End - CommandStart } } ])), Data = binary:part(Buffer, DataStart, End - (DataStart)), { Tag, Command, Data } end. eol_found(Buffer, nomatch) -> {false, size(Buffer)}; eol_found(_Buffer, { MatchStart, _MatchLength }) -> {true, MatchStart}. searched_in_buffer(Buffer, Start, End, nomatch) -> { binary:part(Buffer, Start, End - Start), End }; searched_in_buffer(Buffer, Start, _End, { MatchStart, MatchLength } ) -> { binary:part(Buffer, Start, MatchStart - Start), MatchStart + MatchLength }. is_no_token_found(Data, Tag, nomatch) -> BadToken = <>, BadTokenLength = byte_size(BadToken), Match = binary:match(Data, BadToken, [ { scope, { 0, BadTokenLength } } ]), is_bad_token_found(Data, Tag, Match); is_no_token_found(Data, _Tag, { Start, Length }) -> ReasonStart = Start + Length, Reason = binary:part(Data, ReasonStart, byte_size(Data) - ReasonStart), { no, chop_newlines(Reason) }. is_bad_token_found(_Data, _Tag, nomatch) -> ok; is_bad_token_found(Data, _Tag, { Start, Length }) -> ReasonStart = Start + Length, %% -2 is due to the traling \r\n Reason = binary:part(Data, ReasonStart, byte_size(Data) - ReasonStart), { bad, chop_newlines(Reason) }. chop_newlines(Data) -> Size = size(Data), chop_newline(Data, binary:at(Data, Size - 1), Size - 1). chop_newline(Data, $\r, Size) -> chop_newline(Data, binary:at(Data, Size - 1), Size - 1); chop_newline(Data, $\n, Size) -> chop_newline(Data, binary:at(Data, Size - 1), Size - 1); chop_newline(Data, _, Size) -> binary_part(Data, 0, Size + 1). imap_folder_path_from_parts(none, _HierarchyDelim, [], _Domain, Path) -> Path; imap_folder_path_from_parts(SharedPrefix, _HierarchyDelim, [], _Domain, Path) -> case string:str(Path, SharedPrefix) of 1 -> string:substr(Path, length(SharedPrefix) + 1); _ -> Path end; imap_folder_path_from_parts(_SharedPrefix, HierarchyDelim, User, Domain, "INBOX") -> string:join(["user", string:join([User, Domain], "@")], HierarchyDelim); imap_folder_path_from_parts(_SharedPrefix, HierarchyDelim, User , Domain, Path) -> string:join(["user", User, string:join([Path, Domain], "@")], HierarchyDelim). imap_folder_path(_SharedPrefix, _HierarchyDelim, { error, Reason }) -> lager:info("ERROR! ~p", [Reason]), bad_uri; imap_folder_path(SharedPrefix, HierarchyDelim, { ok, {_Scheme, User, Domain, _Port, FullPath, _Query} }) -> { VDomain, _ImapHost } = split_imap_uri_domain(string:tokens(Domain, "@")), [ [_|Path] | _ ] = string:tokens(FullPath, ";"), %%lager:info("PARSED IMAP URI: ~p ~p ~p", [User, VDomain, Path]), CanonicalPath = imap_folder_path_from_parts(SharedPrefix, HierarchyDelim, User, VDomain, http_uri:decode(Path)), %%lager:info("PUT TOGETHER AS: ~p", [CanonicalPath]), CanonicalPath. split_imap_uri_domain([ ImapHost ]) -> { ImapHost, ImapHost }; split_imap_uri_domain([ VDomain, ImapHost ]) -> { VDomain, ImapHost }. ensure_binary(Arg) when is_list(Arg) -> list_to_binary(Arg); ensure_binary(Arg) when is_binary(Arg) -> Arg; ensure_binary(Arg) when is_atom(Arg) -> atom_to_binary(Arg, latin1); ensure_binary(_Arg) -> <<>>. new_imap_compressors() -> Inflator = zlib:open(), ok = zlib:inflateInit(Inflator, -15), Deflator = zlib:open(), ok = zlib:deflateInit(Deflator, 1, deflated, -15, 8, default), { Inflator, Deflator }. -spec only_full_lines(Buffer :: binary()) -> { BufferOfFullLines :: binary(), TrailingFragmentaryLine :: binary() }. only_full_lines(Buffer) -> BufferLength = size(Buffer), only_full_lines(Buffer, BufferLength, binary:at(Buffer, BufferLength - 1), BufferLength). only_full_lines(Buffer, BufferLength, $\n, Pos) when Pos =:= BufferLength -> { Buffer, <<>> }; only_full_lines(Buffer, BufferLength, $\n, Pos) -> { binary:part(Buffer, 0, Pos + 1), binary:part(Buffer, Pos + 1, BufferLength - Pos - 1) }; only_full_lines(Buffer, _BufferLength, _, 0) -> { <<>>, Buffer }; only_full_lines(Buffer, BufferLength, _, Pos) -> only_full_lines(Buffer, BufferLength, binary:at(Buffer, Pos - 1), Pos - 1). -spec binary_to_atom(Value :: binary()) -> ValueAsAtom :: atom(). binary_to_atom(Value) -> list_to_atom(string:to_lower(binary_to_list(Value))). diff --git a/test/eimap_command_getmetadata_tests.erl b/test/eimap_command_getmetadata_tests.erl index 9884b2e..0a7e521 100644 --- a/test/eimap_command_getmetadata_tests.erl +++ b/test/eimap_command_getmetadata_tests.erl @@ -1,83 +1,99 @@ %% Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU Library General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_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\\\"\" /private/comment \"My own comment\")\r\nabcd OK Begin TLS negotiation now\r\n">>, <<"abcd">>, { fini, [ { <<"Tasks">>, [ { <<"/private/comment">>, <<"My own comment">> }, { <<"/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\"">> } ] } ] } }, + { + <<"* METADATA \"INBOX.Test Folder\" (/shared/vendor/kolab/folder-type {5}\r\nevent)\r\nEG0001 OK Getmetadata completed (0.014 + 0.000 + 0.013 secs).\r\n">>, + <<"EG0001">>, + { fini, [ + { <<"INBOX.Test Folder">>, [ {<<"/shared/vendor/kolab/folder-type">>, <<"event">> } ] } + ] + } + }, + { + <<"* METADATA \"INBOX.Test Folder\" (/shared/vendor/kolab/folder-type {0}\r\n)\r\nEG0001 OK Getmetadata completed (0.014 + 0.000 + 0.013 secs).\r\n">>, + <<"EG0001">>, + { fini, [ + { <<"INBOX.Test Folder">>, [ {<<"/shared/vendor/kolab/folder-type">>, <<>> } ] } + ] + } + }, { <<"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_tests.erl b/test/eimap_command_tests.erl index a678e4e..519ef39 100644 --- a/test/eimap_command_tests.erl +++ b/test/eimap_command_tests.erl @@ -1,65 +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 Library General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_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 1 (MESSAGES 231 {0}\r\n)\r\nabcd OK Begin TLS negotiation now\r\n">>, + <<"abcd">>, + { fini, [ { marked, <<"* STATUS 1 (MESSAGES 231 )">> } ] } + }, { <<"* 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_utils_tests.erl b/test/eimap_utils_tests.erl index f45092a..ebf19bc 100644 --- a/test/eimap_utils_tests.erl +++ b/test/eimap_utils_tests.erl @@ -1,200 +1,201 @@ %% Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) %% %% Aaron Seigo (Kolab Systems) %% %% This program is free software: you can redistribute it and/or modify %% it under the terms of the GNU Library General Public License as published by %% the Free Software Foundation, either version 3 of the License, or %% (at your option) any later version. %% %% This program is distributed in the hope that it will be useful, %% but WITHOUT ANY WARRANTY; without even the implied warranty of %% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the %% GNU Library General Public License for more details. %% %% You should have received a copy of the GNU Library General Public License %% along with this program. If not, see . -module(eimap_utils_tests). -include_lib("eunit/include/eunit.hrl"). % c("test/eimap_utils_tests.erl"). eunit:test(eimap_utils). extract_path_from_uri_test_() -> Data = [ { <<"user/john.doe/Calendar@example.org">>, none, "/", <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"user/john.doe/Personal Calendar@example.org">>, none, "/", <<"imap://john.doe@example.org@kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, none, "/", <<"imap://kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, "Shared/", "/", <<"imap://kolab.example.org/Shared/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"Personal Calendar">>, "Shared/", "/", <<"imap://kolab.example.org/Personal%20Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"user/john.doe@example.org">>, "Shared/", "/", <<"imap://john.doe@example.org@kolab.example.org/INBOX;UIDVALIDITY=1424683684/;UID=1">> }, { bad_uri, none, "/", <<"merf">> } ], lists:foldl(fun({ Val, SharePrefix, Sep, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:extract_path_from_uri(SharePrefix, Sep, Input))|Acc] end, [], Data). extract_uid_from_uri_test_() -> Data = [ { <<"1">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1">> }, { <<"12">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=12">> }, { <<"123">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=123">> }, { <<"1">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=1;foo">> }, { <<"12">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=12;foo=bar">> }, { <<"123">>, <<"imap://john.doe@example.org@kolab.example.org/Calendar;UIDVALIDITY=1424683684/;UID=123;foo=bar">> } ], lists:foldl(fun({ Val, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:extract_uidset_from_uri(Input))|Acc] end, [], Data). split_command_into_components_test_() -> Data = [ { { <<>>, <<>>, <<>> }, <<>> }, { { <<>>, <<"DONE">>, <<>> }, <<"DONE\r\n">> }, { { <<".">>, <<"LIST">>, <<"\"\" \"*\"">> }, <<". LIST \"\" \"*\"">> }, { { <<"1">>, <<"STARTTLS">>, <<>> }, <<"1 STARTTLS">> }, { { <<"1">>, <<"STARTTLS">>, <<>> }, <<"1 STARTTLS\r\n">> }, { { <<"3">>, <<"ID">>, <<"(\"name\" \"Thunderbird\" \"version\" \"38.3.0\")">> }, <<"3 ID (\"name\" \"Thunderbird\" \"version\" \"38.3.0\")">> } ], lists:foldl(fun({ Val, Input }, Acc) -> [?_assertEqual(Val, eimap_utils:split_command_into_components(Input)) | Acc] end, [], Data). check_response_for_failure_test_() -> Tag = <<"abcdef">>, Data = [ { Tag, <>, { no, <<"reasons">> } }, { Tag, <>, { no, <<"reasons">> } }, { Tag, <>, { bad, <<"reasons">> } }, { Tag, <>, { bad, <<"reasons">> } }, { Tag, <>, ok }, { Tag, <<"short">>, ok }, { undefined, <<"* OK reasons">>, ok } ], lists:foldl(fun({ Tag2, Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:check_response_for_failure(Input, Tag2)) | Acc] end, [], Data). is_tagged_response_test_() -> Tag = <<"abcd">>, Data = [ { <>, tagged }, { <>, tagged }, { <<"one">>, untagged }, { <<"* Yeah baby">>, untagged } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:is_tagged_response(Input, Tag)) | Acc] end, [], Data). remove_tag_from_response_test_() -> Tag = <<"abcd">>, Data = [ { Tag, <>, check, <<"Indeed\r\n">> }, { Tag, <>, trust, <<"Indeed\r\n">> }, { Tag, <>, check, <<"Indeed">>}, { Tag, <>, trust, <<"Indeed">>}, { undefined, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { undefined, <<"abcd4 Indeed">>, trust, <<"abcd4 Indeed">>}, { <<>>, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { <<>>, <<"abcd4 Indeed">>, trust, <<"abcd4 Indeed">>}, { Tag, <<"abcd4 Indeed">>, check, <<"abcd4 Indeed">>}, { Tag, <<"abcd4 Indeed">>, trust, <<" Indeed">>}, { Tag, <<"* Yeah baby">>, check, <<"* Yeah baby">> }, { Tag, <<"">>, check, <<"">> }, { Tag, <<"">>, trust, <<"">> }, { Tag, <<>>, check, <<>> }, { Tag, <<>>, trust, <<>> } ], lists:foldl(fun({ Tag2, Input, Check, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:remove_tag_from_response(Input, Tag2, Check)) | Acc] end, [], Data). header_name_test_() -> Data = [ { mailbox_uid , <<"/vendor/cmu/cyrus-imapd/uniqueid">> }, { groupware_type , <<"X-Kolab-Type">> }, { groupware_uid , <<"Subject">> }, { dunno, unknown }, { "dunno", unknown }, { <<"dunno">>, unknown }, { 134, unknown } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:header_name(Input)) | Acc] end, [], Data). ensure_binary_test_() -> Data = [ { "yep", <<"yep">> }, { <<"yep">>, <<"yep">> }, { [1, 2, 3], <<1, 2, 3>> }, { yep, <<"yep">> }, { 123, <<>> } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:ensure_binary(Input)) | Acc] end, [], Data). only_full_lines_test_() -> Data = [ { <<"yep">>, { <<>>, <<"yep">> } }, { <<"yep\r\nhohoho">>, { <<"yep\r\n">>, <<"hohoho">> } }, { <<"nope\r\nyep\r\nhohoho">>, { <<"nope\r\nyep\r\n">>, <<"hohoho">> } }, { <<"nope\r\nyep\r\nhohoho\r\n">>, { <<"nope\r\nyep\r\nhohoho\r\n">>, <<>> } } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:only_full_lines(Input)) | Acc] end, [], Data). parse_flags_test_() -> Data = [ { <<"()">>, [] }, { <<>>, [] }, { <<"\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen">>, [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] }, { <<"(\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen)">>, [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] }, { "(\\\\Answered \\\\Flagged \\\\Draft \\\\Deleted \\\\Seen)", [<<"\\\\Answered">>, <<"\\\\Flagged">>, <<"\\\\Draft">>, <<"\\\\Deleted">>, <<"\\\\Seen">> ] } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:parse_flags(Input)) | Acc] end, [], Data). num_literal_continuation_bytes_test_() -> Data = [ - { <<"abcd">>, { <<"abcd">>, 0 } }, + { <<"abcd">>, { <<"abcd">>, none } }, + { <<"abcd{0}">>, { <<"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 } }, + { <<"abcd{aa0}">>, { <<"abcd{aa0}">>, none } }, + { <<"abcd{10aa0}">>, { <<"abcd{10aa0}">>, none } }, + { <<"abcd100}">>, { <<"abcd100}">>, none } }, + { <<"abcd100}">>, { <<"abcd100}">>, none } }, { <<"abcd{5+}">>, { <<"abcd">>, 5 } } ], lists:foldl(fun({ Input, Output}, Acc) -> [?_assertEqual(Output, eimap_utils:num_literal_continuation_bytes(Input)) | Acc] end, [], Data).