Permalink
Browse files

Giant horrible hack to support literals

  • Loading branch information...
Vagabond committed Dec 3, 2011
1 parent 7499600 commit 1cf070cafa0594259b9ed3ef1758f3af398d7151
Showing with 172 additions and 19 deletions.
  1. +69 −8 src/diemap_session.erl
  2. +9 −3 src/imap_parser.peg
  3. +11 −4 src/imap_server_example.erl
  4. +4 −4 test/imap_parser_test.erl
  5. +79 −0 test/imap_server_test.erl
View
@@ -51,9 +51,12 @@ handle_info({_SocketType, Socket, Packet},
#state{socket = Socket, transport=Transport, connstate=C} = State) ->
io:format("C: [~p] ~s", [C, Packet]),
Result = case imap_parser:parse(Packet) of
- {fail, _} ->
+ {fail, _X} ->
io:format("Failed to parse ~p~n", [Packet]),
{stop, normal, State};
+ {_Tag, {incomplete, Command, {literal, RecvLen}}} ->
+ {Tag, Command2} = receive_literals(Transport, Socket, Packet, Command, RecvLen, []),
+ handle_command(Tag, Command2, State);
{Tag, {invalid, _Command}} ->
io:format("Failed to parse ~p~n", [Packet]),
reply(Transport, Socket, [Tag, " BAD Command not recognized or bad arguments\r\n"]),
@@ -229,7 +232,7 @@ handle_command(Tag, {fetch, Sequence, Spec},
#state{socket=Socket,transport=Transport,module=Module,modstate=ModState,connstate=?SELECTED} = State) ->
%io:format("fetch ~p ~p~n", [Sequence, Spec]),
F = fun({fetch_response, Seq, Attributes}) ->
- Msg = ["* ", integer_to_list(Seq), " FETCH ", encode_fetch_response(Attributes, [])],
+ Msg = ["* ", integer_to_list(Seq), " FETCH ", encode_fetch_response(Attributes, []), "\r\n"],
reply(Transport, Socket, Msg)
end,
Reply = Module:handle_FETCH(Sequence, Spec, false, F, ModState),
@@ -239,12 +242,13 @@ handle_command(Tag, {fetch, Sequence, Spec},
handle_command(Tag, {uid, {fetch, Sequence, Spec}},
#state{socket=Socket,transport=Transport,module=Module,modstate=ModState,connstate=?SELECTED} = State) ->
%io:format("fetch ~p ~p~n", [Sequence, Spec]),
- F = fun(Msg) -> reply(Transport, Socket, Msg) end,
- Module:handle_FETCH(Sequence, Spec, true, F, ModState),
- %io:format("Fetch results ~p~n", [list_to_binary(FetchResults)]),
- reply(Transport, Socket, [Tag, " OK FETCH completed\r\n"]),
- %halt(),
- {noreply, State};
+ F = fun({fetch_response, Seq, Attributes}) ->
+ Msg = ["* ", integer_to_list(Seq), " FETCH ", encode_fetch_response(Attributes, []), "\r\n"],
+ reply(Transport, Socket, Msg)
+ end,
+ Reply = Module:handle_FETCH(Sequence, Spec, true, F, ModState),
+ NewState = handle_common_reply(Reply, Tag, "UID FETCH", State),
+ {noreply, NewState};
handle_command(Tag, {uid, {store, _Sequence, _Flags}},
#state{socket=Socket,transport=Transport,module=_Module,modstate=_ModState,connstate=?SELECTED} = State) ->
%io:format("fetch ~p ~p~n", [Sequence, Spec]),
@@ -298,3 +302,60 @@ encode_value(Val) when is_integer(Val) ->
encode_value(Val) ->
Val.
+receive_literals(Transport, Socket, Packet, _Command, RecvLen, Literals) ->
+ reply(Transport, Socket, "+ Ready\r\n"),
+ %{ok, NextPacket} = Transport:recv(Socket, RecvLen, 1000),
+ Transport:setopts(Socket, [{active, once}]),
+ receive
+ {_SocketType, Socket, NextPacket} ->
+ ok
+ end,
+ Literal = binary:part(NextPacket, 0, RecvLen),
+ Remainder = binary:part(NextPacket, RecvLen, byte_size(NextPacket) - RecvLen),
+ OldPart = binary:part(Packet, 0, byte_size(Packet) - 2),
+ NewPacket = <<OldPart/binary, Remainder/binary>>,
+ NewLiterals = Literals ++ [Literal],
+ case imap_parser:parse(NewPacket) of
+ {_Tag, {incomplete, Command2, {literal, RecvLen2}}} ->
+ receive_literals(Transport, Socket, NewPacket, Command2, RecvLen2, NewLiterals);
+ {Tag, Command2} ->
+ try inline_literals(Command2, NewLiterals) of
+ {FixedCommand, _} ->
+ {Tag, FixedCommand}
+ catch
+ throw:{need_more_literals, Len} ->
+ reply(Transport, Socket, "+ Ready\r\n"),
+ Transport:setopts(Socket, [{active, once}]),
+ receive
+ {_SocketType2, Socket, NextPacket2} ->
+ ok
+ end,
+ Literal2 = binary:part(NextPacket2, 0, Len),
+ {FixedCommand, _} = inline_literals(Command2, NewLiterals ++ [Literal2]),
+ {Tag, FixedCommand}
+ end
+ end.
+
+inline_literals({literal, Len}, []) ->
+ throw({need_more_literals, Len});
+inline_literals(R, []) ->
+ {R, []};
+inline_literals({literal, _}, [L|Tail]) ->
+ {L, Tail};
+inline_literals(X, Literals) when is_tuple(X) ->
+ {Res, Literals2} = inline_literals(tuple_to_list(X), Literals),
+ {list_to_tuple(Res), Literals2};
+inline_literals(X, Literals) when is_list(X) ->
+ {Res, Literals2} = lists:foldl(fun({literal, _}, {Acc, [L|Tail]}) ->
+ {[L|Acc], Tail};
+ ({literal, Len}, {_, []}) ->
+ throw({need_more_literals, Len});
+ (E, {Acc, Lits}) ->
+ {Res, Tail} = inline_literals(E, Lits),
+ {[Res|Acc], Tail}
+ end, {[], Literals}, X),
+ {lists:reverse(Res), Literals2};
+inline_literals(X, Literals) ->
+ {X, Literals}.
+
+
View
@@ -1,5 +1,5 @@
-command <- tag SP (command_any / command_nonauth / command_auth / command_select / command_invalid) CRLF`
+command <- tag SP (command_any / command_nonauth / command_auth / command_select / command_incomplete / command_invalid) CRLF`
[Tag, _, Command, _] = Node,
{Tag, Command}`;
@@ -91,7 +91,7 @@ list_char <- (atom_char / list_wildcards / resp_specials);
literal <- "{" number "}"`
[_, Number, _] = Node,
-Number`;
+{literal, Number}`;
%% TODO we can convert this into gregorian seconds or something if we need to
date_time <- '"' date_day_fixed "-" date_month "-" date_year SP time SP zone '"'`
@@ -198,6 +198,10 @@ case Node of
{search, <<"us-ascii">>, despace(Keys)}
end`;
+command_incomplete <- (astring SP)+ literal`
+[Command, LiteralLength] = Node,
+{incomplete, list_to_binary(Command), LiteralLength}`;
+
command_invalid <- [^\r\n]+`
{invalid, list_to_binary(Node)}`;
@@ -280,7 +284,9 @@ list_to_integer(binary_to_list(iolist_to_binary(Node)))`;
nz_number <- [1-9]+ [0-9]*`
list_to_integer(binary_to_list(iolist_to_binary(Node)))`;
-string <- '"' chars:(!'"' ("\\\\" / '\\"' / .))* '"' `iolist_to_binary(proplists:get_value(chars, Node))`;
+string <- ( quoted / literal );
+
+quoted <- '"' chars:(!'"' ("\\\\" / '\\"' / .))* '"' `iolist_to_binary(proplists:get_value(chars, Node))`;
atom <- atom_char+`
list_to_binary(Node)`;
@@ -3,6 +3,7 @@
-export([init/2, handle_CAPABILITY/2, handle_NOOP/1, handle_LOGOUT/1, handle_LOGIN/3, handle_SELECT/2, handle_FETCH/5, handle_LIST/3, handle_CLOSE/1, handle_STATUS/3, handle_LSUB/3]).
-include_lib("kernel/include/file.hrl").
+-include_lib("eunit/include/eunit.hrl").
-record(state, {
rootdir :: string(),
@@ -31,7 +32,6 @@ handle_LOGIN(User, _Pass, #state{rootdir=RootDir} = State) ->
true ->
{ok, State#state{user=User}};
false ->
- ?debugFmt("~p does not exist~n", [filename:join(RootDir, User)]),
{error, State}
end.
@@ -67,6 +67,8 @@ mailbox_details(Mailbox, #state{rootdir=RootDir} = State) ->
{length(Files), 0, length(Files) + 1, ["\\Answered", "\\Flagged", "\\Deleted", "\\Seen", "\\Draft"], ["\\Seen"], {UidFirst, UidNext+1}, UIDValidity, read_only}.
mail_details(Uid, Attributes, true, Files, Cb, State) -> %% UID fetch
+ ?debugFmt("UID is ~p~n", [Uid]),
+ ?debugFmt("ID is ~p~n", [Uid-32000]),
mail_details(Uid-32000, Attributes, false, Files, Cb, State);
mail_details(Seq, Attributes, _, Files, Cb, _State) when Seq > 0->
%io:format("contents ~p~n", [Files]),
@@ -75,6 +77,7 @@ mail_details(Seq, Attributes, _, Files, Cb, _State) when Seq > 0->
Result = {fetch_response, Seq, get_attribute(File, Seq+32000, undefined, undefined, Attributes, [])},
Cb(Result)
catch _:_ ->
+ ?debugFmt("can't find file~n", []),
ok
end;
mail_details(_Seq, _Attributes, _, _Files, _Cb, _State) ->
@@ -170,6 +173,8 @@ get_attribute(File, UID, FInfo, Message0, [<<"ENVELOPE">>|T], Acc) ->
{_, _, Headers, _, _} = Message = get_message(File, Message0),
%% date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, and message-id
%% sender & reply-to should fall back to From
+ %% TODO From, To, CC, & BCC headers should be NIL if missing, the others should
+ %% be the empty string if missing.
Env = ["(", get_header(<<"Date">>, Headers), " ", get_header(<<"Subject">>, Headers), " ",
get_address_struct(<<"From">>, Headers), " ", get_address_struct(<<"Sender">>, <<"From">>, Headers), " ",
get_address_struct(<<"Reply-To">>, <<"From">>, Headers), " ", get_address_struct(<<"To">>, Headers), " ",
@@ -182,8 +187,10 @@ get_attribute(File, UID, FInfo, Message, [Att|T], Acc) ->
make_bodystructure({<<"multipart">>, SubType, _Header, _Params, BodyParts}) ->
["(", [make_bodystructure(Part) || Part <- BodyParts], " \"", SubType, "\")"];
-make_bodystructure({Type, SubType, _Header, _Params, Body}) ->
- ["(\"", Type, "\" \"", SubType, "\" (\"CHARSET\" \"us-ascii\") NIL NIL \"7BIT\" ", integer_to_list(byte_size(Body)), " ", integer_to_list(length(binstr:split(Body, <<"\n">>))), ")"].
+make_bodystructure({Type, SubType, Header, Params, Body}) ->
+ TransferEncoding = mimemail:get_header_value(<<"Content-Transfer-Encoding">>, Header, <<"7bit">>),
+ ContentParams = ["(", [["\"", Key, "\" \"", Value, "\""] || {Key, Value} <- proplists:get_value(<<"content-type-params">>, Params, [])], ")"],
+ ["(\"", Type, "\" \"", SubType, "\" ",ContentParams," NIL NIL \"",TransferEncoding, "\" ", integer_to_list(byte_size(Body)), " ", integer_to_list(length(binstr:split(Body, <<"\n">>))), ")"].
get_header(Header, Headers) ->
%% either NIL or a quoted string
@@ -213,7 +220,7 @@ get_address_struct(Header, Headers) ->
end.
get_headers([], _, Acc) ->
- list_to_binary([lists:reverse(Acc), "\r\n"]);
+ list_to_binary(lists:reverse(Acc));
get_headers([Header|T], {_, _, Headers, _, _} = Message, Acc) ->
case mimemail:get_header_value(Header, Headers) of
undefined ->
@@ -27,10 +27,10 @@ starttls_test() ->
?assertMatch({fail, _}, imap_parser:parse("Tag STARTTLS GSSAPI\r\n")).
append_test() ->
- ?assertEqual({<<"Tag">>, {append, inbox, undefined, undefined, 4096}}, imap_parser:parse("Tag APPEND InBoX {4096}\r\n")),
- ?assertEqual({<<"Tag">>, {append, inbox, [<<"\\Draft">>], undefined, 4096}}, imap_parser:parse("Tag APPEND InBoX (\\Draft) {4096}\r\n")),
- ?assertEqual({<<"Tag">>, {append, inbox, undefined, <<"10-May-2011 11:10:00 +0500">>, 4096}}, imap_parser:parse("Tag APPEND InBoX \"10-May-2011 11:10:00 +0500\" {4096}\r\n")),
- ?assertEqual({<<"Tag">>, {append, inbox, undefined, <<"10-May-2011 11:10:00 +0500">>, 4096}}, imap_parser:parse("Tag APPEND \"INBOX\" \"10-May-2011 11:10:00 +0500\" {4096}\r\n")).
+ ?assertEqual({<<"Tag">>, {append, inbox, undefined, undefined, {literal, 4096}}}, imap_parser:parse("Tag APPEND InBoX {4096}\r\n")),
+ ?assertEqual({<<"Tag">>, {append, inbox, [<<"\\Draft">>], undefined, {literal, 4096}}}, imap_parser:parse("Tag APPEND InBoX (\\Draft) {4096}\r\n")),
+ ?assertEqual({<<"Tag">>, {append, inbox, undefined, <<"10-May-2011 11:10:00 +0500">>, {literal, 4096}}}, imap_parser:parse("Tag APPEND InBoX \"10-May-2011 11:10:00 +0500\" {4096}\r\n")),
+ ?assertEqual({<<"Tag">>, {append, inbox, undefined, <<"10-May-2011 11:10:00 +0500">>, {literal, 4096}}}, imap_parser:parse("Tag APPEND \"INBOX\" \"10-May-2011 11:10:00 +0500\" {4096}\r\n")).
create_test() ->
?assertEqual({<<"Tag">>, {create, <<"mybox">>}}, imap_parser:parse("Tag CREATE mybox\r\n")).
View
@@ -119,7 +119,86 @@ server_test_() ->
?assertEqual("2 OK [READ-ONLY] SELECT completed\r\n", get_msg(CSock))
end
}
+ end,
+ fun({CSock, _Pid}) ->
+ {"FETCH should work",
+ fun() ->
+ ?assertMatch("* OK "++_Stuff, get_msg(CSock)),
+ gen_tcp:send(CSock, "1 LOGIN lay-k password\r\n"),
+ ?assertEqual("1 OK LOGIN completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "2 SELECT inbox\r\n"),
+ ?assertEqual("* 9 EXISTS\r\n", get_msg(CSock)),
+ ?assertEqual("* 0 RECENT\r\n", get_msg(CSock)),
+ ?assertEqual("* OK [UNSEEN 10]\r\n", get_msg(CSock)),
+ ?assertMatch("* OK [UIDNEXT"++_, get_msg(CSock)),
+ ?assertMatch("* OK [UIDVALIDITY"++_, get_msg(CSock)),
+ ?assertMatch("* OK [PERMANENTFLAGS"++_, get_msg(CSock)),
+ ?assertMatch("* FLAGS"++_, get_msg(CSock)),
+ ?assertEqual("2 OK [READ-ONLY] SELECT completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "3 FETCH 1 (RFC822.SIZE)\r\n"),
+ [First|_] = lists:sort(element(2, file:list_dir("../testdata/lay-k/inbox"))),
+ {ok, Bin} = file:read_file(filename:join(["..", "testdata", "lay-k", "inbox", First])),
+ {Type, SubType, Headers, Params, _Body} = _Email = mimemail:decode(Bin),
+ ?assertEqual("* 1 FETCH (RFC822.SIZE "++integer_to_list(byte_size(Bin))++")\r\n", get_msg(CSock)),
+ ?assertEqual("3 OK FETCH completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "4 FETCH 1 (UID)\r\n"),
+ ?assertEqual("* 1 FETCH (UID 32001)\r\n", get_msg(CSock)),
+ ?assertEqual("4 OK FETCH completed\r\n", get_msg(CSock)),
+ Subject = list_to_binary(["SUBJECT: ", mimemail:get_header_value(<<"Subject">>, Headers), "\r\n"]),
+ gen_tcp:send(CSock, "5 FETCH 1 BODY[HEADER.FIELDS (SUBJECT)]\r\n"),
+ ?assertEqual("* 1 FETCH (BODY[HEADER.FIELDS (SUBJECT)] {"++integer_to_list(byte_size(Subject))++"}\r\n", get_msg(CSock)),
+ ?assertEqual(binary_to_list(Subject), get_msg(CSock)),
+ ?assertEqual(")\r\n", get_msg(CSock)),
+ ?assertEqual("5 OK FETCH completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "6 FETCH 1 BODYSTRUCTURE\r\n"),
+ [_Header, RawBody] = binary:split(Bin, <<"\r\n\r\n">>),
+ Lines = length(binary:split(RawBody, <<"\n">>, [global, trim])),
+ ?debugFmt("params ~p~n", [Params]),
+ ?assertEqual("* 1 FETCH (BODYSTRUCTURE (\""++binary_to_list(Type)++"\" \""++binary_to_list(SubType)++"\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" "++integer_to_list(byte_size(RawBody))++" "++integer_to_list(Lines)++"))\r\n", get_msg(CSock)),
+ ?assertEqual("6 OK FETCH completed\r\n", get_msg(CSock)),
+ ok
+ end
+ }
+ end,
+ fun({CSock, _Pid}) ->
+ {"UID FETCH should work",
+ fun() ->
+ ?assertMatch("* OK "++_Stuff, get_msg(CSock)),
+ gen_tcp:send(CSock, "1 LOGIN lay-k password\r\n"),
+ ?assertEqual("1 OK LOGIN completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "2 SELECT inbox\r\n"),
+ ?assertEqual("* 9 EXISTS\r\n", get_msg(CSock)),
+ ?assertEqual("* 0 RECENT\r\n", get_msg(CSock)),
+ ?assertEqual("* OK [UNSEEN 10]\r\n", get_msg(CSock)),
+ ?assertMatch("* OK [UIDNEXT"++_, get_msg(CSock)),
+ ?assertMatch("* OK [UIDVALIDITY"++_, get_msg(CSock)),
+ ?assertMatch("* OK [PERMANENTFLAGS"++_, get_msg(CSock)),
+ ?assertMatch("* FLAGS"++_, get_msg(CSock)),
+ ?assertEqual("2 OK [READ-ONLY] SELECT completed\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "3 UID FETCH 32001 (RFC822.SIZE)\r\n"),
+ [First|_] = lists:sort(element(2, file:list_dir("../testdata/lay-k/inbox"))),
+ {ok, Bin} = file:read_file(filename:join(["..", "testdata", "lay-k", "inbox", First])),
+ ?assertEqual("* 1 FETCH (UID 32001 RFC822.SIZE "++integer_to_list(byte_size(Bin))++")\r\n", get_msg(CSock)),
+ ?assertEqual("3 OK UID FETCH completed\r\n", get_msg(CSock)),
+ ok
+ end
+ }
+ end,
+ fun({CSock, _Pid}) ->
+ {"LOGIN using literals",
+ fun() ->
+ ?assertMatch("* OK "++_Stuff, get_msg(CSock)),
+ gen_tcp:send(CSock, "1 LOGIN {9}\r\n"),
+ ?assertEqual("+ Ready\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "Some Dude {9}\r\n"),
+ ?assertEqual("+ Ready\r\n", get_msg(CSock)),
+ gen_tcp:send(CSock, "Pass Word\r\n"),
+ ?assertEqual("1 BAD LOGIN failed\r\n", get_msg(CSock)),
+ ok
+ end
+ }
end
+
]
}.

0 comments on commit 1cf070c

Please sign in to comment.