Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117878004
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
37 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/README.md b/README.md
index d0ed350..eddaa3b 100644
--- a/README.md
+++ b/README.md
@@ -1,200 +1,200 @@
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:
- { 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,
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.
+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 = #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")
* 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 <aseigo@kolabsystems.com>
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/src/eimap_command.erl b/src/eimap_command.erl
index 990191f..95bc22e 100644
--- a/src/eimap_command.erl
+++ b/src/eimap_command.erl
@@ -1,63 +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 = <<LastPartialLine/binary, Data/binary>>,
{ 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) ->
- 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, <<Line/binary, ${, BytesAsBinary/binary, $}, LastPartialLine/binary>>, [], 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 = <<Line/binary, StrippedNextLine/binary>>,
+ 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 1958aea..163229b 100644
--- a/src/eimap_utils.erl
+++ b/src/eimap_utils.erl
@@ -1,209 +1,246 @@
%% Copyright 2014 Kolab Systems AG (http://www.kolabsys.com)
%%
%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com>
%%
%% 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 <http://www.gnu.org/licenses/>.
-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
+ 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 = <<Tag/binary, " NO ">>,
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()) -> 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 -> <<Tag/binary, " ">> =:= binary:part(Buffer, 0, TagSize);
- false -> false
+ case
+ case (TagSize =< BufferSize) of
+ true -> <<Tag/binary, " ">> =:= 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 <<Tag/binary, " ">> =:= 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 = <<Tag/binary, " BAD ">>,
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_tests.erl b/test/eimap_command_tests.erl
new file mode 100644
index 0000000..0a347d0
--- /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) <seigo a kolabsys.com>
+%%
+%% 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 <http://www.gnu.org/licenses/>.
+
+-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
index 798b299..389be6f 100644
--- a/test/eimap_utils_tests.erl
+++ b/test/eimap_utils_tests.erl
@@ -1,181 +1,198 @@
%% Copyright 2014 Kolab Systems AG (http://www.kolabsys.com)
%%
%% Aaron Seigo (Kolab Systems) <seigo a kolabsys.com>
%%
%% 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 <http://www.gnu.org/licenses/>.
-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, <<Tag/binary, " NO reasons\r\n">>, { no, <<"reasons">> } },
{ Tag, <<Tag/binary, " NO reasons">>, { no, <<"reasons">> } },
{ Tag, <<Tag/binary, " BAD reasons\r\n">>, { bad, <<"reasons">> } },
{ Tag, <<Tag/binary, " BAD reasons">>, { bad, <<"reasons">> } },
{ Tag, <<Tag/binary, " OK reasons">>, 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 =
[
- { <<Tag/binary, " Indeed\r\n">>, true },
- { <<Tag/binary, " Indeed">>, true },
- { <<"one">>, false },
- { <<"* Yeah baby">>, false }
+ { <<Tag/binary, " Indeed\r\n">>, tagged },
+ { <<Tag/binary, " Indeed">>, 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, <<Tag/binary, " Indeed\r\n">>, check, <<"Indeed\r\n">> },
{ Tag, <<Tag/binary, " Indeed\r\n">>, trust, <<"Indeed\r\n">> },
{ Tag, <<Tag/binary, " Indeed">>, check, <<"Indeed">>},
{ Tag, <<Tag/binary, " Indeed">>, 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).
+
+
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Apr 5, 9:53 PM (3 w, 12 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831310
Default Alt Text
(37 KB)
Attached To
Mode
rEI eimap
Attached
Detach File
Event Timeline