geary/src/engine/imap/ClientSession.vala
Jim Nelson 9ac3fd0b68 Added SelectExamineResults decoder.
The result of a SELECT or EXAMINE command is now parsed and returned to the caller.  This information is boiled down to the Geary.Folder interface, which adds information about the folder to the object.
2011-05-31 15:40:39 -07:00

1091 lines
40 KiB
Vala

/* Copyright 2011 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class Geary.Imap.ClientSession {
// 30 min keepalive required to maintain session; back off by 30 sec for breathing room
public const int MIN_KEEPALIVE_SEC = (30 * 60) - 30;
public const int DEFAULT_KEEPALIVE_SEC = 60;
public enum Context {
UNCONNECTED,
UNAUTHORIZED,
AUTHORIZED,
SELECTED,
EXAMINED,
IN_PROGRESS
}
public enum DisconnectReason {
LOCAL_CLOSE,
LOCAL_ERROR,
REMOTE_CLOSE,
REMOTE_ERROR
}
// Need this because delegates with targets cannot be stored in ADTs.
private class CommandCallback {
public SourceFunc callback;
public CommandCallback(SourceFunc callback) {
this.callback = callback;
}
}
private class AsyncCommandResponse {
public CommandResponse? cmd_response { get; private set; }
public Object? user { get; private set; }
public Error? err { get; private set; }
public AsyncCommandResponse(CommandResponse? cmd_response, Object? user, Error? err) {
this.cmd_response = cmd_response;
this.user = user;
this.err = err;
}
}
// Many of the async commands go through the FSM, and this is used to pass state around until
// the multiple transitions are completed
private class AsyncParams : Object {
public Cancellable? cancellable;
public SourceFunc cb;
public CommandResponse? cmd_response = null;
public Error? err = null;
public bool do_yield = false;
public AsyncParams(Cancellable? cancellable, SourceFunc cb) {
this.cancellable = cancellable;
this.cb = cb;
}
}
private class LoginParams : AsyncParams {
public string user;
public string pass;
public LoginParams(string user, string pass, Cancellable? cancellable, SourceFunc cb) {
base (cancellable, cb);
this.user = user;
this.pass = pass;
}
}
private class SelectParams : AsyncParams {
public string mailbox;
public bool is_select;
public SelectParams(string mailbox, bool is_select, Cancellable? cancellable, SourceFunc cb) {
base (cancellable, cb);
this.mailbox = mailbox;
this.is_select = is_select;
}
}
private class SendCommandParams : AsyncParams {
public Command cmd;
public SendCommandParams(Command cmd, Cancellable? cancellable, SourceFunc cb) {
base (cancellable, cb);
this.cmd = cmd;
}
}
private enum State {
// canonical IMAP session states
DISCONNECTED,
NOAUTH,
AUTHORIZED,
SELECTED,
LOGGED_OUT,
// transitional states
CONNECTING,
AUTHORIZING,
SELECTING,
CLOSING_MAILBOX,
LOGGING_OUT,
DISCONNECTING,
// terminal state
BROKEN,
COUNT
}
private static string state_to_string(uint state) {
return ((State) state).to_string();
}
private enum Event {
// user-initated events
CONNECT,
LOGIN,
SEND_CMD,
SELECT,
CLOSE_MAILBOX,
LOGOUT,
DISCONNECT,
// async-response events
CONNECTED,
CONNECT_DENIED,
LOGIN_SUCCESS,
LOGIN_FAILED,
SENT_COMMAND,
SEND_COMMAND_FAILED,
SELECTED,
SELECT_FAILED,
CLOSED_MAILBOX,
CLOSE_MAILBOX_FAILED,
LOGOUT_SUCCESS,
LOGOUT_FAILED,
DISCONNECTED,
// I/O errors
RECV_ERROR,
SEND_ERROR,
COUNT;
}
private static string event_to_string(uint event) {
return ((Event) event).to_string();
}
private static Geary.State.MachineDescriptor machine_desc = new Geary.State.MachineDescriptor(
"Geary.Imap.ClientSession", State.DISCONNECTED, State.COUNT, Event.COUNT,
state_to_string, event_to_string);
private string server;
private uint default_port;
private Geary.State.Machine fsm;
private ClientConnection? cx = null;
private string? current_mailbox = null;
private bool current_mailbox_readonly = false;
private Gee.Queue<CommandCallback> cb_queue = new Gee.LinkedList<CommandCallback>();
private Gee.Queue<CommandResponse> cmd_response_queue = new Gee.LinkedList<CommandResponse>();
private CommandResponse current_cmd_response = new CommandResponse();
private uint keepalive_id = 0;
// state used only during connect and disconnect
private bool awaiting_connect_response = false;
private ServerData? connect_response = null;
private AsyncParams? connect_params = null;
private AsyncParams? disconnect_params = null;
public virtual signal void connected() {
}
public virtual signal void authorized() {
}
public virtual signal void logged_out() {
}
public virtual signal void disconnected(DisconnectReason reason) {
}
/**
* If the mailbox name is null it indicates the type of state change that has occurred
* (authorized -> selected/examined or vice-versa). If new_name is null readonly should be
* ignored.
*/
public virtual signal void current_mailbox_changed(string? old_name, string? new_name, bool readonly) {
}
public virtual signal void unsolicited_expunged(MessageNumber msg) {
}
public virtual signal void unsolicited_exists(int exists) {
}
public virtual signal void unsolicitied_recent(int recent) {
}
public virtual signal void unsolicited_flags(FetchResults flags) {
}
public ClientSession(string server, uint default_port) {
this.server = server;
this.default_port = default_port;
Geary.State.Mapping[] mappings = {
new Geary.State.Mapping(State.DISCONNECTED, Event.CONNECT, on_connect),
new Geary.State.Mapping(State.DISCONNECTED, Event.LOGIN, on_early_command),
new Geary.State.Mapping(State.DISCONNECTED, Event.SEND_CMD, on_early_command),
new Geary.State.Mapping(State.DISCONNECTED, Event.SELECT, on_early_command),
new Geary.State.Mapping(State.DISCONNECTED, Event.CLOSE_MAILBOX, on_early_command),
new Geary.State.Mapping(State.DISCONNECTED, Event.LOGOUT, on_early_command),
new Geary.State.Mapping(State.DISCONNECTED, Event.DISCONNECT, Geary.State.nop),
new Geary.State.Mapping(State.CONNECTING, Event.CONNECT, Geary.State.nop),
new Geary.State.Mapping(State.CONNECTING, Event.LOGIN, on_early_command),
new Geary.State.Mapping(State.CONNECTING, Event.SEND_CMD, on_early_command),
new Geary.State.Mapping(State.CONNECTING, Event.SELECT, on_early_command),
new Geary.State.Mapping(State.CONNECTING, Event.CLOSE_MAILBOX, on_early_command),
new Geary.State.Mapping(State.CONNECTING, Event.LOGOUT, on_early_command),
new Geary.State.Mapping(State.CONNECTING, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.CONNECTING, Event.CONNECTED, on_connected),
new Geary.State.Mapping(State.CONNECTING, Event.CONNECT_DENIED, on_connect_denied),
new Geary.State.Mapping(State.CONNECTING, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.CONNECTING, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.NOAUTH, Event.LOGIN, on_login),
new Geary.State.Mapping(State.NOAUTH, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.NOAUTH, Event.SELECT, on_unauthenticated),
new Geary.State.Mapping(State.NOAUTH, Event.CLOSE_MAILBOX, on_unauthenticated),
new Geary.State.Mapping(State.NOAUTH, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.NOAUTH, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.NOAUTH, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.NOAUTH, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.AUTHORIZING, Event.LOGIN, Geary.State.nop),
new Geary.State.Mapping(State.AUTHORIZING, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.AUTHORIZING, Event.SELECT, on_unauthenticated),
new Geary.State.Mapping(State.AUTHORIZING, Event.CLOSE_MAILBOX, on_unauthenticated),
new Geary.State.Mapping(State.AUTHORIZING, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.AUTHORIZING, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.AUTHORIZING, Event.LOGIN_SUCCESS, on_login_success),
new Geary.State.Mapping(State.AUTHORIZING, Event.LOGIN_FAILED, on_login_failed),
new Geary.State.Mapping(State.AUTHORIZING, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.AUTHORIZED, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.AUTHORIZED, Event.SELECT, on_select),
new Geary.State.Mapping(State.AUTHORIZED, Event.CLOSE_MAILBOX, Geary.State.nop),
new Geary.State.Mapping(State.AUTHORIZED, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.AUTHORIZED, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.AUTHORIZED, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.AUTHORIZED, Event.RECV_ERROR, on_recv_error),
// TODO: technically, if the user selects while selecting, we should handle this
// in some fashion
new Geary.State.Mapping(State.SELECTING, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.SELECTING, Event.SELECT, Geary.State.nop),
new Geary.State.Mapping(State.SELECTING, Event.CLOSE_MAILBOX, on_close_mailbox),
new Geary.State.Mapping(State.SELECTING, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.SELECTING, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.SELECTING, Event.SELECTED, on_selected),
new Geary.State.Mapping(State.SELECTING, Event.SELECT_FAILED, on_select_failed),
new Geary.State.Mapping(State.SELECTING, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.SELECTING, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.SELECTED, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.SELECTED, Event.SELECT, on_select),
new Geary.State.Mapping(State.SELECTED, Event.CLOSE_MAILBOX, on_close_mailbox),
new Geary.State.Mapping(State.SELECTED, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.SELECTED, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.SELECTED, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.SELECTED, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.SEND_CMD, on_send_command),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.CLOSE_MAILBOX, Geary.State.nop),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.CLOSED_MAILBOX, on_closed_mailbox),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.CLOSE_MAILBOX_FAILED, on_close_mailbox_failed),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.LOGOUT, on_logout),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.RECV_ERROR, on_recv_error),
new Geary.State.Mapping(State.LOGGING_OUT, Event.CONNECT, on_late_command),
new Geary.State.Mapping(State.LOGGING_OUT, Event.LOGIN, on_late_command),
new Geary.State.Mapping(State.LOGGING_OUT, Event.SEND_CMD, on_late_command),
new Geary.State.Mapping(State.LOGGING_OUT, Event.SELECT, on_late_command),
new Geary.State.Mapping(State.LOGGING_OUT, Event.CLOSE_MAILBOX, on_late_command),
new Geary.State.Mapping(State.LOGGING_OUT, Event.LOGOUT, Geary.State.nop),
new Geary.State.Mapping(State.LOGGING_OUT, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.LOGGING_OUT, Event.LOGOUT_SUCCESS, on_logged_out),
new Geary.State.Mapping(State.LOGGING_OUT, Event.LOGOUT_FAILED, Geary.State.nop),
new Geary.State.Mapping(State.LOGGING_OUT, Event.RECV_ERROR, Geary.State.nop),
new Geary.State.Mapping(State.LOGGING_OUT, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.LOGGED_OUT, Event.CONNECT, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.LOGIN, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.SEND_CMD, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.SELECT, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.CLOSE_MAILBOX, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.LOGOUT, on_late_command),
new Geary.State.Mapping(State.LOGGED_OUT, Event.DISCONNECT, on_disconnect),
new Geary.State.Mapping(State.LOGGED_OUT, Event.RECV_ERROR, Geary.State.nop),
new Geary.State.Mapping(State.LOGGED_OUT, Event.SEND_ERROR, on_send_error),
new Geary.State.Mapping(State.DISCONNECTING, Event.CONNECT, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.LOGIN, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.SEND_CMD, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.SELECT, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.CLOSE_MAILBOX, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.LOGOUT, on_late_command),
new Geary.State.Mapping(State.DISCONNECTING, Event.DISCONNECTED, on_disconnected),
new Geary.State.Mapping(State.DISCONNECTING, Event.SEND_ERROR, Geary.State.nop),
new Geary.State.Mapping(State.DISCONNECTING, Event.RECV_ERROR, Geary.State.nop),
new Geary.State.Mapping(State.BROKEN, Event.CONNECT, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.LOGIN, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.SEND_CMD, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.SELECT, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.CLOSE_MAILBOX, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.LOGOUT, on_late_command),
new Geary.State.Mapping(State.BROKEN, Event.DISCONNECT, Geary.State.nop)
};
fsm = new Geary.State.Machine(machine_desc, mappings, on_ignored_transition);
fsm.set_logging(false);
}
public Tag? generate_tag() {
return (cx != null) ? cx.generate_tag() : null;
}
public string? get_current_mailbox() {
return current_mailbox;
}
public bool is_current_mailbox_readonly() {
return current_mailbox_readonly;
}
public Context get_context(out string? current_mailbox) {
current_mailbox = null;
switch (fsm.get_state()) {
case State.DISCONNECTED:
case State.LOGGED_OUT:
case State.LOGGING_OUT:
case State.DISCONNECTING:
case State.BROKEN:
return Context.UNCONNECTED;
case State.NOAUTH:
return Context.UNAUTHORIZED;
case State.AUTHORIZED:
return Context.AUTHORIZED;
case State.SELECTED:
current_mailbox = this.current_mailbox;
return current_mailbox_readonly ? Context.EXAMINED : Context.SELECTED;
case State.CONNECTING:
case State.AUTHORIZING:
case State.SELECTING:
case State.CLOSING_MAILBOX:
return Context.IN_PROGRESS;
default:
assert_not_reached();
}
}
//
// connect
//
public async void connect_async(Cancellable? cancellable = null) throws Error {
AsyncParams params = new AsyncParams(cancellable, connect_async.callback);
fsm.issue(Event.CONNECT, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
debug("Connected to %s: %s", to_full_string(), connect_response.to_string());
}
private uint on_connect(uint state, uint event, void *user, Object? object) {
assert(object != null);
assert(connect_params == null);
connect_params = (AsyncParams) object;
assert(cx == null);
cx = new ClientConnection(server, ClientConnection.DEFAULT_PORT_TLS);
cx.connected.connect(on_network_connected);
cx.disconnected.connect(on_network_disconnected);
cx.sent_command.connect(on_network_sent_command);
cx.received_status_response.connect(on_received_status_response);
cx.received_server_data.connect(on_received_server_data);
cx.received_bad_response.connect(on_received_bad_response);
cx.receive_failure.connect(on_receive_failed);
cx.connect_async.begin(connect_params.cancellable, on_connect_completed);
connect_params.do_yield = true;
return State.CONNECTING;
}
private void on_connect_completed(Object? source, AsyncResult result) {
assert(connect_params != null);
try {
cx.connect_async.end(result);
} catch (Error err) {
fsm.issue(Event.SEND_ERROR, null, null, err);
connect_params.err = err;
Idle.add(connect_params.cb);
connect_params = null;
return;
}
// wait for the initial greeting from the server
cb_queue.offer(new CommandCallback(on_connect_response_received));
awaiting_connect_response = true;
}
private bool on_connect_response_received() {
assert(connect_params != null);
assert(connect_response != null);
// initial greeting from server is an untagged response where the first parameter is a
// status code
try {
StringParameter status_param = (StringParameter) connect_response.get_as(
1, typeof(StringParameter));
if (issue_status(Status.from_parameter(status_param), Event.CONNECTED, Event.CONNECT_DENIED,
connect_response)) {
connected();
}
} catch (ImapError imap_err) {
connect_params.err = imap_err;
fsm.issue(Event.CONNECT_DENIED);
}
Idle.add(connect_params.cb);
connect_params = null;
return false;
}
private uint on_connected(uint state, uint event, void *user) {
return State.NOAUTH;
}
private uint on_connect_denied(uint state, uint event, void *user) {
debug("Connection to %s denied by server", to_full_string());
return State.BROKEN;
}
//
// login
//
public async void login_async(string user, string pass, Cancellable? cancellable = null) throws Error {
LoginParams params = new LoginParams(user, pass, cancellable, login_async.callback);
fsm.issue(Event.LOGIN, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
debug("Logged in to %s: %s", to_full_string(), params.cmd_response.to_string());
}
private uint on_login(uint state, uint event, void *user, Object? object) {
assert(object != null);
LoginParams params = (LoginParams) object;
issue_command_async.begin(new LoginCommand(cx.generate_tag(), params.user, params.pass),
params, params.cancellable, on_login_completed);
params.do_yield = true;
return State.AUTHORIZING;
}
private void on_login_completed(Object? source, AsyncResult result) {
if (generic_issue_command_completed(result, Event.LOGIN_SUCCESS, Event.LOGIN_FAILED))
authorized();
}
private uint on_login_success(uint state, uint event, void *user) {
return State.AUTHORIZED;
}
private uint on_login_failed(uint state, uint event, void *user) {
debug("Login to %s failed", to_full_string());
return State.NOAUTH;
}
//
// keepalives (nop idling to keep the session alive and to periodically receive notifications
// of changes)
//
/**
* If seconds is negative or zero, keepalives will be disabled. (This is not recommended.)
*
* Although keepalives can be enabled at any time, if they're enabled and trigger sending
* a command prior to connection, error signals may be fired.
*/
public void enable_keepalives(int seconds = DEFAULT_KEEPALIVE_SEC) {
if (seconds <= 0) {
disable_keepalives();
return;
}
if (keepalive_id != 0)
Source.remove(keepalive_id);
keepalive_id = Timeout.add_seconds(seconds, on_keepalive);
}
/**
* Returns true if keepalives are disactivated, false if already disabled.
*/
public bool disable_keepalives() {
if (keepalive_id == 0)
return false;
Source.remove(keepalive_id);
keepalive_id = 0;
return true;
}
private bool on_keepalive() {
send_command_async.begin(new NoopCommand(generate_tag()), null, on_keepalive_completed);
return true;
}
private void on_keepalive_completed(Object? source, AsyncResult result) {
NoopResults results;
try {
results = NoopResults.decode(send_command_async.end(result));
} catch (Error err) {
message("Keepalive error: %s", err.message);
return;
}
if (results.status_response.status != Status.OK) {
debug("Keepalive failed: %s", results.status_response.to_string());
return;
}
if (results.expunged != null) {
foreach (MessageNumber msg in results.expunged)
unsolicited_expunged(msg);
}
if (results.has_exists())
unsolicited_exists(results.exists);
if (results.has_recent())
unsolicitied_recent(results.recent);
if (results.flags != null) {
foreach (FetchResults flags in results.flags)
unsolicited_flags(flags);
}
}
//
// send commands
//
public async CommandResponse send_command_async(Command cmd, Cancellable? cancellable = null)
throws Error {
// look for special commands that we wish to handle directly, as they affect the state
// machine
//
// TODO: Convert commands into proper calls to avoid throwing an exception
if (cmd.has_name(LoginCommand.NAME) || cmd.has_name(LogoutCommand.NAME)
|| cmd.has_name(SelectCommand.NAME) || cmd.has_name(ExamineCommand.NAME)
|| cmd.has_name(CloseCommand.NAME)) {
throw new IOError.NOT_SUPPORTED("Use direct calls rather than commands");
}
SendCommandParams params = new SendCommandParams(cmd, cancellable, send_command_async.callback);
fsm.issue(Event.SEND_CMD, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
return params.cmd_response;
}
private uint on_send_command(uint state, uint event, void *user, Object? object) {
assert(object != null);
SendCommandParams params = (SendCommandParams) object;
issue_command_async.begin(params.cmd, params, params.cancellable, on_send_command_completed);
params.do_yield = true;
return state;
}
private void on_send_command_completed(Object? source, AsyncResult result) {
generic_issue_command_completed(result, Event.SENT_COMMAND, Event.SEND_COMMAND_FAILED);
}
//
// select/examine
//
public async SelectExamineResults select_async(string mailbox, Cancellable? cancellable = null)
throws Error {
return yield select_examine_async(mailbox, true, cancellable);
}
public async SelectExamineResults examine_async(string mailbox, Cancellable? cancellable = null)
throws Error {
return yield select_examine_async(mailbox, false, cancellable);
}
public async SelectExamineResults select_examine_async(string mailbox, bool is_select,
Cancellable? cancellable) throws Error {
string? old_mailbox = current_mailbox;
SelectParams params = new SelectParams(mailbox, is_select, cancellable,
select_examine_async.callback);
fsm.issue(Event.SELECT, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
// TODO: We may want to move this signal into the async completion handler rather than
// fire it here because async callbacks are scheduled on the event loop and their order
// of execution is not guaranteed
assert(current_mailbox != null);
current_mailbox_changed(old_mailbox, current_mailbox, current_mailbox_readonly);
return SelectExamineResults.decode(params.cmd_response);
}
private uint on_select(uint state, uint event, void *user, Object? object) {
assert(object != null);
SelectParams params = (SelectParams) object;
if (current_mailbox != null && current_mailbox == params.mailbox)
return state;
// TODO: Currently don't handle situation where one mailbox is selected and another is
// asked for without closing
assert(current_mailbox == null);
Command cmd;
if (params.is_select)
cmd = new SelectCommand(cx.generate_tag(), params.mailbox);
else
cmd = new ExamineCommand(cx.generate_tag(), params.mailbox);
issue_command_async.begin(cmd, params, params.cancellable, on_select_completed);
params.do_yield = true;
return State.SELECTING;
}
private void on_select_completed(Object? source, AsyncResult result) {
generic_issue_command_completed(result, Event.SELECTED, Event.SELECT_FAILED);
}
private uint on_selected(uint state, uint event, void *user, Object? object) {
assert(object != null);
SelectParams params = (SelectParams) object;
assert(current_mailbox == null);
current_mailbox = params.mailbox;
current_mailbox_readonly = !params.is_select;
return State.SELECTED;
}
private uint on_select_failed(uint state, uint event, void *user, Object? object) {
assert(object != null);
SelectParams params = (SelectParams) object;
params.err = new ImapError.COMMAND_FAILED("Unable to select mailbox \"%s\": %s",
params.mailbox, params.cmd_response.to_string());
return State.AUTHORIZED;
}
//
// close mailbox
//
public async void close_mailbox_async(Cancellable? cancellable = null) throws Error {
string? old_mailbox = current_mailbox;
AsyncParams params = new AsyncParams(cancellable, close_mailbox_async.callback);
fsm.issue(Event.CLOSE_MAILBOX, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
assert(current_mailbox == null);
// possible for a close_mailbox to occur when already closed, but don't fire signal in
// that case
//
// TODO: See note in select_examine_async() for why it might be better to fire this signal
// in the async completion handler rather than here
if (old_mailbox != null)
current_mailbox_changed(old_mailbox, null, false);
}
private uint on_close_mailbox(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
issue_command_async.begin(new CloseCommand(cx.generate_tag()), params, params.cancellable,
on_close_mailbox_completed);
params.do_yield = true;
return State.CLOSING_MAILBOX;
}
private void on_close_mailbox_completed(Object? source, AsyncResult result) {
generic_issue_command_completed(result, Event.CLOSED_MAILBOX, Event.CLOSE_MAILBOX_FAILED);
}
private uint on_closed_mailbox(uint state, uint event) {
current_mailbox = null;
return State.AUTHORIZED;
}
private uint on_close_mailbox_failed(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
params.err = new ImapError.COMMAND_FAILED("Unable to close mailbox \"%s\": %s",
current_mailbox, params.cmd_response.to_string());
return State.SELECTED;
}
//
// logout
//
public async void logout_async(Cancellable? cancellable = null) throws Error {
AsyncParams params = new AsyncParams(cancellable, logout_async.callback);
fsm.issue(Event.LOGOUT, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
debug("Logged out of %s: %s", to_string(), params.cmd_response.to_string());
}
private uint on_logout(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
issue_command_async.begin(new LogoutCommand(cx.generate_tag()), params, params.cancellable,
on_logout_completed);
params.do_yield = true;
return State.LOGGING_OUT;
}
private void on_logout_completed(Object? source, AsyncResult result) {
if (generic_issue_command_completed(result, Event.LOGOUT_SUCCESS, Event.LOGOUT_FAILED))
logged_out();
}
private uint on_logged_out(uint state, uint event, void *user) {
return State.LOGGED_OUT;
}
//
// disconnect
//
public async void disconnect_async(Cancellable? cancellable = null) throws Error {
AsyncParams params = new AsyncParams(cancellable, disconnect_async.callback);
fsm.issue(Event.DISCONNECT, null, params);
if (params.do_yield)
yield;
if (params.err != null)
throw params.err;
}
private uint on_disconnect(uint state, uint event, void *user, Object? object) {
assert(object != null);
assert(disconnect_params == null);
disconnect_params = (AsyncParams) object;
cx.disconnect_async.begin(disconnect_params.cancellable, on_disconnect_completed);
disconnect_params.do_yield = true;
return State.DISCONNECTING;
}
private void on_disconnect_completed(Object? source, AsyncResult result) {
assert(disconnect_params != null);
try {
cx.disconnect_async.end(result);
fsm.issue(Event.DISCONNECTED);
disconnected(DisconnectReason.LOCAL_CLOSE);
} catch (Error err) {
fsm.issue(Event.SEND_ERROR, null, null, err);
disconnect_params.err = err;
}
Idle.add(disconnect_params.cb);
disconnect_params = null;
}
private uint on_disconnected(uint state, uint event) {
cx = null;
// although we could go to the DISCONNECTED state, that implies the object can be reused ...
// while possible, that requires all state (not just the FSM) be reset at this point, and
// it just seems simpler and less buggy to require the user to discard this object and
// instantiate a new one
return State.BROKEN;
}
//
// error handling
//
private uint on_send_error(uint state, uint event, void *user, Object? object, Error? err) {
assert(err != null);
debug("Send error on %s: %s", to_full_string(), err.message);
cx = null;
Idle.add(on_fire_send_error_signal);
return State.BROKEN;
}
private bool on_fire_send_error_signal() {
disconnected(DisconnectReason.LOCAL_ERROR);
return false;
}
private uint on_recv_error(uint state, uint event, void *user, Object? object, Error? err) {
assert(err != null);
debug("Receive error on %s: %s", to_full_string(), err.message);
cx = null;
Idle.add(on_fire_recv_error_signal);
return State.BROKEN;
}
private bool on_fire_recv_error_signal() {
disconnected(DisconnectReason.REMOTE_ERROR);
return false;
}
// This handles the situation where the user submits a command before the connection has been
// established
private uint on_early_command(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
params.err = new ImapError.NOT_CONNECTED("Not connected to %s", to_string());
return state;
}
// This handles the situation where the user submits a command after the connection has been
// logged out, terminated, or errored-out
private uint on_late_command(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
params.err = new IOError.CLOSED("Connection to %s closing or closed", to_string());
return state;
}
private uint on_unauthenticated(uint state, uint event, void *user, Object? object) {
assert(object != null);
AsyncParams params = (AsyncParams) object;
params.err = new ImapError.UNAUTHENTICATED("Not authenticated with %s", to_string());
return state;
}
private uint on_ignored_transition(uint state, uint event) {
#if VERBOSE_SESSION
debug("Ignored transition: %s@%s", fsm.get_event_string(event), fsm.get_state_string(state));
#endif
return state;
}
//
// command submission
//
private bool issue_status(Status status, Event ok_event, Event error_event, Object? object) {
fsm.issue((status == Status.OK) ? ok_event : error_event, null, object);
return (status == Status.OK);
}
private async AsyncCommandResponse issue_command_async(Command cmd, Object? user = null,
Cancellable? cancellable = null) {
if (cx == null) {
return new AsyncCommandResponse(null, user,
new IOError.CLOSED("Not connected to %s", server));
}
try {
yield cx.send_async(cmd, Priority.DEFAULT, cancellable);
} catch (Error err) {
return new AsyncCommandResponse(null, user, err);
}
cb_queue.offer(new CommandCallback(issue_command_async.callback));
yield;
CommandResponse? cmd_response = cmd_response_queue.poll();
assert(cmd_response != null);
assert(cmd_response.is_sealed());
assert(cmd_response.status_response.tag.equals(cmd.tag));
return new AsyncCommandResponse(cmd_response, user, null);
}
private bool generic_issue_command_completed(AsyncResult result, Event ok_event, Event error_event) {
AsyncCommandResponse async_response = issue_command_async.end(result);
assert(async_response.user != null);
AsyncParams params = (AsyncParams) async_response.user;
params.cmd_response = async_response.cmd_response;
params.err = async_response.err;
bool success;
if (async_response.err != null) {
fsm.issue(Event.SEND_ERROR, null, null, async_response.err);
success = false;
} else {
issue_status(async_response.cmd_response.status_response.status, ok_event, error_event,
params);
success = true;
}
Idle.add(params.cb);
return success;
}
//
// network connection event handlers
//
private void on_network_connected() {
#if VERBOSE_SESSION
debug("Connected to %s", server);
#endif
}
private void on_network_disconnected() {
#if VERBOSE_SESSION
debug("Disconnected from %s", server);
#endif
}
private void on_network_sent_command(Command cmd) {
#if VERBOSE_SESSION
debug("Sent command %s", cmd.to_string());
#endif
}
private void on_received_status_response(StatusResponse status_response) {
assert(!current_cmd_response.is_sealed());
current_cmd_response.seal(status_response);
assert(current_cmd_response.is_sealed());
cmd_response_queue.offer(current_cmd_response);
current_cmd_response = new CommandResponse();
CommandCallback? cmd_callback = cb_queue.poll();
assert(cmd_callback != null);
Idle.add(cmd_callback.callback);
}
private void on_received_server_data(ServerData server_data) {
// The first response from the server is an untagged status response, which is considered
// ServerData in our model. This captures that and treats it as such.
if (awaiting_connect_response) {
awaiting_connect_response = false;
connect_response = server_data;
CommandCallback? cmd_callback = cb_queue.poll();
assert(cmd_callback != null);
Idle.add(cmd_callback.callback);
return;
}
current_cmd_response.add_server_data(server_data);
}
private void on_received_bad_response(RootParameters root, ImapError err) {
debug("Received bad response %s: %s", root.to_string(), err.message);
}
private void on_receive_failed(Error err) {
debug("Receive failed: %s", err.message);
fsm.issue(Event.RECV_ERROR, null, null, err);
}
public string to_string() {
return "ClientSession:%s:%u".printf(server, default_port);
}
public string to_full_string() {
return "%s [%s]".printf(to_string(), fsm.get_state_string(fsm.get_state()));
}
}