Merge branch 'mjog/imap-connection-fixes' into 'mainline'

IMAP connection fixes

See merge request GNOME/geary!479
This commit is contained in:
Michael Gratton 2020-03-30 11:35:18 +00:00
commit 297a59ca80
28 changed files with 1440 additions and 757 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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);

View file

@ -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<Namespace> 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)) {

View file

@ -1,7 +1,9 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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.
*

View file

@ -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); }
);
}

View file

@ -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);

View file

@ -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);

View file

@ -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;
}

View file

@ -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);
}
/**

View file

@ -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);
}
/**

View file

@ -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",

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,9 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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;
}

View file

@ -1,32 +1,35 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2018 Michael Gratton <mike@vee.net>
* Copyright © 2016 Software Freedom Conservancy Inc.
* Copyright © 2018, 2020 Michael Gratton <mike@vee.net>
*
* 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);
}
}

View file

@ -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',

View file

@ -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<string>? 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);
}
}

View file

@ -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.
*/

View file

@ -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)
)
);

View file

@ -0,0 +1,160 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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);
}
}

View file

@ -0,0 +1,415 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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);
}
}

View file

@ -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;});

View file

@ -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);
}
}

View file

@ -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());
}

View file

@ -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',

View file

@ -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) {

View file

@ -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());

222
test/test-server.vala Normal file
View file

@ -0,0 +1,222 @@
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* 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<Line?> script = new Gee.ArrayList<Line?>();
private GLib.AsyncQueue<Result?> completion_queue =
new GLib.AsyncQueue<Result?>();
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);
}
}