diff --git a/po/POTFILES.in b/po/POTFILES.in index 152d7e45..0e6ed8cb 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -214,6 +214,7 @@ src/engine/db/db-versioned-database.vala src/engine/imap/imap.vala src/engine/imap/imap-error.vala src/engine/imap/api/imap-account-session.vala +src/engine/imap/api/imap-capabilities.vala src/engine/imap/api/imap-client-service.vala src/engine/imap/api/imap-email-flags.vala src/engine/imap/api/imap-email-properties.vala @@ -325,7 +326,6 @@ src/engine/imap/parameter/imap-quoted-string-parameter.vala src/engine/imap/parameter/imap-root-parameters.vala src/engine/imap/parameter/imap-string-parameter.vala src/engine/imap/parameter/imap-unquoted-string-parameter.vala -src/engine/imap/response/imap-capabilities.vala src/engine/imap/response/imap-continuation-response.vala src/engine/imap/response/imap-fetch-data-decoder.vala src/engine/imap/response/imap-fetched-data.vala diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala index 3fee1063..157eb2b1 100644 --- a/src/engine/api/geary-engine.vala +++ b/src/engine/api/geary-engine.vala @@ -284,10 +284,12 @@ public class Geary.Engine : BaseObject { (security, cx) => account.untrusted_host(service, security, cx) ); - Geary.Imap.ClientSession client = new Imap.ClientSession(endpoint); + var client = new Imap.ClientSession(endpoint); GLib.Error? imap_err = null; try { - yield client.connect_async(cancellable); + yield client.connect_async( + Imap.ClientSession.DEFAULT_GREETING_TIMEOUT_SEC, cancellable + ); } catch (GLib.Error err) { imap_err = err; } diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala b/src/engine/imap-engine/imap-engine-minimal-folder.vala index 923b8e71..3417f1da 100644 --- a/src/engine/imap-engine/imap-engine-minimal-folder.vala +++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala @@ -770,7 +770,7 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport this.remote_wait_semaphore.reset(); } - Imap.FolderSession session = this.remote_session; + Imap.FolderSession? session = this.remote_session; this.remote_session = null; if (session != null) { session.appended.disconnect(on_remote_appended); diff --git a/src/engine/imap/api/imap-account-session.vala b/src/engine/imap/api/imap-account-session.vala index 4f7f99d6..f0bdfef1 100644 --- a/src/engine/imap/api/imap-account-session.vala +++ b/src/engine/imap/api/imap-account-session.vala @@ -46,11 +46,12 @@ internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject { public async FolderPath get_default_personal_namespace(Cancellable? cancellable) throws Error { ClientSession session = claim_session(); - if (session.personal_namespaces.is_empty) { + Gee.List personal = session.get_personal_namespaces(); + if (personal.is_empty) { throw new ImapError.INVALID("No personal namespace found"); } - Namespace ns = session.personal_namespaces[0]; + Namespace ns = personal[0]; string prefix = ns.prefix; string? delim = ns.delim; if (delim != null && prefix.has_suffix(delim)) { diff --git a/src/engine/imap/response/imap-capabilities.vala b/src/engine/imap/api/imap-capabilities.vala similarity index 59% rename from src/engine/imap/response/imap-capabilities.vala rename to src/engine/imap/api/imap-capabilities.vala index 22179c9a..c38980d0 100644 --- a/src/engine/imap/response/imap-capabilities.vala +++ b/src/engine/imap/api/imap-capabilities.vala @@ -1,7 +1,9 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. + * (version 2.1 or later). See the COPYING file in this distribution. */ public class Geary.Imap.Capabilities : Geary.GenericCapabilities { @@ -13,6 +15,7 @@ public class Geary.Imap.Capabilities : Geary.GenericCapabilities { public const string COMPRESS = "COMPRESS"; public const string DEFLATE_SETTING = "DEFLATE"; public const string IDLE = "IDLE"; + public const string IMAP4REV1 = "IMAP4rev1"; public const string NAMESPACE = "NAMESPACE"; public const string SPECIAL_USE = "SPECIAL-USE"; public const string STARTTLS = "STARTTLS"; @@ -22,29 +25,49 @@ public class Geary.Imap.Capabilities : Geary.GenericCapabilities { public const string NAME_SEPARATOR = "="; public const string? VALUE_SEPARATOR = null; - + /** + * The version of this set of capabilities for an IMAP session. + * + * The capabilities that an IMAP session offers changes over time, + * for example after login or STARTTLS. This property supports + * detecting these changes. + * + * @see ClientSession.capabilities + */ public int revision { get; private set; } /** - * Creates an empty set of capabilities. revision represents the different variations of - * capabilities that an IMAP session might offer (i.e. changes after login or STARTTLS, for - * example). + * Creates an empty set of capabilities. */ - public Capabilities(int revision) { - base (NAME_SEPARATOR, VALUE_SEPARATOR); - - this.revision = revision; + public Capabilities(StringParameter[] capabilities, int revision) { + this.empty(revision); + foreach (var cap in capabilities) { + parse_and_add_capability(cap.ascii); + } } - public bool add_parameter(StringParameter stringp) { - return parse_and_add_capability(stringp.ascii); + /** + * Creates an empty set of capabilities. + */ + public Capabilities.empty(int revision) { + base(NAME_SEPARATOR, VALUE_SEPARATOR); + this.revision = revision; } public override string to_string() { return "#%d: %s".printf(revision, base.to_string()); } + /** + * Indicates an IMAP session reported support for IMAP 4rev1. + * + * See [[https://tools.ietf.org/html/rfc2177]] + */ + public bool supports_imap4rev1() { + return has_capability(IMAP4REV1); + } + /** * Indicates the {@link ClientSession} reported support for IDLE. * diff --git a/src/engine/imap/api/imap-client-service.vala b/src/engine/imap/api/imap-client-service.vala index 26d4aa6d..7a8a7f84 100644 --- a/src/engine/imap/api/imap-client-service.vala +++ b/src/engine/imap/api/imap-client-service.vala @@ -222,14 +222,17 @@ internal class Geary.Imap.ClientService : Geary.ClientService { this.all_sessions.size > this.min_pool_size ); - if (!this.is_running || this.discard_returned_sessions || too_many_free) { - yield disconnect_session(session); - } else if (yield check_session(session, false)) { - bool free = true; - MailboxSpecifier? mailbox = null; - ClientSession.ProtocolState proto = session.get_protocol_state(out mailbox); + bool disconnect = ( + too_many_free || + this.discard_returned_sessions || + !this.is_running || + !yield check_session(session, false) + ); + + if (!disconnect) { // If the session has a mailbox selected, close it before // adding it back to the pool + ClientSession.ProtocolState proto = session.get_protocol_state(); if (proto == ClientSession.ProtocolState.SELECTED || proto == ClientSession.ProtocolState.SELECTING) { // always close mailbox to return to authorized state @@ -238,32 +241,20 @@ internal class Geary.Imap.ClientService : Geary.ClientService { } catch (ImapError imap_error) { debug("Error attempting to close released session %s: %s", session.to_string(), imap_error.message); - free = false; + disconnect = true; } - - // Double check the session after closing it - switch (session.get_protocol_state(null)) { - case AUTHORIZED: - // This is the desired state, so all good - break; - - case NOT_CONNECTED: - // No longer connected, so just drop it - free = false; - break; - - default: + if (session.get_protocol_state() != AUTHORIZED) { // Closing it didn't leave it in the desired - // state, so log out and drop it - yield disconnect_session(session); - free = false; - break; + // state, so drop it + disconnect = true; } } - if (free) { + if (!disconnect) { debug("Unreserving session %s", session.to_string()); this.free_queue.send(session); + } else { + yield disconnect_session(session); } } } @@ -381,7 +372,7 @@ internal class Geary.Imap.ClientService : Geary.ClientService { /** Determines if a session is valid, disposing of it if not. */ private async bool check_session(ClientSession target, bool claiming) { bool valid = false; - switch (target.get_protocol_state(null)) { + switch (target.get_protocol_state()) { case ClientSession.ProtocolState.AUTHORIZED: case ClientSession.ProtocolState.CLOSING_MAILBOX: valid = true; @@ -396,15 +387,6 @@ internal class Geary.Imap.ClientService : Geary.ClientService { } break; - case ClientSession.ProtocolState.NOT_CONNECTED: - // Already disconnected, so drop it on the floor - try { - yield remove_session_async(target); - } catch (Error err) { - debug("Error removing unconnected session: %s", err.message); - } - break; - default: yield disconnect_session(target); break; @@ -447,11 +429,13 @@ internal class Geary.Imap.ClientService : Geary.ClientService { ClientSession new_session = new ClientSession(remote); new_session.set_logging_parent(this); - yield new_session.connect_async(cancellable); + yield new_session.connect_async( + ClientSession.DEFAULT_GREETING_TIMEOUT_SEC, cancellable + ); try { yield new_session.initiate_session_async(login, cancellable); - } catch (Error err) { + } catch (GLib.Error err) { // need to disconnect before throwing error ... don't // honor Cancellable here, it's important to disconnect // the client before dropping the ref @@ -504,44 +488,43 @@ internal class Geary.Imap.ClientService : Geary.ClientService { } private async void disconnect_session(ClientSession session) { - debug("Logging out session: %s", session.to_string()); - - // Log out before removing the session since close() only - // hangs around until all sessions have been removed before - // exiting. - try { - yield session.logout_async(this.close_cancellable); + if (session.get_protocol_state() != NOT_CONNECTED) { + debug("Logging out session: %s", session.to_string()); + // No need to remove it after logging out, the + // disconnected handler will do that for us. + try { + yield session.logout_async(this.close_cancellable); + } catch (GLib.Error err) { + debug("Error logging out of session: %s", err.message); + yield force_disconnect_session(session); + } + } else { yield remove_session_async(session); - } catch (GLib.Error err) { - debug("Error logging out of session: %s", err.message); - yield force_disconnect_session(session); } - } private async void force_disconnect_session(ClientSession session) { debug("Dropping session: %s", session.to_string()); - - try { - yield remove_session_async(session); - } catch (Error err) { - debug("Error removing session: %s", err.message); - } + yield remove_session_async(session); // Don't wait for this to finish because we don't want to // block claiming a new session, shutdown, etc. session.disconnect_async.begin(null); } - private async bool remove_session_async(ClientSession session) throws Error { + private async bool remove_session_async(ClientSession session) { // Ensure the session isn't held on to, anywhere this.free_queue.revoke(session); bool removed = false; - yield this.sessions_mutex.execute_locked(() => { - removed = this.all_sessions.remove(session); - }); + try { + yield this.sessions_mutex.execute_locked(() => { + removed = this.all_sessions.remove(session); + }); + } catch (GLib.Error err) { + debug("Error removing session: %s", err.message); + } if (removed) { session.disconnected.disconnect(on_disconnected); @@ -549,21 +532,15 @@ internal class Geary.Imap.ClientService : Geary.ClientService { return removed; } - private void on_disconnected(ClientSession session, ClientSession.DisconnectReason reason) { + private void on_disconnected(ClientSession session, + ClientSession.DisconnectReason reason) { debug( - "Session unexpected disconnect: %s: %s", + "Session disconnected: %s: %s", session.to_string(), reason.to_string() ); this.remove_session_async.begin( session, - (obj, res) => { - try { - this.remove_session_async.end(res); - } catch (Error err) { - debug("Error removing disconnected session: %s", - err.message); - } - } + (obj, res) => { this.remove_session_async.end(res); } ); } diff --git a/src/engine/imap/api/imap-session-object.vala b/src/engine/imap/api/imap-session-object.vala index 248f9b70..33365b06 100644 --- a/src/engine/imap/api/imap-session-object.vala +++ b/src/engine/imap/api/imap-session-object.vala @@ -103,7 +103,7 @@ public abstract class Geary.Imap.SessionObject : BaseObject, Logging.Source { } private void on_disconnected(ClientSession.DisconnectReason reason) { - debug("DISCONNECTED %s", reason.to_string()); + debug("Disconnected %s", reason.to_string()); close(); disconnected(reason); diff --git a/src/engine/imap/command/imap-command.vala b/src/engine/imap/command/imap-command.vala index 570fe3ba..58163671 100644 --- a/src/engine/imap/command/imap-command.vala +++ b/src/engine/imap/command/imap-command.vala @@ -17,7 +17,7 @@ * * See [[http://tools.ietf.org/html/rfc3501#section-6]] */ -public class Geary.Imap.Command : BaseObject { +public abstract class Geary.Imap.Command : BaseObject { /** * Default timeout to wait for a server response for a command. @@ -97,7 +97,7 @@ public class Geary.Imap.Command : BaseObject { * * @see Tag */ - public Command(string name, string[]? args = null) { + protected Command(string name, string[]? args = null) { this.tag = Tag.get_unassigned(); this.name = name; if (args != null) { @@ -250,7 +250,7 @@ public class Geary.Imap.Command : BaseObject { * cancelled, if the command timed out, or if the command's * response was bad. */ - public async void wait_until_complete(GLib.Cancellable cancellable) + public async void wait_until_complete(GLib.Cancellable? cancellable) throws GLib.Error { yield this.complete_lock.wait_async(cancellable); diff --git a/src/engine/imap/imap-error.vala b/src/engine/imap/imap-error.vala index df1821df..8e467556 100644 --- a/src/engine/imap/imap-error.vala +++ b/src/engine/imap/imap-error.vala @@ -63,6 +63,11 @@ public errordomain Geary.ImapError { /** * The remote IMAP server not currently available. + * + * This does not indicate a network error, rather it indicates a + * connection to the server was established but the server + * indicated it is not currently servicing the connection. */ - UNAVAILABLE + UNAVAILABLE; + } diff --git a/src/engine/imap/response/imap-response-code.vala b/src/engine/imap/response/imap-response-code.vala index 2f283332..af6e799a 100644 --- a/src/engine/imap/response/imap-response-code.vala +++ b/src/engine/imap/response/imap-response-code.vala @@ -69,24 +69,22 @@ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter { /** * Parses the {@link ResponseCode} into {@link Capabilities}, if possible. * - * Since Capabilities are revised with various {@link ClientSession} states, this method accepts - * a ref to an int that will be incremented after handed to the Capabilities constructor. This - * can be used to track the revision of capabilities seen on the connection. - * * @throws ImapError.INVALID if Capability was not specified. */ - public Capabilities get_capabilities(ref int next_revision) throws ImapError { + public Capabilities get_capabilities(int revision) throws ImapError { if (!get_response_code_type().is_value(ResponseCodeType.CAPABILITY)) throw new ImapError.INVALID("Not CAPABILITY response code: %s", to_string()); - Capabilities capabilities = new Capabilities(next_revision++); + var params = new StringParameter[this.size]; + int count = 0; for (int ctr = 1; ctr < size; ctr++) { StringParameter? param = get_if_string(ctr); - if (param != null) - capabilities.add_parameter(param); + if (param != null) { + params[count++] = param; + } } - return capabilities; + return new Capabilities(params[0:count], revision); } /** diff --git a/src/engine/imap/response/imap-server-data.vala b/src/engine/imap/response/imap-server-data.vala index a4202956..29aee4b5 100644 --- a/src/engine/imap/response/imap-server-data.vala +++ b/src/engine/imap/response/imap-server-data.vala @@ -50,24 +50,22 @@ public class Geary.Imap.ServerData : ServerResponse { /** * Parses the {@link ServerData} into {@link Capabilities}, if possible. * - * Since Capabilities are revised with various {@link ClientSession} states, this method accepts - * a ref to an int that will be incremented after handed to the Capabilities constructor. This - * can be used to track the revision of capabilities seen on the connection. - * * @throws ImapError.INVALID if not a Capability. */ - public Capabilities get_capabilities(ref int next_revision) throws ImapError { - if (server_data_type != ServerDataType.CAPABILITY) + public Capabilities get_capabilities(int revision) throws ImapError { + if (this.server_data_type != ServerDataType.CAPABILITY) throw new ImapError.INVALID("Not CAPABILITY data: %s", to_string()); - Capabilities capabilities = new Capabilities(next_revision++); - for (int ctr = 2; ctr < size; ctr++) { + var params = new StringParameter[this.size]; + int count = 0; + for (int ctr = 1; ctr < size; ctr++) { StringParameter? param = get_if_string(ctr); - if (param != null) - capabilities.add_parameter(param); + if (param != null) { + params[count++] = param; + } } - return capabilities; + return new Capabilities(params[0:count], revision); } /** diff --git a/src/engine/imap/transport/imap-client-connection.vala b/src/engine/imap/transport/imap-client-connection.vala index 7513adb7..8d7e54d9 100644 --- a/src/engine/imap/transport/imap-client-connection.vala +++ b/src/engine/imap/transport/imap-client-connection.vala @@ -40,12 +40,6 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { private static int next_cx_id = 0; - /** - * This identifier is used only for debugging, to differentiate connections from one another - * in logs and debug output. - */ - public int cx_id { get; private set; } - /** * Determines if the connection will use IMAP IDLE when idle. * @@ -69,11 +63,10 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { private weak Logging.Source? _logging_parent = null; private Geary.Endpoint endpoint; - private SocketConnection? cx = null; - private IOStream? ios = null; - private Serializer? ser = null; - private BufferedOutputStream? ser_buffer = null; - private Deserializer? des = null; + private int cx_id; + private IOStream? cx = null; + private Deserializer? deserializer = null; + private Serializer? serializer = null; private int tag_counter = 0; private char tag_prefix = 'a'; @@ -89,14 +82,6 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { private GLib.Cancellable? open_cancellable = null; - public virtual signal void connected() { - debug("Connected to %s", endpoint.to_string()); - } - - public virtual signal void disconnected() { - debug("Disconnected from %s", endpoint.to_string()); - } - public virtual signal void sent_command(Command cmd) { debug("SEND: %s", cmd.to_string()); } @@ -113,34 +98,14 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { debug("RECV: %s", continuation_response.to_string()); } - public virtual signal void received_bytes(size_t bytes) { - // this generates a *lot* of debug logging if one was placed here, so it's not - } + public signal void received_bytes(size_t bytes); - public virtual signal void received_bad_response(RootParameters root, - ImapError err) { - warning("Received bad response: %s", err.message); - } + public signal void received_bad_response(RootParameters root, + ImapError err); - public virtual signal void received_eos() { - debug("Received eos"); - } + public signal void send_failure(Error err); - public virtual signal void send_failure(Error err) { - warning("Send failure: %s", err.message); - } - - public virtual signal void receive_failure(Error err) { - warning("Receive failure: %s", err.message); - } - - public virtual signal void deserialize_failure(Error err) { - warning("Deserialize failure: %s", err.message); - } - - public virtual signal void close_error(Error err) { - warning("Close error: %s", err.message); - } + public signal void receive_failure(GLib.Error err); public ClientConnection( @@ -158,8 +123,9 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { /** Returns the remote address of this connection, if any. */ public GLib.SocketAddress? get_remote_address() throws GLib.Error { GLib.SocketAddress? addr = null; - if (cx != null) { - addr = cx.get_remote_address(); + var tcp_cx = getTcpConnection(); + if (tcp_cx != null) { + addr = tcp_cx.get_remote_address(); } return addr; } @@ -167,8 +133,9 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { /** Returns the local address of this connection, if any. */ public SocketAddress? get_local_address() throws GLib.Error { GLib.SocketAddress? addr = null; - if (cx != null) { - addr = cx.get_local_address(); + var tcp_cx = getTcpConnection(); + if (tcp_cx != null) { + addr = tcp_cx.get_local_address(); } return addr; } @@ -209,30 +176,22 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { if (this.cx != null) { throw new ImapError.ALREADY_CONNECTED("Client already connected"); } - - this.cx = yield endpoint.connect_async(cancellable); - this.ios = cx; + this.cx = yield this.endpoint.connect_async(cancellable); this.pending_queue.clear(); this.sent_queue.clear(); - connected(); - try { yield open_channels_async(); - } catch (Error err) { - // if this fails, need to close connection because the caller will not call - // disconnect_async() + } catch (GLib.Error err) { + // if this fails, need to close connection because the + // caller will not call disconnect_async() try { yield cx.close_async(); - } catch (Error close_err) { + } catch (GLib.Error close_err) { // ignored } - this.cx = null; - this.ios = null; - - receive_failure(err); throw err; } @@ -243,17 +202,14 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { } public async void disconnect_async(Cancellable? cancellable = null) throws Error { - if (cx == null) + if (this.cx == null) return; this.idle_timer.reset(); // To guard against reentrancy - SocketConnection close_cx = cx; - cx = null; - - // close the Serializer and Deserializer - yield close_channels_async(cancellable); + GLib.IOStream old_cx = this.cx; + this.cx = null; // Cancel any pending commands foreach (Command pending in this.pending_queue.get_all()) { @@ -263,20 +219,14 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { this.pending_queue.clear(); // close the actual streams and the connection itself - Error? close_err = null; - try { - yield ios.close_async(Priority.DEFAULT, cancellable); - yield close_cx.close_async(Priority.DEFAULT, cancellable); - } catch (Error err) { - close_err = err; - } finally { - ios = null; + yield close_channels_async(cancellable); + yield old_cx.close_async(Priority.DEFAULT, cancellable); - if (close_err != null) { - close_error(close_err); - } - - disconnected(); + var tls_cx = old_cx as GLib.TlsConnection; + if (tls_cx != null && !tls_cx.base_io_stream.is_closed()) { + yield tls_cx.base_io_stream.close_async( + Priority.DEFAULT, cancellable + ); } } @@ -300,9 +250,7 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { yield close_channels_async(cancellable); // wrap connection with TLS connection - TlsClientConnection tls_cx = yield endpoint.starttls_handshake_async(cx, cancellable); - - ios = tls_cx; + this.cx = yield endpoint.starttls_handshake_async(this.cx, cancellable); // re-open Serializer/Deserializer with the new streams yield open_channels_async(); @@ -352,33 +300,39 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { this._logging_parent = parent; } - private async void open_channels_async() throws Error { - assert(ios != null); - assert(ser == null); - assert(des == null); + private GLib.TcpConnection? getTcpConnection() { + var cx = this.cx; + var tls_cx = cx as GLib.TlsConnection; + if (tls_cx != null) { + cx = tls_cx.base_io_stream; + } + return cx as TcpConnection; + } + private async void open_channels_async() throws Error { this.open_cancellable = new GLib.Cancellable(); - // Not buffering the Deserializer because it uses a DataInputStream, which is buffered - ser_buffer = new BufferedOutputStream(ios.output_stream); - ser_buffer.set_close_base_stream(false); - - // Use ClientConnection cx_id for debugging aid with Serializer/Deserializer string id = "%04d".printf(cx_id); - ser = new Serializer(id, ser_buffer); - des = new Deserializer(id, ios.input_stream); - des.parameters_ready.connect(on_parameters_ready); - des.bytes_received.connect(on_bytes_received); - des.receive_failure.connect(on_receive_failure); - des.deserialize_failure.connect(on_deserialize_failure); - des.eos.connect(on_eos); + var serializer_buffer = new GLib.BufferedOutputStream( + this.cx.output_stream + ); + serializer_buffer.set_close_base_stream(false); + this.serializer = new Serializer(serializer_buffer); + + // Not buffering the Deserializer because it uses a + // DataInputStream, which is already buffered + this.deserializer = new Deserializer(id, this.cx.input_stream); + this.deserializer.bytes_received.connect(on_bytes_received); + this.deserializer.deserialize_failure.connect(on_deserialize_failure); + this.deserializer.end_of_stream.connect(on_eos); + this.deserializer.parameters_ready.connect(on_parameters_ready); + this.deserializer.receive_failure.connect(on_receive_failure); + yield this.deserializer.start_async(); // Start this running in the "background", it will stop when // open_cancellable is cancelled this.send_loop.begin(); - - yield des.start_async(); } /** Disconnect and deallocates the Serializer and Deserializer. */ @@ -392,26 +346,21 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { } this.sent_queue.clear(); - // disconnect from Deserializer before yielding to stop it - if (des != null) { - des.parameters_ready.disconnect(on_parameters_ready); - des.bytes_received.disconnect(on_bytes_received); - des.receive_failure.disconnect(on_receive_failure); - des.deserialize_failure.disconnect(on_deserialize_failure); - des.eos.disconnect(on_eos); - - yield des.stop_async(); + if (this.serializer != null) { + yield this.serializer.close_stream(cancellable); + this.serializer = null; } - des = null; - ser = null; - // Close the Serializer's buffered stream after it as been - // deallocated so it can't possibly write to the stream again, - // and so the stream's async thread doesn't attempt to flush - // its buffers from its finaliser at some later unspecified - // point, possibly writing to an invalid underlying stream. - if (ser_buffer != null) { - yield ser_buffer.close_async(GLib.Priority.DEFAULT, cancellable); - ser_buffer = null; + + var deserializer = this.deserializer; + if (deserializer != null) { + deserializer.bytes_received.disconnect(on_bytes_received); + deserializer.deserialize_failure.disconnect(on_deserialize_failure); + deserializer.end_of_stream.disconnect(on_eos); + deserializer.parameters_ready.disconnect(on_parameters_ready); + deserializer.receive_failure.disconnect(on_receive_failure); + + yield deserializer.stop_async(); + this.deserializer = null; } } @@ -454,7 +403,7 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { // Check the queue is still empty after sending the // command, since that might have changed. if (this.pending_queue.is_empty) { - yield this.ser.flush_stream(cancellable); + yield this.serializer.flush_stream(cancellable); } } catch (GLib.Error err) { if (!(err is GLib.IOError.CANCELLED)) { @@ -482,12 +431,13 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { // Set timeout per session policy command.response_timeout = this.command_timeout; + command.response_timed_out.connect(on_command_timeout); this.current_command = command; this.sent_queue.add(command); - yield command.send(this.ser, cancellable); + yield command.send(this.serializer, cancellable); sent_command(command); - yield command.send_wait(this.ser, cancellable); + yield command.send_wait(this.serializer, cancellable); } catch (GLib.Error err) { ser_error = err; } @@ -586,32 +536,29 @@ public class Geary.Imap.ClientConnection : BaseObject, Logging.Source { received_bytes(bytes); } + private void on_eos() { + receive_failure( + new ImapError.NOT_CONNECTED( + "End of stream reading from %s", to_string() + ) + ); + } + private void on_receive_failure(Error err) { receive_failure(err); } private void on_deserialize_failure() { - deserialize_failure( + receive_failure( new ImapError.PARSE_ERROR( "Unable to deserialize from %s", to_string() ) ); } - private void on_eos() { - received_eos(); - } - private void on_command_timeout(Command command) { this.sent_queue.remove(command); command.response_timed_out.disconnect(on_command_timeout); - - // turn off graceful disconnect ... if the connection is hung, - // don't want to be stalled trying to flush the pipe - TcpConnection? tcp_cx = cx as TcpConnection; - if (tcp_cx != null) - tcp_cx.set_graceful_disconnect(false); - receive_failure( new ImapError.TIMED_OUT( "No response to command after %u seconds: %s", diff --git a/src/engine/imap/transport/imap-client-session.vala b/src/engine/imap/transport/imap-client-session.vala index a40c5986..fabee175 100644 --- a/src/engine/imap/transport/imap-client-session.vala +++ b/src/engine/imap/transport/imap-client-session.vala @@ -74,7 +74,9 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { /** Default keep-alive interval when not in the Selected state. */ public const uint DEFAULT_UNSELECTED_KEEPALIVE_SEC = RECOMMENDED_KEEPALIVE_SEC; - private const uint GREETING_TIMEOUT_SEC = Command.DEFAULT_RESPONSE_TIMEOUT_SEC; + /** Default time to wait for the server greeting when connecting. */ + public const uint DEFAULT_GREETING_TIMEOUT_SEC = + Command.DEFAULT_RESPONSE_TIMEOUT_SEC; /** @@ -193,12 +195,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { private enum Event { // user-initiated events CONNECT, + DISCONNECT, + + // command-initiated events LOGIN, SEND_CMD, SELECT, CLOSE_MAILBOX, LOGOUT, - DISCONNECT, // server events CONNECTED, @@ -224,26 +228,35 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { state_to_string, event_to_string); /** - * {@link ClientSession} tracks server extensions reported via the CAPABILITY server data - * response. + * Set of IMAP extensions reported as being supported by the server. * - * ClientSession stores the last seen list as a service for users and uses it internally - * (specifically for IDLE support). + * The capabilities that an IMAP session offers changes over time, + * for example after login or STARTTLS. The instance assigned to + * this property will change as these change and the instance's + * {@link Capabilities.revision} property will also monotonically + * increase over the lifetime of a single session. */ - public Capabilities capabilities { get; private set; default = new Capabilities(0); } + public Capabilities capabilities { + get; private set; default = new Capabilities.empty(0); + } /** Determines if this session supports the IMAP IDLE extension. */ public bool is_idle_supported { get { return this.capabilities.has_capability(Capabilities.IDLE); } } + /** The currently selected mailbox, if any. */ + public MailboxSpecifier? selected_mailbox = null; + /** - * Determines when the last successful command response was received. + * Specifies if the current selected state is readonly. * - * Returns the system wall clock time the last successful command - * response was received, in microseconds since the UNIX epoch. + * This property specifies if the current selected state was + * entered by a SELECT command -- in which case mailbox access is + * read-write, or by an EXAMINE command -- in which case mailbox + * access is read-only. */ - public int64 last_seen = 0; + public bool selected_readonly = false; /** {@inheritDoc} */ public Logging.Flag logging_flags { @@ -254,6 +267,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { public Logging.Source? logging_parent { get { return _logging_parent; } } private weak Logging.Source? _logging_parent = null; + /** + * Determines when the last successful command response was received. + * + * Returns the system wall clock time the last successful command + * response was received, in microseconds since the UNIX epoch. + */ + internal int64 last_seen { get; private set; default = 0; } + // While the following inbox and namespace data should be server // specific, there is a small chance they will differ between // connections if the connections connect to different servers in @@ -263,25 +284,21 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { // initiate_session_async() has successfully completed. /** Records the actual name and delimiter used for the inbox */ - internal MailboxInformation? inbox = null; + internal MailboxInformation? inbox { get; private set; default = null; } - /** The locations personal mailboxes on this connection. */ - internal Gee.List personal_namespaces = new Gee.ArrayList(); + // Locations personal mailboxes for this session + private Gee.List personal_namespaces = new Gee.ArrayList(); - /** The locations of other user's mailboxes on this connection. */ - internal Gee.List user_namespaces = new Gee.ArrayList(); - - /** The locations of shared mailboxes on this connection. */ - internal Gee.List shared_namespaces = new Gee.ArrayList(); + // Locations of other user's mailboxes for this session + private Gee.List user_namespaces = new Gee.ArrayList(); + // The locations of shared mailboxes for this sesion + private Gee.List shared_namespaces = new Gee.ArrayList(); private Endpoint imap_endpoint; private Geary.State.Machine fsm; private ClientConnection? cx = null; - private MailboxSpecifier? current_mailbox = null; - private bool current_mailbox_readonly = false; - private uint keepalive_id = 0; private uint selected_keepalive_secs = 0; private uint unselected_keepalive_secs = 0; @@ -291,7 +308,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { private Nonblocking.Semaphore? connect_waiter = null; private Error? connect_err = null; - private int next_capabilities_revision = 1; private Gee.Map namespaces = new Gee.HashMap(); @@ -300,28 +316,12 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { // Connection state changes // - public signal void connected(); - - public signal void session_denied(string? reason); - - public signal void authorized(); - - public signal void logged_out(); - - public signal void login_failed(StatusResponse? response); - + /** Emitted when the session is disconnected for any reason. */ public signal void disconnected(DisconnectReason reason); + /** Emitted when an IMAP command status response is received. */ public signal void status_response_received(StatusResponse status_response); - /** - * Fired after the specific {@link ServerData} signals (i.e. {@link capability}, {@link exists} - * {@link expunge}, etc.) - */ - public signal void server_data_received(ServerData server_data); - - public signal void capability(Capabilities capabilities); - public signal void exists(int count); public signal void expunge(SequenceNumber seq_num); @@ -343,15 +343,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { public signal void status(StatusData status_data); - public signal void @namespace(NamespaceResponse namespace); - - /** - * 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 signal void current_mailbox_changed(MailboxSpecifier? old_name, MailboxSpecifier? new_name, - bool readonly); public ClientSession(Endpoint imap_endpoint) { this.imap_endpoint = imap_endpoint; @@ -366,12 +357,12 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { new Geary.State.Mapping(State.NOT_CONNECTED, Event.DISCONNECT, Geary.State.nop), new Geary.State.Mapping(State.CONNECTING, Event.CONNECT, on_already_connected), + new Geary.State.Mapping(State.CONNECTING, Event.DISCONNECT, on_disconnect), 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.RECV_STATUS, on_connecting_recv_status), new Geary.State.Mapping(State.CONNECTING, Event.RECV_COMPLETION, on_dropped_response), @@ -380,99 +371,96 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { new Geary.State.Mapping(State.CONNECTING, Event.TIMEOUT, on_connecting_timeout), new Geary.State.Mapping(State.NOAUTH, Event.CONNECT, on_already_connected), + new Geary.State.Mapping(State.NOAUTH, Event.DISCONNECT, on_disconnect), 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.RECV_STATUS, on_recv_status), new Geary.State.Mapping(State.NOAUTH, Event.RECV_COMPLETION, on_recv_status), 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.CONNECT, on_already_connected), + new Geary.State.Mapping(State.AUTHORIZING, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.AUTHORIZING, Event.LOGIN, on_logging_in), new Geary.State.Mapping(State.AUTHORIZING, Event.SEND_CMD, on_unauthenticated), 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.RECV_STATUS, on_recv_status), new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_COMPLETION, on_login_recv_completion), 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.CONNECT, on_already_connected), + new Geary.State.Mapping(State.AUTHORIZED, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.AUTHORIZED, Event.LOGIN, on_already_logged_in), 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, on_not_selected), 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.RECV_STATUS, on_recv_status), new Geary.State.Mapping(State.AUTHORIZED, Event.RECV_COMPLETION, on_recv_status), new Geary.State.Mapping(State.AUTHORIZED, Event.SEND_ERROR, on_send_error), new Geary.State.Mapping(State.AUTHORIZED, Event.RECV_ERROR, on_recv_error), new Geary.State.Mapping(State.SELECTING, Event.CONNECT, on_already_connected), + new Geary.State.Mapping(State.SELECTING, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.SELECTING, Event.LOGIN, on_already_logged_in), new Geary.State.Mapping(State.SELECTING, Event.SEND_CMD, on_send_command), new Geary.State.Mapping(State.SELECTING, Event.SELECT, on_select), 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.RECV_STATUS, on_recv_status), - new Geary.State.Mapping(State.SELECTING, Event.RECV_COMPLETION, on_selecting_recv_completion), 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.CONNECT, on_already_connected), + new Geary.State.Mapping(State.SELECTED, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.SELECTED, Event.LOGIN, on_already_logged_in), 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.RECV_STATUS, on_recv_status), new Geary.State.Mapping(State.SELECTED, Event.RECV_COMPLETION, on_recv_status), 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.CONNECT, on_already_connected), + new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.LOGIN, on_already_logged_in), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.SEND_CMD, on_send_command), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.SELECT, on_select), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.CLOSE_MAILBOX, on_not_selected), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.LOGOUT, on_logout), - new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.RECV_STATUS, on_recv_status), new Geary.State.Mapping(State.CLOSING_MAILBOX, Event.RECV_COMPLETION, on_closing_recv_completion), 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.LOGOUT, Event.CONNECT, on_already_connected), + new Geary.State.Mapping(State.LOGOUT, Event.DISCONNECT, on_disconnect), new Geary.State.Mapping(State.LOGOUT, Event.LOGIN, on_already_logged_in), new Geary.State.Mapping(State.LOGOUT, Event.SEND_CMD, on_late_command), new Geary.State.Mapping(State.LOGOUT, Event.SELECT, on_late_command), new Geary.State.Mapping(State.LOGOUT, Event.CLOSE_MAILBOX, on_late_command), new Geary.State.Mapping(State.LOGOUT, Event.LOGOUT, on_late_command), - new Geary.State.Mapping(State.LOGOUT, Event.DISCONNECT, on_disconnect), - new Geary.State.Mapping(State.LOGOUT, Event.DISCONNECTED, on_disconnected), new Geary.State.Mapping(State.LOGOUT, Event.RECV_STATUS, on_logging_out_recv_status), new Geary.State.Mapping(State.LOGOUT, Event.RECV_COMPLETION, on_logging_out_recv_completion), new Geary.State.Mapping(State.LOGOUT, Event.RECV_ERROR, on_recv_error), new Geary.State.Mapping(State.LOGOUT, Event.SEND_ERROR, on_send_error), new Geary.State.Mapping(State.CLOSED, Event.CONNECT, on_late_command), + new Geary.State.Mapping(State.CLOSED, Event.DISCONNECT, Geary.State.nop), new Geary.State.Mapping(State.CLOSED, Event.LOGIN, on_late_command), new Geary.State.Mapping(State.CLOSED, Event.SEND_CMD, on_late_command), new Geary.State.Mapping(State.CLOSED, Event.SELECT, on_late_command), new Geary.State.Mapping(State.CLOSED, Event.CLOSE_MAILBOX, on_late_command), new Geary.State.Mapping(State.CLOSED, Event.LOGOUT, on_late_command), - new Geary.State.Mapping(State.CLOSED, Event.DISCONNECT, Geary.State.nop), - new Geary.State.Mapping(State.CLOSED, Event.DISCONNECTED, on_disconnected), new Geary.State.Mapping(State.CLOSED, Event.RECV_STATUS, on_dropped_response), new Geary.State.Mapping(State.CLOSED, Event.RECV_COMPLETION, on_dropped_response), new Geary.State.Mapping(State.CLOSED, Event.SEND_ERROR, Geary.State.nop), @@ -495,12 +483,63 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { } } - public MailboxSpecifier? get_current_mailbox() { - return current_mailbox; + /** + * Returns the list of known personal mailbox namespaces. + * + * A personal namespace is a common prefix of a set of mailboxes + * that belong to the currently authenticated account. It may + * contain the account's Inbox and Sent mailboxes, for example. + * + * The list will be empty when the session is not in the + * authenticated or selected states, it will contain at least one + * after having successfully logged in. + * + * See [[https://tools.ietf.org/html/rfc2342|RFC 2342]] for more + * information. + */ + public Gee.List get_personal_namespaces() { + return this.personal_namespaces.read_only_view; } - public bool is_current_mailbox_readonly() { - return current_mailbox_readonly; + /** + * Returns the list of known shared mailbox namespaces. + * + * A shared namespace is a common prefix of a set of mailboxes + * that are normally accessible by multiple accounts on the + * server, for example shared email mailboxes and NNTP news + * mailboxes. + * + * The list will be empty when the session is not in the + * authenticated or selected states, it will only be non-empty + * after having successfully logged in and if the server supports + * shared mailboxes. + * + * See [[https://tools.ietf.org/html/rfc2342|RFC 2342]] for more + * information. + */ + public Gee.List get_shared_namespaces() { + return this.shared_namespaces.read_only_view; + } + + /** + * Returns the list of known other-users mailbox namespaces. + * + * An other-user namespace is a common prefix of a set of + * mailboxes that are the personal mailboxes of other accounts on + * the server. These would not normally be accessible to the + * currently authenticated account unless the account has + * administration privileges. + * + * The list will be empty when the session is not in the + * authenticated or selected states, it will only be non-empty + * after having successfully logged in and if the server or + * account supports accessing other account's mailboxes. + * + * See [[https://tools.ietf.org/html/rfc2342|RFC 2342]] for more + * information. + */ + public Gee.List get_other_users_namespaces() { + return this.user_namespaces.read_only_view; } /** @@ -584,9 +623,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { * Returns the current {@link ProtocolState} of the {@link ClientSession} and, if selected, * the current mailbox. */ - public ProtocolState get_protocol_state(out MailboxSpecifier? current_mailbox) { - current_mailbox = null; - + public ProtocolState get_protocol_state() { switch (fsm.get_state()) { case State.NOT_CONNECTED: case State.LOGOUT: @@ -600,8 +637,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { return ProtocolState.AUTHORIZED; case State.SELECTED: - current_mailbox = this.current_mailbox; - return ProtocolState.SELECTED; case State.CONNECTING: @@ -659,20 +694,19 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { /** * Connect to the server. * - * This performs no transaction or session initiation with the server. See {@link login_async} - * and {@link initiate_session_async} for next steps. + * This performs no transaction or session initiation with the + * server. See {@link login_async} and {@link + * initiate_session_async} for next steps. * - * The signals {@link connected} or {@link session_denied} will be fired in the context of this - * call, depending on the results of the connection greeting from the server. However, - * command should only be transmitted (login, initiate session, etc.) after this call has - * completed. - * - * If the connection fails (if this call throws an Error) the ClientSession will be disconnected, - * even if the error was from the server (that is, not a network problem). The - * {@link ClientSession} should be discarded. + * If the connection fails (if this call throws an Error) the + * ClientSession will be disconnected, even if the error was from + * the server (that is, not a network problem). The {@link + * ClientSession} should be discarded. */ - public async void connect_async(GLib.Cancellable? cancellable) - throws GLib.Error { + public async void connect_async( + uint greeting_timeout_sec = DEFAULT_GREETING_TIMEOUT_SEC, + GLib.Cancellable? cancellable = null + ) throws GLib.Error { MachineParams params = new MachineParams(null); fsm.issue(Event.CONNECT, null, params); @@ -688,17 +722,20 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { // connect and let ClientConnection's signals drive the show try { yield cx.connect_async(cancellable); - } catch (Error err) { + fsm.issue(Event.CONNECTED); + } catch (GLib.Error err) { fsm.issue(Event.SEND_ERROR, null, null, err); - throw err; } // set up timer to wait for greeting from server - Scheduler.Scheduled timeout = Scheduler.after_sec(GREETING_TIMEOUT_SEC, on_greeting_timeout); + Scheduler.Scheduled timeout = Scheduler.after_sec( + greeting_timeout_sec, on_greeting_timeout + ); - // wait for the initial greeting or a timeout ... this prevents the caller from turning - // around and issuing a command while still in CONNECTING state + // wait for the initial greeting or a timeout ... this + // prevents the caller from turning around and issuing a + // command while still in CONNECTING state try { yield connect_waiter.wait_async(cancellable); } catch (GLib.IOError.CANCELLED err) { @@ -739,8 +776,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { assert(cx == null); cx = new ClientConnection(imap_endpoint); cx.set_logging_parent(this); - cx.connected.connect(on_network_connected); - cx.disconnected.connect(on_network_disconnected); cx.sent_command.connect(on_network_sent_command); cx.send_failure.connect(on_network_send_error); cx.received_status_response.connect(on_received_status_response); @@ -748,9 +783,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { cx.received_continuation_response.connect(on_received_continuation_response); cx.received_bytes.connect(on_received_bytes); cx.received_bad_response.connect(on_received_bad_response); - cx.received_eos.connect(on_received_eos); cx.receive_failure.connect(on_network_receive_failure); - cx.deserialize_failure.connect(on_network_receive_failure); assert(connect_waiter == null); connect_waiter = new Nonblocking.Semaphore(); @@ -760,14 +793,10 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { return State.CONNECTING; } - // this is used internally to tear-down the ClientConnection object and unhook it from - // ClientSession private void drop_connection() { unschedule_keepalive(); if (cx != null) { - cx.connected.disconnect(on_network_connected); - cx.disconnected.disconnect(on_network_disconnected); cx.sent_command.disconnect(on_network_sent_command); cx.send_failure.disconnect(on_network_send_error); cx.received_status_response.disconnect(on_received_status_response); @@ -775,9 +804,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { cx.received_continuation_response.disconnect(on_received_continuation_response); cx.received_bytes.disconnect(on_received_bytes); cx.received_bad_response.disconnect(on_received_bad_response); - cx.received_eos.connect(on_received_eos); cx.receive_failure.disconnect(on_network_receive_failure); - cx.deserialize_failure.disconnect(on_network_receive_failure); cx = null; } @@ -791,19 +818,30 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { return state; } - private uint on_disconnected(uint state, - uint event, - void *user = null, - GLib.Object? obj = null, - GLib.Error? err = null) { + private uint on_disconnect(uint state, + uint event, + void *user = null, + GLib.Object? object = null, + GLib.Error? err = null) { debug("Disconnected from %s", this.imap_endpoint.to_string()); + MachineParams params = (MachineParams) object; + params.proceed = true; return State.CLOSED; } private uint on_connecting_recv_status(uint state, uint event, void *user, Object? object) { StatusResponse status_response = (StatusResponse) object; - // see on_connected() why signals and semaphore are delayed for this event + uint new_state = State.NOAUTH; + if (status_response.status != Status.OK) { + // Don't need to manually disconnect here, by setting + // connect_err here that will be done in connect_async + this.connect_err = new ImapError.UNAVAILABLE( + "Session denied: %s", status_response.get_text() + ); + new_state = State.LOGOUT; + } + try { connect_waiter.notify(); } catch (Error err) { @@ -813,25 +851,16 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { ); } - if (status_response.status == Status.OK) { - fsm.do_post_transition(() => { connected(); }); - - return State.NOAUTH; - } - - fsm.do_post_transition(() => { session_denied(status_response.get_text()); }); - - // Don't need to manually disconnect here, by setting - // connect_err here that will be done in connect_async - this.connect_err = new ImapError.UNAVAILABLE( - "Session denied: %s", status_response.get_text() - ); - - return State.LOGOUT; + return new_state; } private uint on_connecting_timeout(uint state, uint event) { - // wake up the waiting task in connect_async + // Don't need to manually disconnect here, by setting + // connect_err here that will be done in connect_async + this.connect_err = new GLib.IOError.TIMED_OUT( + "Session greeting not sent" + ); + try { connect_waiter.notify(); } catch (Error err) { @@ -840,13 +869,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { ); } - // Don't need to manually disconnect here, by setting - // connect_err here that will be done in connect_async - this.connect_err = new IOError.TIMED_OUT( - "Session greeting not seen in %u seconds", - GREETING_TIMEOUT_SEC - ); - return State.LOGOUT; } @@ -949,14 +971,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { GLib.Cancellable? cancellable) throws GLib.Error { // If no capabilities available, get them now - if (capabilities.is_empty()) + if (this.capabilities.is_empty()) { yield send_command_async(new CapabilityCommand(), cancellable); + } - // store them for comparison later - Imap.Capabilities caps = capabilities; + var last_capabilities = this.capabilities.revision; if (imap_endpoint.tls_method == TlsNegotiationMethod.START_TLS) { - if (!caps.has_capability(Capabilities.STARTTLS)) { + if (!this.capabilities.has_capability(Capabilities.STARTTLS)) { throw new ImapError.NOT_SUPPORTED( "STARTTLS unavailable for %s", to_string()); } @@ -977,51 +999,54 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { resp.status.to_string() ); } + + if (last_capabilities == capabilities.revision) { + // Per RFC3501 §6.2.1, after TLS is established, all + // capabilities must be cleared and re-acquired to + // mitigate main-in-the-middle attacks. If the TLS + // command response did not update capabilities, + // explicitly do so now. + yield send_command_async(new CapabilityCommand(), cancellable); + last_capabilities = this.capabilities.revision; + } } // Login after STARTTLS yield login_async(credentials, cancellable); // if new capabilities not offered after login, get them now - if (caps.revision == capabilities.revision) { + if (last_capabilities == capabilities.revision) { yield send_command_async(new CapabilityCommand(), cancellable); } - // either way, new capabilities should be available - caps = capabilities; - - Gee.List server_data = new Gee.ArrayList(); - ulong data_id = this.server_data_received.connect((data) => { server_data.add(data); }); + var list_results = new Gee.ArrayList(); + ulong list_id = this.list.connect( + (mailbox) => { list_results.add(mailbox); } + ); try { // Determine what this connection calls the inbox Imap.StatusResponse response = yield send_command_async( new ListCommand(MailboxSpecifier.inbox, false, null), cancellable ); - if (response.status == Status.OK && !server_data.is_empty) { - this.inbox = server_data[0].get_list(); + if (response.status == Status.OK && !list_results.is_empty) { + this.inbox = list_results[0]; + list_results.clear(); debug("Using INBOX: %s", this.inbox.to_string()); } else { throw new ImapError.INVALID("Unable to find INBOX"); } // Try to determine what the connection's namespaces are - server_data.clear(); - if (caps.has_capability(Capabilities.NAMESPACE)) { + if (this.capabilities.has_capability(Capabilities.NAMESPACE)) { response = yield send_command_async( new NamespaceCommand(), cancellable ); - if (response.status == Status.OK && !server_data.is_empty) { - NamespaceResponse ns = server_data[0].get_namespace(); - update_namespaces(ns.personal, this.personal_namespaces); - update_namespaces(ns.user, this.user_namespaces); - update_namespaces(ns.shared, this.shared_namespaces); - } else { + if (response.status != Status.OK) { warning("NAMESPACE command failed"); } } - server_data.clear(); if (!this.personal_namespaces.is_empty) { debug( "Default personal namespace: %s", @@ -1045,8 +1070,8 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { new ListCommand(new MailboxSpecifier(prefix), false, null), cancellable ); - if (response.status == Status.OK && !server_data.is_empty) { - MailboxInformation list = server_data[0].get_list(); + if (response.status == Status.OK && !list_results.is_empty) { + MailboxInformation list = list_results[0]; delim = list.delim; } else { throw new ImapError.INVALID("Unable to determine personal namespace delimiter"); @@ -1058,21 +1083,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { this.personal_namespaces[0].to_string()); } } finally { - disconnect(data_id); - } - } - - private inline void update_namespaces(Gee.List? response, Gee.List list) { - if (response != null) { - foreach (Namespace ns in response) { - list.add(ns); - string prefix = ns.prefix; - string? delim = ns.delim; - if (delim != null && prefix.has_suffix(delim)) { - prefix = prefix.substring(0, prefix.length - delim.length); - } - this.namespaces.set(prefix, ns); - } + disconnect(list_id); } } @@ -1099,19 +1110,12 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { if (!validate_state_change_cmd(completion_response)) return state; - // Remember: only you can prevent firing signals inside state transition handlers - switch (completion_response.status) { - case Status.OK: - fsm.do_post_transition(() => { authorized(); }); - - return State.AUTHORIZED; - - default: - debug("LOGIN failed: %s", completion_response.to_string()); - fsm.do_post_transition((resp) => { login_failed((StatusResponse)resp); }, completion_response); - - return State.NOAUTH; + uint new_state = State.AUTHORIZED; + if (completion_response.status != OK) { + debug("LOGIN failed: %s", completion_response.to_string()); + new_state = State.NOAUTH; } + return new_state; } // @@ -1120,18 +1124,24 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { // /** - * If seconds is negative or zero, keepalives will be disabled. (This is not recommended.) + * Enables sending keep-alive commands for the sesion. * - * 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. + * 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. + * + * If values are negative or zero, keepalives will be disabled. + * (This is not recommended.) */ public void enable_keepalives(uint seconds_while_selected, - uint seconds_while_unselected, uint seconds_while_selected_with_idle) { - selected_keepalive_secs = seconds_while_selected; - selected_with_idle_keepalive_secs = seconds_while_selected_with_idle; - unselected_keepalive_secs = seconds_while_unselected; + uint seconds_while_unselected, + uint seconds_while_selected_with_idle) { + this.selected_keepalive_secs = seconds_while_selected; + this.selected_with_idle_keepalive_secs = seconds_while_selected_with_idle; + this.unselected_keepalive_secs = seconds_while_unselected; - // schedule one now, although will be rescheduled if traffic is received before it fires + // schedule one now, although will be rescheduled if traffic + // is received before it fires schedule_keepalive(); } @@ -1164,7 +1174,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { public void enable_idle() throws GLib.Error { if (this.is_idle_supported) { - switch (get_protocol_state(null)) { + switch (get_protocol_state()) { case ProtocolState.AUTHORIZING: case ProtocolState.AUTHORIZED: case ProtocolState.SELECTED: @@ -1185,7 +1195,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { unschedule_keepalive(); uint seconds; - switch (get_protocol_state(null)) { + switch (get_protocol_state()) { case ProtocolState.NOT_CONNECTED: case ProtocolState.CONNECTING: return; @@ -1341,8 +1351,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { status_response.to_string()); // nothing more we can do; drop connection and report disconnect to user - cx.disconnect_async.begin(null, on_bye_disconnect_completed); - + this.do_disconnect.begin(DisconnectReason.REMOTE_CLOSE); state = State.CLOSED; break; @@ -1355,10 +1364,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { return state; } - private void on_bye_disconnect_completed(Object? source, AsyncResult result) { - dispatch_disconnect_results(DisconnectReason.REMOTE_CLOSE, result); - } - // // select/examine // @@ -1415,45 +1420,34 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { } private uint on_selecting_recv_completion(uint state, uint event, void *user, Object? object) { + uint new_state = state; StatusResponse completion_response = (StatusResponse) object; - Command? cmd; - if (!validate_state_change_cmd(completion_response, out cmd)) - return state; - - // get the mailbox from the command - MailboxSpecifier? mailbox = null; - if (cmd is SelectCommand) { - mailbox = ((SelectCommand) cmd).mailbox; - current_mailbox_readonly = false; - } else if (cmd is ExamineCommand) { - mailbox = ((ExamineCommand) cmd).mailbox; - current_mailbox_readonly = true; - } - - // should only get to this point if cmd was SELECT or EXAMINE - assert(mailbox != null); - - switch (completion_response.status) { + Command? cmd = null; + if (validate_state_change_cmd(completion_response, out cmd)) { + switch (completion_response.status) { case Status.OK: - // mailbox is SELECTED/EXAMINED, report change after completion of transition - MailboxSpecifier? old_mailbox = current_mailbox; - current_mailbox = mailbox; - - if (old_mailbox != current_mailbox) - fsm.do_post_transition(notify_select_completed, null, old_mailbox); - - return State.SELECTED; + if (cmd is SelectCommand) { + this.selected_mailbox = ((SelectCommand) cmd).mailbox; + this.selected_readonly = false; + } else if (cmd is ExamineCommand) { + this.selected_mailbox = ((ExamineCommand) cmd).mailbox; + this.selected_readonly = true; + } + new_state = State.SELECTED; + break; default: + this.selected_mailbox = null; + this.selected_readonly = false; + new_state = State.AUTHORIZED; warning("SELECT/EXAMINE failed: %s", completion_response.to_string()); - return State.AUTHORIZED; + break; + } } - } - private void notify_select_completed(void *user, Object? object) { - current_mailbox_changed((MailboxSpecifier) object, current_mailbox, current_mailbox_readonly); + return new_state; } // @@ -1494,29 +1488,28 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { switch (completion_response.status) { case Status.OK: - MailboxSpecifier? old_mailbox = current_mailbox; - current_mailbox = null; - - if (old_mailbox != null) - fsm.do_post_transition(notify_mailbox_closed, null, old_mailbox); - + this.selected_mailbox = null; + this.selected_readonly = false; return State.AUTHORIZED; default: warning("CLOSE failed: %s", completion_response.to_string()); - return State.SELECTED; } } - private void notify_mailbox_closed(void *user, Object? object) { - current_mailbox_changed((MailboxSpecifier) object, null, false); - } - // // logout // + /** + * Sends a logout command. + * + * If the connection is still available and the server still + * responding, this will result in the connection being closed + * gracefully. Thus unless an error occurs, {@link + * disconnect_async} would not need to be called afterwards. + */ public async void logout_async(GLib.Cancellable? cancellable) throws GLib.Error { LogoutCommand cmd = new LogoutCommand(); @@ -1529,14 +1522,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { if (params.proceed) { yield command_transaction_async(cmd, cancellable); - logged_out(); - this.cx.disconnect_async.begin( - cancellable, (obj, res) => { - dispatch_disconnect_results( - DisconnectReason.LOCAL_CLOSE, res - ); - } - ); + yield do_disconnect(DisconnectReason.LOCAL_CLOSE); } } @@ -1581,10 +1567,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { private uint on_logging_out_recv_completion(uint state, uint event, void *user, Object? object) { StatusResponse completion_response = (StatusResponse) object; - if (!validate_state_change_cmd(completion_response)) - return state; + uint new_state = state; + if (validate_state_change_cmd(completion_response)) { + new_state = State.CLOSED; - return State.CLOSED; + // Namespaces are only valid in AUTH and SELECTED states + clear_namespaces(); + } + return new_state; } // @@ -1626,17 +1616,17 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { /** {@inheritDoc} */ public Logging.State to_logging_state() { - return (this.current_mailbox == null) + return (this.selected_mailbox == null) ? new Logging.State( this, this.fsm.get_state_string(fsm.get_state()) ) : new Logging.State( this, - "%s:%s %s", + "%s:%s selected %s", this.fsm.get_state_string(fsm.get_state()), - this.current_mailbox.to_string(), - this.current_mailbox_readonly ? "RO" : "RW" + this.selected_mailbox.to_string(), + this.selected_readonly ? "RO" : "RW" ); } @@ -1645,12 +1635,15 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { this._logging_parent = parent; } - private uint on_disconnect(uint state, uint event, void *user, Object? object) { - MachineParams params = (MachineParams) object; + private async void do_disconnect(DisconnectReason reason) { + try { + yield this.cx.disconnect_async(); + } catch (GLib.Error err) { + debug("IMAP disconnect failed: %s", err.message); + } - params.proceed = true; - - return State.CLOSED; + drop_connection(); + disconnected(reason); } // @@ -1667,56 +1660,37 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { void *user, GLib.Object? object, GLib.Error? err) { - debug("Connecting send/recv error, dropping client connection: %s", - err != null ? err.message : "EOS"); + debug( + "Connecting send/recv error, dropping client connection: %s", + err != null ? err.message : "(no error)" + ); fsm.do_post_transition(() => { drop_connection(); }); return State.CLOSED; } private uint on_send_error(uint state, uint event, void *user, Object? object, Error? err) { - assert(err != null); - - if (err is IOError.CANCELLED) + if (err is IOError.CANCELLED) { return state; + } debug("Send error, disconnecting: %s", err.message); - - cx.disconnect_async.begin(null, on_fire_send_error_signal); - + this.do_disconnect.begin(DisconnectReason.LOCAL_ERROR); return State.CLOSED; } - private void on_fire_send_error_signal(Object? object, AsyncResult result) { - dispatch_disconnect_results(DisconnectReason.LOCAL_ERROR, result); - } - private uint on_recv_error(uint state, uint event, void *user, GLib.Object? object, GLib.Error? err) { - debug("Receive error, disconnecting: %s", - (err != null) ? err.message : "EOS" + debug( + "Receive error, disconnecting: %s", + (err != null) ? err.message : "(no error)" ); - cx.disconnect_async.begin(null, on_fire_recv_error_signal); + this.do_disconnect.begin(DisconnectReason.REMOTE_ERROR); return State.CLOSED; } - private void on_fire_recv_error_signal(Object? object, AsyncResult result) { - dispatch_disconnect_results(DisconnectReason.REMOTE_ERROR, result); - } - - private void dispatch_disconnect_results(DisconnectReason reason, AsyncResult result) { - try { - cx.disconnect_async.end(result); - } catch (Error err) { - debug("Send/recv disconnect failed: %s", err.message); - } - - drop_connection(); - disconnected(reason); - } - // 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) { @@ -1807,14 +1781,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { // network connection event handlers // - private void on_network_connected() { - fsm.issue(Event.CONNECTED); - } - - private void on_network_disconnected() { - fsm.issue(Event.DISCONNECTED); - } - private void on_network_sent_command(Command cmd) { // resechedule keepalive schedule_keepalive(); @@ -1844,12 +1810,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { if (response_code != null) { try { if (response_code.get_response_code_type().is_value(ResponseCodeType.CAPABILITY)) { - capabilities = response_code.get_capabilities(ref next_capabilities_revision); - debug("%s %s", - status_response.status.to_string(), - capabilities.to_string()); - - capability(capabilities); + this.capabilities = response_code.get_capabilities( + this.capabilities.revision + 1 + ); + debug( + "%s set capabilities to: %s", + status_response.status.to_string(), + this.capabilities.to_string() + ); } } catch (GLib.Error err) { warning( @@ -1876,12 +1844,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { case ServerDataType.CAPABILITY: // update ClientSession capabilities before firing signal, so external signal // handlers that refer back to property aren't surprised - capabilities = server_data.get_capabilities(ref next_capabilities_revision); - debug("%s %s", - server_data.server_data_type.to_string(), - capabilities.to_string()); - - capability(capabilities); + this.capabilities = server_data.get_capabilities( + this.capabilities.revision + 1 + ); + debug( + "%s set capabilities to: %s", + server_data.server_data_type.to_string(), + this.capabilities.to_string() + ); break; case ServerDataType.EXISTS: @@ -1918,7 +1888,14 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { break; case ServerDataType.NAMESPACE: - namespace(server_data.get_namespace()); + // Clear namespaces before updating them, since if + // they have changed the new ones take full + // precedence. + clear_namespaces(); + NamespaceResponse ns = server_data.get_namespace(); + update_namespaces(ns.personal, this.personal_namespaces); + update_namespaces(ns.shared, this.shared_namespaces); + update_namespaces(ns.user, this.user_namespaces); break; // TODO: LSUB @@ -1929,8 +1906,28 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { server_data.to_string()); break; } + } - server_data_received(server_data); + private void clear_namespaces() { + this.namespaces.clear(); + this.personal_namespaces.clear(); + this.shared_namespaces.clear(); + this.user_namespaces.clear(); + } + + private void update_namespaces(Gee.List? response, + Gee.List list) { + if (response != null) { + foreach (Namespace ns in response) { + list.add(ns); + string prefix = ns.prefix; + string? delim = ns.delim; + if (delim != null && prefix.has_suffix(delim)) { + prefix = prefix.substring(0, prefix.length - delim.length); + } + this.namespaces.set(prefix, ns); + } + } } private void on_received_server_data(ServerData server_data) { @@ -1968,11 +1965,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source { fsm.issue(Event.RECV_ERROR, null, null, err); } - private void on_received_eos(ClientConnection cx) { - fsm.issue(Event.RECV_ERROR, null, null, null); - } - - private void on_network_receive_failure(Error err) { + private void on_network_receive_failure(GLib.Error err) { fsm.issue(Event.RECV_ERROR, null, null, err); } diff --git a/src/engine/imap/transport/imap-deserializer.vala b/src/engine/imap/transport/imap-deserializer.vala index 6959b09d..2ad9120b 100644 --- a/src/engine/imap/transport/imap-deserializer.vala +++ b/src/engine/imap/transport/imap-deserializer.vala @@ -1,7 +1,9 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2019 Michael Gratton * * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. + * (version 2.1 or later). See the COPYING file in this distribution. */ /** @@ -72,7 +74,7 @@ public class Geary.Imap.Deserializer : BaseObject { state_to_string, event_to_string); private string identifier; - private DataInputStream dins; + private DataInputStream input; private Geary.State.Machine fsm; private ListParameter context; @@ -81,7 +83,6 @@ public class Geary.Imap.Deserializer : BaseObject { private Cancellable? cancellable = null; private Nonblocking.Semaphore closed_semaphore = new Nonblocking.Semaphore(); - private Geary.Stream.MidstreamConverter midstream = new Geary.Stream.MidstreamConverter("Deserializer"); private StringBuilder? current_string = null; private size_t literal_length_remaining = 0; private Geary.Memory.GrowableBuffer? block_buffer = null; @@ -116,37 +117,37 @@ public class Geary.Imap.Deserializer : BaseObject { */ public signal void bytes_received(size_t bytes); - /** - * Fired when the underlying InputStream is closed, whether due to normal EOS or input error. - * - * @see receive_failure - */ - public signal void eos(); - /** * Fired when a syntax error has occurred. * - * This generally means the data looks like garbage and further deserialization is unlikely - * or impossible. + * This generally means the data looks like garbage and further + * deserialization is unlikely or impossible. */ public signal void deserialize_failure(); /** * Fired when an Error is trapped on the input stream. * - * This is nonrecoverable and means the stream should be closed and this Deserializer destroyed. + * This is nonrecoverable and means the stream should be closed + * and this Deserializer destroyed. */ - public signal void receive_failure(Error err); + public signal void receive_failure(GLib.Error err); + + /** + * Fired when the underlying InputStream is closed. + * + * This is nonrecoverable and means the stream should be closed + * and this Deserializer destroyed. + */ + public signal void end_of_stream(); - public Deserializer(string identifier, InputStream ins) { + public Deserializer(string identifier, GLib.InputStream input) { this.identifier = identifier; - ConverterInputStream cins = new ConverterInputStream(ins, midstream); - cins.set_close_base_stream(false); - dins = new DataInputStream(cins); - dins.set_newline_type(DataStreamNewlineType.CR_LF); - dins.set_close_base_stream(false); + this.input = new GLib.DataInputStream(input); + this.input.set_close_base_stream(false); + this.input.set_newline_type(CR_LF); Geary.State.Mapping[] mappings = { new Geary.State.Mapping(State.TAG, Event.CHAR, on_tag_char), @@ -210,15 +211,6 @@ public class Geary.Imap.Deserializer : BaseObject { reset_params(); } - /** - * Install a custom Converter into the input stream. - * - * Can be used for decompression, decryption, and so on. - */ - public bool install_converter(Converter converter) { - return midstream.install(converter); - } - /** * Begin deserializing IMAP responses from the input stream. * @@ -252,6 +244,7 @@ public class Geary.Imap.Deserializer : BaseObject { // wait for outstanding I/O to exit yield closed_semaphore.wait_async(); + yield this.input.close_async(GLib.Priority.DEFAULT, null); Logging.debug(Logging.Flag.DESERIALIZER, "[%s] Deserializer closed", to_string()); } @@ -279,7 +272,9 @@ public class Geary.Imap.Deserializer : BaseObject { private void next_deserialize_step() { switch (get_mode()) { case Mode.LINE: - dins.read_line_async.begin(ins_priority, cancellable, on_read_line); + this.input.read_line_async.begin( + ins_priority, cancellable, on_read_line + ); break; case Mode.BLOCK: @@ -293,7 +288,9 @@ public class Geary.Imap.Deserializer : BaseObject { current_buffer = block_buffer.allocate( size_t.min(MAX_BLOCK_READ_SIZE, literal_length_remaining)); - dins.read_async.begin(current_buffer, ins_priority, cancellable, on_read_block); + this.input.read_async.begin( + current_buffer, ins_priority, cancellable, on_read_block + ); break; case Mode.FAILED: @@ -309,7 +306,9 @@ public class Geary.Imap.Deserializer : BaseObject { private void on_read_line(Object? source, AsyncResult result) { try { size_t bytes_read; - string? line = dins.read_line_async.end(result, out bytes_read); + string? line = this.input.read_line_async.end( + result, out bytes_read + ); if (line == null) { Logging.debug(Logging.Flag.DESERIALIZER, "[%s] line EOS", to_string()); @@ -333,7 +332,7 @@ public class Geary.Imap.Deserializer : BaseObject { try { // Zero-byte literals are legal (see note in next_deserialize_step()), so EOS only // happens when actually pulling data - size_t bytes_read = dins.read_async.end(result); + size_t bytes_read = this.input.read_async.end(result); if (bytes_read == 0 && literal_length_remaining > 0) { Logging.debug(Logging.Flag.DESERIALIZER, "[%s] block EOS", to_string()); @@ -816,8 +815,8 @@ public class Geary.Imap.Deserializer : BaseObject { flush_params(); // always signal as closed and notify subscribers - closed_semaphore.blind_notify(); - eos(); + this.closed_semaphore.blind_notify(); + end_of_stream(); return State.CLOSED; } @@ -833,9 +832,7 @@ public class Geary.Imap.Deserializer : BaseObject { } // always signal as closed and notify - closed_semaphore.blind_notify(); - eos(); - + this.closed_semaphore.blind_notify(); return State.CLOSED; } diff --git a/src/engine/imap/transport/imap-serializer.vala b/src/engine/imap/transport/imap-serializer.vala index 549391fc..bbe52e2a 100644 --- a/src/engine/imap/transport/imap-serializer.vala +++ b/src/engine/imap/transport/imap-serializer.vala @@ -1,32 +1,35 @@ /* - * Copyright 2016 Software Freedom Conservancy Inc. - * Copyright 2018 Michael Gratton + * Copyright © 2016 Software Freedom Conservancy Inc. + * Copyright © 2018, 2020 Michael Gratton * * This software is licensed under the GNU Lesser General Public License * (version 2.1 or later). See the COPYING file in this distribution. */ /** - * Writes IMAP protocol strings to a supplied output stream. + * Writes IMAP protocol strings to the supplied output stream. * - * This class uses a {@link GLib.DataOutputStream} for writing strings - * to the given stream. Since that does not support asynchronous - * writes, it is highly desirable that the stream passed to this class - * is a {@link GLib.BufferedOutputStream}, or some other type that - * uses a memory buffer large enough to write a typical command - * completely without causing disk or network I/O. + * Since most IMAP commands are small (with the exception of literal + * data) this class writes directly, synchronously to the given + * stream. Thus it is highly desirable that the stream passed to the + * constructor is buffered, either a {@link + * GLib.BufferedOutputStream}, or some other type that uses a memory + * buffer large enough to write a typical command completely without + * causing disk or network I/O. * * @see Deserializer */ public class Geary.Imap.Serializer : BaseObject { - private string identifier; - private GLib.DataOutputStream output; - public Serializer(string identifier, GLib.OutputStream output) { - this.identifier = identifier; - this.output = new GLib.DataOutputStream(output); - this.output.set_close_base_stream(false); + private const string EOL = "\r\n"; + private const string SPACE = " "; + + private GLib.OutputStream output; + + + public Serializer(GLib.OutputStream output) { + this.output = output; } /** @@ -39,7 +42,7 @@ public class Geary.Imap.Serializer : BaseObject { public void push_unquoted_string(string str, GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_string(str, cancellable); + this.output.write_all(str.data, null, cancellable); } /** @@ -52,17 +55,19 @@ public class Geary.Imap.Serializer : BaseObject { public void push_quoted_string(string str, GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_byte('"'); + StringBuilder buf = new StringBuilder.sized(str.length + 2); + buf.append_c('"'); int index = 0; char ch = str[index]; while (ch != String.EOS) { if (ch == '"' || ch == '\\') { - this.output.put_byte('\\'); + buf.append_c('\\'); } - this.output.put_byte(ch); + buf.append_c(ch); ch = str[++index]; } - this.output.put_byte('"'); + buf.append_c('"'); + this.output.write_all(buf.data, null, cancellable); } /** @@ -73,15 +78,17 @@ public class Geary.Imap.Serializer : BaseObject { */ public void push_ascii(char ch, GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_byte(ch, cancellable); + // allocate array on the stack to avoid mem alloc overhead + uint8 buf[1] = { ch }; + this.output.write_all(buf, null, cancellable); } /** - * Writes a single ASCII space character. + * Writes a ASCII space character. */ public void push_space(GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_byte(' ', cancellable); + this.output.write_all(SPACE.data, null, cancellable); } /** @@ -89,7 +96,7 @@ public class Geary.Imap.Serializer : BaseObject { */ public void push_nil(GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_string(NilParameter.VALUE, cancellable); + this.output.write_all(NilParameter.VALUE.data, null, cancellable); } /** @@ -97,7 +104,7 @@ public class Geary.Imap.Serializer : BaseObject { */ public void push_eol(GLib.Cancellable? cancellable = null) throws GLib.Error { - this.output.put_string("\r\n", cancellable); + this.output.write_all(EOL.data, null, cancellable); } /** @@ -121,14 +128,15 @@ public class Geary.Imap.Serializer : BaseObject { */ public async void flush_stream(GLib.Cancellable? cancellable = null) throws GLib.Error { - yield this.output.flush_async(Priority.DEFAULT, cancellable); + yield this.output.flush_async(GLib.Priority.DEFAULT, cancellable); } /** - * Returns a string representation for debugging. + * Closes the stream, ensuring a command has been sent. */ - public string to_string() { - return "ser:%s".printf(identifier); + public async void close_stream(GLib.Cancellable? cancellable) + throws GLib.IOError { + yield this.output.close_async(GLib.Priority.DEFAULT, cancellable); } } diff --git a/src/engine/meson.build b/src/engine/meson.build index 23b5fa48..0d6cbe71 100644 --- a/src/engine/meson.build +++ b/src/engine/meson.build @@ -87,6 +87,7 @@ geary_engine_vala_sources = files( 'imap/imap.vala', 'imap/imap-error.vala', 'imap/api/imap-account-session.vala', + 'imap/api/imap-capabilities.vala', 'imap/api/imap-client-service.vala', 'imap/api/imap-email-flags.vala', 'imap/api/imap-email-properties.vala', @@ -150,7 +151,6 @@ geary_engine_vala_sources = files( 'imap/parameter/imap-root-parameters.vala', 'imap/parameter/imap-string-parameter.vala', 'imap/parameter/imap-unquoted-string-parameter.vala', - 'imap/response/imap-capabilities.vala', 'imap/response/imap-continuation-response.vala', 'imap/response/imap-fetch-data-decoder.vala', 'imap/response/imap-fetched-data.vala', diff --git a/src/engine/util/util-generic-capabilities.vala b/src/engine/util/util-generic-capabilities.vala index b4b48ae4..a312786b 100644 --- a/src/engine/util/util-generic-capabilities.vala +++ b/src/engine/util/util-generic-capabilities.vala @@ -32,39 +32,6 @@ public class Geary.GenericCapabilities : BaseObject { return (map.size == 0); } - public bool parse_and_add_capability(string text) { - string[] name_values = text.split(name_separator, 2); - switch (name_values.length) { - case 1: - add_capability(name_values[0]); - break; - - case 2: - if (value_separator == null) { - add_capability(name_values[0], name_values[1]); - } else { - // break up second token for multiple values - string[] values = name_values[1].split(value_separator); - if (values.length <= 1) { - add_capability(name_values[0], name_values[1]); - } else { - foreach (string value in values) - add_capability(name_values[0], value); - } - } - break; - - default: - return false; - } - - return true; - } - - public void add_capability(string name, string? setting = null) { - map.set(name, String.is_empty(setting) ? null : setting); - } - /** * Returns true only if the capability was named as available by the server. */ @@ -103,13 +70,6 @@ public class Geary.GenericCapabilities : BaseObject { return (names.size > 0) ? names : null; } - private void append(StringBuilder builder, string text) { - if (!String.is_empty(builder.str)) - builder.append(String.is_empty(value_separator) ? " " : value_separator); - - builder.append(text); - } - public virtual string to_string() { Gee.Set? names = get_all_names(); if (names == null || names.size == 0) @@ -132,5 +92,45 @@ public class Geary.GenericCapabilities : BaseObject { return builder.str; } -} + private inline void append(StringBuilder builder, string text) { + if (!String.is_empty(builder.str)) + builder.append(String.is_empty(value_separator) ? " " : value_separator); + + builder.append(text); + } + + protected bool parse_and_add_capability(string text) { + string[] name_values = text.split(name_separator, 2); + switch (name_values.length) { + case 1: + add_capability(name_values[0]); + break; + + case 2: + if (value_separator == null) { + add_capability(name_values[0], name_values[1]); + } else { + // break up second token for multiple values + string[] values = name_values[1].split(value_separator); + if (values.length <= 1) { + add_capability(name_values[0], name_values[1]); + } else { + foreach (string value in values) + add_capability(name_values[0], value); + } + } + break; + + default: + return false; + } + + return true; + } + + private inline void add_capability(string name, string? setting = null) { + this.map.set(name, String.is_empty(setting) ? null : setting); + } + +} diff --git a/src/engine/util/util-stream.vala b/src/engine/util/util-stream.vala index 2cffb05a..18df50fb 100644 --- a/src/engine/util/util-stream.vala +++ b/src/engine/util/util-stream.vala @@ -41,80 +41,6 @@ namespace Geary.Stream { yield write_all_async(outs, new Memory.StringBuffer(str), cancellable); } - - public class MidstreamConverter : BaseObject, Converter { - public uint64 total_bytes_read { get; private set; default = 0; } - public uint64 total_bytes_written { get; private set; default = 0; } - public uint64 converted_bytes_read { get; private set; default = 0; } - public uint64 converted_bytes_written { get; private set; default = 0; } - - public bool log_performance { get; set; default = false; } - - private string name; - private Converter? converter = null; - - public MidstreamConverter(string name) { - this.name = name; - } - - public bool install(Converter converter) { - if (this.converter != null) - return false; - - this.converter = converter; - - return true; - } - - public ConverterResult convert(uint8[] inbuf, uint8[] outbuf, ConverterFlags flags, - out size_t bytes_read, out size_t bytes_written) throws Error { - if (converter != null) { - ConverterResult result = converter.convert(inbuf, outbuf, flags, out bytes_read, out bytes_written); - - total_bytes_read += bytes_read; - total_bytes_written += bytes_written; - - converted_bytes_read += bytes_read; - converted_bytes_written += bytes_written; - - if (log_performance && (bytes_read > 0 || bytes_written > 0)) { - double pct = (converted_bytes_read > converted_bytes_written) - ? (double) converted_bytes_written / (double) converted_bytes_read - : (double) converted_bytes_read / (double) converted_bytes_written; - debug("%s read/written: %s/%s (%lld%%)", name, converted_bytes_read.to_string(), - converted_bytes_written.to_string(), (long) (pct * 100.0)); - } - - return result; - } - - // passthrough - size_t copied = size_t.min(inbuf.length, outbuf.length); - if (copied > 0) - GLib.Memory.copy(outbuf, inbuf, copied); - - bytes_read = copied; - bytes_written = copied; - - total_bytes_read += copied; - total_bytes_written += copied; - - if ((flags & ConverterFlags.FLUSH) != 0) - return ConverterResult.FLUSHED; - - if ((flags & ConverterFlags.INPUT_AT_END) != 0) - return ConverterResult.FINISHED; - - return ConverterResult.CONVERTED; - } - - public void reset() { - if (converter != null) - converter.reset(); - } - } - - /** * Adaptor from a GMime stream to a GLib OutputStream. */ diff --git a/test/engine/imap-db/imap-db-account-test.vala b/test/engine/imap-db/imap-db-account-test.vala index 3d1e90ab..61b62506 100644 --- a/test/engine/imap-db/imap-db-account-test.vala +++ b/test/engine/imap-db/imap-db-account-test.vala @@ -84,7 +84,7 @@ class Geary.ImapDB.AccountTest : TestCase { new Imap.UIDValidity(7), 6 //unseen ), - new Imap.Capabilities(1) + new Imap.Capabilities.empty(0) ) ); @@ -123,7 +123,7 @@ class Geary.ImapDB.AccountTest : TestCase { new Imap.UIDValidity(7), 6 //unseen ), - new Imap.Capabilities(1) + new Imap.Capabilities.empty(0) ) ); diff --git a/test/engine/imap/transport/imap-client-connection-test.vala b/test/engine/imap/transport/imap-client-connection-test.vala new file mode 100644 index 00000000..c98f0dc5 --- /dev/null +++ b/test/engine/imap/transport/imap-client-connection-test.vala @@ -0,0 +1,160 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +class Geary.Imap.ClientConnectionTest : TestCase { + + + private class TestCommand : Command { + + public TestCommand() { + base("TEST"); + } + + } + + private TestServer? server = null; + + + public ClientConnectionTest() { + base("Geary.Imap.ClientConnectionTest"); + add_test("connect_disconnect", connect_disconnect); + if (GLib.Test.slow()) { + add_test("idle", idle); + add_test("command_timeout", command_timeout); + } + } + + protected override void set_up() throws GLib.Error { + this.server = new TestServer(); + } + + protected override void tear_down() { + this.server.stop(); + this.server = null; + } + + public void connect_disconnect() throws GLib.Error { + var test_article = new ClientConnection(new_endpoint()); + + test_article.connect_async.begin(null, this.async_complete_full); + test_article.connect_async.end(async_result()); + + assert_non_null(test_article.get_remote_address()); + assert_non_null(test_article.get_local_address()); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + assert_null(test_article.get_remote_address()); + assert_null(test_article.get_local_address()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert(result.succeeded); + } + + public void idle() throws GLib.Error { + this.server.add_script_line(RECEIVE_LINE, "a001 IDLE"); + this.server.add_script_line(SEND_LINE, "+ idling"); + this.server.add_script_line(RECEIVE_LINE, "DONE"); + this.server.add_script_line(SEND_LINE, "a001 OK Completed"); + this.server.add_script_line(RECEIVE_LINE, "a002 TEST"); + this.server.add_script_line(SEND_LINE, "a002 OK Looks good"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + const int COMMAND_TIMEOUT = 1; + const int IDLE_TIMEOUT = 1; + + var test_article = new ClientConnection( + new_endpoint(), COMMAND_TIMEOUT, IDLE_TIMEOUT + ); + test_article.connect_async.begin(null, this.async_complete_full); + test_article.connect_async.end(async_result()); + + assert_false(test_article.is_in_idle(), "Initial idle state"); + test_article.enable_idle_when_quiet(true); + assert_false(test_article.is_in_idle(), "Post-enabled idle state"); + + // Wait for idle to kick in + GLib.Timer timer = new GLib.Timer(); + timer.start(); + while (!test_article.is_in_idle() && + timer.elapsed() < IDLE_TIMEOUT * 2) { + this.main_loop.iteration(false); + } + + assert_true(test_article.is_in_idle(), "Entered idle"); + + // Ensure idle outlives command timeout + timer.start(); + while (timer.elapsed() < COMMAND_TIMEOUT * 2) { + this.main_loop.iteration(false); + } + + assert_true(test_article.is_in_idle(), "Post idle command timeout"); + + var command = new TestCommand(); + test_article.send_command(command); + command.wait_until_complete.begin(null, this.async_complete_full); + command.wait_until_complete.end(async_result()); + + assert_false(test_article.is_in_idle(), "Post test command"); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert(result.succeeded); + } + + public void command_timeout() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 TEST"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + const int TIMEOUT = 2; + + bool sent = false; + bool recv_fail = false; + bool timed_out = false; + + var test_article = new ClientConnection(new_endpoint(), TIMEOUT); + test_article.sent_command.connect(() => { sent = true; }); + test_article.receive_failure.connect(() => { recv_fail = true; }); + test_article.connect_async.begin(null, this.async_complete_full); + test_article.connect_async.end(async_result()); + + var command = new TestCommand(); + command.response_timed_out.connect(() => { timed_out = true; }); + + test_article.send_command(command); + + GLib.Timer timer = new GLib.Timer(); + timer.start(); + while (!timed_out && timer.elapsed() < TIMEOUT * 2) { + this.main_loop.iteration(false); + } + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + assert_true(sent, "connection.sent_command"); + assert_true(recv_fail, "command.receive_failure"); + assert_true(timed_out, "command.response_timed_out"); + + debug("Waiting for server..."); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true(result.succeeded); + } + + protected Endpoint new_endpoint() { + return new Endpoint(this.server.get_client_address(), NONE, 10); + } + +} diff --git a/test/engine/imap/transport/imap-client-session-test.vala b/test/engine/imap/transport/imap-client-session-test.vala new file mode 100644 index 00000000..7ff91357 --- /dev/null +++ b/test/engine/imap/transport/imap-client-session-test.vala @@ -0,0 +1,415 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +class Geary.Imap.ClientSessionTest : TestCase { + + private const uint CONNECT_TIMEOUT = 2; + + private TestServer? server = null; + + + public ClientSessionTest() { + base("Geary.Imap.ClientSessionTest"); + add_test("connect_disconnect", connect_disconnect); + add_test("connect_with_capabilities", connect_with_capabilities); + if (GLib.Test.slow()) { + add_test("connect_timeout", connect_timeout); + } + add_test("login", login); + add_test("login_with_capabilities", login_with_capabilities); + add_test("logout", logout); + add_test("login_logout", login_logout); + add_test("initiate_request_capabilities", initiate_request_capabilities); + add_test("initiate_implicit_capabilities", initiate_implicit_capabilities); + add_test("initiate_namespace", initiate_namespace); + } + + protected override void set_up() throws GLib.Error { + this.server = new TestServer(); + } + + protected override void tear_down() { + this.server.stop(); + this.server = null; + } + + public void connect_disconnect() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void connect_with_capabilities() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK [CAPABILITY IMAP4rev1] localhost test server ready" + ); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + + assert_true(test_article.capabilities.supports_imap4rev1()); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true(result.succeeded); + } + + public void connect_timeout() throws GLib.Error { + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + + GLib.Timer timer = new GLib.Timer(); + timer.start(); + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + try { + test_article.connect_async.end(async_result()); + assert_not_reached(); + } catch (GLib.IOError.TIMED_OUT err) { + assert_double(timer.elapsed(), CONNECT_TIMEOUT, CONNECT_TIMEOUT * 0.5); + } + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true(result.succeeded); + } + + public void login_with_capabilities() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 login test password"); + this.server.add_script_line( + SEND_LINE, "a001 OK [CAPABILITY IMAP4rev1] ohhai" + ); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + test_article.login_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.login_async.end(async_result()); + + assert_true(test_article.capabilities.supports_imap4rev1()); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void login() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 login test password"); + this.server.add_script_line(SEND_LINE, "a001 OK ohhai"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.login_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.login_async.end(async_result()); + assert_true(test_article.get_protocol_state() == AUTHORIZED); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void logout() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 logout"); + this.server.add_script_line(SEND_LINE, "* BYE fine"); + this.server.add_script_line(SEND_LINE, "a001 OK laters"); + this.server.add_script_line(DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.logout_async.begin(null, this.async_complete_full); + test_article.logout_async.end(async_result()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void login_logout() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 login test password"); + this.server.add_script_line(SEND_LINE, "a001 OK ohhai"); + this.server.add_script_line(RECEIVE_LINE, "a002 logout"); + this.server.add_script_line(SEND_LINE, "* BYE fine"); + this.server.add_script_line(SEND_LINE, "a002 OK laters"); + this.server.add_script_line(DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.login_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.login_async.end(async_result()); + assert_true(test_article.get_protocol_state() == AUTHORIZED); + + test_article.logout_async.begin(null, this.async_complete_full); + test_article.logout_async.end(async_result()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void initiate_request_capabilities() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 capability"); + this.server.add_script_line(SEND_LINE, "* CAPABILITY IMAP4rev1 LOGIN"); + this.server.add_script_line(SEND_LINE, "a001 OK enjoy"); + this.server.add_script_line(RECEIVE_LINE, "a002 login test password"); + this.server.add_script_line(SEND_LINE, "a002 OK ohhai"); + this.server.add_script_line(RECEIVE_LINE, "a003 capability"); + this.server.add_script_line(SEND_LINE, "* CAPABILITY IMAP4rev1"); + this.server.add_script_line(SEND_LINE, "a003 OK thanks"); + this.server.add_script_line(RECEIVE_LINE, "a004 LIST \"\" INBOX"); + this.server.add_script_line(SEND_LINE, "* LIST (\\HasChildren) \".\" Inbox"); + this.server.add_script_line(SEND_LINE, "a004 OK there"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.initiate_session_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.initiate_session_async.end(async_result()); + + assert_true(test_article.capabilities.supports_imap4rev1()); + assert_false(test_article.capabilities.has_capability("AUTH")); + assert_int(2, test_article.capabilities.revision); + + assert_string("Inbox", test_article.inbox.mailbox.name); + assert_true(test_article.inbox.mailbox.is_inbox); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void initiate_implicit_capabilities() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, "* OK [CAPABILITY IMAP4rev1 LOGIN] localhost test server ready" + ); + this.server.add_script_line(RECEIVE_LINE, "a001 login test password"); + this.server.add_script_line(SEND_LINE, "a001 OK [CAPABILITY IMAP4rev1] ohhai"); + this.server.add_script_line(RECEIVE_LINE, "a002 LIST \"\" INBOX"); + this.server.add_script_line(SEND_LINE, "* LIST (\\HasChildren) \".\" Inbox"); + this.server.add_script_line(SEND_LINE, "a002 OK there"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.initiate_session_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.initiate_session_async.end(async_result()); + + assert_true(test_article.capabilities.supports_imap4rev1()); + assert_false(test_article.capabilities.has_capability("AUTH")); + assert_int(2, test_article.capabilities.revision); + + assert_string("Inbox", test_article.inbox.mailbox.name); + assert_true(test_article.inbox.mailbox.is_inbox); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + public void initiate_namespace() throws GLib.Error { + this.server.add_script_line( + SEND_LINE, + "* OK [CAPABILITY IMAP4rev1 LOGIN] localhost test server ready" + ); + this.server.add_script_line( + RECEIVE_LINE, "a001 login test password" + ); + this.server.add_script_line( + SEND_LINE, "a001 OK [CAPABILITY IMAP4rev1 NAMESPACE] ohhai" + ); + this.server.add_script_line( + RECEIVE_LINE, "a002 LIST \"\" INBOX" + ); + this.server.add_script_line( + SEND_LINE, "* LIST (\\HasChildren) \".\" Inbox" + ); + this.server.add_script_line( + SEND_LINE, "a002 OK there" + ); + this.server.add_script_line( + RECEIVE_LINE, "a003 NAMESPACE" + ); + this.server.add_script_line( + SEND_LINE, + """* NAMESPACE (("INBOX." ".")) (("user." ".")) (("shared." "."))""" + ); + this.server.add_script_line(SEND_LINE, "a003 OK there"); + this.server.add_script_line(WAIT_FOR_DISCONNECT, ""); + + var test_article = new ClientSession(new_endpoint()); + assert_true(test_article.get_protocol_state() == NOT_CONNECTED); + + test_article.connect_async.begin( + CONNECT_TIMEOUT, null, this.async_complete_full + ); + test_article.connect_async.end(async_result()); + assert_true(test_article.get_protocol_state() == UNAUTHORIZED); + + test_article.initiate_session_async.begin( + new Credentials(PASSWORD, "test", "password"), + null, + this.async_complete_full + ); + test_article.initiate_session_async.end(async_result()); + + assert_int(1, test_article.get_personal_namespaces().size); + assert_string( + "INBOX.", test_article.get_personal_namespaces()[0].prefix + ); + + assert_int(1, test_article.get_shared_namespaces().size); + assert_string( + "shared.", test_article.get_shared_namespaces()[0].prefix + ); + + assert_int(1, test_article.get_other_users_namespaces().size); + assert_string( + "user.", test_article.get_other_users_namespaces()[0].prefix + ); + + test_article.disconnect_async.begin(null, this.async_complete_full); + test_article.disconnect_async.end(async_result()); + + TestServer.Result result = this.server.wait_for_script(this.main_loop); + assert_true( + result.succeeded, + result.error != null ? result.error.message : "Server result failed" + ); + } + + protected Endpoint new_endpoint() { + return new Endpoint(this.server.get_client_address(), NONE, 10); + } + +} diff --git a/test/engine/imap/transport/imap-deserializer-test.vala b/test/engine/imap/transport/imap-deserializer-test.vala index 0960331d..f090057e 100644 --- a/test/engine/imap/transport/imap-deserializer-test.vala +++ b/test/engine/imap/transport/imap-deserializer-test.vala @@ -265,7 +265,7 @@ class Geary.Imap.DeserializerTest : TestCase { this.stream.add_data(bye.data); bool eos = false; - this.deser.eos.connect(() => { eos = true; }); + this.deser.end_of_stream.connect(() => { eos = true; }); this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); }); RootParameters? message = this.process.end(async_result()); @@ -283,7 +283,7 @@ class Geary.Imap.DeserializerTest : TestCase { this.deser.parameters_ready.connect((param) => { message = param; }); this.deser.bytes_received.connect((count) => { bytes_received += count; }); - this.deser.eos.connect((param) => { eos = true; }); + this.deser.end_of_stream.connect((param) => { eos = true; }); this.deser.deserialize_failure.connect(() => { deserialize_failure = true; }); this.deser.receive_failure.connect((err) => { receive_failure = true;}); diff --git a/test/engine/util-timeout-manager-test.vala b/test/engine/util-timeout-manager-test.vala index a9a01a64..dcecba8d 100644 --- a/test/engine/util-timeout-manager-test.vala +++ b/test/engine/util-timeout-manager-test.vala @@ -87,7 +87,7 @@ class Geary.TimeoutManagerTest : TestCase { this.main_loop.iteration(true); } - assert_epsilon(timer.elapsed(), 1.0, SECONDS_EPSILON); + assert_double(timer.elapsed(), 1.0, SECONDS_EPSILON); } public void milliseconds() throws Error { @@ -101,7 +101,7 @@ class Geary.TimeoutManagerTest : TestCase { this.main_loop.iteration(true); } - assert_epsilon(timer.elapsed(), 0.1, MILLISECONDS_EPSILON); + assert_double(timer.elapsed(), 0.1, MILLISECONDS_EPSILON); } public void repeat_forever() throws Error { @@ -118,11 +118,7 @@ class Geary.TimeoutManagerTest : TestCase { } timer.stop(); - assert_epsilon(timer.elapsed(), 2.0, SECONDS_EPSILON * 2); - } - - private inline void assert_epsilon(double actual, double expected, double epsilon) { - assert(actual + epsilon >= expected && actual - epsilon <= expected); + assert_double(timer.elapsed(), 2.0, SECONDS_EPSILON * 2); } } diff --git a/test/integration/imap/client-session.vala b/test/integration/imap/client-session.vala index e2d38a63..f6221f63 100644 --- a/test/integration/imap/client-session.vala +++ b/test/integration/imap/client-session.vala @@ -33,7 +33,7 @@ class Integration.Imap.ClientSession : TestCase { } public override void tear_down() throws GLib.Error { - if (this.session.get_protocol_state(null) != NOT_CONNECTED) { + if (this.session.get_protocol_state() != NOT_CONNECTED) { this.session.disconnect_async.begin(null, async_complete_full); this.session.disconnect_async.end(async_result()); } @@ -41,7 +41,7 @@ class Integration.Imap.ClientSession : TestCase { } public void session_connect() throws GLib.Error { - this.session.connect_async.begin(null, async_complete_full); + this.session.connect_async.begin(2, null, async_complete_full); this.session.connect_async.end(async_result()); this.session.disconnect_async.begin(null, async_complete_full); @@ -98,7 +98,7 @@ class Integration.Imap.ClientSession : TestCase { } private void do_connect() throws GLib.Error { - this.session.connect_async.begin(null, async_complete_full); + this.session.connect_async.begin(5, null, async_complete_full); this.session.connect_async.end(async_result()); } diff --git a/test/meson.build b/test/meson.build index 38a3aae2..9cd4717f 100644 --- a/test/meson.build +++ b/test/meson.build @@ -3,6 +3,7 @@ subdir('data') geary_test_lib_sources = [ 'mock-object.vala', 'test-case.vala', + 'test-server.vala', ] geary_test_engine_sources = [ @@ -41,6 +42,8 @@ geary_test_engine_sources = [ 'engine/imap/message/imap-mailbox-specifier-test.vala', 'engine/imap/parameter/imap-list-parameter-test.vala', 'engine/imap/response/imap-namespace-response-test.vala', + 'engine/imap/transport/imap-client-connection-test.vala', + 'engine/imap/transport/imap-client-session-test.vala', 'engine/imap/transport/imap-deserializer-test.vala', 'engine/imap-db/imap-db-account-test.vala', 'engine/imap-db/imap-db-attachment-test.vala', diff --git a/test/test-case.vala b/test/test-case.vala index 7e65603d..6292c498 100644 --- a/test/test-case.vala +++ b/test/test-case.vala @@ -96,6 +96,10 @@ public void assert_int64(int64 expected, int64 actual, string? context = null) } } +public void assert_double(double actual, double expected, double epsilon) { + assert(actual + epsilon >= expected && actual - epsilon <= expected); +} + public void assert_uint(uint expected, uint actual, string? context = null) throws GLib.Error { if (expected != actual) { diff --git a/test/test-engine.vala b/test/test-engine.vala index a8e262fa..a70cb11c 100644 --- a/test/test-engine.vala +++ b/test/test-engine.vala @@ -46,20 +46,28 @@ int main(string[] args) { engine.add_suite(new Geary.Db.DatabaseTest().get_suite()); engine.add_suite(new Geary.Db.VersionedDatabaseTest().get_suite()); engine.add_suite(new Geary.HTML.UtilTest().get_suite()); - // Other IMAP tests rely on DataFormat working, so test that first + + // Other IMAP tests rely on these working, so test them first engine.add_suite(new Geary.Imap.DataFormatTest().get_suite()); + engine.add_suite(new Geary.Imap.CreateCommandTest().get_suite()); - engine.add_suite(new Geary.Imap.DeserializerTest().get_suite()); engine.add_suite(new Geary.Imap.FetchCommandTest().get_suite()); engine.add_suite(new Geary.Imap.ListParameterTest().get_suite()); engine.add_suite(new Geary.Imap.MailboxSpecifierTest().get_suite()); engine.add_suite(new Geary.Imap.NamespaceResponseTest().get_suite()); + + // Depends on IMAP commands working + engine.add_suite(new Geary.Imap.DeserializerTest().get_suite()); + engine.add_suite(new Geary.Imap.ClientConnectionTest().get_suite()); + engine.add_suite(new Geary.Imap.ClientSessionTest().get_suite()); + engine.add_suite(new Geary.ImapDB.AccountTest().get_suite()); engine.add_suite(new Geary.ImapDB.AttachmentTest().get_suite()); engine.add_suite(new Geary.ImapDB.AttachmentIoTest().get_suite()); engine.add_suite(new Geary.ImapDB.DatabaseTest().get_suite()); engine.add_suite(new Geary.ImapDB.EmailIdentifierTest().get_suite()); engine.add_suite(new Geary.ImapDB.FolderTest().get_suite()); + engine.add_suite(new Geary.ImapEngine.AccountProcessorTest().get_suite()); engine.add_suite(new Geary.ImapEngine.GenericAccountTest().get_suite()); diff --git a/test/test-server.vala b/test/test-server.vala new file mode 100644 index 00000000..39e99e70 --- /dev/null +++ b/test/test-server.vala @@ -0,0 +1,222 @@ +/* + * Copyright 2019 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A simple mock server for testing network connections. + * + * To use it, unit tests should construct an instance as a fixture in + * set up, specify a test script by adding lines and then check the + * result, before stopping the server in tear down. + */ +public class TestServer : GLib.Object { + + + /** Possible actions a script may take. */ + public enum Action { + /** + * The implicit first action. + * + * This does not need to be specified as a script action, it + * will always be taken when a client connects. + */ + CONNECTED, + + /** Send a line to the client. */ + SEND_LINE, + + /** Receive a line from the client. */ + RECEIVE_LINE, + + /** Wait for the client to disconnect. */ + WAIT_FOR_DISCONNECT, + + /** Disconnect immediately. */ + DISCONNECT; + } + + + /** A line of the server's script. */ + public struct Line { + + /** The action to take for this line. */ + public Action action; + + /** + * The value for the action. + * + * If sending, this string will be sent. If receiving, the + * expected line. + */ + public string value; + + } + + /** The result of executing a script line. */ + public struct Result { + + /** The expected action. */ + public Line line; + + /** Was the expected action successful. */ + public bool succeeded; + + /** The actual string sent by a client when not as expected. */ + public string? actual; + + /** In case of an error being thrown, the error itself. */ + public GLib.Error? error; + + } + + + private GLib.DataStreamNewlineType line_ending; + private uint16 port; + private GLib.ThreadedSocketService service = + new GLib.ThreadedSocketService(10); + private GLib.Cancellable running = new GLib.Cancellable(); + private Gee.List script = new Gee.ArrayList(); + private GLib.AsyncQueue completion_queue = + new GLib.AsyncQueue(); + + + public TestServer(GLib.DataStreamNewlineType line_ending = CR_LF) + throws GLib.Error { + this.line_ending = line_ending; + this.port = this.service.add_any_inet_port(null); + this.service.run.connect((conn) => { + handle_connection(conn); + return true; + }); + this.service.start(); + } + + public GLib.SocketConnectable get_client_address() { + return new GLib.NetworkAddress("localhost", this.port); + } + + public void add_script_line(Action action, string value) { + this.script.add({ action, value }); + } + + public Result wait_for_script(GLib.MainContext loop) { + Result? result = null; + while (result == null) { + loop.iteration(false); + result = this.completion_queue.try_pop(); + } + return result; + } + + public void stop() { + this.service.stop(); + this.running.cancel(); + } + + private void handle_connection(GLib.SocketConnection connection) { + debug("Connected"); + var input = new GLib.DataInputStream( + connection.input_stream + ); + input.set_newline_type(this.line_ending); + + var output = new GLib.DataOutputStream( + connection.output_stream + ); + + Line connected_line = { CONNECTED, "" }; + Result result = { connected_line, true, null, null }; + foreach (var line in this.script) { + result.line = line; + switch (line.action) { + case SEND_LINE: + debug("Sending: %s", line.value); + try { + output.put_string(line.value); + switch (this.line_ending) { + case CR: + output.put_byte('\r'); + break; + case LF: + output.put_byte('\n'); + break; + default: + output.put_byte('\r'); + output.put_byte('\n'); + break; + } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case RECEIVE_LINE: + debug("Waiting for: %s", line.value); + try { + size_t len; + string? received = input.read_line(out len, this.running); + if (received == null || received != line.value) { + result.succeeded = false; + result.actual = received; + } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case WAIT_FOR_DISCONNECT: + debug("Waiting for disconnect"); + var socket = connection.get_socket(); + try { + uint8 buffer[4096]; + while (socket.receive_with_blocking(buffer, true) > 0) { } + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + + case DISCONNECT: + debug("Disconnecting"); + try { + connection.close(this.running); + } catch (GLib.Error err) { + result.succeeded = false; + result.error = err; + } + break; + } + + if (!result.succeeded) { + break; + } + } + if (result.succeeded) { + debug("Done"); + } else if (result.error != null) { + warning("Error: %s", result.error.message); + } else if (result.line.action == RECEIVE_LINE) { + warning("Received unexpected line: %s", result.actual ?? "(null)"); + } else { + warning("Failed for unknown reason"); + } + + if (connection.is_connected()) { + try { + connection.close(this.running); + } catch (GLib.Error err) { + warning( + "Error closing test server connection: %s", err.message + ); + } + } + + this.completion_queue.push(result); + } + +}