diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To use eimap from your imap application add the following line to your rebar config: - { eimap, "*", {git, "git://git.kolab.org/diffusion/EI/eimap.git" } + { 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, @@ -28,7 +28,7 @@ ============ 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. +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 @@ -38,14 +38,14 @@ 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 = #eimap_server_config{ host = "acme.com" }, + 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 eimap_server_config record is defined in eimap.hrl and allows one to set +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") diff --git a/src/eimap_command.erl b/src/eimap_command.erl --- a/src/eimap_command.erl +++ b/src/eimap_command.erl @@ -38,14 +38,29 @@ process_lines(_ParseTaggedLine, _Tag, LastPartialLine, [], Acc, Module) -> { more, { LastPartialLine, Acc, Module } }; process_lines(ParseTaggedLine, Tag, LastPartialLine, [Line|MoreLines], Acc, Module) -> - process_line(ParseTaggedLine, eimap_utils:is_tagged_response(Line, Tag), 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(ParseTaggedLine, true, Tag, _LastPartialLine, Line, _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) -> Checked = eimap_utils:check_response_for_failure(Line, Tag), Module:formulate_response(Checked, parse_tagged(Checked, ParseTaggedLine, Line, Acc, Module)); -process_line(ParseTaggedLine, false, Tag, LastPartialLine, Line, MoreLines, Acc, Module) -> - process_lines(ParseTaggedLine, Tag, LastPartialLine, MoreLines, Module:process_line(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_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 }. diff --git a/src/eimap_utils.erl b/src/eimap_utils.erl --- a/src/eimap_utils.erl +++ b/src/eimap_utils.erl @@ -24,7 +24,8 @@ ensure_binary/1, new_imap_compressors/0, only_full_lines/1, - binary_to_atom/1 + binary_to_atom/1, + num_literal_continuation_bytes/1 ]). %% Translate the folder name in to a fully qualified folder path such as it @@ -83,13 +84,49 @@ split_command_into_components(Buffer) when is_binary(Buffer) -> split_command(Buffer). --spec is_tagged_response(Buffer :: binary(), Tag :: binary()) -> true | false. +-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 (TagSize =< BufferSize) of - true -> <> =:= binary:part(Buffer, 0, TagSize); - false -> false + 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(). diff --git a/test/eimap_command_tests.erl b/test/eimap_command_tests.erl new file mode 100644 --- /dev/null +++ b/test/eimap_command_tests.erl @@ -0,0 +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 +%% the Free Software Foundation, either version 3 of the License, or +%% (at your option) any later version. +%% +%% This program is distributed in the hope that it will be useful, +%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +%% GNU General Public License for more details. +%% +%% You should have received a copy of the GNU General Public License +%% along with this program. If not, see . + +-module(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_utils_tests.erl b/test/eimap_utils_tests.erl --- a/test/eimap_utils_tests.erl +++ b/test/eimap_utils_tests.erl @@ -105,10 +105,10 @@ Tag = <<"abcd">>, Data = [ - { <>, true }, - { <>, true }, - { <<"one">>, false }, - { <<"* Yeah baby">>, false } + { <>, 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). @@ -179,3 +179,20 @@ ], 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). + +