Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117880657
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
17 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/eimap.erl b/src/eimap.erl
index b82b3c9..f7eda9c 100644
--- a/src/eimap.erl
+++ b/src/eimap.erl
@@ -1,322 +1,322 @@
%% 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).
-behaviour(gen_fsm).
-include("eimap.hrl").
%% API
-export([start_link/1,
%% passthrough mode, where data is just sent to the server blindly and
%% responses passed back equally blindly. in this mode the user is on
%% their own and better know what they are doing. can only be activated
%% when disconnected or idle
start_passthrough/1, end_passthrough/1, passthrough/3,
%% mutators
set_credentials/3,
%% connection management
connect/1, disconnect/1,
%% commands
get_folder_annotations/4,
get_message_headers_and_body/5,
get_path_tokens/3]).
%% gen_fsm callbacks
-export([disconnected/2, authenticate/2, authenticating/2, idle/2, passthrough/2, wait_response/2]).
-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
%% state record definition
-record(state, { host, port, tls, user, pass, authed = false, socket,
command_serial = 1, command_queue = queue:new(),
current_command, current_mbox, parse_state, passthrough = false, passthrough_send_buffer = <<>> }).
-record(command, { tag, mbox, message, from, response_token, parse_fun }).
%% public API
start_link(ServerConfig) when is_record(ServerConfig, eimap_server_config) -> gen_fsm:start_link(?MODULE, [ServerConfig], []).
start_passthrough(PID) -> gen_fsm:send_event(PID, enter_passthrough).
end_passthrough(PID) -> gen_fsm:send_event(PID, exit_passthrough).
passthrough(PID, Receiver, Data) when is_pid(Receiver), is_binary(Data) -> gen_fsm:send_event(PID, { passthrough, Data }).
set_credentials(PID, User, Password) -> gen_fsm:send_all_state_event(PID, [set_credentials, User, Password]).
connect(PID) -> gen_fsm:send_all_state_event(PID, connect).
disconnect(PID) -> gen_fsm:send_all_state_event(PID, disconnect).
get_folder_annotations(PID, From, ResponseToken, Folder) when is_list(Folder) ->
get_folder_annotations(PID, From, ResponseToken, list_to_binary(Folder));
get_folder_annotations(PID, From, ResponseToken, Folder) when is_binary(Folder) ->
Command = #command{ message = eimap_command_annotation:new(Folder),
from = From, response_token = ResponseToken,
parse_fun = fun eimap_command_annotation:parse/2 },
gen_fsm:send_all_state_event(PID, { ready_command, Command }).
get_message_headers_and_body(PID, From, ResponseToken, Folder, MessageID) ->
%%lager:info("SELECT_DEBUG: peeking message ~p ~p", [Folder, MessageID]),
Command = #command{ mbox = Folder, message = eimap_command_peek_message:new(MessageID),
from = From, response_token = ResponseToken,
parse_fun = fun eimap_command_peek_message:parse/2 },
gen_fsm:send_all_state_event(PID, { ready_command, Command }).
get_path_tokens(PID, From, ResponseToken) ->
Command = #command{ message = eimap_command_namespace:new([]),
from = From, response_token = ResponseToken,
parse_fun = fun eimap_command_namespace:parse/2 },
gen_fsm:send_all_state_event(PID, { ready_command, Command }).
%% gen_server API
init([#eimap_server_config{ host = Host, port = Port, tls = TLS, user = User, pass = Pass }]) ->
State = #state {
host = Host,
port = Port,
tls = TLS,
user = ensure_binary(User),
pass = ensure_binary(Pass)
},
{ ok, disconnected, State }.
disconnected(start_passthrough, State) ->
{ next_state, disconnected, State#state{ passthrough = true } };
disconnected(end_passthrough, State) ->
{ next_state, disconnected, State#state{ passthrough = false } };
disconnected({ passthrough, Data }, #state{ passthrough = true, passthrough_send_buffer = Buffer } = State) ->
{ next_state, disconnected, State#state{ passthrough_send_buffer = <<Buffer/binary, Data>> } };
disconnected(connect, #state{ host = Host, port = Port, tls = TLS, socket = undefined, user = User, passthrough = false} = State) ->
{ok, Socket} = create_socket(Host, Port, TLS),
{ next_state, state_on_connect(User), State#state { socket = Socket } };
disconnected(connect, #state{ host = Host, port = Port, tls = TLS, socket = undefined, passthrough = true } = State) ->
{ok, Socket} = create_socket(Host, Port, TLS),
gen_fsm:send_event(self(), flush_passthrough_buffer),
{ next_state, passthrough, State#state { socket = Socket } };
disconnected(Command, State) when is_record(Command, command) ->
{ next_state, disconnected, enque_command(Command, State) }.
state_on_connect(User) when is_list(User), length(User) > 0 -> authenticate;
state_on_connect(_User) -> idle.
authenticate({ data, _Data }, #state{ user = User, pass = Pass, authed = false } = State) ->
Message = <<"LOGIN ", User/binary, " ", Pass/binary>>,
Command = #command{ message = Message },
send_command(Command, State),
{ next_state, authenticating, State#state{ authed = in_process } };
authenticate(Command, State) when is_record(Command, command) ->
{ next_state, authenticate, enque_command(Command, State) }.
authenticating({ data, Data }, #state{ authed = in_process } = State) ->
Token = <<" OK ">>, %TODO would be nice to have the tag there
case binary:match(Data, Token) of
nomatch ->
lager:warning("Log in to IMAP server failed: ~p", [Data]),
close_socket(State),
{ next_state, idle, State#state{ socket = undefined, authed = false } };
_ ->
%%lager:info("Logged in to IMAP server successfully"),
gen_fsm:send_event(self(), process_command_queue),
{ next_state, idle, State#state{ authed = true } }
end;
authenticating(Command, State) when is_record(Command, command) ->
{ next_state, authenticating, enque_command(Command, State) }.
passthrough(flush_passthrough_buffer, #state{ passthrough_send_buffer = Buffer } = State) ->
passthrough({ passthrough, Buffer }, State#state{ passthrough_send_buffer = <<>> });
passthrough({ passthrough, Data }, #state{ socket = Socket, tls = true } = State) ->
ssl:send(Data, Socket),
{ next_state, passthrough, State };
passthrough({ passthrough, Data }, #state{ socket = Socket } = State) ->
gen_tcp:send(Data, Socket),
{ next_state, passthrough, State };
passthrough({ data, Data }, State) ->
%%TODO: return Data to sender
{ next_state, passthrough, State };
passthrough(start_passthrough, State) ->
%% already in passthrough, but what the hell ...
lager:warning("Already in passthrough mode, and passthrough mode was requested again!"),
%% TODO: should this count the # of times start is called and require an equal # of ends?
{ next_state, passthrough, State };
passthrough(end_passthrough, State) ->
gen_fsm:send_event(self(), process_command_queue),
- { next_state, idle, State }.
+ { next_state, idle, State#state{ passthrough = false } }.
idle(process_command_queue, #state{ command_queue = Queue } = State) ->
case queue:out(Queue) of
{ { value, Command }, ModifiedQueue } when is_record(Command, command) ->
%%lager:info("Clearing queue of ~p", [Command]),
StateWithNewQueue = State#state{ command_queue = ModifiedQueue },
NewState = send_command(Command, StateWithNewQueue),
{ next_state, wait_response, NewState };
{ empty, ModifiedQueue } ->
{ next_state, idle, State#state{ command_queue = ModifiedQueue } }
end;
idle({ data, _Data }, State) ->
%%lager:info("Idling, server sent: ~p", [_Data]),
{ next_state, idle, State };
idle(Command, State) when is_record(Command, command) ->
%%lager:info("Idling"),
NewState = send_command(Command, State),
{ next_state, wait_response, NewState };
idle(_Event, State) ->
{ next_state, idle, State }.
%%TODO a variant that checks "#command{ from = undefined }" to avoid parsing responses which will go undelivered?
wait_response(Command, State) when is_record(Command, command) ->
{ next_state, wait_response, enque_command(Command, State) };
wait_response({ data, _Data }, #state{ current_command = #command{ parse_fun = undefined } } = State) ->
gen_fsm:send_event(self(), process_command_queue),
{ next_state, idle, State };
wait_response({ data, Data }, #state{ current_command = #command{ parse_fun = Fun, tag = Tag } } = State) when is_function(Fun, 2) ->
Response = Fun(Data, Tag),
%%lager:info("Response from parser was ~p ~p, size of queue ~p", [More, Response, queue:len(State#state.command_queue)]),
next_command_after_response(Response, State);
wait_response({ data, Data }, #state{ parse_state = ParseState, current_command = #command{ parse_fun = Fun, tag = Tag } } = State) when is_function(Fun, 3) ->
Response = Fun(Data, Tag, ParseState),
%%lager:info("Response from parser was ~p ~p, size of queue ~p", [More, Response, queue:len(State#state.command_queue)]),
next_command_after_response(Response, State).
handle_event(connect, disconnected, State) ->
gen_fsm:send_event(self(), connect),
{ next_state, disconnected, State };
handle_event(connect, _Statename, State) ->
%%lager:info("Already connected to IMAP server!"),
{ next_state, _Statename, State };
handle_event(disconnect, _StateName, State) ->
close_socket(State),
{ next_state, disconnected, reset_state(State) };
handle_event([set_credentials, User, Pass], disconnected, State) ->
{ next_state, authenticate, State#state{ user = User, pass = Pass } };
handle_event([set_credentials, User, Pass], idle, #state{ authed = Authed } = State) when Authed =:= false->
{ next_state, authenticate, State#state{ user = User, pass = Pass } };
handle_event([set_credentials, _User, _Pass], CurrentState, #state{ authed = Authed } = State) ->
lager:warning("Attempted to set user and password while connected but not ready: ~p ~p", [CurrentState, Authed]),
{ next_state, CurrentState, State };
handle_event({ ready_command, Command }, StateName, State) when is_record(Command, command) ->
%%lager:info("Making command .. ~p", [Command]),
?MODULE:StateName(Command, State);
handle_event(_Event, StateName, State) -> { next_state, StateName, State}.
handle_sync_event(_Event, _From, StateName, State) -> { next_state, StateName, State}.
handle_info({ ssl, Socket, Bin }, StateName, #state{ socket = Socket } = State) ->
% Flow control: enable forwarding of next TCP message
%lager:info("Received from server: ~p", [Bin]),
ssl:setopts(Socket, [{ active, once }]),
?MODULE:StateName({ data, Bin }, State);
handle_info({ tcp, Socket, Bin }, StateName, #state{ socket = Socket } = State) ->
% Flow control: enable forwarding of next TCP message
%%lager:info("Got us ~p", [Bin]),
inet:setopts(Socket, [{ active, once }]),
?MODULE:StateName({ data, Bin }, State);
handle_info({tcp_closed, Socket}, _StateName, #state{ socket = Socket, host = Host } = State) ->
lager:info("~p Client ~p disconnected.\n", [self(), Host]),
{ stop, normal, State };
handle_info({ { selected, MBox }, ok }, StateName, State) ->
%%lager:info("~p Selected mbox ~p", [self(), MBox]),
{ next_state, StateName, State#state{ current_mbox = MBox } };
handle_info({ { selected, MBox }, { error, Reason } }, StateName, State) ->
lager:info("Failed to select mbox ~p: ~p", [MBox, Reason]),
NewQueue = queue:filter(fun(Command) -> notify_of_mbox_failure_during_filter(Command, Command#command.mbox =:= MBox) end, State#state.command_queue),
{ next_state, StateName, State#state{ command_queue = NewQueue } };
handle_info(_Info, StateName, State) ->
{ next_state, StateName, State }.
terminate(_Reason, _Statename, State) -> close_socket(State), ok.
code_change(_OldVsn, Statename, State, _Extra) -> { ok, Statename, State }.
%% private API
notify_of_response(none, _Command) -> ok;
notify_of_response(_Response, #command { from = undefined }) -> ok;
notify_of_response(Response, #command { from = From, response_token = undefined }) -> From ! Response;
notify_of_response(Response, #command { from = From, response_token = Token }) -> From ! { Token, Response };
notify_of_response(_, _) -> ok.
%% the return is inverted for filtering
notify_of_mbox_failure_during_filter(Command, true) -> notify_of_response({ error, mailboxnotfound }, Command), false;
notify_of_mbox_failure_during_filter(_Command, false) -> true.
next_command_after_response({ more, Fun, ParseState }, State) when is_function(Fun, 3) ->
{ next_state, wait_response, State#state{ parse_state = ParseState, current_command = State#state.current_command#command{ parse_fun = Fun } } };
next_command_after_response({ more, ParseState }, State) ->
{ next_state, wait_response, State#state{ parse_state = ParseState } };
next_command_after_response({ error, _ } = ErrorResponse, State) ->
notify_of_response(ErrorResponse, State#state.current_command),
gen_fsm:send_event(self(), process_command_queue),
{ next_state, idle, State#state{ parse_state = none } };
next_command_after_response({ fini, Response }, State) ->
notify_of_response(Response, State#state.current_command),
gen_fsm:send_event(self(), process_command_queue),
{ next_state, idle, State#state{ parse_state = none } }.
tag_field_width(Serial) when Serial < 10000 -> 4;
tag_field_width(Serial) -> tag_field_width(Serial / 10000, 5).
tag_field_width(Serial, Count) when Serial < 10 -> Count;
tag_field_width(Serial, Count) -> tag_field_width(Serial / 10, Count + 1).
create_socket(Host, Port, true) -> ssl:connect(Host, Port, socket_options(), 1000);
create_socket(Host, Port, _) -> gen_tcp:connect(Host, Port, socket_options(), 1000).
socket_options() -> [binary, { active, once }, { send_timeout, 5000 }].
close_socket(#state{ socket = undefined }) -> ok;
close_socket(#state{ socket = Socket, tls = true }) -> ssl:close(Socket);
close_socket(#state{ socket = Socket }) -> gen_tcp:close(Socket).
reset_state(State) -> State#state{ socket = undefined, authed = false, command_serial = 1 }.
%% sending command code paths:
%% 0. not connected, TLS/SSL, unencrypted
%% 1. no mbox needed, mbox is already selected, mbox needs selecting
send_command(Command, #state{ socket = undefined } = State) ->
lager:warning("Not connected, dropping command on floor: ~s", [Command]),
State;
send_command(Command, #state{ tls = true} = State) ->
send_command(fun ssl:send/2, Command, State);
send_command(Command, State) ->
send_command(fun gen_tcp:send/2, Command, State).
send_command(Fun, #command{ mbox = undefined } = Command, State) ->
%%lager:info("~p SELECT_DEBUG issuing command without mbox: ~p", [self(), Command#command.message]),
send_command_now(Fun, Command, State);
send_command(Fun, #command{ mbox = MBox } = Command, #state{ current_mbox = CurrentMbox } = State) ->
%%lager:info("~p SELECT_DEBUG issuing command with mbox ~p (current: ~p, equal -> ~p): ~p", [self(), MBox, CurrentMbox, (MBox =:= CurrentMbox), Command#command.message]),
send_command_or_select_mbox(Fun, Command, State, MBox, MBox =:= CurrentMbox).
send_command_or_select_mbox(Fun, Command, State, _MBox, true) ->
send_command_now(Fun, Command, State);
send_command_or_select_mbox(Fun, DelayedCommand, State, MBox, false) ->
NextState = reenque_command(DelayedCommand, State),
SelectMessage = eimap_command_examine:new(MBox),
SelectCommand = #command{ message = SelectMessage, parse_fun = fun eimap_command_examine:parse/2,
from = self(), response_token = { selected, MBox } },
%%lager:info("~p SELECT_DEBUG: Doing a select first ~p", [self(), SelectMessage]),
send_command_now(Fun, SelectCommand, NextState).
send_command_now(Fun, #command{ message = Message } = Command, #state{ command_serial = Serial, socket = Socket } = State) ->
Tag = list_to_binary(io_lib:format("EG~*..0B", [tag_field_width(Serial), Serial])),
Data = <<Tag/binary, " ", Message/binary, "\n">>,
%%lager:info("Sending command via ~p: ~s", [Fun, Data]),
Fun(Socket, Data),
State#state{ command_serial = Serial + 1, current_command = Command#command{ tag = Tag } }.
enque_command(Command, State) ->
%%lager:info("Enqueuing command ~p", [Command]),
State#state { command_queue = queue:in(Command, State#state.command_queue) }.
reenque_command(Command, State) ->
%%lager:info("Re-queueing command ~p", [Command]),
State#state { command_queue = queue:in_r(Command, State#state.command_queue) }.
ensure_binary(Arg) when is_list(Arg) -> list_to_binary(Arg);
ensure_binary(Arg) when is_binary(Arg) -> Arg;
ensure_binary(_Arg) -> <<>>.
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Apr 5, 11:25 PM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831565
Default Alt Text
(17 KB)
Attached To
Mode
rEI eimap
Attached
Detach File
Event Timeline