[geary/mjog/imap-connection-fixes: 19/23] Update Geary.Imap.Capabilities handling



commit 708a81a879e8623a17a2e2a114fd51ba3b29cc1c
Author: Michael Gratton <mike vee net>
Date:   Sun Dec 29 23:15:34 2019 +1030

    Update Geary.Imap.Capabilities handling
    
    Move Capabilities to the api directory and make immutable.
    
    Don't pass around out params to simply increment the revision and use a
    field in ClientSession, just use the last capability instance. Ensure
    after starting a TLS session capabilities are cleared. Add unit tests
    for getting both implicit and explicity capaibilities when initiating
    a client session.

 po/POTFILES.in                                     |   2 +-
 .../imap/{response => api}/imap-capabilities.vala  |  47 +++++--
 src/engine/imap/response/imap-response-code.vala   |  16 +--
 src/engine/imap/response/imap-server-data.vala     |  20 ++-
 src/engine/imap/transport/imap-client-session.vala |  71 +++++-----
 src/engine/meson.build                             |   2 +-
 src/engine/util/util-generic-capabilities.vala     |  82 ++++++------
 test/engine/imap-db/imap-db-account-test.vala      |   4 +-
 .../imap/transport/imap-client-session-test.vala   | 149 ++++++++++++++++++++-
 9 files changed, 285 insertions(+), 108 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d2494804..759f1eec 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -208,6 +208,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
@@ -319,7 +320,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/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 <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.
      *
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-session.vala 
b/src/engine/imap/transport/imap-client-session.vala
index d20d65fb..f5f8702a 100644
--- a/src/engine/imap/transport/imap-client-session.vala
+++ b/src/engine/imap/transport/imap-client-session.vala
@@ -228,13 +228,17 @@ 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 {
@@ -278,7 +282,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
     /** The locations of shared mailboxes on this connection. */
     internal Gee.List<Namespace> shared_namespaces = new Gee.ArrayList<Namespace>();
 
-
     private Endpoint imap_endpoint;
     private Geary.State.Machine fsm;
     private ClientConnection? cx = null;
@@ -295,7 +298,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<string,Namespace> namespaces = new Gee.HashMap<string,Namespace>();
 
 
@@ -316,8 +318,6 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
      */
     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);
@@ -929,14 +929,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());
             }
@@ -957,19 +957,26 @@ 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<ServerData> server_data = new Gee.ArrayList<ServerData>();
         ulong data_id = this.server_data_received.connect((data) => { server_data.add(data); });
         try {
@@ -987,7 +994,7 @@ public class Geary.Imap.ClientSession : BaseObject, Logging.Source {
 
             // 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
@@ -1796,12 +1803,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(
@@ -1828,12 +1837,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:
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<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);
+    }
+
+}
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-session-test.vala 
b/test/engine/imap/transport/imap-client-session-test.vala
index 63358ed0..8fb304b9 100644
--- a/test/engine/imap/transport/imap-client-session-test.vala
+++ b/test/engine/imap/transport/imap-client-session-test.vala
@@ -15,12 +15,16 @@ class Geary.Imap.ClientSessionTest : TestCase {
     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);
     }
 
     protected override void set_up() throws GLib.Error {
@@ -58,6 +62,27 @@ class Geary.Imap.ClientSessionTest : TestCase {
         );
     }
 
+    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, "");
 
@@ -72,13 +97,47 @@ class Geary.Imap.ClientSessionTest : TestCase {
             test_article.connect_async.end(async_result());
             assert_not_reached();
         } catch (GLib.IOError.TIMED_OUT err) {
-            assert_double(timer.elapsed(), CONNECT_TIMEOUT, 0.5);
+            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"
@@ -183,6 +242,94 @@ class Geary.Imap.ClientSessionTest : TestCase {
         );
     }
 
+    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(null) == 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(null) == 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);
+
+        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(null) == 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(null) == 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);
+
+        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);
     }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]