diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ff9ddb1e9c0ee606f21e06de97bf2d32daa3b0f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,110 @@
+#
+# Geary CI config.
+#
+
+stages:
+  - build
+
+variables:
+  BUILD_DIR: build
+  CONFIG_CMD: ./configure
+  BUILD_CMD: make
+  TEST_CMD: xvfb-run make test
+  INSTALL_CMD: make install
+  FEDORA_BUILD_DEPS: gcc make
+  FEDORA_DEPS: vala gobject-introspection-devel intltool cmake
+               desktop-file-utils gnome-doc-utils libcanberra-devel
+               libgee-devel glib2-devel gmime-devel gtk3-devel
+               libnotify-devel sqlite-devel webkitgtk4-devel
+               libsecret-devel libxml2-devel vala-tools gcr-devel
+               enchant-devel
+  FEDORA_TEST_DEPS: Xvfb
+  DEBIAN_DEPS: valac libgirepository1.0-dev intltool cmake
+               desktop-file-utils gnome-doc-utils libcanberra-dev
+               libgee-0.8-dev libglib2.0-dev libgmime-2.6-dev
+               libgtk-3-dev libsecret-1-dev libxml2-dev libnotify-dev
+               libsqlite3-dev libwebkit2gtk-4.0-dev libgcr-3-dev
+               libenchant-dev
+  DEBIAN_TEST_DEPS: xauth xvfb
+  UBUNTU_DEPS: $DEBIAN_DEPS libunity-dev libmessaging-menu-dev
+  UBUNTU_TEST_DEPS: xauth xvfb
+
+#
+# Stages
+#
+
+fedora:
+  stage: build
+  image: fedora:latest
+  before_script:
+    - dnf update -y --nogpgcheck
+    - dnf install -y --nogpgcheck $FEDORA_BUILD_DEPS $FEDORA_DEPS $FEDORA_TEST_DEPS
+  script:
+    - $CONFIG_CMD
+    - $BUILD_CMD
+    - $TEST_CMD
+    - $INSTALL_CMD
+
+debian:
+  stage: build
+  image: debian:stable
+  before_script:
+    - apt-get update
+    - apt-get install -q -y --no-install-recommends $DEBIAN_DEPS $DEBIAN_TEST_DEPS
+  script:
+    - $CONFIG_CMD
+    - $BUILD_CMD
+    - $TEST_CMD
+    - $INSTALL_CMD
+
+ubuntu:
+  stage: build
+  image: ubuntu:xenial
+  before_script:
+    - apt-get update
+    - apt-get install -q -y --no-install-recommends $UBUNTU_DEPS $UBUNTU_TEST_DEPS
+  script:
+    - $CONFIG_CMD
+    - $BUILD_CMD
+    - $TEST_CMD
+    - $INSTALL_CMD
+
+deb-package:
+  stage: build
+  image: ubuntu:xenial
+  before_script:
+    - apt-get update
+    - apt-get install -q -y --no-install-recommends packaging-dev $UBUNTU_DEPS
+  script:
+    - dpkg-buildpackage -b -us -uc
+
+flatpak-package:
+  image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:3.28
+  stage: build
+
+  variables:
+    GIT_SUBMODULE_STRATEGY: normal
+    FLATPAK_ARTIFACT: geary-git.flatpak
+
+  script:
+    - flatpak-builder flatpak-build org.gnome.Geary.json
+    - flatpak build-export flatpak-repo flatpak-build --update-appstream
+    - flatpak build-bundle flatpak-repo $FLATPAK_ARTIFACT
+        --runtime-repo=https://sdk.gnome.org/gnome-nightly.flatpakrepo
+        org.gnome.Geary
+
+  artifacts:
+    paths:
+      - $FLATPAK_ARTIFACT
+    expire_in: 2 days
+
+  cache:
+    # JOB_NAME - Each job will have it's own cache
+    # COMMIT_REF_SLUG = Lowercase name of the branch
+    # ^ Keep diffrerent caches for each branch
+    key: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"
+    paths:
+      # Cache .flatpak-builder
+      - .flatpak-builder/cache/
+      - .flatpak-builder/downloads/
+      - .flatpak-builder/git/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2b412e24b45ad628c3310e37c16a890080d0ed39..3c8d1781e659cba576a927beb359952c00ee71a2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,7 +19,7 @@ list(APPEND
 #
 set(GETTEXT_PACKAGE "geary")
 set(RELEASE_NAME "Lightweight email client for GNOME.")
-set(VERSION "0.12.2")
+set(VERSION "0.12.4")
 set(VERSION_INFO "Release")
 set(LANGUAGE_SUPPORT_DIRECTORY ${CMAKE_INSTALL_PREFIX}/share/locale)
 
diff --git a/NEWS b/NEWS
index 4981383db6df7143179d8da4cf1ad845177aa783..a01d5f6f247c99510ac1a9e742ee801c09229726 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,31 @@
+Version 0.12.4
+~~~~~~~~~~~~~~
+Released: 2018-08-29
+
+Bug fixes included in this release:
+ * Fix handling folder names with IMAP reserved characters, such as
+   backslashes. Issue #40
+ * Fix dialog windows not focused after being first shown. Issue #43
+ * Actually include the fix for "Move to folder" selection bug. Issue #24
+ * Fix build under vala >= 0.41. Issue #86
+ * Fixes for miscellaneous crashers
+
+Version 0.12.3
+~~~~~~~~~~~~~~
+Released: 2018-07-14
+
+Bug fixes included in this release:
+ * Not syncing mail using Turkish locale. Bug 795906
+ * Fix crash saving an attachment with unknown content type
+ * Fix crash in secret_collection_get_locked. Bug 795328
+ * "Move to folder" selection bug. Issue #24
+ * Subfolders with special folders not displayed in list. Issue #11
+ * Add OARS metadata for Flathub
+
+Thanks to all who contributed code fixes and enhancements to this
+release:
+ * Nick Richards
+
 Version 0.12.2
 ~~~~~~~~~~~~~~
 Released: 2018-04-24
diff --git a/THANKS b/THANKS
index 34affae4c9d6f23aadb5f80309b2634d254cc441..15a76c541fe0dc3434aed866d495ca9679c39a7b 100644
--- a/THANKS
+++ b/THANKS
@@ -56,6 +56,7 @@ Ralph Plawetzki <ralph@purejava.org>
 Mario Sanchez Prada <msanchez@igalia.com>
 Tiago Quelhas <tiagoq@gmail.com>
 Viko Adi Rahmawan <vikoadi@gmail.com>
+nick richards <nick.richards@gmail.com>
 ritchiew <rawilson52@gmail.com>
 Leonardo Robol <leo@robol.it>
 Didier Roche <didrocks@ubuntu.com>
diff --git a/desktop/org.gnome.Geary.appdata.xml.in b/desktop/org.gnome.Geary.appdata.xml.in
index 3ba640ed841c3dd5d752a9772445bcb1d5f5d4bb..0780b958278758f3f2e965e064ab59274d8f36d1 100644
--- a/desktop/org.gnome.Geary.appdata.xml.in
+++ b/desktop/org.gnome.Geary.appdata.xml.in
@@ -77,6 +77,34 @@
   <translation type="gettext">geary</translation>
 
   <releases>
+    <release version="0.12.4" date="2018-08-29">
+      <description>
+        <p>Bug fixes included in this release:</p>
+        <ul>
+          <li>Fix handling folder names with IMAP reserved characters, such as backslashes. Issue #40</li>
+          <li>Fix dialog windows not focused after being first shown. Issue #43</li>
+          <li>Actually include the fix for "Move to folder" selection bug. Issue #24</li>
+          <li>Fix build under vala >= 0.41. Issue #86</li>
+          <li>Fixes for miscellaneous crashers</li>
+        </ul>
+      </description>
+    </release>
+
+    <release version="0.12.3" date="2018-07-14">
+      <description>
+        <p>Bug fixes included in this release:</p>
+        <ul>
+          <li>Not syncing mail using Turkish locale. Bug 795906</li>
+          <li>Fix crash saving an attachment with unknown content type</li>
+          <li>Fix crash in secret_collection_get_locked. Bug 795328</li>
+          <li>"Move to folder" selection bug. Issue #24</li>
+          <li>Subfolders with special folders not displayed in
+          list. Issue #11</li>
+          <li>Add OARS metadata for Flathub</li>
+        </ul>
+      </description>
+    </release>
+
     <release version="0.12.2" date="2018-04-24">
       <description>
         <p>Bug fixes included in this release:</p>
@@ -165,4 +193,33 @@
       </description>
     </release>
   </releases>
+  <content_rating type="oars-1.1">
+    <content_attribute id="violence-cartoon">none</content_attribute>
+    <content_attribute id="violence-fantasy">none</content_attribute>
+    <content_attribute id="violence-realistic">none</content_attribute>
+    <content_attribute id="violence-bloodshed">none</content_attribute>
+    <content_attribute id="violence-sexual">none</content_attribute>
+    <content_attribute id="violence-desecration">none</content_attribute>
+    <content_attribute id="violence-slavery">none</content_attribute>
+    <content_attribute id="violence-worship">none</content_attribute>
+    <content_attribute id="drugs-alcohol">none</content_attribute>
+    <content_attribute id="drugs-narcotics">none</content_attribute>
+    <content_attribute id="drugs-tobacco">none</content_attribute>
+    <content_attribute id="sex-nudity">none</content_attribute>
+    <content_attribute id="sex-themes">none</content_attribute>
+    <content_attribute id="sex-homosexuality">none</content_attribute>
+    <content_attribute id="sex-prostitution">none</content_attribute>
+    <content_attribute id="sex-adultery">none</content_attribute>
+    <content_attribute id="sex-appearance">none</content_attribute>
+    <content_attribute id="language-profanity">none</content_attribute>
+    <content_attribute id="language-humor">none</content_attribute>
+    <content_attribute id="language-discrimination">none</content_attribute>
+    <content_attribute id="social-chat">intense</content_attribute>
+    <content_attribute id="social-info">none</content_attribute>
+    <content_attribute id="social-audio">none</content_attribute>
+    <content_attribute id="social-location">none</content_attribute>
+    <content_attribute id="social-contacts">intense</content_attribute>
+    <content_attribute id="money-purchasing">none</content_attribute>
+    <content_attribute id="money-gambling">none</content_attribute>
+  </content_rating>
 </component>
diff --git a/org.gnome.Geary.json b/org.gnome.Geary.json
index 1656a764904301486b81427b0216150ab36261f8..15759cede4c2125b128dc1a26fa8206039983e37 100644
--- a/org.gnome.Geary.json
+++ b/org.gnome.Geary.json
@@ -1,6 +1,7 @@
 /* flatpak-builder config for Geary. */
 {
     "app-id": "org.gnome.Geary",
+    "branch": "geary-0.12",
     "runtime": "org.gnome.Platform",
     "runtime-version": "3.28",
     "sdk": "org.gnome.Sdk",
@@ -78,7 +79,7 @@
             "sources": [
                 {
                     "type": "git",
-                    "url": "https://git.gnome.org/browse/libgee",
+                    "url": "https://gitlab.gnome.org/GNOME/libgee.git",
                     "tag": "0.20.0"
                 }
             ]
@@ -88,7 +89,7 @@
             "sources": [
                 {
                     "type": "git",
-                    "url": "https://git.gnome.org/browse/gmime",
+                    "url": "https://github.com/jstedfast/gmime.git",
                     "branch": "gmime-2-6"
                 }
             ]
@@ -98,7 +99,7 @@
             "sources": [
                 {
                     "type": "git",
-                    "url": "https://git.gnome.org/browse/geary",
+                    "url": "https://gitlab.gnome.org/GNOME/geary.git",
                     "branch": "geary-0.12"
                 }
             ]
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 345dcbfffd01c8da6e556e8ec78fc69f004cfbdc..1867d7af77e6095141bc87c0aa06c3202f4a52e4 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -269,7 +269,6 @@ src/engine/imap/message/imap-fetch-data-specifier.vala
 src/engine/imap/message/imap-flag.vala
 src/engine/imap/message/imap-flags.vala
 src/engine/imap/message/imap-internal-date.vala
-src/engine/imap/message/imap-mailbox-parameter.vala
 src/engine/imap/message/imap-mailbox-specifier.vala
 src/engine/imap/message/imap-message-data.vala
 src/engine/imap/message/imap-message-flag.vala
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 1e4213fcdeb9eaa98ec1a77ff6ca2c9831fe1a8f..bce938d94645c2354eb55059be61bdbef2b8d7fc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -124,7 +124,6 @@ engine/imap/message/imap-flag.vala
 engine/imap/message/imap-flags.vala
 engine/imap/message/imap-internal-date.vala
 engine/imap/message/imap-mailbox-specifier.vala
-engine/imap/message/imap-mailbox-parameter.vala
 engine/imap/message/imap-message-data.vala
 engine/imap/message/imap-message-flag.vala
 engine/imap/message/imap-message-flags.vala
diff --git a/src/client/application/geary-application.vala b/src/client/application/geary-application.vala
index 5c1711698f8f5d48ea7930a1f00bda1c22d28ab1..545379ffe80f2d13c2db3870e741ad9dbe2e3974 100644
--- a/src/client/application/geary-application.vala
+++ b/src/client/application/geary-application.vala
@@ -234,8 +234,11 @@ public class GearyApplication : Gtk.Application {
         // Use present_with_time and a synthesised time so the present
         // actually works, as a work around for Bug 766284
         // <https://bugzilla.gnome.org/show_bug.cgi?id=766284>.
+        // Subtract 10ms from the current time to avoid the main
+        // window stealing the focus when presented just before
+        // showing a dialog (issue #43).
         this.controller.main_window.present_with_time(
-            (uint32) (get_monotonic_time() / 1000)
+            (uint32) (get_monotonic_time() / 1000) - 10
         );
 
         return true;
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 049b1db2f31f4c531541cf0c0da2be2ed3181649..d73afa3fe257701181844e2ecd802cc9f1c84cae 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1606,12 +1606,23 @@ public class GearyController : Geary.BaseObject {
         );
     }
 
-    private void on_special_folder_type_changed(Geary.Folder folder, Geary.SpecialFolderType old_type,
-        Geary.SpecialFolderType new_type) {
+    private void on_special_folder_type_changed(Geary.Folder folder,
+                                                Geary.SpecialFolderType old_type,
+                                                Geary.SpecialFolderType new_type) {
         main_window.folder_list.remove_folder(folder);
         main_window.folder_list.add_folder(folder);
+        // Since removing the folder will also remove its children, we
+        // need to check for any and re-add them. See isssue #11.
+        try {
+            foreach (Geary.Folder child in
+                     folder.account.list_matching_folders(folder.path)) {
+                main_window.folder_list.add_folder(child);
+            }
+        } catch (Error err) {
+            // Oh well
+        }
     }
-    
+
     private void on_engine_opened() {
         // Locate the first account so we can select its inbox when available.
         try {
diff --git a/src/client/application/main.vala b/src/client/application/main.vala
index f41d18e5782627e70e5e0a4d9863731196819d74..f6bdbf9c86cd1d838311d83e069f8724a18cedff 100644
--- a/src/client/application/main.vala
+++ b/src/client/application/main.vala
@@ -17,7 +17,7 @@ int main(string[] args) {
     //
     // Packages can disable this fix with the --disable-poodle-ssl3 configure option.
 #if !DISABLE_POODLE
-    Environment.set_variable("G_TLS_GNUTLS_PRIORITY", "NORMAL:%COMPAT:%LATEST_RECORD_VERSION:!VERS-SSL3.0", false);
+    Environment.set_variable("G_TLS_GNUTLS_PRIORITY", "NORMAL:%COMPAT:!VERS-SSL3.0", false);
 #endif
 
     // Disable WebKit2 accelerated compositing here while we can't
diff --git a/src/client/application/secret-mediator.vala b/src/client/application/secret-mediator.vala
index 3f5bd0045e042739c553e9769310e31373798963..0a354dc2dfe1a855a47d0a01ff3b6f63fee369a2 100644
--- a/src/client/application/secret-mediator.vala
+++ b/src/client/application/secret-mediator.vala
@@ -157,13 +157,19 @@ public class SecretMediator : Geary.CredentialsMediator, Object {
         Secret.Service service = yield Secret.Service.get(
             Secret.ServiceFlags.OPEN_SESSION, cancellable
         );
-        Secret.Collection collection = yield Secret.Collection.for_alias(
+        Secret.Collection? collection = yield Secret.Collection.for_alias(
             service,
             Secret.COLLECTION_DEFAULT,
             Secret.CollectionFlags.NONE,
             cancellable
         );
-        if (collection.get_locked()) {
+
+        // For custom desktop setups, it is possible that the current
+        // session has a service responding on DBus but no password
+        // keyring. There's no much we can do in this case except just
+        // check for the collection being null so we don't crash. See
+        // Bug 795328.
+        if (collection != null && collection.get_locked()) {
             List<Secret.Collection> to_lock = new List<Secret.Collection>();
             to_lock.append(collection);
             List<DBusProxy> unlocked;
diff --git a/src/client/components/folder-popover.vala b/src/client/components/folder-popover.vala
index 9b377c8f6073705cd287b4f3401f32d2645eaf80..6612d5ceb12309598508056c1eda910da64f8b64 100644
--- a/src/client/components/folder-popover.vala
+++ b/src/client/components/folder-popover.vala
@@ -107,6 +107,9 @@ public class FolderPopover : Gtk.Popover {
     [GtkCallback]
     private void on_search_entry_search_changed() {
         invalidate_filter();
+        if (this.search_entry.get_text() != "") {
+            this.list_box.unselect_all();
+        }
     }
 
     private void invalidate_filter() {
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 30d24cb1ad378e84727a22cbd455300cc41d4da2..bb9be40830a3fc03e401aad1cae40e3a273269b1 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -1150,9 +1150,10 @@ public class ComposerWidget : Gtk.EventBox {
     private void on_detach() {
         if (this.state == ComposerState.DETACHED)
             return;
-        Gtk.Widget? focus = this.container.top_window.get_focus();
+
+        Gtk.Widget? focused_widget = this.container.top_window.get_focus();
         this.container.remove_composer();
-        ComposerWindow window = new ComposerWindow(this);
+        ComposerWindow new_window = new ComposerWindow(this);
 
         // Workaround a GTK+ crasher, Bug 771812. When the composer is
         // re-parented, its menu_button's popover keeps a reference to
@@ -1168,11 +1169,19 @@ public class ComposerWidget : Gtk.EventBox {
         this.state = ComposerWidget.ComposerState.DETACHED;
         this.header.detached();
         update_composer_view();
-        if (focus != null && focus.parent.visible) {
-            ComposerWindow focus_win = focus.get_toplevel() as ComposerWindow;
-            if (focus_win != null && focus_win == window)
-                focus.grab_focus();
-        } else {
+
+        // If the previously focused widget is in the new composer
+        // window then focus that, else focus something useful.
+        bool refocus = true;
+        if (focused_widget != null) {
+            ComposerWindow? focused_window =
+                focused_widget.get_toplevel() as ComposerWindow;
+            if (new_window == focused_window) {
+                focused_widget.grab_focus();
+                refocus = false;
+            }
+        }
+        if (refocus) {
             set_focus();
         }
     }
diff --git a/src/client/conversation-viewer/conversation-web-view.vala b/src/client/conversation-viewer/conversation-web-view.vala
index 588d39bda1d898f4404be7e2a008e43bcc3ba188..3d2ac5fbfc48cb9b49b7fefa6b869148a29dd32a 100644
--- a/src/client/conversation-viewer/conversation-web-view.vala
+++ b/src/client/conversation-viewer/conversation-web-view.vala
@@ -15,7 +15,7 @@ public class ConversationWebView : ClientWebView {
     // Key codes we don't forward on to the super class on key press
     // since we want to override them elsewhere, especially
     // ConversationListBox.
-    private const int[] BLACKLISTED_KEY_CODES = {
+    private const uint[] BLACKLISTED_KEY_CODES = {
         Gdk.Key.space,
         Gdk.Key.KP_Space,
         Gdk.Key.Up,
diff --git a/src/client/dialogs/attachment-dialog.vala b/src/client/dialogs/attachment-dialog.vala
index ecf1d68050bb989c66679d96ab3e2af4bcbe39ff..88e59e17cd2c080c3534ff36e153372667d1ebcb 100644
--- a/src/client/dialogs/attachment-dialog.vala
+++ b/src/client/dialogs/attachment-dialog.vala
@@ -60,7 +60,12 @@ public class AttachmentDialog : Object {
     public int run() {
         int response = this.chooser.run();
         if (response == Gtk.ResponseType.ACCEPT) {
-            this.config.attachments_dir = this.chooser.get_current_folder();
+            // Current folder can be null, e.g. if selecting an
+            // attachment from Recent Files
+            string? current_folder = this.chooser.get_current_folder();
+            if (!Geary.String.is_empty(current_folder)) {
+                this.config.attachments_dir = current_folder;
+            }
         }
         return response;
     }
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 0b0ce5e41ffe3518c99face230dcc95f29f06083..05e669ac3bad0a8e662daef8d022f60b4e77bcb0 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -138,7 +138,6 @@ public class Geary.AccountInformation : BaseObject {
         // being saved.
         get { return (allow_save_sent_mail() ? _save_sent_mail : true); }
         set { _save_sent_mail = value; }
-        default = true;
     }
 
     // Order for display purposes.
diff --git a/src/engine/api/geary-attachment.vala b/src/engine/api/geary-attachment.vala
index d46b66e15cd67fe0e1892212e16fe30e7d97f1ce..4c0a38f55611e4c92cea7dc68db747a8a55ce73d 100644
--- a/src/engine/api/geary-attachment.vala
+++ b/src/engine/api/geary-attachment.vala
@@ -155,7 +155,7 @@ public abstract class Geary.Attachment : BaseObject {
                 }
             }
             string? ext = mime_type.get_file_name_extension();
-            if (!file_name.has_suffix(ext)) {
+            if (ext != null && !file_name.has_suffix(ext)) {
                 file_name = file_name + (ext ?? "");
             }
         }
diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala
index af315646d544ba84332e18a8b35cd9856e36bb3c..80adf4c9ea300c4c5e117be191f8353b5e47b14c 100644
--- a/src/engine/imap-db/imap-db-folder.vala
+++ b/src/engine/imap-db/imap-db-folder.vala
@@ -1919,25 +1919,27 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
         MessageRow row = do_fetch_message_row(cx, location.message_id, Geary.Email.Field.FLAGS,
             out pre_fields, cancellable);
         post_fields = pre_fields;
-        
-        // compare flags for (a) any change at all and (b) unread changes
+
+        // Only update if changed
         Geary.Email row_email = row.to_email(location.email_id);
-        
-        if (row_email.email_flags != null && row_email.email_flags.equal_to(email.email_flags))
-            return;
-        
-        if (row_email.email_flags.is_unread() != email.email_flags.is_unread())
-            unread_count_change += email.email_flags.is_unread() ? 1 : -1;
-        
-        // write them out to the message row
-        Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map = new Gee.HashMap<ImapDB.EmailIdentifier,
-            Geary.EmailFlags>();
-        map.set((ImapDB.EmailIdentifier) email.id, email.email_flags);
-        
-        do_set_email_flags(cx, map, cancellable);
-        post_fields |= Geary.Email.Field.FLAGS;
+        if (row_email.email_flags == null ||
+            !row_email.email_flags.equal_to(email.email_flags)) {
+
+            // Check for unread count changes
+            if (row_email.email_flags != null &&
+                row_email.email_flags.is_unread() != email.email_flags.is_unread()) {
+                unread_count_change += email.email_flags.is_unread() ? 1 : -1;
+            }
+
+            Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags> map =
+               new Gee.HashMap<ImapDB.EmailIdentifier, Geary.EmailFlags>();
+            map.set((ImapDB.EmailIdentifier) email.id, email.email_flags);
+            do_set_email_flags(cx, map, cancellable);
+
+            post_fields |= Geary.Email.Field.FLAGS;
+        }
     }
-    
+
     private void do_merge_email(Db.Connection cx, LocationIdentifier location, Geary.Email email,
         out Geary.Email.Field pre_fields, out Geary.Email.Field post_fields,
         out Gee.Collection<Contact> updated_contacts, ref int unread_count_change,
diff --git a/src/engine/imap/message/imap-data-format.vala b/src/engine/imap/message/imap-data-format.vala
index 17a7ed5402f835ff1173f6d1fde6f42fd7e74caf..b3cd21b6ed1c2c851c362afeb802559212110aa5 100644
--- a/src/engine/imap/message/imap-data-format.vala
+++ b/src/engine/imap/message/imap-data-format.vala
@@ -7,7 +7,10 @@
 namespace Geary.Imap.DataFormat {
 
 private const char[] ATOM_SPECIALS = {
-    '(', ')', '{', ' ', '%', '*', '"'
+    '(', ')', '{', ' ', // CTL chars are handled by is_special_char
+    '%', '*',           // list-wildcards
+    '"', '\\',          // quoted-specials
+    ']'                 // resp-specials
 };
 
 private const char[] TAG_SPECIALS = {
@@ -21,11 +24,14 @@ public enum Quoting {
 }
 
 private bool is_special_char(char ch, char[] ar, string? exceptions) {
-    if (ch > 0x7F || ch.iscntrl())
+    // Check for CTL chars
+    if (ch <= 0x1F || ch >= 0x7F) {
         return true;
+    }
 
-    if (ch in ar)
-        return (exceptions != null) ? exceptions.index_of_char(ch) < 0 : true;
+    if (ch in ar) {
+        return (exceptions != null) ? Ascii.index_of(exceptions, ch) < 0 : true;
+    }
 
     return false;
 }
diff --git a/src/engine/imap/message/imap-fetch-body-data-specifier.vala b/src/engine/imap/message/imap-fetch-body-data-specifier.vala
index b45ba89718d43a6e6d028ba3bafa882efc58806d..b2dda653cfa20d8cb75b4f566f54063c12f99053 100644
--- a/src/engine/imap/message/imap-fetch-body-data-specifier.vala
+++ b/src/engine/imap/message/imap-fetch-body-data-specifier.vala
@@ -73,15 +73,15 @@ public class Geary.Imap.FetchBodyDataSpecifier : BaseObject, Gee.Hashable<FetchB
                     assert_not_reached();
             }
         }
-
+        
         public static SectionPart deserialize(string value) throws ImapError {
             if (String.is_empty(value))
                 return NONE;
-
-            switch (value.down()) {
+            
+            switch (Ascii.strdown(value)) {
                 case "header":
                     return HEADER;
-
+                
                 case "header.fields":
                     return HEADER_FIELDS;
                 
@@ -178,25 +178,23 @@ public class Geary.Imap.FetchBodyDataSpecifier : BaseObject, Gee.Hashable<FetchB
         this.subset_start = subset_start;
         this.subset_count = subset_count;
         this.is_peek = is_peek;
-
+        
         if (field_names != null && field_names.length > 0) {
-            this.field_names = new Gee.TreeSet<string>((s1, s2) => {
-                    return GLib.strcmp(s1, s2);
-                });
+            this.field_names = new Gee.TreeSet<string>(Ascii.strcmp);
             foreach (string field_name in field_names) {
-                string converted = field_name.strip().down();
-
+                string converted = Ascii.strdown(field_name.strip());
+                
                 if (!String.is_empty(converted))
                     this.field_names.add(converted);
             }
         } else {
             this.field_names = null;
         }
-
+        
         // see equal_to() for why the response version is used
         hashable = serialize_response();
     }
-
+    
     /**
      * Returns the {@link FetchBodyDataSpecifier} in a string ready for a {@link Command}.
      *
diff --git a/src/engine/imap/message/imap-flag.vala b/src/engine/imap/message/imap-flag.vala
index c26f59fb863d463ce49db722c674afa974b19419..96dbfa56d2056ba0776a24aa915dd9b849e3a4dc 100644
--- a/src/engine/imap/message/imap-flag.vala
+++ b/src/engine/imap/message/imap-flag.vala
@@ -15,11 +15,16 @@
 
 public abstract class Geary.Imap.Flag : BaseObject, Gee.Hashable<Geary.Imap.Flag> {
     public string value { get; private set; }
-    
-    public Flag(string value) {
-        this.value = value;
+
+    /**
+     * Constructs a new flag.
+     *
+     * The given keyword must be an IMAP atom.
+     */
+    public Flag(string name) {
+        this.value = name;
     }
-    
+
     public bool is_system() {
         return value[0] == '\\';
     }
@@ -31,14 +36,14 @@ public abstract class Geary.Imap.Flag : BaseObject, Gee.Hashable<Geary.Imap.Flag
     public bool equal_to(Geary.Imap.Flag flag) {
         return (flag == this) ? true : flag.equals_string(value);
     }
-    
+
     /**
      * Returns the {@link Flag} as an appropriate {@link Parameter}.
      */
     public StringParameter to_parameter() throws ImapError {
-        return StringParameter.get_best_for(value);
+        return new UnquotedStringParameter(value);
     }
-    
+
     public uint hash() {
         return Ascii.stri_hash(value);
     }
diff --git a/src/engine/imap/message/imap-internal-date.vala b/src/engine/imap/message/imap-internal-date.vala
index 3240ba0b5327fa4981abe95e17cb606e6c4b84fc..50317e2132e28a0c0c33464d8efa247e7685553d 100644
--- a/src/engine/imap/message/imap-internal-date.vala
+++ b/src/engine/imap/message/imap-internal-date.vala
@@ -64,18 +64,18 @@ public class Geary.Imap.InternalDate : Geary.MessageData.AbstractMessageData, Ge
             || year < 1970) {
             throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad numerical range", internaldate);
         }
-
+        
         // check month (this catches localization problems)
         int month = -1;
-        string mon_down = ((string) mon).down();
+        string mon_down = Ascii.strdown(((string) mon));
         for (int ctr = 0; ctr < EN_US_MON_DOWN.length; ctr++) {
             if (mon_down == EN_US_MON_DOWN[ctr]) {
                 month = ctr;
-
+                
                 break;
             }
         }
-
+        
         if (month < 0)
             throw new ImapError.PARSE_ERROR("Invalid INTERNALDATE \"%s\": bad month", internaldate);
         
diff --git a/src/engine/imap/message/imap-mailbox-parameter.vala b/src/engine/imap/message/imap-mailbox-parameter.vala
deleted file mode 100644
index 7447a859703e6c37a0595bf96d8e629eab764ed7..0000000000000000000000000000000000000000
--- a/src/engine/imap/message/imap-mailbox-parameter.vala
+++ /dev/null
@@ -1,60 +0,0 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
- *
- * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
- */
-
-/**
- * A {@link StringParameter} that holds a mailbox reference (can be wildcarded).
- *
- * Used to juggle between our internal UTF-8 representation of mailboxes and IMAP's
- * odd "modified UTF-7" representation.  The value is stored in IMAP's encoded
- * format since that's how it comes across the wire.
- */
-
-public class Geary.Imap.MailboxParameter : StringParameter {
-    public MailboxParameter(string mailbox) {
-        base (utf8_to_imap_utf7(mailbox));
-    }
-    
-    public MailboxParameter.from_string_parameter(StringParameter string_parameter) {
-        base (string_parameter.ascii);
-    }
-    
-    private static string utf8_to_imap_utf7(string utf8) {
-        try {
-            return Geary.ImapUtf7.utf8_to_imap_utf7(utf8);
-        } catch (ConvertError e) {
-            debug("Error encoding mailbox name '%s': %s", utf8, e.message);
-            return utf8;
-        }
-    }
-    
-    private static string imap_utf7_to_utf8(string imap_utf7) {
-        try {
-            return Geary.ImapUtf7.imap_utf7_to_utf8(imap_utf7);
-        } catch (ConvertError e) {
-            debug("Invalid mailbox name '%s': %s", imap_utf7, e.message);
-            return imap_utf7;
-        }
-    }
-    
-    public string decode() {
-        return imap_utf7_to_utf8(ascii);
-    }
-    
-    /**
-     * {@inheritDoc}
-     */
-    public override void serialize(Serializer ser, Tag tag) throws Error {
-        serialize_string(ser);
-    }
-    
-    /**
-     * {@inheritDoc}
-     */
-    public override string to_string() {
-        return ascii;
-    }
-}
-
diff --git a/src/engine/imap/message/imap-mailbox-specifier.vala b/src/engine/imap/message/imap-mailbox-specifier.vala
index fa0c7c0930be0a8ae5a7b4f2f981ddf4903a54fb..19306d5ac9e86a835dba684c43a6fdab394859c1 100644
--- a/src/engine/imap/message/imap-mailbox-specifier.vala
+++ b/src/engine/imap/message/imap-mailbox-specifier.vala
@@ -43,15 +43,37 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
      * See [[http://tools.ietf.org/html/rfc3501#section-5.1]]
      */
     public bool is_inbox { get; private set; }
-    
+
+    /**
+     * Constructs a new specifier from a UTF-8 name.
+     */
     public MailboxSpecifier(string name) {
         init(name);
     }
-    
-    public MailboxSpecifier.from_parameter(MailboxParameter param) {
-        init(param.decode());
+
+    /**
+     * Constructs a new specifier from a IMAP modified-UTF-7 string.
+     *
+     * If a modified-UTF-7 decoding error occurs, the parameter will
+     * assumed to be UTF-8, repaired, and used instead.
+     */
+    public MailboxSpecifier.from_parameter(StringParameter param) {
+        string? name = null;
+        try {
+            name = Geary.ImapUtf7.imap_utf7_to_utf8(param.ascii);
+        } catch (ConvertError err) {
+            // Could no decode the name as IMAP modified UTF7, so per
+            // https://imapwiki.org/ClientImplementation/MailboxList
+            // assume UTF8.
+            debug(
+                "Error decoding mailbox name, assuming UTF-8: %s", err.message
+            );
+            name = param.ascii;
+        }
+
+        init(name);
     }
-    
+
     /**
      * Returns true if the {@link Geary.FolderPath} points to the IMAP Inbox.
      */
@@ -72,7 +94,7 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
     public static bool is_inbox_name(string name) {
         return Ascii.stri_equal(name, CANONICAL_INBOX_NAME);
     }
-
+    
     /**
      * Returns true if the string is the ''canonical'' name of the IMAP Inbox.
      *
@@ -85,9 +107,9 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
      * @see is_inbox_name
      */
     public static bool is_canonical_inbox_name(string name) {
-        return (name == CANONICAL_INBOX_NAME);
+        return Ascii.str_equal(name, CANONICAL_INBOX_NAME);
     }
-
+    
     /**
      * Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
      */
@@ -167,35 +189,42 @@ public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpeci
         
         return !String.is_empty(basename) ? basename : name;
     }
-    
+
     public Parameter to_parameter() {
-        return new MailboxParameter(name);
+        string encoded= Geary.ImapUtf7.utf8_to_imap_utf7(this.name);
+        Parameter? param = null;
+        try {
+            param = StringParameter.get_best_for(encoded);
+        } catch (ImapError err) {
+            param = new LiteralParameter(new Geary.Memory.StringBuffer(encoded));
+        }
+        return param;
     }
-    
+
     public uint hash() {
         return is_inbox ? Ascii.stri_hash(name) : Ascii.str_hash(name);
     }
-
+    
     public bool equal_to(MailboxSpecifier other) {
         if (this == other)
             return true;
-
+        
         if (is_inbox)
             return Ascii.stri_equal(name, other.name);
-
-        return (name == other.name);
+        
+        return Ascii.str_equal(name, other.name);
     }
-
+    
     public int compare_to(MailboxSpecifier other) {
         if (this == other)
             return 0;
-
+        
         if (is_inbox && other.is_inbox)
             return 0;
-
-        return GLib.strcmp(name, other.name);
+        
+        return Ascii.strcmp(name, other.name);
     }
-
+    
     public string to_string() {
         return name;
     }
diff --git a/src/engine/imap/parameter/imap-string-parameter.vala b/src/engine/imap/parameter/imap-string-parameter.vala
index 5398451798e77a6b5d64cf92e0b79c31a88326ef..aee3553543b31ec99b189f4d642f913398bf3326 100644
--- a/src/engine/imap/parameter/imap-string-parameter.vala
+++ b/src/engine/imap/parameter/imap-string-parameter.vala
@@ -134,35 +134,35 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
     public bool is_empty() {
         return String.is_empty(ascii);
     }
-
+    
     /**
      * Case-sensitive comparison.
      */
     public bool equals_cs(string value) {
-        return (ascii == value);
+        return Ascii.str_equal(ascii, value);
     }
-
+    
     /**
      * Case-insensitive comparison.
      */
     public bool equals_ci(string value) {
         return Ascii.stri_equal(ascii, value);
     }
-
+    
     /**
      * Returns the string lowercased.
      */
     public string as_lower() {
-        return ascii.down();
+        return Ascii.strdown(ascii);
     }
-
+    
     /**
      * Returns the string uppercased.
      */
     public string as_upper() {
-        return ascii.up();
+        return Ascii.strup(ascii);
     }
-
+    
     /**
      * Converts the {@link ascii} to a signed 32-bit integer, clamped between clamp_min and
      * clamp_max.
diff --git a/src/engine/imap/response/imap-mailbox-information.vala b/src/engine/imap/response/imap-mailbox-information.vala
index eb331c1a780ee2d111c64d201af16b2d878f4f02..6d8c3a0cdc1f6ed6781ca85763b79c266bdb854c 100644
--- a/src/engine/imap/response/imap-mailbox-information.vala
+++ b/src/engine/imap/response/imap-mailbox-information.vala
@@ -71,19 +71,21 @@ public class Geary.Imap.MailboxInformation : BaseObject {
         // decode everything
         MailboxAttributes attributes = new MailboxAttributes(attrlist);
         StringParameter? delim = server_data.get_as_nullable_string(3);
-        MailboxParameter mailbox = new MailboxParameter.from_string_parameter(
-            server_data.get_as_string(4));
-        
-        // Set \Inbox to standard path
-        if (canonical_inbox && Geary.Imap.MailboxAttribute.SPECIAL_FOLDER_INBOX in attributes) {
-            return new MailboxInformation(MailboxSpecifier.inbox,
-                (delim != null) ? delim.nullable_ascii : null, attributes);
-        } else {
-            return new MailboxInformation(new MailboxSpecifier.from_parameter(mailbox),
-                (delim != null) ? delim.nullable_ascii : null, attributes);
-        }
+        StringParameter mailbox = server_data.get_as_string(4);
+
+        // If special-use flag \Inbox is set just use the canonical
+        // Inbox name, otherwise decode it
+        MailboxSpecifier? specifier =
+            (canonical_inbox &&
+             Geary.Imap.MailboxAttribute.SPECIAL_FOLDER_INBOX in attributes)
+            ? MailboxSpecifier.inbox
+            : new MailboxSpecifier.from_parameter(mailbox);
+
+        return new MailboxInformation(
+            specifier, (delim != null) ? delim.nullable_ascii : null, attributes
+        );
     }
-    
+
     /**
      * The {@link Geary.FolderPath} for the {@link mailbox}.
      *
diff --git a/src/engine/imap/response/imap-response-code-type.vala b/src/engine/imap/response/imap-response-code-type.vala
index f0f48ee50c3383a9c020ddc3f0d2f9949f30f05f..79cf75a4d5c28b61d5e7781bbaa0042046f3e90e 100644
--- a/src/engine/imap/response/imap-response-code-type.vala
+++ b/src/engine/imap/response/imap-response-code-type.vala
@@ -64,17 +64,17 @@ public class Geary.Imap.ResponseCodeType : BaseObject, Gee.Hashable<ResponseCode
     public ResponseCodeType.from_parameter(StringParameter stringp) throws ImapError {
         init(stringp.ascii);
     }
-
+    
     private void init(string ascii) throws ImapError {
         // note that is_quoting_required() also catches empty strings (as they require quoting)
         if (DataFormat.is_quoting_required(ascii) != DataFormat.Quoting.OPTIONAL)
             throw new ImapError.INVALID("\"%s\" cannot be represented as a ResponseCodeType", ascii);
-
+        
         // store lowercased so it's easily compared with const strings above
         original = ascii;
-        value = ascii.down();
+        value = Ascii.strdown(ascii);
     }
-
+    
     public bool is_value(string str) {
         return Ascii.stri_equal(value, str);
     }
diff --git a/src/engine/imap/response/imap-status-data.vala b/src/engine/imap/response/imap-status-data.vala
index 81a31a397b139e0c4bd7f93bed6d8469bfff750f..8992d227f4bf075ca1acb9e804108ae117323fa2 100644
--- a/src/engine/imap/response/imap-status-data.vala
+++ b/src/engine/imap/response/imap-status-data.vala
@@ -74,10 +74,9 @@ public class Geary.Imap.StatusData : Object {
             throw new ImapError.PARSE_ERROR("Bad STATUS command name in response \"%s\"",
                 server_data.to_string());
         }
-        
-        MailboxParameter mailbox_param = new MailboxParameter.from_string_parameter(
-            server_data.get_as_string(2));
-        
+
+        StringParameter mailbox_param = server_data.get_as_string(2);
+
         int messages = UNSET;
         int recent = UNSET;
         UID? uid_next = null;
diff --git a/src/engine/mime/mime-content-parameters.vala b/src/engine/mime/mime-content-parameters.vala
index 8691655e22ef9dbbb88a131b7fbc387fbf98c30e..71e579ee92881ef0cadbe793ab1ac97cf9cb4046 100644
--- a/src/engine/mime/mime-content-parameters.vala
+++ b/src/engine/mime/mime-content-parameters.vala
@@ -78,7 +78,7 @@ public class Geary.Mime.ContentParameters : BaseObject {
         
         return (stored != null) ? Ascii.stri_equal(stored, value) : false;
     }
-
+    
     /**
      * Returns true if the attribute has the supplied value (case-sensitive comparison).
      *
@@ -86,10 +86,10 @@ public class Geary.Mime.ContentParameters : BaseObject {
      */
     public bool has_value_cs(string attribute, string value) {
         string? stored = params.get(attribute);
-
-        return (stored != null) ? (stored == value) : false;
+        
+        return (stored != null) ? Ascii.str_equal(stored, value) : false;
     }
-
+    
     /**
      * Add or replace the parameter.
      *
diff --git a/src/engine/mime/mime-content-type.vala b/src/engine/mime/mime-content-type.vala
index 59d530b33301d082627d5ba15d4c8d0ab79815b9..8f12e5501bb35b0fa10d42686aa14b41d151fd2a 100644
--- a/src/engine/mime/mime-content-type.vala
+++ b/src/engine/mime/mime-content-type.vala
@@ -79,9 +79,9 @@ public class Geary.Mime.ContentType : Geary.BaseObject {
             int max_len = 4096;
             // XXX determine actual max needed buffer size using
             // xdg_mime_get_max_buffer_extents?
-            uint8[] data = (max_len > buf.size)
-                ? buf.get_bytes()[0:max_len - 1].get_data()
-                : buf.get_uint8_array();
+            uint8[] data = (buf.size <= max_len)
+                ? buf.get_uint8_array()
+                : buf.get_bytes()[0:max_len].get_data();
 
             // XXX might just want to use xdgmime lib directly here to
             // avoid the intermediate glib_content_type step here?
diff --git a/src/engine/mime/mime-disposition-type.vala b/src/engine/mime/mime-disposition-type.vala
index 01d195f22d7b2b17e3a9e1fc33507536692dc9a7..18e27b24b28c8e43182dd34838a5f4aafa51549d 100644
--- a/src/engine/mime/mime-disposition-type.vala
+++ b/src/engine/mime/mime-disposition-type.vala
@@ -31,24 +31,24 @@ public enum Geary.Mime.DispositionType {
      */
     public static DispositionType deserialize(string? str, out bool is_unknown) {
         is_unknown = false;
-
+        
         if (String.is_empty_or_whitespace(str))
             return UNSPECIFIED;
-
-        switch (str.down()) {
+        
+        switch (Ascii.strdown(str)) {
             case "inline":
                 return INLINE;
-
+            
             case "attachment":
                 return ATTACHMENT;
-
+            
             default:
                 is_unknown = true;
-
+                
                 return ATTACHMENT;
         }
     }
-
+    
     /**
      * Returns null if value is {@link UNSPECIFIED}
      */
diff --git a/src/engine/mime/mime-multipart-subtype.vala b/src/engine/mime/mime-multipart-subtype.vala
index 050a38ed189aa160ba7d4271e6a9bc0a55902f9b..4de09a3e13d3970a95087d2de2da31d1e711a772 100644
--- a/src/engine/mime/mime-multipart-subtype.vala
+++ b/src/engine/mime/mime-multipart-subtype.vala
@@ -48,24 +48,24 @@ public enum Geary.Mime.MultipartSubtype {
     public static MultipartSubtype from_content_type(ContentType? content_type, out bool is_unknown) {
         if (content_type == null || !content_type.has_media_type("multipart")) {
             is_unknown = true;
-
+            
             return MIXED;
         }
-
+        
         is_unknown = false;
-        switch (content_type.media_subtype.down()) {
+        switch (Ascii.strdown(content_type.media_subtype)) {
             case "mixed":
                 return MIXED;
-
+            
             case "alternative":
                 return ALTERNATIVE;
-
+            
             case "related":
                 return RELATED;
-
+            
             default:
                 is_unknown = true;
-
+                
                 return MIXED;
         }
     }
diff --git a/src/engine/rfc822/rfc822-mailbox-address.vala b/src/engine/rfc822/rfc822-mailbox-address.vala
index 1c3af97c399022a6c4fba492cfa9af8c121451b6..345bed8aaef0865d7f6bb9dfa3e6b1fc188a412d 100644
--- a/src/engine/rfc822/rfc822-mailbox-address.vala
+++ b/src/engine/rfc822/rfc822-mailbox-address.vala
@@ -53,7 +53,7 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa
 
         source_route = null;
 
-        int atsign = address.index_of_char('@');
+        int atsign = Ascii.index_of(address, '@');
         if (atsign > 0) {
             mailbox = address.slice(0, atsign);
             domain = address.slice(atsign + 1, address.length);
@@ -62,7 +62,7 @@ public class Geary.RFC822.MailboxAddress : Geary.MessageData.SearchableMessageDa
             domain = "";
         }
     }
-
+    
     public MailboxAddress.imap(string? name, string? source_route, string mailbox, string domain) {
         this.name = (name != null) ? decode_name(name) : null;
         this.source_route = source_route;
diff --git a/src/engine/smtp/smtp-command.vala b/src/engine/smtp/smtp-command.vala
index 2a091d24ca726a653270042d15b4def19f93e249..47db7648ef8b8a01d083eec48d356ac45557eb63 100644
--- a/src/engine/smtp/smtp-command.vala
+++ b/src/engine/smtp/smtp-command.vala
@@ -56,36 +56,36 @@ public enum Geary.Smtp.Command {
                 assert_not_reached();
         }
     }
-
+    
     public static Command deserialize(string str) throws SmtpError {
-        switch (str.down()) {
+        switch (Ascii.strdown(str)) {
             case "helo":
                 return HELO;
-
+            
             case "ehlo":
                 return EHLO;
-
+            
             case "quit":
                 return QUIT;
-
+            
             case "help":
                 return HELP;
-
+            
             case "noop":
                 return NOOP;
-
+            
             case "rset":
                 return RSET;
-
+            
             case "auth":
                 return AUTH;
-
+            
             case "mail":
                 return MAIL;
-
+            
             case "rcpt":
                 return RCPT;
-
+            
             case "data":
                 return DATA;
 
diff --git a/src/engine/smtp/smtp-greeting.vala b/src/engine/smtp/smtp-greeting.vala
index 3eb91154fc789918f95a69bbf94c375fec2ff456..676ef2500aa4e828dcf797b4f489be098e94cbdc 100644
--- a/src/engine/smtp/smtp-greeting.vala
+++ b/src/engine/smtp/smtp-greeting.vala
@@ -25,21 +25,21 @@ public class Geary.Smtp.Greeting : Response {
                     return "";
             }
         }
-
+        
         public static ServerFlavor deserialize(string str) {
-            switch (str.up()) {
+            switch (Ascii.strup(str)) {
                 case "SMTP":
                     return SMTP;
-
+                
                 case "ESMTP":
                     return ESMTP;
-
+                
                 default:
                     return UNSPECIFIED;
             }
         }
     }
-
+    
     public string? domain { get; private set; default = null; }
     public ServerFlavor flavor { get; private set; default = ServerFlavor.UNSPECIFIED; }
     public string? message { get; private set; default = null; }
diff --git a/src/engine/util/util-ascii.vala b/src/engine/util/util-ascii.vala
index 20fecc44053e453fa5c3fdbc089cdcf13691161e..4d915f68a3169d8df1162a5b4dd387c71654cdd8 100644
--- a/src/engine/util/util-ascii.vala
+++ b/src/engine/util/util-ascii.vala
@@ -4,40 +4,71 @@
  * (version 2.1 or later).  See the COPYING file in this distribution.
  */
 
+// These calls are bound to the string class in Vala 0.26.  When that version of Vala is the
+// minimum, these can be dropped and Ascii.strup and Ascii.strdown can use the string methods.
+extern string g_ascii_strup(string str, ssize_t len = -1);
+extern string g_ascii_strdown(string str, ssize_t len = -1);
+
 namespace Geary.Ascii {
 
+public int index_of(string str, char ch) {
+    char *strptr = str;
+    int index = 0;
+    for (;;) {
+        char strch = *strptr++;
+        
+        if (strch == String.EOS)
+            return -1;
+        
+        if (strch == ch)
+            return index;
+        
+        index++;
+    }
+}
+
 public bool get_next_char(string str, ref int index, out char ch) {
     ch = str[index++];
-
+    
     return ch != String.EOS;
 }
 
-public bool stri_equal(string a, string b) {
-    // XXX Is this marginally faster than a.down() == b.down() in the
-    // best case, slower in the worse case, so not worth it?
+public inline int strcmp(string a, string b) {
+    return GLib.strcmp(a, b);
+}
+
+public int stricmp(string a, string b) {
     char *aptr = a;
     char *bptr = b;
     for (;;) {
         int diff = (int) (*aptr).tolower() - (int) (*bptr).tolower();
         if (diff != 0)
-            return false;
-
+            return diff;
+        
         if (*aptr == String.EOS)
-            return true;
-
+            return 0;
+        
         aptr++;
         bptr++;
     }
 }
 
+public inline bool str_equal(string a, string b) {
+    return a == b;
+}
+
+public inline bool stri_equal(string a, string b) {
+    return stricmp(a, b) == 0;
+}
+
 public bool nullable_stri_equal(string? a, string? b) {
     if (a == null)
         return (b == null);
-
+    
     // a != null, so always false
     if (b == null)
         return false;
-
+    
     return stri_equal(a, b);
 }
 
@@ -55,6 +86,14 @@ public uint nullable_stri_hash(string? str) {
     return (str != null) ? stri_hash(str) : 0;
 }
 
+public string strdown(string str) {
+    return g_ascii_strdown(str);
+}
+
+public string strup(string str) {
+    return g_ascii_strup(str);
+}
+
 /**
  * Returns true if the ASCII string contains only whitespace and at least one numeric character.
  */
diff --git a/src/engine/util/util-imap-utf7.vala b/src/engine/util/util-imap-utf7.vala
index ccd1078279f4abe2e94a51b62eeb2a740cf1c610..72ee8f38d5e4abb345e05f58a10bf1df8a9d6018 100644
--- a/src/engine/util/util-imap-utf7.vala
+++ b/src/engine/util/util-imap-utf7.vala
@@ -94,7 +94,7 @@ private int first_encode_index(string str) {
     return -1;
 }
 
-public string utf8_to_imap_utf7(string str) throws ConvertError {
+public string utf8_to_imap_utf7(string str) {
     int p = first_encode_index(str);
     if (p < 0) {
         /* no characters that need to be encoded */
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index afcca95c9ac9a9e82958d3b6ec4953f3147a4c41..f2d38668b966e87e9ed708e1c93b6e80ca813af8 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -8,6 +8,8 @@ set(TEST_SRC
 
   engine/api/geary-attachment-test.vala
   engine/api/geary-engine-test.vala
+  engine/imap/message/imap-data-format-test.vala
+  engine/imap/message/imap-mailbox-specifier-test.vala
   engine/imap/transport/imap-deserializer-test.vala
   engine/mime-content-type-test.vala
   engine/rfc822-mailbox-address-test.vala
diff --git a/test/engine/api/geary-attachment-test.vala b/test/engine/api/geary-attachment-test.vala
index 5269c02bca45c5e8aa41b5129b22ee4cb9f24efc..7121330f5d264cd8bb35ffffc1428c27b807aede 100644
--- a/test/engine/api/geary-attachment-test.vala
+++ b/test/engine/api/geary-attachment-test.vala
@@ -56,6 +56,8 @@ class Geary.AttachmentTest : Gee.TestCase {
                  get_safe_file_name_with_default_content_type);
         add_test("get_safe_file_name_with_default_content_type_bad_file_name",
                  get_safe_file_name_with_default_content_type_bad_file_name);
+        add_test("get_safe_file_name_with_unknown_content_type",
+                 get_safe_file_name_with_unknown_content_type);
     }
 
     public override void set_up() {
@@ -235,4 +237,24 @@ class Geary.AttachmentTest : Gee.TestCase {
         assert(test.get_safe_file_name.end(async_result()) == RESULT_FILENAME);
     }
 
+    public void get_safe_file_name_with_unknown_content_type() {
+        const string TEST_FILENAME = "test-filename.unlikely";
+        Attachment test = new TestAttachment(
+            ATTACHMENT_ID,
+            this.default_type,
+            CONTENT_ID,
+            CONTENT_DESC,
+            content_disposition,
+            TEST_FILENAME,
+            File.new_for_path(TEST_FILENAME),
+            742
+        );
+
+        test.get_safe_file_name.begin(null, (obj, ret) => {
+                async_complete(ret);
+            });
+
+        assert(TEST_FILENAME == test.get_safe_file_name.end(async_result()));
+    }
+
 }
diff --git a/test/engine/imap/message/imap-data-format-test.vala b/test/engine/imap/message/imap-data-format-test.vala
new file mode 100644
index 0000000000000000000000000000000000000000..b92503ec3272aeff894b9af6c948b98e7558be34
--- /dev/null
+++ b/test/engine/imap/message/imap-data-format-test.vala
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2018 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.DataFormatTest : Gee.TestCase {
+
+
+    public DataFormatTest() {
+        base("Geary.Imap.DataFormatTest");
+        add_test("is_atom_special", is_atom_special);
+    }
+
+    public void is_atom_special() {
+        assert_true(
+            !DataFormat.is_atom_special('a') && !DataFormat.is_atom_special('z')
+        );
+        assert_true(
+            !DataFormat.is_atom_special('A') && !DataFormat.is_atom_special('Z')
+        );
+        assert_true(
+            !DataFormat.is_atom_special('0') && !DataFormat.is_atom_special('9')
+        );
+        assert_true(
+            !DataFormat.is_atom_special('#') &&
+            !DataFormat.is_atom_special('.') &&
+            !DataFormat.is_atom_special('+') &&
+            !DataFormat.is_atom_special('/') &&
+            !DataFormat.is_atom_special('~') &&
+            !DataFormat.is_atom_special(':')
+        );
+
+        // atom-specials
+        assert_true(
+            DataFormat.is_atom_special('(')
+        );
+        assert_true(
+            DataFormat.is_atom_special(')')
+        );
+        assert_true(
+            DataFormat.is_atom_special('{')
+        );
+        assert_true(
+            DataFormat.is_atom_special(' ')
+        );
+        assert_true(
+            DataFormat.is_atom_special(0x00)
+        );
+        assert_true(
+            DataFormat.is_atom_special(0x1F)
+        );
+        assert_true(
+            DataFormat.is_atom_special(0x7F)
+        );
+        assert_true(
+            DataFormat.is_atom_special(0x80)
+        );
+        assert_true(
+            DataFormat.is_atom_special(0xFE)
+        );
+
+        // list-wildcards
+        assert_true(
+            DataFormat.is_atom_special('%')
+        );
+        assert_true(
+            DataFormat.is_atom_special('*')
+        );
+
+        // quoted-specials
+        assert_true(
+            DataFormat.is_atom_special('\"')
+        );
+        assert_true(
+            DataFormat.is_atom_special('\\')
+        );
+
+        // resp-specials
+        assert_true(
+            DataFormat.is_atom_special(']')
+        );
+    }
+
+}
diff --git a/test/engine/imap/message/imap-mailbox-specifier-test.vala b/test/engine/imap/message/imap-mailbox-specifier-test.vala
new file mode 100644
index 0000000000000000000000000000000000000000..336202ad302577719af2d4b17bfe58052b9c264e
--- /dev/null
+++ b/test/engine/imap/message/imap-mailbox-specifier-test.vala
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2018 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.MailboxSpecifierTest : Gee.TestCase {
+
+
+    public MailboxSpecifierTest() {
+        base("Geary.Imap.MailboxSpecifierTest");
+        add_test("to_parameter", to_parameter);
+        add_test("from_parameter", from_parameter);
+    }
+
+    public void to_parameter() {
+        assert(
+            "test" ==
+            new MailboxSpecifier("test").to_parameter().to_string()
+        );
+        assert(
+            "foo/bar" ==
+            new MailboxSpecifier("foo/bar").to_parameter().to_string()
+        );
+
+        // The param won't be quoted or escaped since
+        // QuotedStringParameter doesn't actually handle that, so just
+        // check that it is correct type
+        Parameter quoted = new MailboxSpecifier("""foo\bar""").to_parameter();
+        assert(quoted is QuotedStringParameter);
+
+        assert(
+            "ol&AOk-" ==
+            new MailboxSpecifier("olé").to_parameter().to_string()
+        );
+    }
+
+    public void from_parameter() {
+        assert(
+            "test" ==
+            new MailboxSpecifier.from_parameter(
+                new UnquotedStringParameter("test")).name
+        );
+
+        // This won't be quoted or escaped since QuotedStringParameter
+        // doesn't actually handle that.
+        assert(
+            "foo\\bar" ==
+            new MailboxSpecifier.from_parameter(
+                new QuotedStringParameter("""foo\bar""")).name
+        );
+        assert(
+            "olé" ==
+            new MailboxSpecifier.from_parameter(
+                new UnquotedStringParameter("ol&AOk-")).name
+        );
+    }
+
+}
diff --git a/test/engine/imap/transport/imap-deserializer-test.vala b/test/engine/imap/transport/imap-deserializer-test.vala
index d8b1f1ab8fcbbe8f4868aa3555fc194ea2816632..6d9136f8f1d680653e1cb9ee5a5d5a34b79c54ff 100644
--- a/test/engine/imap/transport/imap-deserializer-test.vala
+++ b/test/engine/imap/transport/imap-deserializer-test.vala
@@ -48,8 +48,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_gmail_greeting() {
         string greeting = "* OK Gimap ready for requests from 115.187.245.46 c194mb399904375ivc";
-        this.stream.add_data(greeting.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(greeting.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -59,8 +59,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_cyrus_2_4_greeting() {
         string greeting = "* OK [CAPABILITY IMAP4rev1 LITERAL+ ID ENABLE AUTH=PLAIN SASL-IR] mogul Cyrus IMAP v2.4.12-Debian-2.4.12-2 server ready";
-        this.stream.add_data(greeting.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(greeting.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -71,8 +71,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
     public void test_aliyun_greeting() {
         string greeting = "* OK AliYun IMAP Server Ready(10.147.40.164)";
         string parsed = "* OK AliYun IMAP Server Ready (10.147.40.164)";
-        this.stream.add_data(greeting.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(greeting.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -82,8 +82,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_invalid_atom_prefix() {
         string flags = """* OK %atom""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.DESER_FAIL, (obj, ret) => { async_complete(ret); });
         this.process.end(async_result());
@@ -91,8 +91,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_gmail_flags() {
         string flags = """* FLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing)""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -102,8 +102,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_gmail_permanent_flags() {
         string flags = """* OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing \*)] Flags permitted.""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -113,8 +113,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_cyrus_flags() {
         string flags = """* 2934 FETCH (FLAGS (\Answered \Seen $Quuxo::Spam::Trained) UID 3041)""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -128,8 +128,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
         // distinct atom.
         string flags = """* OK \*atom""";
         string expected = """* OK \* atom""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.MESSAGE, (obj, ret) => { async_complete(ret); });
         RootParameters? message = this.process.end(async_result());
@@ -139,8 +139,8 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_invalid_flag_prefix() {
         string flags = """* OK \%atom""";
-        this.stream.add_data(flags.data);
-        this.stream.add_data(EOL.data);
+        this.stream.add_data(flags.data, g_free);
+        this.stream.add_data(EOL.data, g_free);
 
         this.process.begin(Expect.DESER_FAIL, (obj, ret) => { async_complete(ret); });
         this.process.end(async_result());
@@ -154,7 +154,7 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
 
     public void test_bye_eos() {
         string bye = """* OK bye""";
-        this.stream.add_data(bye.data);
+        this.stream.add_data(bye.data, g_free);
 
         bool eos = false;
         this.deser.eos.connect(() => { eos = true; });
@@ -211,6 +211,12 @@ class Geary.Imap.DeserializerTest : Gee.TestCase {
             assert_not_reached();
         }
 
+        // Process any remaining async tasks the deserializer might
+        // have left over.
+        while (this.main_loop.pending()) {
+            this.main_loop.iteration(true);
+        }
+
         return message;
     }
 
diff --git a/test/main.vala b/test/main.vala
index 7d708f07efc532df940875b55fb8223f8ebe3f45..83506753a591db1cdcd067d389397ff43edf3fc0 100644
--- a/test/main.vala
+++ b/test/main.vala
@@ -40,7 +40,9 @@ int main(string[] args) {
     engine.add_suite(new Geary.AttachmentTest().get_suite());
     engine.add_suite(new Geary.EngineTest().get_suite());
     engine.add_suite(new Geary.HTML.UtilTest().get_suite());
+    engine.add_suite(new Geary.Imap.DataFormatTest().get_suite());
     engine.add_suite(new Geary.Imap.DeserializerTest().get_suite());
+    engine.add_suite(new Geary.Imap.MailboxSpecifierTest().get_suite());
     engine.add_suite(new Geary.IdleManagerTest().get_suite());
     engine.add_suite(new Geary.Inet.Test().get_suite());
     engine.add_suite(new Geary.JS.Test().get_suite());