Bug-1562313 - Convert javascript components from chat to static registration. r=mkmelin draft
authorKhushil Mistry <khushil324@gmail.com>
Wed, 04 Dec 2019 15:17:01 +0530
changeset 82240 bd3db1e73766063f83876f8031936895105cf6ac
parent 82239 9a2171cc93933aa39ee042e7379548c8eacbd9e2
child 82241 73153356fea5ecd76ca787017c64cbaad4efa1b3
push id9938
push userkhushil324@gmail.com
push dateWed, 04 Dec 2019 10:03:25 +0000
treeherdertry-comm-central@73153356fea5 [default view] [failures only]
reviewersmkmelin
bugs1562313
Bug-1562313 - Convert javascript components from chat to static registration. r=mkmelin
chat/components/src/components.conf
chat/components/src/imAccounts.js
chat/components/src/imAccounts.jsm
chat/components/src/imAccounts.manifest
chat/components/src/imCommands.js
chat/components/src/imCommands.jsm
chat/components/src/imCommands.manifest
chat/components/src/imContacts.js
chat/components/src/imContacts.jsm
chat/components/src/imContacts.manifest
chat/components/src/imConversations.js
chat/components/src/imConversations.jsm
chat/components/src/imConversations.manifest
chat/components/src/imCore.js
chat/components/src/imCore.jsm
chat/components/src/imCore.manifest
chat/components/src/logger.js
chat/components/src/logger.jsm
chat/components/src/logger.manifest
chat/components/src/moz.build
chat/components/src/smileProtocolHandler.js
chat/components/src/smileProtocolHandler.jsm
chat/components/src/smileProtocolHandler.manifest
chat/components/src/test/test_commands.js
chat/components/src/test/test_conversations.js
chat/components/src/test/test_logger.js
chat/protocols/facebook/components.conf
chat/protocols/facebook/facebook.js
chat/protocols/facebook/facebook.jsm
chat/protocols/facebook/facebook.manifest
chat/protocols/facebook/moz.build
chat/protocols/gtalk/components.conf
chat/protocols/gtalk/gtalk.js
chat/protocols/gtalk/gtalk.jsm
chat/protocols/gtalk/gtalk.manifest
chat/protocols/gtalk/moz.build
chat/protocols/irc/components.conf
chat/protocols/irc/irc.js
chat/protocols/irc/irc.jsm
chat/protocols/irc/irc.manifest
chat/protocols/irc/moz.build
chat/protocols/irc/test/test_ctcpQuote.js
chat/protocols/irc/test/test_ircCommands.js
chat/protocols/irc/test/test_ircMessage.js
chat/protocols/irc/test/test_ircNonStandard.js
chat/protocols/irc/test/test_ircServerTime.js
chat/protocols/irc/test/test_sendBufferedCommand.js
chat/protocols/irc/test/test_setMode.js
chat/protocols/irc/test/test_splitLongMessages.js
chat/protocols/irc/test/test_tryNewNick.js
chat/protocols/jsTest/components.conf
chat/protocols/jsTest/jsTestProtocol.js
chat/protocols/jsTest/jsTestProtocol.jsm
chat/protocols/jsTest/jsTestProtocol.manifest
chat/protocols/jsTest/moz.build
chat/protocols/matrix/components.conf
chat/protocols/matrix/matrix.js
chat/protocols/matrix/matrix.jsm
chat/protocols/matrix/matrix.manifest
chat/protocols/matrix/moz.build
chat/protocols/odnoklassniki/components.conf
chat/protocols/odnoklassniki/moz.build
chat/protocols/odnoklassniki/odnoklassniki.js
chat/protocols/odnoklassniki/odnoklassniki.jsm
chat/protocols/odnoklassniki/odnoklassniki.manifest
chat/protocols/skype/components.conf
chat/protocols/skype/moz.build
chat/protocols/skype/skype.js
chat/protocols/skype/skype.jsm
chat/protocols/skype/skype.manifest
chat/protocols/skype/test/test_MagicSha256.js
chat/protocols/skype/test/test_contactUrlToName.js
chat/protocols/twitter/components.conf
chat/protocols/twitter/moz.build
chat/protocols/twitter/twitter.js
chat/protocols/twitter/twitter.jsm
chat/protocols/twitter/twitter.manifest
chat/protocols/xmpp/components.conf
chat/protocols/xmpp/moz.build
chat/protocols/xmpp/test/test_dnsSrv.js
chat/protocols/xmpp/test/test_parseJidAndNormalization.js
chat/protocols/xmpp/test/test_parseVCard.js
chat/protocols/xmpp/xmpp-base.jsm
chat/protocols/xmpp/xmpp.js
chat/protocols/xmpp/xmpp.jsm
chat/protocols/xmpp/xmpp.manifest
chat/protocols/yahoo/components.conf
chat/protocols/yahoo/moz.build
chat/protocols/yahoo/yahoo.js
chat/protocols/yahoo/yahoo.jsm
chat/protocols/yahoo/yahoo.manifest
mail/installer/package-manifest.in
new file mode 100644
--- /dev/null
+++ b/chat/components/src/components.conf
@@ -0,0 +1,57 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{a94b5427-cd8d-40cf-b47e-b67671953e70}',
+    'contract_ids': ['@mozilla.org/chat/accounts-service;1'],
+    'jsm': 'resource:///modules/imAccounts.jsm',
+    'constructor': 'AccountsService',
+  },
+  {
+    'cid': '{7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}',
+    'contract_ids': ['@mozilla.org/chat/commands-service;1'],
+    'jsm': 'resource:///modules/imCommands.jsm',
+    'constructor': 'CommandsService',
+  },
+  {
+    'cid': '{8c3725dd-ee26-489d-8135-736015af8c7f}',
+    'contract_ids': ['@mozilla.org/chat/contacts-service;1'],
+    'jsm': 'resource:///modules/imContacts.jsm',
+    'constructor': 'ContactsService',
+  },
+  {
+    'cid': '{1fa92237-4303-4384-b8ac-4e65b50810a5}',
+    'contract_ids': ['@mozilla.org/chat/tags-service;1'],
+    'jsm': 'resource:///modules/imContacts.jsm',
+    'constructor': 'TagsService',
+  },
+  {
+    'cid': '{b2397cd5-c76d-4618-8410-f344c7c6443a}',
+    'contract_ids': ['@mozilla.org/chat/conversations-service;1'],
+    'jsm': 'resource:///modules/imConversations.jsm',
+    'constructor': 'ConversationsService',
+  },
+  {
+    'cid': '{073f5953-853c-4a38-bd81-255510c31c2e}',
+    'contract_ids': ['@mozilla.org/chat/core-service;1'],
+    'jsm': 'resource:///modules/imCore.jsm',
+    'constructor': 'CoreService',
+  },
+  {
+    'cid': '{fb0dc220-2c7a-4216-9f19-6b8f3480eae9}',
+    'contract_ids': ['@mozilla.org/chat/logger;1'],
+    'jsm': 'resource:///modules/logger.jsm',
+    'constructor': 'Logger',
+    'categories': {'profile-after-change': 'Logger'},
+  },
+  {
+    'cid': '{04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}',
+    'contract_ids': ['@mozilla.org/network/protocol;1?name=smile'],
+    'jsm': 'resource:///modules/smileProtocolHandler.jsm',
+    'constructor': 'smileProtocolHandler',
+  },
+]
rename from chat/components/src/imAccounts.js
rename to chat/components/src/imAccounts.jsm
--- a/chat/components/src/imAccounts.js
+++ b/chat/components/src/imAccounts.jsm
@@ -1,11 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["AccountsService"];
+
 var {
   ClassInfo,
   EmptyEnumerator,
   nsSimpleEnumerator,
   XPCOMUtils,
   setTimeout,
   clearTimeout,
   executeSoon,
@@ -1290,13 +1293,9 @@ AccountsService.prototype = {
     this._accountList = list
       .split(",")
       .filter(k => k.trim() != aAccountId)
       .join(",");
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imIAccountsService]),
   classDescription: "Accounts",
-  classID: Components.ID("{a94b5427-cd8d-40cf-b47e-b67671953e70}"),
-  contractID: "@mozilla.org/chat/accounts-service;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([AccountsService]);
deleted file mode 100644
--- a/chat/components/src/imAccounts.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {a94b5427-cd8d-40cf-b47e-b67671953e70} imAccounts.js
-contract @mozilla.org/chat/accounts-service;1 {a94b5427-cd8d-40cf-b47e-b67671953e70}
rename from chat/components/src/imCommands.js
rename to chat/components/src/imCommands.jsm
--- a/chat/components/src/imCommands.js
+++ b/chat/components/src/imCommands.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["CommandsService"];
+
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/commands.properties")
 );
@@ -280,13 +282,9 @@ CommandsService.prototype = {
       // If they all failed, print help message.
       this.executeCommand("/help " + name, aConversation);
     }
     return true;
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imICommandsService]),
   classDescription: "Commands",
-  classID: Components.ID("{7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}"),
-  contractID: "@mozilla.org/chat/commands-service;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandsService]);
deleted file mode 100644
--- a/chat/components/src/imCommands.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23} imCommands.js
-contract @mozilla.org/chat/commands-service;1 {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}
rename from chat/components/src/imContacts.js
rename to chat/components/src/imContacts.jsm
--- a/chat/components/src/imContacts.js
+++ b/chat/components/src/imContacts.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["TagsService", "ContactsService"];
+
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { XPCOMUtils, executeSoon, ClassInfo, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/contacts.properties")
 );
@@ -196,18 +198,16 @@ TagsService.prototype = {
   },
   get otherContactsTag() {
     otherContactsTag._initContacts();
     return otherContactsTag;
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imITagsService]),
   classDescription: "Tags",
-  classID: Components.ID("{1fa92237-4303-4384-b8ac-4e65b50810a5}"),
-  contractID: "@mozilla.org/chat/tags-service;1",
 };
 
 // TODO move into the tagsService
 var Tags = [];
 var TagsById = {};
 
 function Tag(aId, aName) {
   this._id = aId;
@@ -1795,16 +1795,9 @@ ContactsService.prototype = {
       statement.execute();
     } finally {
       statement.finalize();
     }
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imIContactsService]),
   classDescription: "Contacts",
-  classID: Components.ID("{8c3725dd-ee26-489d-8135-736015af8c7f}"),
-  contractID: "@mozilla.org/chat/contacts-service;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([
-  ContactsService,
-  TagsService,
-]);
deleted file mode 100644
--- a/chat/components/src/imContacts.manifest
+++ /dev/null
@@ -1,4 +0,0 @@
-component {8c3725dd-ee26-489d-8135-736015af8c7f} imContacts.js
-contract @mozilla.org/chat/contacts-service;1 {8c3725dd-ee26-489d-8135-736015af8c7f}
-component {1fa92237-4303-4384-b8ac-4e65b50810a5} imContacts.js
-contract @mozilla.org/chat/tags-service;1 {1fa92237-4303-4384-b8ac-4e65b50810a5}
rename from chat/components/src/imConversations.js
rename to chat/components/src/imConversations.jsm
--- a/chat/components/src/imConversations.js
+++ b/chat/components/src/imConversations.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["ConversationsService"];
+
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { Status } = ChromeUtils.import("resource:///modules/imStatusUtils.jsm");
 var { XPCOMUtils, nsSimpleEnumerator, ClassInfo } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { Message } = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
 
 var gLastUIConvId = 0;
@@ -821,13 +823,9 @@ ConversationsService.prototype = {
         return conv;
       }
     }
     return null;
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imIConversationsService]),
   classDescription: "Conversations",
-  classID: Components.ID("{b2397cd5-c76d-4618-8410-f344c7c6443a}"),
-  contractID: "@mozilla.org/chat/conversations-service;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([ConversationsService]);
deleted file mode 100644
--- a/chat/components/src/imConversations.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {b2397cd5-c76d-4618-8410-f344c7c6443a} imConversations.js
-contract @mozilla.org/chat/conversations-service;1 {b2397cd5-c76d-4618-8410-f344c7c6443a}
rename from chat/components/src/imCore.js
rename to chat/components/src/imCore.jsm
--- a/chat/components/src/imCore.js
+++ b/chat/components/src/imCore.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["CoreService"];
+
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var {
   XPCOMUtils,
   ClassInfo,
   initLogModule,
   nsSimpleEnumerator,
 } = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
 
@@ -268,20 +270,17 @@ UserStatus.prototype = {
   },
   _notifyObservers(aTopic, aData) {
     for (let observer of this._observers) {
       observer.observe(this, aTopic, aData);
     }
   },
 };
 
-var gCoreService;
-function CoreService() {
-  gCoreService = this;
-}
+function CoreService() {}
 CoreService.prototype = {
   globalUserStatus: null,
 
   _initialized: false,
   get initialized() {
     return this._initialized;
   },
   init() {
@@ -408,13 +407,9 @@ CoreService.prototype = {
     }
 
     this._protos[aPrplId] = proto;
     return proto;
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.imICoreService]),
   classDescription: "Core",
-  classID: Components.ID("{073f5953-853c-4a38-bd81-255510c31c2e}"),
-  contractID: "@mozilla.org/chat/core-service;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([CoreService]);
deleted file mode 100644
--- a/chat/components/src/imCore.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {073f5953-853c-4a38-bd81-255510c31c2e} imCore.js
-contract @mozilla.org/chat/core-service;1 {073f5953-853c-4a38-bd81-255510c31c2e}
rename from chat/components/src/logger.js
rename to chat/components/src/logger.jsm
--- a/chat/components/src/logger.js
+++ b/chat/components/src/logger.jsm
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var CC = Components.Constructor;
+var EXPORTED_SYMBOLS = ["Logger"];
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { EmptyEnumerator, l10nHelper, XPCOMUtils } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { GenericMessagePrototype } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
@@ -1125,13 +1125,9 @@ Logger.prototype = {
         break;
       default:
         throw new Error("Unexpected notification " + aTopic);
     }
   },
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, Ci.imILogger]),
   classDescription: "Logger",
-  classID: Components.ID("{fb0dc220-2c7a-4216-9f19-6b8f3480eae9}"),
-  contractID: "@mozilla.org/chat/logger;1",
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([Logger]);
deleted file mode 100644
--- a/chat/components/src/logger.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {fb0dc220-2c7a-4216-9f19-6b8f3480eae9} logger.js
-contract @mozilla.org/chat/logger;1 {fb0dc220-2c7a-4216-9f19-6b8f3480eae9}
-category profile-after-change Logger @mozilla.org/chat/logger;1
--- a/chat/components/src/moz.build
+++ b/chat/components/src/moz.build
@@ -1,24 +1,20 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
-EXTRA_COMPONENTS += [
-    'imAccounts.js',
-    'imAccounts.manifest',
-    'imCommands.js',
-    'imCommands.manifest',
-    'imContacts.js',
-    'imContacts.manifest',
-    'imConversations.js',
-    'imConversations.manifest',
-    'imCore.js',
-    'imCore.manifest',
-    'logger.js',
-    'logger.manifest',
-    'smileProtocolHandler.js',
-    'smileProtocolHandler.manifest',
+EXTRA_JS_MODULES += [
+    'imAccounts.jsm',
+    'imCommands.jsm',
+    'imContacts.jsm',
+    'imConversations.jsm',
+    'imCore.jsm',
+    'logger.jsm',
+    'smileProtocolHandler.jsm'
 ]
 
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
rename from chat/components/src/smileProtocolHandler.js
rename to chat/components/src/smileProtocolHandler.jsm
--- a/chat/components/src/smileProtocolHandler.js
+++ b/chat/components/src/smileProtocolHandler.jsm
@@ -1,16 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["smileProtocolHandler"];
+
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-var { XPCOMUtils } = ChromeUtils.import(
-  "resource://gre/modules/XPCOMUtils.jsm"
-);
 var { getSmileRealURI } = ChromeUtils.import(
   "resource:///modules/imSmileys.jsm"
 );
 
 var kSmileRegexp = /^smile:\/\//;
 
 function smileProtocolHandler() {}
 
@@ -28,15 +27,11 @@ smileProtocolHandler.prototype = {
     let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
     channel.originalURI = aURI;
     return channel;
   },
   allowPort(aPort, aScheme) {
     return false;
   },
 
+  QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolHandler]),
   classDescription: "Smile Protocol Handler",
-  classID: Components.ID("{04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}"),
-  contractID: "@mozilla.org/network/protocol;1?name=smile",
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolHandler]),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([smileProtocolHandler]);
deleted file mode 100644
--- a/chat/components/src/smileProtocolHandler.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4} smileProtocolHandler.js
-contract @mozilla.org/network/protocol;1?name=smile {04e58eae-dfbc-4c9e-8130-6d9ef19cbff4}
--- a/chat/components/src/test/test_commands.js
+++ b/chat/components/src/test/test_commands.js
@@ -2,17 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 // We don't load the command service via Services as we want to access
 // _findCommands in order to avoid having to intercept command execution.
 var imCommands = {};
 Services.scriptloader.loadSubScript(
-  "resource:///components/imCommands.js",
+  "resource:///modules/imCommands.jsm",
   imCommands
 );
 
 var kPrplId = "green";
 var kPrplId2 = "red";
 
 var fakeAccount = {
   connected: true,
--- a/chat/components/src/test/test_conversations.js
+++ b/chat/components/src/test/test_conversations.js
@@ -1,19 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { GenericConvIMPrototype, Message } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
-
 var imConversations = {};
 Services.scriptloader.loadSubScript(
-  "resource:///components/imConversations.js",
+  "resource:///modules/imConversations.jsm",
   imConversations
 );
 
 // Fake prplConversation
 var _id = 0;
 function Conversation(aName) {
   this._name = aName;
   this._observers = [];
--- a/chat/components/src/test/test_logger.js
+++ b/chat/components/src/test/test_logger.js
@@ -3,20 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 do_get_profile();
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
 
 var gLogger = {};
-Services.scriptloader.loadSubScript(
-  "resource:///components/logger.js",
-  gLogger
-);
+Services.scriptloader.loadSubScript("resource:///modules/logger.jsm", gLogger);
 
 var logDirPath = OS.Path.join(OS.Constants.Path.profileDir, "logs");
 
 var dummyAccount = {
   name: "dummy-account",
   normalizedName: "dummyaccount",
   protocol: {
     normalizedName: "dummy",
new file mode 100644
--- /dev/null
+++ b/chat/protocols/facebook/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{1d1d0bc5-610c-472f-b2cb-4b89857d80dc}',
+    'contract_ids': ['@mozilla.org/chat/facebook;1'],
+    'jsm': 'resource:///modules/facebook.jsm',
+    'constructor': 'FacebookProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-facebook'},
+  },
+]
rename from chat/protocols/facebook/facebook.js
rename to chat/protocols/facebook/facebook.jsm
--- a/chat/protocols/facebook/facebook.js
+++ b/chat/protocols/facebook/facebook.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["FacebookProtocol"];
+
 var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { GenericAccountPrototype, GenericProtocolPrototype } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
@@ -45,12 +47,9 @@ FacebookProtocol.prototype = {
     return _("facebook.chat.name");
   },
   get iconBaseURI() {
     return "chrome://prpl-facebook/skin/";
   },
   getAccount(aImAccount) {
     return new FacebookAccount(this, aImAccount);
   },
-  classID: Components.ID("{1d1d0bc5-610c-472f-b2cb-4b89857d80dc}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([FacebookProtocol]);
deleted file mode 100644
--- a/chat/protocols/facebook/facebook.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {1d1d0bc5-610c-472f-b2cb-4b89857d80dc} facebook.js
-contract @mozilla.org/chat/facebook;1 {1d1d0bc5-610c-472f-b2cb-4b89857d80dc}
-category im-protocol-plugin prpl-facebook @mozilla.org/chat/facebook;1
--- a/chat/protocols/facebook/moz.build
+++ b/chat/protocols/facebook/moz.build
@@ -1,11 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-EXTRA_COMPONENTS += [
-    'facebook.js',
-    'facebook.manifest',
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'facebook.jsm',
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
new file mode 100644
--- /dev/null
+++ b/chat/protocols/gtalk/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{38a224c1-6748-49a9-8ab2-efc362b1000d}',
+    'contract_ids': ['@mozilla.org/chat/gtalk;1'],
+    'jsm': 'resource:///modules/gtalk.jsm',
+    'constructor': 'GTalkProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-gtalk'},
+  },
+]
rename from chat/protocols/gtalk/gtalk.js
rename to chat/protocols/gtalk/gtalk.jsm
--- a/chat/protocols/gtalk/gtalk.js
+++ b/chat/protocols/gtalk/gtalk.jsm
@@ -1,20 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["GTalkProtocol"];
+
 var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { GenericProtocolPrototype } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
 var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
+  "resource:///modules/xmpp-base.jsm"
 );
 var { XMPPSession } = ChromeUtils.import(
   "resource:///modules/xmpp-session.jsm"
 );
 var { Stanza } = ChromeUtils.import("resource:///modules/xmpp-xml.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/xmpp.properties")
@@ -107,12 +109,9 @@ GTalkProtocol.prototype = {
         return _("options.resource");
       },
       default: "",
     },
   },
   get chatHasTopic() {
     return true;
   },
-  classID: Components.ID("{38a224c1-6748-49a9-8ab2-efc362b1000d}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([GTalkProtocol]);
deleted file mode 100644
--- a/chat/protocols/gtalk/gtalk.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {38a224c1-6748-49a9-8ab2-efc362b1000d} gtalk.js
-contract @mozilla.org/chat/gtalk;1 {38a224c1-6748-49a9-8ab2-efc362b1000d}
-category im-protocol-plugin prpl-gtalk @mozilla.org/chat/gtalk;1
--- a/chat/protocols/gtalk/moz.build
+++ b/chat/protocols/gtalk/moz.build
@@ -1,11 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-EXTRA_COMPONENTS += [
-    'gtalk.js',
-    'gtalk.manifest',
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'gtalk.jsm',
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
new file mode 100644
--- /dev/null
+++ b/chat/protocols/irc/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{607b2c0b-9504-483f-ad62-41de09238aec}',
+    'contract_ids': ['@mozilla.org/chat/irc;1'],
+    'jsm': 'resource:///modules/irc.jsm',
+    'constructor': 'ircProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-irc'},
+  },
+]
rename from chat/protocols/irc/irc.js
rename to chat/protocols/irc/irc.jsm
--- a/chat/protocols/irc/irc.js
+++ b/chat/protocols/irc/irc.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["ircProtocol"];
+
 var {
   ClassInfo,
   clearTimeout,
   EmptyEnumerator,
   setTimeout,
   executeSoon,
   l10nHelper,
   XPCOMUtils,
@@ -2407,12 +2409,9 @@ ircProtocol.prototype = {
   //  Passwords in IRC are optional, and are needed for certain functionality.
   get passwordOptional() {
     return true;
   },
 
   getAccount(aImAccount) {
     return new ircAccount(this, aImAccount);
   },
-  classID: Components.ID("{607b2c0b-9504-483f-ad62-41de09238aec}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([ircProtocol]);
deleted file mode 100644
--- a/chat/protocols/irc/irc.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {607b2c0b-9504-483f-ad62-41de09238aec} irc.js
-contract @mozilla.org/chat/irc;1 {607b2c0b-9504-483f-ad62-41de09238aec}
-category im-protocol-plugin prpl-irc @mozilla.org/chat/irc;1
--- a/chat/protocols/irc/moz.build
+++ b/chat/protocols/irc/moz.build
@@ -1,21 +1,17 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
-EXTRA_COMPONENTS += [
-    'irc.js',
-    'irc.manifest',
-]
-
 EXTRA_JS_MODULES += [
+    'irc.jsm',
     'ircBase.jsm',
     'ircCAP.jsm',
     'ircCommands.jsm',
     'ircCTCP.jsm',
     'ircDCC.jsm',
     'ircHandlers.jsm',
     'ircISUPPORT.jsm',
     'ircMultiPrefix.jsm',
@@ -23,8 +19,12 @@ EXTRA_JS_MODULES += [
     'ircSASL.jsm',
     'ircServerTime.jsm',
     'ircServices.jsm',
     'ircUtils.jsm',
     'ircWatchMonitor.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
+
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
--- a/chat/protocols/irc/test/test_ctcpQuote.js
+++ b/chat/protocols/irc/test/test_ctcpQuote.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 var input = [
   undefined,
   "test",
   "\\test",
   "te\\st",
   "test\\",
   "\\\\test",
--- a/chat/protocols/irc/test/test_ircCommands.js
+++ b/chat/protocols/irc/test/test_ircCommands.js
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var { commands } = ChromeUtils.import("resource:///modules/ircCommands.jsm");
-
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 // Ensure the commands have been initialized.
 Services.conversations.initConversations();
 
 var fakeProto = {
   id: "fake-proto",
 };
 
--- a/chat/protocols/irc/test/test_ircMessage.js
+++ b/chat/protocols/irc/test/test_ircMessage.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 var testData = [
   // First off, let's test the messages from RFC 2812.
   "PASS secretpasswordhere",
   "NICK Wiz",
   ":WiZ!jto@tolsun.oulu.fi NICK Kilroy",
   "USER guest 0 * :Ronnie Reagan",
   "USER guest 8 * :Ronnie Reagan",
@@ -148,17 +148,17 @@ function testRFC2812Messages() {
     }
 
     equal(stringMessage, expectedStringMessage);
   }
 
   run_next_test();
 }
 
-// Unreal sends a couple of broken messages, see ircMessage in irc.js for a
+// Unreal sends a couple of broken messages, see ircMessage in irc.jsm for a
 // description of what's wrong.
 function testBrokenUnrealMessages() {
   let messages = {
     // Two spaces after command.
     ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters": {
       rawMessage:
         ":gravel.mozilla.org 432  #momo :Erroneous Nickname: Illegal characters",
       command: "432",
--- a/chat/protocols/irc/test/test_ircNonStandard.js
+++ b/chat/protocols/irc/test/test_ircNonStandard.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 const { ircNonStandard } = ChromeUtils.import(
   "resource:///modules/ircNonStandard.jsm"
 );
 
 // The function that is under test here.
 var NOTICE = ircNonStandard.commands.NOTICE;
 
 function FakeConversation() {}
--- a/chat/protocols/irc/test/test_ircServerTime.js
+++ b/chat/protocols/irc/test/test_ircServerTime.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { tagServerTime } = ChromeUtils.import(
   "resource:///modules/ircServerTime.jsm"
 );
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 function getTags(aRawMsg) {
   const { tags } = irc.ircMessage(aRawMsg, "doesnt@matter");
 
   return tags;
 }
 
 function run_test() {
--- a/chat/protocols/irc/test/test_sendBufferedCommand.js
+++ b/chat/protocols/irc/test/test_sendBufferedCommand.js
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 function FakeAccount() {
   this._commandBuffers = new Map();
   this.callbacks = [];
 }
 FakeAccount.prototype = {
   __proto__: irc.ircAccount.prototype,
   maxMessageLength: 60,
--- a/chat/protocols/irc/test/test_setMode.js
+++ b/chat/protocols/irc/test/test_setMode.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 Services.conversations.initConversations();
 
 function FakeAccount() {
   this.normalizeNick = irc.ircAccount.prototype.normalizeNick.bind(this);
 }
 FakeAccount.prototype = {
   __proto__: irc.ircAccount.prototype,
   setWhois: (n, f) => true,
--- a/chat/protocols/irc/test/test_splitLongMessages.js
+++ b/chat/protocols/irc/test/test_splitLongMessages.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 var messages = {
   // Exactly 51 characters.
   "This is a test.": ["This is a test."],
   // Too long.
   "This is a message that is too long.": [
     "This is a",
     "message that is",
--- a/chat/protocols/irc/test/test_tryNewNick.js
+++ b/chat/protocols/irc/test/test_tryNewNick.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var irc = {};
-Services.scriptloader.loadSubScript("resource:///components/irc.js", irc);
+Services.scriptloader.loadSubScript("resource:///modules/irc.jsm", irc);
 
 var fakeProto = {
   id: "fake-proto",
   options: { alternateNicks: "" },
   _getOptionDefault(aOption) {
     return this.options[aOption];
   },
 };
new file mode 100644
--- /dev/null
+++ b/chat/protocols/jsTest/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{a0774c5a-4aea-458b-9fbc-8d3cbf1a4630}',
+    'contract_ids': ['@mozilla.org/chat/jstest;1'],
+    'jsm': 'resource:///modules/jsTestProtocol.jsm',
+    'constructor': 'JSTestProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-jstest'},
+  },
+]
rename from chat/protocols/jsTest/jsTestProtocol.js
rename to chat/protocols/jsTest/jsTestProtocol.jsm
--- a/chat/protocols/jsTest/jsTestProtocol.js
+++ b/chat/protocols/jsTest/jsTestProtocol.jsm
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-var { XPCOMUtils, setTimeout } = ChromeUtils.import(
-  "resource:///modules/imXPCOMUtils.jsm"
-);
+var EXPORTED_SYMBOLS = ["JSTestProtocol"];
+
+var { setTimeout } = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
 var {
   GenericAccountPrototype,
   GenericConvIMPrototype,
   GenericProtocolPrototype,
 } = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
 
 function Conversation(aAccount) {
   this._init(aAccount);
@@ -102,18 +102,18 @@ Account.prototype = {
       required: true,
     },
   },
 
   // Nothing to do.
   unInit() {},
 };
 
-function jsTestProtocol() {}
-jsTestProtocol.prototype = {
+function JSTestProtocol() {}
+JSTestProtocol.prototype = {
   __proto__: GenericProtocolPrototype,
   get name() {
     return "JS Test";
   },
   options: {
     text: { label: "Text option", default: "foo" },
     bool: { label: "Boolean option", default: true },
     int: { label: "Integer option", default: 42 },
@@ -133,12 +133,9 @@ jsTestProtocol.prototype = {
       separator: "@",
       defaultValue: "default.server",
       reverse: true,
     },
   ],
   getAccount(aImAccount) {
     return new Account(this, aImAccount);
   },
-  classID: Components.ID("{a0774c5a-4aea-458b-9fbc-8d3cbf1a4630}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([jsTestProtocol]);
deleted file mode 100644
--- a/chat/protocols/jsTest/jsTestProtocol.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {a0774c5a-4aea-458b-9fbc-8d3cbf1a4630} jsTestProtocol.js
-contract @mozilla.org/chat/jstest;1 {a0774c5a-4aea-458b-9fbc-8d3cbf1a4630}
-category im-protocol-plugin prpl-jstest @mozilla.org/chat/jstest;1
--- a/chat/protocols/jsTest/moz.build
+++ b/chat/protocols/jsTest/moz.build
@@ -1,11 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 if CONFIG['MOZ_DEBUG']:
-    EXTRA_COMPONENTS += [
-        'jsTestProtocol.js',
-        'jsTestProtocol.manifest',
+    EXTRA_JS_MODULES += [
+        'jsTestProtocol.jsm',
     ]
 
+    XPCOM_MANIFESTS += [
+        'components.conf',
+    ]
+
new file mode 100644
--- /dev/null
+++ b/chat/protocols/matrix/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{e9653ac6-a671-11e6-bf84-60a44c717042}',
+    'contract_ids': ['@mozilla.org/chat/matrix;1'],
+    'jsm': 'resource:///modules/matrix.jsm',
+    'constructor': 'MatrixProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-matrix'},
+  },
+]
rename from chat/protocols/matrix/matrix.js
rename to chat/protocols/matrix/matrix.jsm
--- a/chat/protocols/matrix/matrix.js
+++ b/chat/protocols/matrix/matrix.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["MatrixProtocol"];
+
 var { XPCOMUtils, nsSimpleEnumerator, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var {
   GenericAccountPrototype,
   GenericConvChatPrototype,
   GenericConvChatBuddyPrototype,
   GenericProtocolPrototype,
@@ -355,13 +357,9 @@ MatrixProtocol.prototype = {
       },
       default: 443,
     },
   },
 
   get chatHasTopic() {
     return true;
   },
-
-  classID: Components.ID("{e9653ac6-a671-11e6-bf84-60a44c717042}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([MatrixProtocol]);
deleted file mode 100644
--- a/chat/protocols/matrix/matrix.manifest
+++ /dev/null
@@ -1,4 +0,0 @@
-component {e9653ac6-a671-11e6-bf84-60a44c717042} matrix.js
-contract @mozilla.org/chat/matrix;1 {e9653ac6-a671-11e6-bf84-60a44c717042}
-category im-protocol-plugin prpl-matrix @mozilla.org/chat/matrix;1
-
--- a/chat/protocols/matrix/moz.build
+++ b/chat/protocols/matrix/moz.build
@@ -5,20 +5,20 @@
 
 # XPCSHELL_TESTS_MANIFESTS += []
 
 DIRS += [
     'lib',
     'shims',
 ]
 
-EXTRA_COMPONENTS += [
-    'matrix.js',
-    'matrix.manifest',
-]
-
 EXTRA_JS_MODULES += [
     'matrix-sdk.jsm',
+    'matrix.jsm',
 ]
 
 JAR_MANIFESTS += [
     'jar.mn',
 ]
+
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
new file mode 100644
--- /dev/null
+++ b/chat/protocols/odnoklassniki/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{29b09a83-81c1-2032-11e2-6d9bc4f8e969}',
+    'contract_ids': ['@mozilla.org/chat/odnoklassniki;1'],
+    'jsm': 'resource:///modules/odnoklassniki.jsm',
+    'constructor': 'OdnoklassnikiProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-odnoklassniki'},
+  },
+]
--- a/chat/protocols/odnoklassniki/moz.build
+++ b/chat/protocols/odnoklassniki/moz.build
@@ -1,11 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-EXTRA_COMPONENTS += [
-    'odnoklassniki.js',
-    'odnoklassniki.manifest',
+JAR_MANIFESTS += ['jar.mn']
+
+XPCOM_MANIFESTS += [
+    'components.conf',
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+EXTRA_JS_MODULES += [
+    'odnoklassniki.jsm',
+]
rename from chat/protocols/odnoklassniki/odnoklassniki.js
rename to chat/protocols/odnoklassniki/odnoklassniki.jsm
--- a/chat/protocols/odnoklassniki/odnoklassniki.js
+++ b/chat/protocols/odnoklassniki/odnoklassniki.jsm
@@ -1,20 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["OdnoklassnikiProtocol"];
+
 var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { GenericProtocolPrototype } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
 var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
+  "resource:///modules/xmpp-base.jsm"
 );
 var { XMPPSession, XMPPDefaultResource } = ChromeUtils.import(
   "resource:///modules/xmpp-session.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/xmpp.properties")
 );
@@ -70,12 +72,9 @@ OdnoklassnikiProtocol.prototype = {
     return "chrome://prpl-odnoklassniki/skin/";
   },
   get usernameEmptyText() {
     return _("odnoklassniki.usernameHint");
   },
   getAccount(aImAccount) {
     return new OdnoklassnikiAccount(this, aImAccount);
   },
-  classID: Components.ID("{29b09a83-81c1-2032-11e2-6d9bc4f8e969}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([OdnoklassnikiProtocol]);
deleted file mode 100644
--- a/chat/protocols/odnoklassniki/odnoklassniki.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {29b09a83-81c1-2032-11e2-6d9bc4f8e969} odnoklassniki.js
-contract @mozilla.org/chat/odnoklassniki;1 {29b09a83-81c1-2032-11e2-6d9bc4f8e969}
-category im-protocol-plugin prpl-odnoklassniki @mozilla.org/chat/odnoklassniki;1
new file mode 100644
--- /dev/null
+++ b/chat/protocols/skype/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{8446c0f6-9f59-4710-844e-eaa6c1f49d35}',
+    'contract_ids': ['@mozilla.org/chat/skype;1'],
+    'jsm': 'resource:///modules/skype.jsm',
+    'constructor': 'SkypeProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-skype'},
+  },
+]
--- a/chat/protocols/skype/moz.build
+++ b/chat/protocols/skype/moz.build
@@ -1,13 +1,16 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
-EXTRA_COMPONENTS += [
-    'skype.js',
-    'skype.manifest',
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'skype.jsm',
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
rename from chat/protocols/skype/skype.js
rename to chat/protocols/skype/skype.jsm
--- a/chat/protocols/skype/skype.js
+++ b/chat/protocols/skype/skype.jsm
@@ -1,12 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["SkypeProtocol"];
 var { httpRequest } = ChromeUtils.import("resource://gre/modules/Http.jsm");
 var { StringToArrayBuffer } = ChromeUtils.import(
   "resource:///modules/ArrayBufferUtils.jsm"
 );
 var { bigInt } = ChromeUtils.import("resource:///modules/BigInteger.jsm");
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var {
   XPCOMUtils,
@@ -951,12 +952,9 @@ SkypeProtocol.prototype = {
 
   get passwordOptional() {
     return false;
   },
 
   getAccount(aImAccount) {
     return new SkypeAccount(this, aImAccount);
   },
-  classID: Components.ID("{8446c0f6-9f59-4710-844e-eaa6c1f49d35}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([SkypeProtocol]);
deleted file mode 100644
--- a/chat/protocols/skype/skype.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {8446c0f6-9f59-4710-844e-eaa6c1f49d35} skype.js
-contract @mozilla.org/chat/skype;1 {8446c0f6-9f59-4710-844e-eaa6c1f49d35}
-category im-protocol-plugin prpl-skype @mozilla.org/chat/skype;1
--- a/chat/protocols/skype/test/test_MagicSha256.js
+++ b/chat/protocols/skype/test/test_MagicSha256.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var skype = {};
-Services.scriptloader.loadSubScript("resource:///components/skype.js", skype);
+Services.scriptloader.loadSubScript("resource:///modules/skype.jsm", skype);
 
 var data = {
   "1416264993": "3a33ac47fe2ec1a33d569f4be5c69ddc",
   "1416387358": "eca9716e1eedcbe93320ba794cea3388",
   "1416392361": "2ed6fc80c3303caa137ae3fd4fcc7d80",
 };
 
 function run_test() {
--- a/chat/protocols/skype/test/test_contactUrlToName.js
+++ b/chat/protocols/skype/test/test_contactUrlToName.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var skype = {};
-Services.scriptloader.loadSubScript("resource:///components/skype.js", skype);
+Services.scriptloader.loadSubScript("resource:///modules/skype.jsm", skype);
 
 var data = {
   "https://bay-client-s.gateway.messenger.live.com/v1/users/ME/contacts/8:clokep":
     "clokep",
   "https://bay-client-s.gateway.messenger.live.com/v1/users/8:clokep/presenceDocs/messagingService":
     "clokep",
 };
 
new file mode 100644
--- /dev/null
+++ b/chat/protocols/twitter/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{31082ff6-1de8-422b-ab60-ca0ac0b2af13}',
+    'contract_ids': ['@mozilla.org/chat/twitter;1'],
+    'jsm': 'resource:///modules/twitter.jsm',
+    'constructor': 'TwitterProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-twitter'},
+  },
+]
--- a/chat/protocols/twitter/moz.build
+++ b/chat/protocols/twitter/moz.build
@@ -1,14 +1,15 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-EXTRA_COMPONENTS += [
-    'twitter.js',
-    'twitter.manifest',
+EXTRA_JS_MODULES += [
+    'twitter-text.jsm',
+    'twitter.jsm',
 ]
 
-EXTRA_JS_MODULES += [
-    'twitter-text.jsm',
+JAR_MANIFESTS += ['jar.mn']
+
+XPCOM_MANIFESTS += [
+    'components.conf',
 ]
-JAR_MANIFESTS += ['jar.mn']
rename from chat/protocols/twitter/twitter.js
rename to chat/protocols/twitter/twitter.jsm
--- a/chat/protocols/twitter/twitter.js
+++ b/chat/protocols/twitter/twitter.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["TwitterProtocol"];
+
 var { httpRequest, percentEncode } = ChromeUtils.import(
   "resource://gre/modules/Http.jsm"
 );
 var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
 var {
   XPCOMUtils,
   setTimeout,
   clearTimeout,
@@ -1567,12 +1569,9 @@ TwitterProtocol.prototype = {
         aMsg.split(" ").forEach(account.stopFollowing, account);
         return true;
       },
     },
   ],
   getAccount(aImAccount) {
     return new Account(this, aImAccount);
   },
-  classID: Components.ID("{31082ff6-1de8-422b-ab60-ca0ac0b2af13}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([TwitterProtocol]);
deleted file mode 100644
--- a/chat/protocols/twitter/twitter.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {31082ff6-1de8-422b-ab60-ca0ac0b2af13} twitter.js
-contract @mozilla.org/chat/twitter;1 {31082ff6-1de8-422b-ab60-ca0ac0b2af13}
-category im-protocol-plugin prpl-twitter @mozilla.org/chat/twitter;1
new file mode 100644
--- /dev/null
+++ b/chat/protocols/xmpp/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{dde786d1-6f59-43d0-9bc8-b505a757fb30}',
+    'contract_ids': ['@mozilla.org/chat/xmpp;1'],
+    'jsm': 'resource:///modules/xmpp.jsm',
+    'constructor': 'XMPPProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-jabber'},
+  },
+]
--- a/chat/protocols/xmpp/moz.build
+++ b/chat/protocols/xmpp/moz.build
@@ -1,21 +1,21 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell.ini']
 
-EXTRA_COMPONENTS += [
-    'xmpp.js',
-    'xmpp.manifest',
-]
-
 EXTRA_JS_MODULES += [
     'xmpp-authmechs.jsm',
+    'xmpp-base.jsm',
     'xmpp-commands.jsm',
     'xmpp-session.jsm',
     'xmpp-xml.jsm',
     'xmpp.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
+
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
--- a/chat/protocols/xmpp/test/test_dnsSrv.js
+++ b/chat/protocols/xmpp/test/test_dnsSrv.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
+  "resource:///modules/xmpp-base.jsm"
 );
 var { XMPPSession } = ChromeUtils.import(
   "resource:///modules/xmpp-session.jsm"
 );
 
 var dns = {};
 Services.scriptloader.loadSubScript("resource:///modules/DNS.jsm", dns);
 
--- a/chat/protocols/xmpp/test/test_parseJidAndNormalization.js
+++ b/chat/protocols/xmpp/test/test_parseJidAndNormalization.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
+  "resource:///modules/xmpp-base.jsm"
 );
 
 var TEST_DATA = {
   "abdelrhman@instantbird": {
     node: "abdelrhman",
     domain: "instantbird",
     jid: "abdelrhman@instantbird",
     normalized: "abdelrhman@instantbird",
--- a/chat/protocols/xmpp/test/test_parseVCard.js
+++ b/chat/protocols/xmpp/test/test_parseVCard.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
+  "resource:///modules/xmpp-base.jsm"
 );
 var { XMPPParser } = ChromeUtils.import("resource:///modules/xmpp-xml.jsm");
 
 /*
  * Open an input stream, instantiate an XMPP parser, and feed the input string
  * into it. Then assert that the resulting vCard matches the expected result.
  */
 function _test_vcard(aInput, aExpectedResult) {
copy from chat/protocols/xmpp/xmpp.jsm
copy to chat/protocols/xmpp/xmpp-base.jsm
deleted file mode 100644
--- a/chat/protocols/xmpp/xmpp.js
+++ /dev/null
@@ -1,106 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
-  "resource:///modules/imXPCOMUtils.jsm"
-);
-var { GenericProtocolPrototype } = ChromeUtils.import(
-  "resource:///modules/jsProtoHelper.jsm"
-);
-var { XMPPAccountPrototype } = ChromeUtils.import(
-  "resource:///modules/xmpp.jsm"
-);
-
-XPCOMUtils.defineLazyGetter(this, "_", () =>
-  l10nHelper("chrome://chat/locale/xmpp.properties")
-);
-
-function XMPPAccount(aProtoInstance, aImAccount) {
-  this._init(aProtoInstance, aImAccount);
-}
-XMPPAccount.prototype = XMPPAccountPrototype;
-
-function XMPPProtocol() {
-  ChromeUtils.import("resource:///modules/xmpp-commands.jsm", this);
-  this.registerCommands();
-}
-XMPPProtocol.prototype = {
-  __proto__: GenericProtocolPrototype,
-  get normalizedName() {
-    return "jabber";
-  },
-  get name() {
-    return "XMPP";
-  },
-  get iconBaseURI() {
-    return "chrome://prpl-jabber/skin/";
-  },
-  getAccount(aImAccount) {
-    return new XMPPAccount(this, aImAccount);
-  },
-
-  usernameSplits: [
-    {
-      get label() {
-        return _("options.domain");
-      },
-      separator: "@",
-      defaultValue: "jabber.org",
-      reverse: true,
-    },
-  ],
-
-  options: {
-    resource: {
-      get label() {
-        return _("options.resource");
-      },
-      default: "",
-    },
-    priority: {
-      get label() {
-        return _("options.priority");
-      },
-      default: 0,
-    },
-    connection_security: {
-      get label() {
-        return _("options.connectionSecurity");
-      },
-      listValues: {
-        get require_tls() {
-          return _("options.connectionSecurity.requireEncryption");
-        },
-        get opportunistic_tls() {
-          return _("options.connectionSecurity.opportunisticTLS");
-        },
-        get allow_unencrypted_plain_auth() {
-          return _("options.connectionSecurity.allowUnencryptedAuth");
-        },
-        // "old_ssl" and "none" are also supported, but not exposed in the UI.
-        // Any unknown value will fallback to the opportunistic_tls behavior.
-      },
-      default: "require_tls",
-    },
-    server: {
-      get label() {
-        return _("options.connectServer");
-      },
-      default: "",
-    },
-    port: {
-      get label() {
-        return _("options.connectPort");
-      },
-      default: 5222,
-    },
-  },
-  get chatHasTopic() {
-    return true;
-  },
-
-  classID: Components.ID("{dde786d1-6f59-43d0-9bc8-b505a757fb30}"),
-};
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([XMPPProtocol]);
--- a/chat/protocols/xmpp/xmpp.jsm
+++ b/chat/protocols/xmpp/xmpp.jsm
@@ -1,3324 +1,104 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = [
-  "XMPPConversationPrototype",
-  "XMPPMUCConversationPrototype",
-  "XMPPAccountBuddyPrototype",
-  "XMPPAccountPrototype",
-];
+var EXPORTED_SYMBOLS = ["XMPPProtocol"];
 
-const { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
-const { Status } = ChromeUtils.import("resource:///modules/imStatusUtils.jsm");
-const {
-  XPCOMUtils,
-  setTimeout,
-  clearTimeout,
-  executeSoon,
-  nsSimpleEnumerator,
-  EmptyEnumerator,
-  ClassInfo,
-  l10nHelper,
-} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
-var {
-  GenericAccountPrototype,
-  GenericAccountBuddyPrototype,
-  GenericConvIMPrototype,
-  GenericConvChatPrototype,
-  GenericConversationPrototype,
-  TooltipInfo,
-} = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
-const { NormalizedMap } = ChromeUtils.import(
-  "resource:///modules/NormalizedMap.jsm"
+var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
+  "resource:///modules/imXPCOMUtils.jsm"
 );
-var { Stanza, SupportedFeatures } = ChromeUtils.import(
-  "resource:///modules/xmpp-xml.jsm"
-);
-var { XMPPSession } = ChromeUtils.import(
-  "resource:///modules/xmpp-session.jsm"
-);
-
-ChromeUtils.defineModuleGetter(
-  this,
-  "DownloadUtils",
-  "resource://gre/modules/DownloadUtils.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "FileUtils",
-  "resource://gre/modules/FileUtils.jsm"
+var { GenericProtocolPrototype } = ChromeUtils.import(
+  "resource:///modules/jsProtoHelper.jsm"
 );
-ChromeUtils.defineModuleGetter(
-  this,
-  "NetUtil",
-  "resource://gre/modules/NetUtil.jsm"
-);
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "imgTools",
-  "@mozilla.org/image/tools;1",
-  "imgITools"
-);
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "UuidGenerator",
-  "@mozilla.org/uuid-generator;1",
-  "nsIUUIDGenerator"
+var { XMPPAccountPrototype } = ChromeUtils.import(
+  "resource:///modules/xmpp-base.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
   l10nHelper("chrome://chat/locale/xmpp.properties")
 );
 
-XPCOMUtils.defineLazyGetter(this, "TXTToHTML", function() {
-  let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(Ci.mozITXTToHTMLConv);
-  return aTxt => cs.scanTXT(aTxt, cs.kEntities);
-});
-
-// Parses the status from a presence stanza into an object of statusType,
-// statusText and idleSince.
-function parseStatus(aStanza) {
-  let statusType = Ci.imIStatusInfo.STATUS_AVAILABLE;
-  let show = aStanza.getElement(["show"]);
-  if (show) {
-    show = show.innerText;
-    if (show == "away") {
-      statusType = Ci.imIStatusInfo.STATUS_AWAY;
-    } else if (show == "chat") {
-      statusType = Ci.imIStatusInfo.STATUS_AVAILABLE; // FIXME
-    } else if (show == "dnd") {
-      statusType = Ci.imIStatusInfo.STATUS_UNAVAILABLE;
-    } else if (show == "xa") {
-      statusType = Ci.imIStatusInfo.STATUS_IDLE;
-    }
-  }
-
-  let idleSince = 0;
-  let date = _getDelay(aStanza);
-  if (date) {
-    idleSince = date.getTime();
-  }
-
-  let query = aStanza.getElement(["query"]);
-  if (query && query.uri == Stanza.NS.last) {
-    let now = Math.floor(Date.now() / 1000);
-    idleSince = now - parseInt(query.attributes.seconds, 10);
-    statusType = Ci.imIStatusInfo.STATUS_IDLE;
-  }
-
-  // Mark official Android clients as mobile.
-  const kAndroidNodeURI = "http://www.android.com/gtalk/client/caps";
-  if (
-    aStanza
-      .getChildrenByNS(Stanza.NS.caps)
-      .some(s => s.localName == "c" && s.attributes.node == kAndroidNodeURI)
-  ) {
-    statusType = Ci.imIStatusInfo.STATUS_MOBILE;
-  }
-
-  let status = aStanza.getElement(["status"]);
-  status = status ? status.innerText : "";
-
-  return { statusType, statusText: status, idleSince };
-}
-
-// Returns a Date object for the delay value (stamp) in aStanza if it exists,
-// otherwise returns undefined.
-function _getDelay(aStanza) {
-  // XEP-0203: Delayed Delivery.
-  let date;
-  let delay = aStanza.getElement(["delay"]);
-  if (delay && delay.uri == Stanza.NS.delay) {
-    if (delay.attributes.stamp) {
-      date = new Date(delay.attributes.stamp);
-    }
-  }
-  if (date && isNaN(date.getTime())) {
-    return undefined;
-  }
-
-  return date;
-}
-
-// Writes aMsg in aConv as an outgoing message with optional date as the
-// message may be sent from another client.
-function _displaySentMsg(aConv, aMsg, aDate) {
-  let who;
-  if (aConv._account._connection) {
-    who = aConv._account._connection._jid.jid;
-  }
-  if (!who) {
-    who = aConv._account.name;
-  }
-
-  let flags = { outgoing: true };
-  flags._alias = aConv.account.alias || aConv.account.statusInfo.displayName;
-
-  if (aDate) {
-    flags.time = aDate / 1000;
-    flags.delayed = true;
-  }
-  aConv.writeMessage(who, aMsg, flags);
-}
-
-// The timespan after which we consider roomInfo to be stale.
-var kListRefreshInterval = 12 * 60 * 60 * 1000; // 12 hours.
-
-/* This is an ordered list, used to determine chat buddy flags:
- *  index < member    -> noFlags
- *  index = member    -> voiced
- *          moderator -> halfOp
- *          admin     -> op
- *          owner     -> founder
- */
-var kRoles = [
-  "outcast",
-  "visitor",
-  "participant",
-  "member",
-  "moderator",
-  "admin",
-  "owner",
-];
-
-function MUCParticipant(aNick, aJid, aPresenceStanza) {
-  this._jid = aJid;
-  this.name = aNick;
-  this.onPresenceStanza(aPresenceStanza);
+function XMPPAccount(aProtoInstance, aImAccount) {
+  this._init(aProtoInstance, aImAccount);
 }
-MUCParticipant.prototype = {
-  __proto__: ClassInfo("prplIConvChatBuddy", "XMPP ConvChatBuddy object"),
-
-  buddy: false,
-
-  // The occupant jid of the participant which is of the form room@domain/nick.
-  _jid: null,
-
-  // The real jid of the participant which is of the form local@domain/resource.
-  accountJid: null,
-
-  statusType: null,
-  statusText: null,
-  get alias() {
-    return this.name;
-  },
-
-  role: 2, // "participant" by default
-
-  // Called when a presence stanza is received for this participant.
-  onPresenceStanza(aStanza) {
-    let statusInfo = parseStatus(aStanza);
-    this.statusType = statusInfo.statusType;
-    this.statusText = statusInfo.statusText;
-
-    let x = aStanza.children.filter(
-      child => child.localName == "x" && child.uri == Stanza.NS.muc_user
-    );
-    if (x.length == 0) {
-      return;
-    }
-
-    // XEP-0045 (7.2.3): We only expect a single <x/> element of this namespace,
-    // so we ignore any others.
-    x = x[0];
-
-    let item = x.getElement(["item"]);
-    if (!item) {
-      return;
-    }
-
-    this.role = Math.max(
-      kRoles.indexOf(item.attributes.role),
-      kRoles.indexOf(item.attributes.affiliation)
-    );
-
-    let accountJid = item.attributes.jid;
-    if (accountJid) {
-      this.accountJid = accountJid;
-    }
-  },
-
-  get noFlags() {
-    return this.role < kRoles.indexOf("member");
-  },
-  get voiced() {
-    return this.role == kRoles.indexOf("member");
-  },
-  get halfOp() {
-    return this.role == kRoles.indexOf("moderator");
-  },
-  get op() {
-    return this.role == kRoles.indexOf("admin");
-  },
-  get founder() {
-    return this.role == kRoles.indexOf("owner");
-  },
-  typing: false,
-};
-
-// MUC (Multi-User Chat)
-var XMPPMUCConversationPrototype = {
-  __proto__: GenericConvChatPrototype,
-  // By default users are not in a MUC.
-  _left: true,
-
-  // Tracks all received messages to avoid possible duplication if the server
-  // sends us the last few messages again when we rejoin a room.
-  _messageIds: new Set(),
-
-  _init(aAccount, aJID, aNick) {
-    this._messageIds = new Set();
-    GenericConvChatPrototype._init.call(this, aAccount, aJID, aNick);
-  },
-
-  _targetResource: "",
-
-  // True while we are rejoining a room previously parted by the user.
-  _rejoined: false,
-
-  get topic() {
-    return this._topic;
-  },
-  set topic(aTopic) {
-    // XEP-0045 (8.1): Modifying the room subject.
-    let subject = Stanza.node("subject", null, null, aTopic.trim());
-    let s = Stanza.message(
-      this.name,
-      null,
-      null,
-      { type: "groupchat" },
-      subject
-    );
-    let notAuthorized = _("conversation.error.changeTopicFailedNotAuthorized");
-    this._account.sendStanza(
-      s,
-      this._account.handleErrors(
-        {
-          forbidden: notAuthorized,
-          notAcceptable: notAuthorized,
-          itemNotFound: notAuthorized,
-        },
-        this
-      )
-    );
-  },
-  get topicSettable() {
-    return true;
-  },
-
-  /* Called when the user enters a chat message */
-  sendMsg(aMsg) {
-    // XEP-0045 (7.4): Sending a message to all occupants in a room.
-    let s = Stanza.message(this.name, aMsg, null, { type: "groupchat" });
-    let notInRoom = _(
-      "conversation.error.sendFailedAsNotInRoom",
-      this.name,
-      aMsg
-    );
-    this._account.sendStanza(
-      s,
-      this._account.handleErrors(
-        {
-          itemNotFound: notInRoom,
-          notAcceptable: notInRoom,
-        },
-        this
-      )
-    );
-  },
-
-  /* Called by the account when a presence stanza is received for this muc */
-  onPresenceStanza(aStanza) {
-    let from = aStanza.attributes.from;
-    let nick = this._account._parseJID(from).resource;
-    let jid = this._account.normalize(from);
-    let x = aStanza.getElements(["x"]).find(e => e.uri == Stanza.NS.muc_user);
-
-    // Check if the join failed.
-    if (this.left && aStanza.attributes.type == "error") {
-      let error = this._account.parseError(aStanza);
-      let message;
-      switch (error.condition) {
-        case "not-authorized":
-        case "registration-required":
-          // XEP-0045 (7.2.7): Members-Only Rooms.
-          message = _("conversation.error.joinFailedNotAuthorized");
-          break;
-        case "not-allowed":
-          message = _("conversation.error.creationFailedNotAllowed");
-          break;
-        case "remote-server-not-found":
-          message = _(
-            "conversation.error.joinFailedRemoteServerNotFound",
-            this.name
-          );
-          break;
-        case "forbidden":
-          // XEP-0045 (7.2.8): Banned users.
-          message = _("conversation.error.joinForbidden", this.name);
-          break;
-        default:
-          message = _("conversation.error.joinFailed", this.name);
-          this.ERROR("Failed to join MUC: " + aStanza.convertToString());
-          break;
-      }
-      this.writeMessage(this.name, message, { system: true, error: true });
-      this.joining = false;
-      return;
-    }
-
-    if (!x) {
-      this.WARN(
-        "Received a MUC presence stanza without an x element or " +
-          "with a namespace we don't handle."
-      );
-      return;
-    }
-    let codes = x.getElements(["status"]).map(elt => elt.attributes.code);
-    let item = x.getElement(["item"]);
-
-    // Changes the nickname of a participant for this muc.
-    let changeNick = () => {
-      if (!item || !item.attributes.nick) {
-        this.WARN(
-          "Received a MUC presence code 303 or 210 stanza without an " +
-            "item element or a nick attribute."
-        );
-        return;
-      }
-      let newNick = item.attributes.nick;
-      this.updateNick(nick, newNick, nick == this.nick);
-    };
-
-    if (aStanza.attributes.type == "unavailable") {
-      if (!this._participants.has(nick)) {
-        this.WARN(
-          "received unavailable presence for an unknown MUC participant: " +
-            from
-        );
-        return;
-      }
-      if (codes.includes("303")) {
-        // XEP-0045 (7.6): Changing Nickname.
-        // Service Updates Nick for user.
-        changeNick();
-        return;
-      }
-      if (item && item.attributes.role == "none") {
-        // XEP-0045: an occupant has left the room.
-        this.removeParticipant(nick);
-
-        // Who caused the participant to leave the room.
-        let actor = item.getElement(["actor"]);
-        let actorNick = actor ? actor.attributes.nick : "";
-        let isActor = actorNick ? ".actor" : "";
-
-        // Why the participant left.
-        let reasonNode = item.getElement(["reason"]);
-        let reason = reasonNode ? reasonNode.innerText : "";
-        let isReason = reason ? ".reason" : "";
+XMPPAccount.prototype = XMPPAccountPrototype;
 
-        let isYou = nick == this.nick ? ".you" : "";
-        let affectedNick = isYou ? "" : nick;
-        if (isYou) {
-          this.left = true;
-        }
-
-        let message;
-        if (codes.includes("301")) {
-          // XEP-0045 (9.1): Banning a User.
-          message = "conversation.message.banned";
-        } else if (codes.includes("307")) {
-          // XEP-0045 (8.2): Kicking an Occupant.
-          message = "conversation.message.kicked";
-        } else if (codes.includes("322") || codes.includes("321")) {
-          // XEP-0045: Inform user that he or she is being removed from the
-          // room because the room has been changed to members-only and the
-          // user is not a member.
-          message = "conversation.message.removedNonMember";
-        } else if (codes.includes("332")) {
-          // XEP-0045: Inform user that he or she is being removed from the
-          // room because the MUC service is being shut down.
-          message = "conversation.message.mucShutdown";
-
-          // The reason here just duplicates what's in the system message.
-          reason = isReason = "";
-        } else {
-          // XEP-0045 (7.14): Received when the user parts a room.
-          message = "conversation.message.parted";
-
-          // The reason is in a status element in this case.
-          reasonNode = aStanza.getElement(["status"]);
-          reason = reasonNode ? reasonNode.innerText : "";
-          isReason = reason ? ".reason" : "";
-        }
-
-        if (message) {
-          let messageID = message + isYou + isActor + isReason;
-          let params = [actorNick, affectedNick, reason].filter(s => s);
-          this.writeMessage(this.name, _(messageID, ...params), {
-            system: true,
-          });
-        }
-      } else {
-        this.WARN("Unhandled type==unavailable MUC presence stanza.");
-      }
-      return;
-    }
-
-    if (codes.includes("201")) {
-      // XEP-0045 (10.1): Creating room.
-      // Service Acknowledges Room Creation
-      // and Room is awaiting configuration.
-      // XEP-0045 (10.1.2): Instant room.
-      let query = Stanza.node(
-        "query",
-        Stanza.NS.muc_owner,
-        null,
-        Stanza.node("x", Stanza.NS.xdata, { type: "submit" })
-      );
-      let s = Stanza.iq("set", null, jid, query);
-      this._account.sendStanza(s, aStanzaReceived => {
-        if (aStanzaReceived.attributes.type != "result") {
-          return false;
-        }
-
-        // XEP-0045: Service Informs New Room Owner of Success
-        // for instant and reserved rooms.
-        this.left = false;
-        this.joining = false;
-        return true;
-      });
-    } else if (codes.includes("210")) {
-      // XEP-0045 (7.6): Changing Nickname.
-      // Service modifies this user's nickname in accordance with local service
-      // policies.
-      changeNick();
-      return;
-    } else if (codes.includes("110")) {
-      // XEP-0045: Room exists and joined successfully.
-      this.left = false;
-      this.joining = false;
-      // TODO (Bug 1172350): Implement Service Discovery Extensions (XEP-0128) to obtain
-      // configuration of this room.
-    }
-
-    if (!this._participants.get(nick)) {
-      let participant = new MUCParticipant(nick, from, aStanza);
-      this._participants.set(nick, participant);
-      this.notifyObservers(
-        new nsSimpleEnumerator([participant]),
-        "chat-buddy-add"
-      );
-      if (this.nick != nick && !this.joining) {
-        this.writeMessage(this.name, _("conversation.message.join", nick), {
-          system: true,
-        });
-      } else if (this.nick == nick && this._rejoined) {
-        this.writeMessage(this.name, _("conversation.message.rejoined"), {
-          system: true,
-        });
-        this._rejoined = false;
-      }
-    } else {
-      this._participants.get(nick).onPresenceStanza(aStanza);
-      this.notifyObservers(this._participants.get(nick), "chat-buddy-update");
-    }
-  },
-
-  /* Called by the account when a message is received for this muc */
-  incomingMessage(aMsg, aStanza, aDate) {
-    let from = this._account._parseJID(aStanza.attributes.from).resource;
-    let id = aStanza.attributes.id;
-    let flags = {};
-    if (!from) {
-      flags.system = true;
-      from = this.name;
-    } else if (aStanza.attributes.type == "error") {
-      aMsg = _("conversation.error.notDelivered", aMsg);
-      flags.system = true;
-      flags.error = true;
-    } else if (from == this._nick) {
-      flags.outgoing = true;
-    } else {
-      flags.incoming = true;
-    }
-    if (aDate) {
-      flags.time = aDate / 1000;
-      flags.delayed = true;
-    }
-    if (id) {
-      // Checks if a message exists in conversation to avoid duplication.
-      if (this._messageIds.has(id)) {
-        return;
-      }
-      this._messageIds.add(id);
-    }
-    this.writeMessage(from, aMsg, flags);
-  },
-
-  getNormalizedChatBuddyName(aNick) {
-    return this._account.normalizeFullJid(this.name + "/" + aNick);
-  },
-
-  // Leaves MUC conversation.
-  part(aMsg = null) {
-    let s = Stanza.presence(
-      { to: this.name + "/" + this._nick, type: "unavailable" },
-      aMsg ? Stanza.node("status", null, null, aMsg.trim()) : null
-    );
-    this._account.sendStanza(s);
-    delete this.chatRoomFields;
+function XMPPProtocol() {
+  ChromeUtils.import("resource:///modules/xmpp-commands.jsm", this);
+  this.registerCommands();
+}
+XMPPProtocol.prototype = {
+  __proto__: GenericProtocolPrototype,
+  get normalizedName() {
+    return "jabber";
   },
-
-  // Invites a user to MUC conversation.
-  invite(aJID, aMsg = null) {
-    // XEP-0045 (7.8): Inviting Another User to a Room.
-    // XEP-0045 (7.8.2): Mediated Invitation.
-    let invite = Stanza.node(
-      "invite",
-      null,
-      { to: aJID },
-      aMsg ? Stanza.node("reason", null, null, aMsg) : null
-    );
-    let x = Stanza.node("x", Stanza.NS.muc_user, null, invite);
-    let s = Stanza.node("message", null, { to: this.name }, x);
-    this._account.sendStanza(
-      s,
-      this._account.handleErrors(
-        {
-          forbidden: _("conversation.error.inviteFailedForbidden"),
-          // ejabberd uses error not-allowed to indicate that this account does not
-          // have the required privileges to invite users instead of forbidden error,
-          // and this is not mentioned in the spec (XEP-0045).
-          notAllowed: _("conversation.error.inviteFailedForbidden"),
-          itemNotFound: _("conversation.error.failedJIDNotFound", aJID),
-        },
-        this
-      )
-    );
-  },
-
-  // Bans a participant from MUC conversation.
-  ban(aNickName, aMsg = null) {
-    // XEP-0045 (9.1): Banning a User.
-    let participant = this._participants.get(aNickName);
-    if (!participant) {
-      this.writeMessage(
-        this.name,
-        _("conversation.error.nickNotInRoom", aNickName),
-        { system: true }
-      );
-      return;
-    }
-    if (!participant.accountJid) {
-      this.writeMessage(
-        this.name,
-        _("conversation.error.banCommandAnonymousRoom"),
-        { system: true }
-      );
-      return;
-    }
-
-    let attributes = { affiliation: "outcast", jid: participant.accountJid };
-    let item = Stanza.node(
-      "item",
-      null,
-      attributes,
-      aMsg ? Stanza.node("reason", null, null, aMsg) : null
-    );
-    let s = Stanza.iq(
-      "set",
-      null,
-      this.name,
-      Stanza.node("query", Stanza.NS.muc_admin, null, item)
-    );
-    this._account.sendStanza(s, this._banKickHandler, this);
-  },
-
-  // Kicks a participant from MUC conversation.
-  kick(aNickName, aMsg = null) {
-    // XEP-0045 (8.2): Kicking an Occupant.
-    let attributes = { role: "none", nick: aNickName };
-    let item = Stanza.node(
-      "item",
-      null,
-      attributes,
-      aMsg ? Stanza.node("reason", null, null, aMsg) : null
-    );
-    let s = Stanza.iq(
-      "set",
-      null,
-      this.name,
-      Stanza.node("query", Stanza.NS.muc_admin, null, item)
-    );
-    this._account.sendStanza(s, this._banKickHandler, this);
-  },
-
-  // Callback for ban and kick commands.
-  _banKickHandler(aStanza) {
-    return this._account._handleResult(
-      {
-        notAllowed: _("conversation.error.banKickCommandNotAllowed"),
-        conflict: _("conversation.error.banKickCommandConflict"),
-      },
-      this
-    )(aStanza);
+  get name() {
+    return "XMPP";
   },
-
-  // Changes nick in MUC conversation to a new one.
-  setNick(aNewNick) {
-    // XEP-0045 (7.6): Changing Nickname.
-    let s = Stanza.presence({ to: this.name + "/" + aNewNick }, null);
-    this._account.sendStanza(
-      s,
-      this._account.handleErrors(
-        {
-          // XEP-0045 (7.6): Changing Nickname (example 53).
-          // TODO: We should discover if the user has a reserved nickname (maybe
-          // before joining a room), cf. XEP-0045 (7.12).
-          notAcceptable: _(
-            "conversation.error.changeNickFailedNotAcceptable",
-            aNewNick
-          ),
-          // XEP-0045 (7.2.9): Nickname Conflict.
-          conflict: _("conversation.error.changeNickFailedConflict", aNewNick),
-        },
-        this
-      )
-    );
-  },
-
-  // Called by the account when a message stanza is received for this muc and
-  // needs to be handled.
-  onMessageStanza(aStanza) {
-    let x = aStanza.getElement(["x"]);
-    let decline = x.getElement(["decline"]);
-    if (decline) {
-      // XEP-0045 (7.8): Inviting Another User to a Room.
-      // XEP-0045 (7.8.2): Mediated Invitation.
-      let invitee = decline.attributes.jid;
-      let reasonNode = decline.getElement(["reason"]);
-      let reason = reasonNode ? reasonNode.innerText : "";
-      let msg;
-      if (reason) {
-        msg = _(
-          "conversation.message.invitationDeclined.reason",
-          invitee,
-          reason
-        );
-      } else {
-        msg = _("conversation.message.invitationDeclined", invitee);
-      }
-
-      this.writeMessage(this.name, msg, { system: true });
-    } else {
-      this.WARN("Unhandled message stanza.");
-    }
+  get iconBaseURI() {
+    return "chrome://prpl-jabber/skin/";
   },
-
-  /* Called when the user closed the conversation */
-  close() {
-    if (!this.left) {
-      this.part();
-    }
-    GenericConvChatPrototype.close.call(this);
-  },
-  unInit() {
-    this._account.removeConversation(this.name);
-    GenericConvChatPrototype.unInit.call(this);
-  },
-};
-function XMPPMUCConversation(aAccount, aJID, aNick) {
-  this._init(aAccount, aJID, aNick);
-}
-XMPPMUCConversation.prototype = XMPPMUCConversationPrototype;
-
-/* Helper class for buddy conversations */
-var XMPPConversationPrototype = {
-  __proto__: GenericConvIMPrototype,
-
-  _typingTimer: null,
-  supportChatStateNotifications: true,
-  _typingState: "active",
-
-  // Indicates that current conversation is with a MUC participant and the
-  // recipient jid (stored in the userName) is of the form room@domain/nick.
-  _isMucParticipant: false,
-
-  get buddy() {
-    return this._account._buddies.get(this.name);
-  },
-  get title() {
-    return this.contactDisplayName;
-  },
-  get contactDisplayName() {
-    return this.buddy ? this.buddy.contactDisplayName : this.name;
-  },
-  get userName() {
-    return this.buddy ? this.buddy.userName : this.name;
-  },
-
-  // Returns jid (room@domain/nick) if it is with a MUC participant, and the
-  // name of conversation otherwise.
-  get normalizedName() {
-    if (this._isMucParticipant) {
-      return this._account.normalizeFullJid(this.name);
-    }
-    return this._account.normalize(this.name);
+  getAccount(aImAccount) {
+    return new XMPPAccount(this, aImAccount);
   },
 
-  // Used to avoid showing full jids in typing notifications.
-  get shortName() {
-    if (this.buddy) {
-      return this.buddy.contactDisplayName;
-    }
-
-    let jid = this._account._parseJID(this.name);
-    if (!jid) {
-      return this.name;
-    }
-
-    // Returns nick of the recipient if conversation is with a participant of
-    // a MUC we are in as jid of the recipient is of the form room@domain/nick.
-    if (this._isMucParticipant) {
-      return jid.resource;
-    }
-
-    return jid.node;
-  },
-
-  get shouldSendTypingNotifications() {
-    return (
-      this.supportChatStateNotifications &&
-      Services.prefs.getBoolPref("purple.conversations.im.send_typing")
-    );
-  },
-
-  /* Called when the user is typing a message
-   * aString - the currently typed message
-   * Returns the number of characters that can still be typed */
-  sendTyping(aString) {
-    if (!this.shouldSendTypingNotifications) {
-      return Ci.prplIConversation.NO_TYPING_LIMIT;
-    }
-
-    this._cancelTypingTimer();
-    if (aString.length) {
-      this._typingTimer = setTimeout(this.finishedComposing.bind(this), 10000);
-    }
-
-    this._setTypingState(aString.length ? "composing" : "active");
-
-    return Ci.prplIConversation.NO_TYPING_LIMIT;
-  },
-
-  finishedComposing() {
-    if (!this.shouldSendTypingNotifications) {
-      return;
-    }
-
-    this._setTypingState("paused");
-  },
-
-  _setTypingState(aNewState) {
-    if (this._typingState == aNewState) {
-      return;
-    }
-
-    let s = Stanza.message(this.to, null, aNewState);
-
-    // We don't care about errors in response to typing notifications
-    // (e.g. because the user has left the room when talking to a MUC
-    // participant).
-    this._account.sendStanza(s, () => true);
-
-    this._typingState = aNewState;
-  },
-  _cancelTypingTimer() {
-    if (this._typingTimer) {
-      clearTimeout(this._typingTimer);
-      delete this._typingTimer;
-    }
-  },
-
-  // Holds the resource of user that you are currently talking to, but if the
-  // user is a participant of a MUC we are in, holds the nick of user you are
-  // talking to.
-  _targetResource: "",
-
-  get to() {
-    if (!this._targetResource || this._isMucParticipant) {
-      return this.userName;
-    }
-    return this.userName + "/" + this._targetResource;
-  },
-
-  /* Called when the user enters a chat message */
-  sendMsg(aMsg) {
-    this._cancelTypingTimer();
-    let cs = this.shouldSendTypingNotifications ? "active" : null;
-    let s = Stanza.message(this.to, aMsg, cs);
-    this._account.sendStanza(s);
-    _displaySentMsg(this, aMsg);
-    delete this._typingState;
-  },
-
-  // Invites the contact to a MUC room.
-  invite(aRoomJid, aPassword = null) {
-    // XEP-0045 (7.8): Inviting Another User to a Room.
-    // XEP-0045 (7.8.1) and XEP-0249: Direct Invitation.
-    let x = Stanza.node("x", Stanza.NS.conference, {
-      jid: aRoomJid,
-      password: aPassword,
-    });
-    this._account.sendStanza(Stanza.node("message", null, { to: this.to }, x));
-  },
-
-  // Query the user for its Software Version.
-  // XEP-0092: Software Version.
-  getVersion() {
-    // TODO: Use Service Discovery to determine if the user's client supports
-    // jabber:iq:version protocol.
-
-    let s = Stanza.iq(
-      "get",
-      null,
-      this.to,
-      Stanza.node("query", Stanza.NS.version)
-    );
-    this._account.sendStanza(s, aStanza => {
-      // TODO: handle other errors that can result from querying
-      // user for its software version.
-      if (
-        this._account.handleErrors(
-          {
-            default: _("conversation.error.version.unknown"),
-          },
-          this
-        )(aStanza)
-      ) {
-        return;
-      }
-
-      let query = aStanza.getElement(["query"]);
-      if (!query || query.uri != Stanza.NS.version) {
-        this.WARN(
-          "Received a response to version query which does not " +
-            "contain query element or 'jabber:iq:version' namespace."
-        );
-        return;
-      }
-
-      let name = query.getElement(["name"]);
-      let version = query.getElement(["version"]);
-      if (!name || !version) {
-        // XEP-0092: name and version elements are REQUIRED.
-        this.WARN(
-          "Received a response to version query which does not " +
-            "contain name or version."
-        );
-        return;
-      }
-
-      let messageID = "conversation.message.version";
-      let params = [this.shortName, name.innerText, version.innerText];
-
-      // XEP-0092: os is OPTIONAL.
-      let os = query.getElement(["os"]);
-      if (os) {
-        params.push(os.innerText);
-        messageID += "WithOS";
-      }
-
-      this.writeMessage(this.name, _(messageID, ...params), { system: true });
-    });
-  },
-
-  /* Perform entity escaping before displaying the message. We assume incoming
-     messages have already been escaped, and will otherwise be filtered. */
-  prepareForDisplaying(aMsg) {
-    if (aMsg.outgoing && !aMsg.system) {
-      aMsg.displayMessage = TXTToHTML(aMsg.displayMessage);
-    }
-    GenericConversationPrototype.prepareForDisplaying.apply(this, arguments);
-  },
-
-  /* Called by the account when a message is received from the buddy */
-  incomingMessage(aMsg, aStanza, aDate) {
-    let from = aStanza.attributes.from;
-    this._targetResource = this._account._parseJID(from).resource;
-    let flags = {};
-    let error = this._account.parseError(aStanza);
-    if (error) {
-      let norm = this._account.normalize(from);
-      let muc = this._account._mucs.get(norm);
-
-      if (!aMsg) {
-        // Failed outgoing message.
-        switch (error.condition) {
-          case "remote-server-not-found":
-            aMsg = _("conversation.error.remoteServerNotFound");
-            break;
-          case "service-unavailable":
-            aMsg = _(
-              "conversation.error.sendServiceUnavailable",
-              this.shortName
-            );
-            break;
-          default:
-            aMsg = _("conversation.error.unknownSendError");
-            break;
-        }
-      } else if (
-        this._isMucParticipant &&
-        muc &&
-        !muc.left &&
-        error.condition == "item-not-found"
-      ) {
-        // XEP-0045 (7.5): MUC private messages.
-        // If we try to send to participant not in a room we are in.
-        aMsg = _(
-          "conversation.error.sendFailedAsRecipientNotInRoom",
-          this._targetResource,
-          aMsg
-        );
-      } else if (
-        this._isMucParticipant &&
-        (error.condition == "item-not-found" ||
-          error.condition == "not-acceptable")
-      ) {
-        // If we left a room and try to send to a participant in it or the
-        // room is removed.
-        aMsg = _(
-          "conversation.error.sendFailedAsNotInRoom",
-          this._account.normalize(from),
-          aMsg
-        );
-      } else {
-        aMsg = _("conversation.error.notDelivered", aMsg);
-      }
-      flags.system = true;
-      flags.error = true;
-    } else {
-      flags = { incoming: true, _alias: this.contactDisplayName };
-    }
-    if (aDate) {
-      flags.time = aDate / 1000;
-      flags.delayed = true;
-    }
-    this.writeMessage(from, aMsg, flags);
-  },
-
-  /* Called when the user closed the conversation */
-  close() {
-    // TODO send the stanza indicating we have left the conversation?
-    GenericConvIMPrototype.close.call(this);
-  },
-  unInit() {
-    this._account.removeConversation(this.normalizedName);
-    GenericConvIMPrototype.unInit.call(this);
-  },
-};
-
-// Creates XMPP conversation.
-function XMPPConversation(aAccount, aNormalizedName, aMucParticipant) {
-  this._init(aAccount, aNormalizedName);
-  if (aMucParticipant) {
-    this._isMucParticipant = true;
-  }
-}
-XMPPConversation.prototype = XMPPConversationPrototype;
-
-/* Helper class for buddies */
-var XMPPAccountBuddyPrototype = {
-  __proto__: GenericAccountBuddyPrototype,
-
-  subscription: "none",
-  // Returns a list of TooltipInfo objects to be displayed when the user
-  // hovers over the buddy.
-  getTooltipInfo() {
-    if (!this._account.connected) {
-      return null;
-    }
-
-    let tooltipInfo = [];
-    if (this._resources) {
-      for (let r in this._resources) {
-        let status = this._resources[r];
-        let statusString = Status.toLabel(status.statusType);
-        if (
-          status.statusType == Ci.imIStatusInfo.STATUS_IDLE &&
-          status.idleSince
-        ) {
-          let now = Math.floor(Date.now() / 1000);
-          let valuesAndUnits = DownloadUtils.convertTimeUnits(
-            now - status.idleSince
-          );
-          if (!valuesAndUnits[2]) {
-            valuesAndUnits.splice(2, 2);
-          }
-          statusString += " (" + valuesAndUnits.join(" ") + ")";
-        }
-        if (status.statusText) {
-          statusString += " - " + status.statusText;
-        }
-        let label = r ? _("tooltip.status", r) : _("tooltip.statusNoResource");
-        tooltipInfo.push(new TooltipInfo(label, statusString));
-      }
-    }
-
-    // The subscription value is interesting to display only in unusual cases.
-    if (this.subscription != "both") {
-      tooltipInfo.push(
-        new TooltipInfo(_("tooltip.subscription"), this.subscription)
-      );
-    }
-
-    return new nsSimpleEnumerator(tooltipInfo);
-  },
-
-  // _rosterAlias is the value stored in the roster on the XMPP
-  // server. For most servers we will be read/write.
-  _rosterAlias: "",
-  set rosterAlias(aNewAlias) {
-    let old = this.displayName;
-    this._rosterAlias = aNewAlias;
-    if (old != this.displayName) {
-      this._notifyObservers("display-name-changed", old);
-    }
-  },
-  _vCardReceived: false,
-  // _vCardFormattedName is the display name the contact has set for
-  // himself in his vCard. It's read-only from our point of view.
-  _vCardFormattedName: "",
-  set vCardFormattedName(aNewFormattedName) {
-    let old = this.displayName;
-    this._vCardFormattedName = aNewFormattedName;
-    if (old != this.displayName) {
-      this._notifyObservers("display-name-changed", old);
-    }
-  },
-
-  // _serverAlias is set by jsProtoHelper to the value we cached in sqlite.
-  // Use it only if we have neither of the other two values; usually because
-  // we haven't connected to the server yet.
-  get serverAlias() {
-    return this._rosterAlias || this._vCardFormattedName || this._serverAlias;
-  },
-  set serverAlias(aNewAlias) {
-    if (!this._rosterItem) {
-      this.ERROR(
-        "attempting to update the server alias of an account buddy " +
-          "for which we haven't received a roster item."
-      );
-      return;
-    }
-
-    let item = this._rosterItem;
-    if (aNewAlias) {
-      item.attributes.name = aNewAlias;
-    } else if ("name" in item.attributes) {
-      delete item.attributes.name;
-    }
+  usernameSplits: [
+    {
+      get label() {
+        return _("options.domain");
+      },
+      separator: "@",
+      defaultValue: "jabber.org",
+      reverse: true,
+    },
+  ],
 
-    let s = Stanza.iq(
-      "set",
-      null,
-      null,
-      Stanza.node("query", Stanza.NS.roster, null, item)
-    );
-    this._account.sendStanza(s);
-
-    // If we are going to change the alias on the server, discard the cached
-    // value that we got from our local sqlite storage at startup.
-    delete this._serverAlias;
-  },
-
-  /* Display name of the buddy */
-  get contactDisplayName() {
-    return this.buddy.contact.displayName || this.displayName;
-  },
-
-  get tag() {
-    return this._tag;
-  },
-  set tag(aNewTag) {
-    let oldTag = this._tag;
-    if (oldTag.name == aNewTag.name) {
-      this.ERROR("attempting to set the tag to the same value");
-      return;
-    }
-
-    this._tag = aNewTag;
-    Services.contacts.accountBuddyMoved(this, oldTag, aNewTag);
-
-    if (!this._rosterItem) {
-      this.ERROR(
-        "attempting to change the tag of an account buddy without roster item"
-      );
-      return;
-    }
-
-    let item = this._rosterItem;
-    let oldXML = item.getXML();
-    // Remove the old tag if it was listed in the roster item.
-    item.children = item.children.filter(
-      c => c.qName != "group" || c.innerText != oldTag.name
-    );
-    // Ensure the new tag is listed.
-    let newTagName = aNewTag.name;
-    if (!item.getChildren("group").some(g => g.innerText == newTagName)) {
-      item.addChild(Stanza.node("group", null, null, newTagName));
-    }
-    // Avoid sending anything to the server if the roster item hasn't changed.
-    // It's possible that the roster item hasn't changed if the roster
-    // item had several groups and the user moved locally the contact
-    // to another group where it already was on the server.
-    if (item.getXML() == oldXML) {
-      return;
-    }
-
-    let s = Stanza.iq(
-      "set",
-      null,
-      null,
-      Stanza.node("query", Stanza.NS.roster, null, item)
-    );
-    this._account.sendStanza(s);
-  },
-
-  remove() {
-    if (!this._account.connected) {
-      return;
-    }
-
-    let s = Stanza.iq(
-      "set",
-      null,
-      null,
-      Stanza.node(
-        "query",
-        Stanza.NS.roster,
-        null,
-        Stanza.node("item", null, {
-          jid: this.normalizedName,
-          subscription: "remove",
-        })
-      )
-    );
-    this._account.sendStanza(s);
-  },
-
-  _photoHash: null,
-  _saveIcon(aPhotoNode) {
-    // Some servers seem to send a photo node without a type declared.
-    let type = aPhotoNode.getElement(["TYPE"]);
-    if (!type) {
-      return;
-    }
-    type = type.innerText;
-    const kExt = {
-      "image/gif": "gif",
-      "image/jpeg": "jpg",
-      "image/png": "png",
-    };
-    if (!kExt.hasOwnProperty(type)) {
-      return;
-    }
-
-    let content = "",
-      data = "";
-    // Strip all characters not allowed in base64 before parsing.
-    let parseBase64 = aBase => atob(aBase.replace(/[^A-Za-z0-9\+\/\=]/g, ""));
-    for (let line of aPhotoNode.getElement(["BINVAL"]).innerText.split("\n")) {
-      data += line;
-      // Mozilla's atob() doesn't handle padding with "=" or "=="
-      // unless it's at the end of the string, so we have to work around that.
-      if (line.endsWith("=")) {
-        content += parseBase64(data);
-        data = "";
-      }
-    }
-    content += parseBase64(data);
-
-    // Store a sha1 hash of the photo we have just received.
-    let ch = Cc["@mozilla.org/security/hash;1"].createInstance(
-      Ci.nsICryptoHash
-    );
-    ch.init(ch.SHA1);
-    let dataArray = Object.keys(content).map(i => content.charCodeAt(i));
-    ch.update(dataArray, dataArray.length);
-    let hash = ch.finish(false);
-    function toHexString(charCode) {
-      return ("0" + charCode.toString(16)).slice(-2);
-    }
-    this._photoHash = Object.keys(hash)
-      .map(i => toHexString(hash.charCodeAt(i)))
-      .join("");
-
-    let istream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
-      Ci.nsIStringInputStream
-    );
-    istream.setData(content, content.length);
-
-    let fileName = this._photoHash + "." + kExt[type];
-    let file = FileUtils.getFile("ProfD", [
-      "icons",
-      this.account.protocol.normalizedName,
-      this.account.normalizedName,
-      fileName,
-    ]);
-    let ostream = FileUtils.openSafeFileOutputStream(file);
-    let buddy = this;
-    NetUtil.asyncCopy(istream, ostream, function(rc) {
-      if (Components.isSuccessCode(rc)) {
-        buddy.buddyIconFilename = Services.io.newFileURI(file).spec;
-      }
-    });
-  },
-
-  _preferredResource: undefined,
-  _resources: null,
-  onAccountDisconnected() {
-    delete this._preferredResource;
-    delete this._resources;
-  },
-  // Called by the account when a presence stanza is received for this buddy.
-  onPresenceStanza(aStanza) {
-    let preferred = this._preferredResource;
-
-    // Facebook chat's XMPP server doesn't send resources, let's
-    // replace undefined resources with empty resources.
-    let resource =
-      this._account._parseJID(aStanza.attributes.from).resource || "";
-
-    let type = aStanza.attributes.type;
-
-    // Reset typing status if the buddy is in a conversation and becomes unavailable.
-    let conv = this._account._conv.get(this.normalizedName);
-    if (type == "unavailable" && conv) {
-      conv.updateTyping(Ci.prplIConvIM.NOT_TYPING, this.contactDisplayName);
-    }
-
-    if (type == "unavailable" || type == "error") {
-      if (!this._resources || !(resource in this._resources)) {
-        // Ignore for already offline resources.
-        return;
-      }
-      delete this._resources[resource];
-      if (preferred == resource) {
-        preferred = undefined;
-      }
-    } else {
-      let statusInfo = parseStatus(aStanza);
-      let priority = aStanza.getElement(["priority"]);
-      priority = priority ? parseInt(priority.innerText, 10) : 0;
-
-      if (!this._resources) {
-        this._resources = {};
-      }
-      this._resources[resource] = {
-        statusType: statusInfo.statusType,
-        statusText: statusInfo.statusText,
-        idleSince: statusInfo.idleSince,
-        priority,
-        stanza: aStanza,
-      };
-    }
-
-    let photo = aStanza.getElement(["x", "photo"]);
-    if (photo && photo.uri == Stanza.NS.vcard_update) {
-      let hash = photo.innerText;
-      if (hash && hash != this._photoHash) {
-        this._account._addVCardRequest(this.normalizedName);
-      } else if (!hash && this._photoHash) {
-        delete this._photoHash;
-        this.buddyIconFilename = "";
-      }
-    }
-
-    for (let r in this._resources) {
-      if (
-        preferred === undefined ||
-        this._resources[r].statusType > this._resources[preferred].statusType
-      ) {
-        // FIXME also compare priorities...
-        preferred = r;
-      }
-    }
-    if (
-      preferred != undefined &&
-      preferred == this._preferredResource &&
-      resource != preferred
-    ) {
-      // The presence information change is only for an unused resource,
-      // only potential buddy tooltips need to be refreshed.
-      this._notifyObservers("status-detail-changed");
-      return;
-    }
-
-    // Presence info has changed enough that if we are having a
-    // conversation with one resource of this buddy, we should send
-    // the next message to all resources.
-    // FIXME: the test here isn't exactly right...
-    if (
-      this._preferredResource != preferred &&
-      this._account._conv.has(this.normalizedName)
-    ) {
-      delete this._account._conv.get(this.normalizedName)._targetResource;
-    }
-
-    this._preferredResource = preferred;
-    if (preferred === undefined) {
-      let statusType = Ci.imIStatusInfo.STATUS_UNKNOWN;
-      if (type == "unavailable") {
-        statusType = Ci.imIStatusInfo.STATUS_OFFLINE;
-      }
-      this.setStatus(statusType, "");
-    } else {
-      preferred = this._resources[preferred];
-      this.setStatus(preferred.statusType, preferred.statusText);
-    }
-  },
-
-  /* Can send messages to buddies who appear offline */
-  get canSendMessage() {
-    return this.account.connected;
-  },
-
-  /* Called when the user wants to chat with the buddy */
-  createConversation() {
-    return this._account.createConversation(this.normalizedName);
-  },
-};
-function XMPPAccountBuddy(aAccount, aBuddy, aTag, aUserName) {
-  this._init(aAccount, aBuddy, aTag, aUserName);
-}
-XMPPAccountBuddy.prototype = XMPPAccountBuddyPrototype;
-
-var XMPPRoomInfoPrototype = {
-  __proto__: ClassInfo("prplIRoomInfo", "XMPP RoomInfo Object"),
-  get topic() {
-    return "";
-  },
-  get participantCount() {
-    return Ci.prplIRoomInfo.NO_PARTICIPANT_COUNT;
-  },
-  get chatRoomFieldValues() {
-    let roomJid = this._account._roomList.get(this.name);
-    return this._account.getChatRoomDefaultFieldValues(roomJid);
-  },
-};
-function XMPPRoomInfo(aName, aAccount) {
-  this.name = aName;
-  this._account = aAccount;
-}
-XMPPRoomInfo.prototype = XMPPRoomInfoPrototype;
-
-/* Helper class for account */
-var XMPPAccountPrototype = {
-  __proto__: GenericAccountPrototype,
-
-  _jid: null, // parsed Jabber ID: node, domain, resource
-  _connection: null, // XMPPSession socket
-  authMechanisms: null, // hook to let prpls tweak the list of auth mechanisms
-
-  // Contains the domain of MUC service which is obtained using service
-  // discovery.
-  _mucService: null,
-
-  // Maps room names to room jid.
-  _roomList: new Map(),
-
-  // Callbacks used when roomInfo is available.
-  _roomInfoCallbacks: new Set(),
-
-  // Determines if roomInfo that we have is expired or not.
-  _lastListTime: 0,
-  get isRoomInfoStale() {
-    return Date.now() - this._lastListTime > kListRefreshInterval;
-  },
-
-  // If true, we are waiting for replies.
-  _pendingList: false,
-
-  // An array of jids for which we still need to request vCards.
-  _pendingVCardRequests: [],
-
-  // XEP-0280: Message Carbons.
-  // If true, message carbons are currently enabled.
-  _isCarbonsEnabled: false,
-
-  /* Generate unique id for a stanza. Using id and unique sid is defined in
-   * RFC 6120 (Section 8.2.3, 4.7.3).
-   */
-  generateId: () =>
-    UuidGenerator.generateUUID()
-      .toString()
-      .slice(1, -1),
-
-  _init(aProtoInstance, aImAccount) {
-    GenericAccountPrototype._init.call(this, aProtoInstance, aImAccount);
-
-    // Ongoing conversations.
-    // The keys of this._conv are assumed to be normalized like account@domain
-    // for normal conversations and like room@domain/nick for MUC participant
-    // convs.
-    this._conv = new NormalizedMap(this.normalizeFullJid.bind(this));
-
-    this._buddies = new NormalizedMap(this.normalize.bind(this));
-    this._mucs = new NormalizedMap(this.normalize.bind(this));
-  },
-
-  get canJoinChat() {
-    return true;
-  },
-  chatRoomFields: {
-    room: {
+  options: {
+    resource: {
+      get label() {
+        return _("options.resource");
+      },
+      default: "",
+    },
+    priority: {
+      get label() {
+        return _("options.priority");
+      },
+      default: 0,
+    },
+    connection_security: {
       get label() {
-        return _("chatRoomField.room");
+        return _("options.connectionSecurity");
       },
-      required: true,
+      listValues: {
+        get require_tls() {
+          return _("options.connectionSecurity.requireEncryption");
+        },
+        get opportunistic_tls() {
+          return _("options.connectionSecurity.opportunisticTLS");
+        },
+        get allow_unencrypted_plain_auth() {
+          return _("options.connectionSecurity.allowUnencryptedAuth");
+        },
+        // "old_ssl" and "none" are also supported, but not exposed in the UI.
+        // Any unknown value will fallback to the opportunistic_tls behavior.
+      },
+      default: "require_tls",
     },
     server: {
       get label() {
-        return _("chatRoomField.server");
-      },
-      required: true,
-    },
-    nick: {
-      get label() {
-        return _("chatRoomField.nick");
+        return _("options.connectServer");
       },
-      required: true,
+      default: "",
     },
-    password: {
+    port: {
       get label() {
-        return _("chatRoomField.password");
+        return _("options.connectPort");
       },
-      isPassword: true,
+      default: 5222,
     },
   },
-  parseDefaultChatName(aDefaultChatName) {
-    if (!aDefaultChatName) {
-      return { nick: this._jid.node };
-    }
-
-    let params = aDefaultChatName.trim().split(/\s+/);
-    let jid = this._parseJID(params[0]);
-
-    // We swap node and domain as domain is required for parseJID, but node and
-    // resource are optional. In MUC join command, Node is required as it
-    // represents a room, but domain and resource are optional as we get muc
-    // domain from service discovery.
-    if (!jid.node && jid.domain) {
-      [jid.node, jid.domain] = [jid.domain, jid.node];
-    }
-
-    let chatFields = {
-      room: jid.node,
-      server: jid.domain || this._mucService,
-      nick: jid.resource || this._jid.node,
-    };
-    if (params.length > 1) {
-      chatFields.password = params[1];
-    }
-    return chatFields;
-  },
-  getChatRoomDefaultFieldValues(aDefaultChatName) {
-    let rv = GenericAccountPrototype.getChatRoomDefaultFieldValues.call(
-      this,
-      aDefaultChatName
-    );
-    if (!rv.values.nick) {
-      rv.values.nick = this._jid.node;
-    }
-    if (!rv.values.server && this._mucService) {
-      rv.values.server = this._mucService;
-    }
-
-    return rv;
-  },
-
-  // XEP-0045: Requests joining room if it exists or
-  // creating room if it does not exist.
-  joinChat(aComponents) {
-    let jid =
-      aComponents.getValue("room") + "@" + aComponents.getValue("server");
-    let nick = aComponents.getValue("nick");
-
-    let muc = this._mucs.get(jid);
-    if (muc) {
-      if (!muc.left) {
-        // We are already in this conversation.
-        return muc;
-      } else if (!muc.chatRoomFields) {
-        // We are rejoining a room that was parted by the user.
-        muc._rejoined = true;
-      }
-    } else {
-      muc = new this._MUCConversationConstructor(this, jid, nick);
-      this._mucs.set(jid, muc);
-    }
-
-    // Store the prplIChatRoomFieldValues to enable later reconnections.
-    muc.chatRoomFields = aComponents;
-    muc.joining = true;
-    muc.removeAllParticipants();
-
-    let password = aComponents.getValue("password");
-    let x = Stanza.node(
-      "x",
-      Stanza.NS.muc,
-      null,
-      password ? Stanza.node("password", null, null, password) : null
-    );
-    let logString;
-    if (password) {
-      logString =
-        "<presence .../> (Stanza containing password to join MUC " +
-        jid +
-        "/" +
-        nick +
-        " not logged)";
-    }
-    this.sendStanza(
-      Stanza.presence({ to: jid + "/" + nick }, x),
-      undefined,
-      undefined,
-      logString
-    );
-    return muc;
-  },
-
-  _idleSince: 0,
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "idle-time-changed") {
-      let idleTime = parseInt(aData, 10);
-      if (idleTime) {
-        this._idleSince = Math.floor(Date.now() / 1000) - idleTime;
-      } else {
-        delete this._idleSince;
-      }
-      this._shouldSendPresenceForIdlenessChange = true;
-      executeSoon(
-        function() {
-          if ("_shouldSendPresenceForIdlenessChange" in this) {
-            this._sendPresence();
-          }
-        }.bind(this)
-      );
-    } else if (aTopic == "status-changed") {
-      this._sendPresence();
-    } else if (aTopic == "user-icon-changed") {
-      delete this._cachedUserIcon;
-      this._forceUserIconUpdate = true;
-      this._sendVCard();
-    } else if (aTopic == "user-display-name-changed") {
-      this._forceUserDisplayNameUpdate = true;
-    }
-    this._sendVCard();
-  },
-
-  /* GenericAccountPrototype events */
-  /* Connect to the server */
-  connect() {
-    this._jid = this._parseJID(this.name);
-
-    // For the resource, if the user has edited the option, always use that.
-    if (this.prefs.prefHasUserValue("resource")) {
-      let resource = this.getString("resource");
-
-      // this._jid needs to be updated. This value is however never used
-      // because while connected it's the jid of the session that's
-      // interesting.
-      this._jid = this._setJID(this._jid.domain, this._jid.node, resource);
-    } else if (this._jid.resource) {
-      // If there is a resource in the account name (inherited from libpurple),
-      // migrate it to the pref so it appears correctly in the advanced account
-      // options next time.
-      this.prefs.setStringPref("resource", this._jid.resource);
-    }
-
-    this._connection = new XMPPSession(
-      this.getString("server") || this._jid.domain,
-      this.getInt("port") || 5222,
-      this.getString("connection_security"),
-      this._jid,
-      this.imAccount.password,
-      this
-    );
-  },
-
-  remove() {
-    this._conv.forEach(conv => conv.close());
-    this._mucs.forEach(muc => muc.close());
-    this._buddies.forEach((buddy, jid) => this._forgetRosterItem(jid));
-  },
-
-  unInit() {
-    if (this._connection) {
-      this._disconnect(undefined, undefined, true);
-    }
-    delete this._jid;
-    delete this._conv;
-    delete this._buddies;
-    delete this._mucs;
-  },
-
-  /* Disconnect from the server */
-  disconnect() {
-    this._disconnect();
-  },
-
-  addBuddy(aTag, aName) {
-    if (!this._connection) {
-      throw new Error("The account isn't connected");
-    }
-
-    let jid = this.normalize(aName);
-    if (!jid || !jid.includes("@")) {
-      throw new Error("Invalid username");
-    }
-
-    if (this._buddies.has(jid)) {
-      let subscription = this._buddies.get(jid).subscription;
-      if (subscription && (subscription == "both" || subscription == "to")) {
-        this.DEBUG("not re-adding an existing buddy");
-        return;
-      }
-    } else {
-      let s = Stanza.iq(
-        "set",
-        null,
-        null,
-        Stanza.node(
-          "query",
-          Stanza.NS.roster,
-          null,
-          Stanza.node(
-            "item",
-            null,
-            { jid },
-            Stanza.node("group", null, null, aTag.name)
-          )
-        )
-      );
-      this.sendStanza(
-        s,
-        this._handleResult({
-          default: aError => {
-            this.WARN(
-              "Unable to add a roster item due to " + aError + " error."
-            );
-          },
-        })
-      );
-    }
-    this.sendStanza(Stanza.presence({ to: jid, type: "subscribe" }));
-  },
-
-  /* Loads a buddy from the local storage.
-   * Called for each buddy locally stored before connecting
-   * to the server. */
-  loadBuddy(aBuddy, aTag) {
-    let buddy = new this._accountBuddyConstructor(this, aBuddy, aTag);
-    this._buddies.set(buddy.normalizedName, buddy);
-    return buddy;
-  },
-
-  /* Replies to a buddy request in order to accept it or deny it. */
-  replyToBuddyRequest(aReply, aRequest) {
-    if (!this._connection) {
-      return;
-    }
-    let s = Stanza.presence({ to: aRequest.userName, type: aReply });
-    this.sendStanza(s);
-    this.removeBuddyRequest(aRequest);
-  },
-
-  requestBuddyInfo(aJid) {
-    if (!this.connected) {
-      Services.obs.notifyObservers(EmptyEnumerator, "user-info-received", aJid);
-      return;
-    }
-
-    let userName;
-    let tooltipInfo = [];
-    let jid = this._parseJID(aJid);
-    let muc = this._mucs.get(jid.node + "@" + jid.domain);
-    let participant;
-    if (muc) {
-      participant = muc._participants.get(jid.resource);
-      if (participant) {
-        if (participant.accountJid) {
-          userName = participant.accountJid;
-        }
-        if (!muc.left) {
-          let statusType = participant.statusType;
-          let statusText = participant.statusText;
-          tooltipInfo.push(
-            new TooltipInfo(statusType, statusText, Ci.prplITooltipInfo.status)
-          );
-
-          if (participant.buddyIconFilename) {
-            tooltipInfo.push(
-              new TooltipInfo(
-                null,
-                participant.buddyIconFilename,
-                Ci.prplITooltipInfo.icon
-              )
-            );
-          }
-        }
-      }
-    }
-    Services.obs.notifyObservers(
-      new nsSimpleEnumerator(tooltipInfo),
-      "user-info-received",
-      aJid
-    );
-
-    let iq = Stanza.iq(
-      "get",
-      null,
-      aJid,
-      Stanza.node("vCard", Stanza.NS.vcard)
-    );
-    this.sendStanza(iq, aStanza => {
-      let vCardInfo = {};
-      let vCardNode = aStanza.getElement(["vCard"]);
-
-      // In the case of an error response, we just notify the observers with
-      // what info we already have.
-      if (aStanza.attributes.type == "result" && vCardNode) {
-        vCardInfo = this.parseVCard(vCardNode);
-      }
-
-      // The real jid of participant which is of the form local@domain/resource.
-      // We consider the jid is provided by server is more correct than jid is
-      // set by the user.
-      if (userName) {
-        vCardInfo.userName = userName;
-      }
-
-      // vCard fields we want to display in the tooltip.
-      const kTooltipFields = [
-        "userName",
-        "fullName",
-        "nickname",
-        "title",
-        "organization",
-        "email",
-        "birthday",
-        "locality",
-        "country",
-        "telephone",
-      ];
-
-      let tooltipInfo = [];
-      for (let field of kTooltipFields) {
-        if (vCardInfo.hasOwnProperty(field)) {
-          tooltipInfo.push(
-            new TooltipInfo(_("tooltip." + field), vCardInfo[field])
-          );
-        }
-      }
-      if (vCardInfo.photo) {
-        let dataURI = this._getPhotoURI(vCardInfo.photo);
-
-        // Store the photo URI for this participant.
-        if (participant) {
-          participant.buddyIconFilename = dataURI;
-        }
-
-        tooltipInfo.push(
-          new TooltipInfo(null, dataURI, Ci.prplITooltipInfo.icon)
-        );
-      }
-      Services.obs.notifyObservers(
-        new nsSimpleEnumerator(tooltipInfo),
-        "user-info-received",
-        aJid
-      );
-    });
-  },
-
-  // Parses the photo node of a received vCard if exists and returns string of
-  // data URI, otherwise returns null.
-  _getPhotoURI(aPhotoNode) {
-    if (!aPhotoNode) {
-      return null;
-    }
-
-    let type = aPhotoNode.getElement(["TYPE"]);
-    let value = aPhotoNode.getElement(["BINVAL"]);
-    if (!type || !value) {
-      return null;
-    }
-
-    return "data:" + type.innerText + ";base64," + value.innerText;
-  },
-
-  // Parses the vCard into the properties of the returned object.
-  parseVCard(aVCardNode) {
-    // XEP-0054: vcard-temp.
-    let aResult = {};
-    for (let node of aVCardNode.children.filter(
-      child => child.type == "node"
-    )) {
-      let localName = node.localName;
-      let innerText = node.innerText;
-      if (innerText) {
-        if (localName == "FN") {
-          aResult.fullName = innerText;
-        } else if (localName == "NICKNAME") {
-          aResult.nickname = innerText;
-        } else if (localName == "TITLE") {
-          aResult.title = innerText;
-        } else if (localName == "BDAY") {
-          aResult.birthday = innerText;
-        } else if (localName == "JABBERID") {
-          aResult.userName = innerText;
-        }
-      }
-      if (localName == "ORG") {
-        let organization = node.getElement(["ORGNAME"]);
-        if (organization && organization.innerText) {
-          aResult.organization = organization.innerText;
-        }
-      } else if (localName == "EMAIL") {
-        let userID = node.getElement(["USERID"]);
-        if (userID && userID.innerText) {
-          aResult.email = userID.innerText;
-        }
-      } else if (localName == "ADR") {
-        let locality = node.getElement(["LOCALITY"]);
-        if (locality && locality.innerText) {
-          aResult.locality = locality.innerText;
-        }
-
-        let country = node.getElement(["CTRY"]);
-        if (country && country.innerText) {
-          aResult.country = country.innerText;
-        }
-      } else if (localName == "PHOTO") {
-        aResult.photo = node;
-      } else if (localName == "TEL") {
-        let number = node.getElement(["NUMBER"]);
-        if (number && number.innerText) {
-          aResult.telephone = number.innerText;
-        }
-      }
-      // TODO: Parse the other fields of vCard and display it in system messages
-      // in response to /whois.
-    }
-    return aResult;
-  },
-
-  // Returns undefined if not an error stanza, and an object
-  // describing the error otherwise:
-  parseError(aStanza) {
-    if (aStanza.attributes.type != "error") {
-      return undefined;
-    }
-
-    let retval = { stanza: aStanza };
-    let error = aStanza.getElement(["error"]);
-
-    // RFC 6120 Section 8.3.2: Type must be one of
-    // auth -- retry after providing credentials
-    // cancel -- do not retry (the error cannot be remedied)
-    // continue -- proceed (the condition was only a warning)
-    // modify -- retry after changing the data sent
-    // wait -- retry after waiting (the error is temporary).
-    retval.type = error.attributes.type;
-
-    // RFC 6120 Section 8.3.3.
-    const kDefinedConditions = [
-      "bad-request",
-      "conflict",
-      "feature-not-implemented",
-      "forbidden",
-      "gone",
-      "internal-server-error",
-      "item-not-found",
-      "jid-malformed",
-      "not-acceptable",
-      "not-allowed",
-      "not-authorized",
-      "policy-violation",
-      "recipient-unavailable",
-      "redirect",
-      "registration-required",
-      "remote-server-not-found",
-      "remote-server-timeout",
-      "resource-constraint",
-      "service-unavailable",
-      "subscription-required",
-      "undefined-condition",
-      "unexpected-request",
-    ];
-    let condition = kDefinedConditions.find(c => error.getElement([c]));
-    if (!condition) {
-      // RFC 6120 Section 8.3.2.
-      this.WARN(
-        "Nonstandard or missing defined-condition element in error stanza."
-      );
-      condition = "undefined-condition";
-    }
-    retval.condition = condition;
-
-    let errortext = error.getElement(["text"]);
-    if (errortext) {
-      retval.text = errortext.innerText;
-    }
-
-    return retval;
-  },
-
-  // Returns an error-handling callback for use with sendStanza generated
-  // from aHandlers, an object defining the error handlers.
-  // If the stanza passed to the callback is an error stanza, it checks if
-  // aHandlers contains a property with the name of the defined condition
-  // of the error.
-  // * If the property is a function, it is called with the parsed error
-  //   as its argument, bound to aThis (if provided).
-  //   It should return true if the error was handled.
-  // * If the property is a string, it is displayed as a system message
-  //   in the conversation given by aThis.
-  handleErrors(aHandlers, aThis) {
-    return aStanza => {
-      if (!aHandlers) {
-        return false;
-      }
-
-      let error = this.parseError(aStanza);
-      if (!error) {
-        return false;
-      }
-
-      let toCamelCase = aStr => {
-        // Turn defined condition string into a valid camelcase
-        // JS property name.
-        let capitalize = s => s[0].toUpperCase() + s.slice(1);
-        let uncapitalize = s => s[0].toLowerCase() + s.slice(1);
-        return uncapitalize(
-          aStr
-            .split("-")
-            .map(capitalize)
-            .join("")
-        );
-      };
-      let condition = toCamelCase(error.condition);
-      // Check if we have a handler property for this kind of error or a
-      // default handler.
-      if (!(condition in aHandlers) && !("default" in aHandlers)) {
-        return false;
-      }
-
-      // Try to get the handler for condition, if we cannot get it, try to get
-      // the default handler.
-      let handler = aHandlers[condition];
-      if (!handler) {
-        handler = aHandlers.default;
-      }
-
-      if (typeof handler == "string") {
-        // The string is an error message to be displayed in the conversation.
-        if (!aThis || !aThis.writeMessage) {
-          this.ERROR(
-            "HandleErrors was passed an error message string, but " +
-              "no conversation to display it in:\n" +
-              handler
-          );
-          return true;
-        }
-        aThis.writeMessage(aThis.name, handler, { system: true, error: true });
-        return true;
-      } else if (typeof handler == "function") {
-        // If we're given a function, call this error handler.
-        return handler.call(aThis, error);
-      }
-
-      // If this happens, there's a bug somewhere.
-      this.ERROR(
-        "HandleErrors was passed a handler for '" +
-          condition +
-          "'' which is neither a function nor a string."
-      );
-      return false;
-    };
-  },
-
-  // Returns a callback suitable for use in sendStanza, to handle type==result
-  // responses. aHandlers and aThis are passed on to handleErrors for error
-  // handling.
-  _handleResult(aHandlers, aThis) {
-    return aStanza => {
-      if (aStanza.attributes.type == "result") {
-        return true;
-      }
-      return this.handleErrors(aHandlers, aThis)(aStanza);
-    };
-  },
-
-  /* XMPPSession events */
-
-  /* Called when the XMPP session is started */
-  onConnection() {
-    // Request the roster. The account will be marked as connected when this is
-    // complete.
-    this.reportConnecting(_("connection.downloadingRoster"));
-    let s = Stanza.iq(
-      "get",
-      null,
-      null,
-      Stanza.node("query", Stanza.NS.roster)
-    );
-    this.sendStanza(s, this.onRoster, this);
-
-    // XEP-0030 and XEP-0045 (6): Service Discovery.
-    // Queries Server for Associated Services.
-    let iq = Stanza.iq(
-      "get",
-      null,
-      this._jid.domain,
-      Stanza.node("query", Stanza.NS.disco_items)
-    );
-    this.sendStanza(iq, this.onServiceDiscovery, this);
-
-    // XEP-0030: Service Discovery Information Features.
-    iq = Stanza.iq(
-      "get",
-      null,
-      this._jid.domain,
-      Stanza.node("query", Stanza.NS.disco_info)
-    );
-    this.sendStanza(iq, this.onServiceDiscoveryInfo, this);
-  },
-
-  /* Called whenever a stanza is received */
-  onXmppStanza(aStanza) {},
-
-  /* Called when a iq stanza is received */
-  onIQStanza(aStanza) {
-    let type = aStanza.attributes.type;
-    if (type == "set") {
-      for (let query of aStanza.getChildren("query")) {
-        if (query.uri != Stanza.NS.roster) {
-          continue;
-        }
-
-        // RFC 6121 2.1.6 (Roster push):
-        // A receiving client MUST ignore the stanza unless it has no 'from'
-        // attribute (i.e., implicitly from the bare JID of the user's
-        // account) or it has a 'from' attribute whose value matches the
-        // user's bare JID <user@domainpart>.
-        let from = aStanza.attributes.from;
-        if (from && from != this._jid.node + "@" + this._jid.domain) {
-          this.WARN("Ignoring potentially spoofed roster push.");
-          return;
-        }
-
-        for (let item of query.getChildren("item")) {
-          this._onRosterItem(item, true);
-        }
-        return;
-      }
-    } else if (type == "get") {
-      let id = aStanza.attributes.id;
-      let from = aStanza.attributes.from;
-
-      // XEP-0199: XMPP server-to-client ping (XEP-0199)
-      let ping = aStanza.getElement(["ping"]);
-      if (ping && ping.uri == Stanza.NS.ping) {
-        if (from == this._jid.domain) {
-          this.sendStanza(Stanza.iq("result", id, this._jid.domain));
-        }
-        return;
-      }
-
-      let query = aStanza.getElement(["query"]);
-      if (query && query.uri == Stanza.NS.version) {
-        // XEP-0092: Software Version.
-        let children = [];
-        children.push(Stanza.node("name", null, null, Services.appinfo.name));
-        children.push(
-          Stanza.node("version", null, null, Services.appinfo.version)
-        );
-        let versionQuery = Stanza.node(
-          "query",
-          Stanza.NS.version,
-          null,
-          children
-        );
-        this.sendStanza(Stanza.iq("result", id, from, versionQuery));
-        return;
-      }
-      if (query && query.uri == Stanza.NS.disco_info) {
-        // XEP-0030: Service Discovery.
-        let children = [];
-        if (aStanza.attributes.node == Stanza.NS.muc_rooms) {
-          // XEP-0045 (6.7): Room query.
-          // TODO: Currently, we return an empty <query/> element, but we
-          // should return non-private rooms.
-        } else {
-          children = SupportedFeatures.map(feature =>
-            Stanza.node("feature", null, { var: feature })
-          );
-          children.unshift(
-            Stanza.node("identity", null, {
-              category: "client",
-              type: "pc",
-              name: Services.appinfo.name,
-            })
-          );
-        }
-        let discoveryQuery = Stanza.node(
-          "query",
-          Stanza.NS.disco_info,
-          null,
-          children
-        );
-        this.sendStanza(Stanza.iq("result", id, from, discoveryQuery));
-        return;
-      }
-    }
-    this.WARN(`Unhandled IQ ${type} stanza.`);
-  },
-
-  /* Called when a presence stanza is received */
-  onPresenceStanza(aStanza) {
-    let from = aStanza.attributes.from;
-    this.DEBUG("Received presence stanza for " + from);
-
-    let jid = this.normalize(from);
-    let type = aStanza.attributes.type;
-    if (type == "subscribe") {
-      this.addBuddyRequest(
-        jid,
-        this.replyToBuddyRequest.bind(this, "subscribed"),
-        this.replyToBuddyRequest.bind(this, "unsubscribed")
-      );
-    } else if (
-      type == "unsubscribe" ||
-      type == "unsubscribed" ||
-      type == "subscribed"
-    ) {
-      // Nothing useful to do for these presence stanzas, as we will also
-      // receive a roster push containing more or less the same information
-    } else if (this._buddies.has(jid)) {
-      this._buddies.get(jid).onPresenceStanza(aStanza);
-    } else if (this._mucs.has(jid)) {
-      this._mucs.get(jid).onPresenceStanza(aStanza);
-    } else if (jid != this.normalize(this._connection._jid.jid)) {
-      this.WARN("received presence stanza for unknown buddy " + from);
-    } else if (
-      jid == this._jid.node + "@" + this._jid.domain &&
-      this._connection._resource != this._parseJID(from).resource
-    ) {
-      // Ignore presence stanzas for another resource.
-    } else {
-      this.WARN("Unhandled presence stanza.");
-    }
-  },
-
-  // XEP-0030: Discovering services and their features that are supported by
-  // the server.
-  onServiceDiscovery(aStanza) {
-    let query = aStanza.getElement(["query"]);
-    if (
-      aStanza.attributes.type != "result" ||
-      !query ||
-      query.uri != Stanza.NS.disco_items
-    ) {
-      this.LOG("Could not get services for this server: " + this._jid.domain);
-      return true;
-    }
-
-    // Discovering the Features that are Supported by each service.
-    query.getElements(["item"]).forEach(item => {
-      let jid = item.attributes.jid;
-      if (!jid) {
-        return;
-      }
-      let iq = Stanza.iq(
-        "get",
-        null,
-        jid,
-        Stanza.node("query", Stanza.NS.disco_info)
-      );
-      this.sendStanza(iq, receivedStanza => {
-        let query = receivedStanza.getElement(["query"]);
-        let from = receivedStanza.attributes.from;
-        if (
-          aStanza.attributes.type != "result" ||
-          !query ||
-          query.uri != Stanza.NS.disco_info
-        ) {
-          this.LOG("Could not get features for this service: " + from);
-          return true;
-        }
-        let features = query
-          .getElements(["feature"])
-          .map(elt => elt.attributes.var);
-        let identity = query.getElement(["identity"]);
-        if (
-          identity &&
-          identity.attributes.category == "conference" &&
-          identity.attributes.type == "text" &&
-          features.includes(Stanza.NS.muc)
-        ) {
-          // XEP-0045 (6.2): this feature is for a MUC Service.
-          // XEP-0045 (15.2): Service Discovery Category/Type.
-          this._mucService = from;
-        }
-        // TODO: Handle other services that are supported by XMPP through
-        // their features.
-
-        return true;
-      });
-    });
-    return true;
-  },
-
-  // XEP-0030: Discovering Service Information and its features that are
-  // supported by the server.
-  onServiceDiscoveryInfo(aStanza) {
-    let query = aStanza.getElement(["query"]);
-    if (
-      aStanza.attributes.type != "result" ||
-      !query ||
-      query.uri != Stanza.NS.disco_info
-    ) {
-      this.LOG("Could not get features for this server: " + this._jid.domain);
-      return true;
-    }
-
-    let features = query
-      .getElements(["feature"])
-      .map(elt => elt.attributes.var);
-    if (features.includes(Stanza.NS.carbons)) {
-      // XEP-0280: Message Carbons.
-      // Enabling Carbons on server, as it's disabled by default on server.
-      if (Services.prefs.getBoolPref("chat.xmpp.messageCarbons")) {
-        let iqStanza = Stanza.iq(
-          "set",
-          null,
-          null,
-          Stanza.node("enable", Stanza.NS.carbons)
-        );
-        this.sendStanza(iqStanza, aStanza => {
-          let error = this.parseError(aStanza);
-          if (error) {
-            this.WARN(
-              "Unable to enable message carbons due to " +
-                error.condition +
-                " error."
-            );
-            return true;
-          }
-
-          let type = aStanza.attributes.type;
-          if (type != "result") {
-            this.WARN(
-              "Received unexpected stanza with " +
-                type +
-                " type " +
-                "while enabling message carbons."
-            );
-            return true;
-          }
-
-          this.LOG("Message carbons enabled.");
-          this._isCarbonsEnabled = true;
-          return true;
-        });
-      }
-    }
-    // TODO: Handle other features that are supported by the server.
+  get chatHasTopic() {
     return true;
   },
-
-  requestRoomInfo(aCallback) {
-    if (this._roomInfoCallbacks.has(aCallback)) {
-      return;
-    }
-
-    if (this.isRoomInfoStale && !this._pendingList) {
-      this._roomList = new Map();
-      this._lastListTime = Date.now();
-      this._roomInfoCallback = aCallback;
-      this._pendingList = true;
-
-      // XEP-0045 (6.3): Discovering Rooms.
-      let iq = Stanza.iq(
-        "get",
-        null,
-        this._mucService,
-        Stanza.node("query", Stanza.NS.disco_items)
-      );
-      this.sendStanza(iq, this.onRoomDiscovery, this);
-    } else {
-      let rooms = [...this._roomList.keys()];
-      aCallback.onRoomInfoAvailable(rooms, !this._pendingList);
-    }
-
-    if (this._pendingList) {
-      this._roomInfoCallbacks.add(aCallback);
-    }
-  },
-
-  onRoomDiscovery(aStanza) {
-    let query = aStanza.getElement(["query"]);
-    if (!query || query.uri != Stanza.NS.disco_items) {
-      this.LOG("Could not get rooms for this server: " + this._jid.domain);
-      return;
-    }
-
-    // XEP-0059: Result Set Management.
-    let set = query.getElement(["set"]);
-    let last = set ? set.getElement(["last"]) : null;
-    if (last) {
-      let iq = Stanza.iq(
-        "get",
-        null,
-        this._mucService,
-        Stanza.node("query", Stanza.NS.disco_items)
-      );
-      this.sendStanza(iq, this.onRoomDiscovery, this);
-    } else {
-      this._pendingList = false;
-    }
-
-    let rooms = [];
-    query.getElements(["item"]).forEach(item => {
-      let jid = this._parseJID(item.attributes.jid);
-      if (!jid) {
-        return;
-      }
-
-      let name = item.attributes.name;
-      if (!name) {
-        name = jid.node ? jid.node : jid.jid;
-      }
-
-      this._roomList.set(name, jid.jid);
-      rooms.push(name);
-    });
-
-    this._roomInfoCallback.onRoomInfoAvailable(rooms, !this._pendingList);
-  },
-
-  getRoomInfo(aName) {
-    return new XMPPRoomInfo(aName, this);
-  },
-
-  // Returns null if not an invitation stanza, and an object
-  // describing the invitation otherwise.
-  parseInvitation(aStanza) {
-    let x = aStanza.getElement(["x"]);
-    if (!x) {
-      return null;
-    }
-    let retVal = {};
-
-    // XEP-0045. Direct Invitation (7.8.1)
-    // Described in XEP-0249.
-    // jid (chatroom) is required.
-    // Password, reason, continue and thread are optional.
-    if (x.uri == Stanza.NS.conference) {
-      if (!x.attributes.jid) {
-        this.WARN("Received an invitation with missing MUC jid.");
-        return null;
-      }
-      retVal.mucJid = this.normalize(x.attributes.jid);
-      retVal.from = this.normalize(aStanza.attributes.from);
-      retVal.password = x.attributes.password;
-      retVal.reason = x.attributes.reason;
-      retVal.continue = x.attributes.continue;
-      retVal.thread = x.attributes.thread;
-      return retVal;
-    }
-
-    // XEP-0045. Mediated Invitation (7.8.2)
-    // Sent by the chatroom on behalf of someone in the chatroom.
-    // jid (chatroom) and from (inviter) are required.
-    // password and reason are optional.
-    if (x.uri == Stanza.NS.muc_user) {
-      let invite = x.getElement(["invite"]);
-      if (!invite || !invite.attributes.from) {
-        this.WARN("Received an invitation with missing MUC invite or from.");
-        return null;
-      }
-      retVal.mucJid = this.normalize(aStanza.attributes.from);
-      retVal.from = this.normalize(invite.attributes.from);
-      let continueElement = invite.getElement(["continue"]);
-      retVal.continue = !!continueElement;
-      if (continueElement) {
-        retVal.thread = continueElement.attributes.thread;
-      }
-      if (x.getElement(["password"])) {
-        retVal.password = x.getElement(["password"]).innerText;
-      }
-      if (invite.getElement(["reason"])) {
-        retVal.reason = invite.getElement(["reason"]).innerText;
-      }
-      return retVal;
-    }
-
-    return null;
-  },
-
-  /* Called when a message stanza is received */
-  onMessageStanza(aStanza) {
-    // XEP-0280: Message Carbons.
-    // Sending and Receiving Messages.
-    // Indicates that the forwarded message was sent or received.
-    let isSent = false;
-    let carbonStanza =
-      aStanza.getElement(["sent"]) || aStanza.getElement(["received"]);
-    if (carbonStanza) {
-      if (carbonStanza.uri != Stanza.NS.carbons) {
-        this.WARN(
-          "Received a forwarded message which does not '" +
-            Stanza.NS.carbons +
-            "' namespace."
-        );
-        return;
-      }
-
-      isSent = carbonStanza.localName == "sent";
-      carbonStanza = carbonStanza.getElement(["forwarded", "message"]);
-      if (this._isCarbonsEnabled) {
-        aStanza = carbonStanza;
-      } else {
-        this.WARN(
-          "Received an unexpected forwarded message while message " +
-            "carbons are not enabled."
-        );
-        return;
-      }
-    }
-
-    // For forwarded sent messages, we need to use "to" attribute to
-    // get the right conversation as from in this case is this account.
-    let convJid = isSent ? aStanza.attributes.to : aStanza.attributes.from;
-
-    let normConvJid = this.normalize(convJid);
-    let isMuc = this._mucs.has(normConvJid);
-
-    let type = aStanza.attributes.type;
-    let x = aStanza.getElement(["x"]);
-    let body;
-    let b = aStanza.getElement(["body"]);
-    if (b) {
-      // If there's a <body> child we have more than just typing notifications.
-      // Prefer HTML (in <html><body>) and use plain text (<body>) as fallback.
-      let htmlBody = aStanza.getElement(["html", "body"]);
-      if (htmlBody) {
-        body = htmlBody.innerXML;
-      } else {
-        // Even if the message is in plain text, the prplIMessage
-        // should contain a string that's correctly escaped for
-        // insertion in an HTML document.
-        body = TXTToHTML(b.innerText);
-      }
-    }
-
-    let subject = aStanza.getElement(["subject"]);
-    // Ignore subject when !isMuc. We're being permissive about subject changes
-    // in the comment below, so we need to be careful about where that makes
-    // sense. Psi+'s OTR plugin includes a subject and body in its message
-    // stanzas.
-    if (subject && isMuc) {
-      // XEP-0045 (7.2.16): Check for a subject element in the stanza and update
-      // the topic if it exists.
-      // We are breaking the spec because only a message that contains a
-      // <subject/> but no <body/> element shall be considered a subject change
-      // for MUC, but we ignore that to be compatible with ejabberd versions
-      // before 15.06.
-      let muc = this._mucs.get(normConvJid);
-      let nick = this._parseJID(convJid).resource;
-      // TODO There can be multiple subject elements with different xml:lang
-      // attributes.
-      muc.setTopic(subject.innerText, nick);
-      return;
-    }
-
-    let invitation = this.parseInvitation(aStanza);
-    if (invitation) {
-      let messageID;
-      if (invitation.reason) {
-        messageID = "conversation.muc.invitationWithReason2";
-      } else {
-        messageID = "conversation.muc.invitationWithoutReason";
-      }
-      if (invitation.password) {
-        messageID += ".password";
-      }
-      let params = [
-        invitation.from,
-        invitation.mucJid,
-        invitation.password,
-        invitation.reason,
-      ].filter(s => s);
-      let message = _(messageID, ...params);
-
-      if (
-        Services.prefs.getIntPref(
-          "messenger.conversations.autoAcceptChatInvitations"
-        ) == 1
-      ) {
-        // Auto-accept the invitation.
-        let chatRoomFields = this.getChatRoomDefaultFieldValues(
-          invitation.mucJid
-        );
-        if (invitation.password) {
-          chatRoomFields.setValue("password", invitation.password);
-        }
-        let muc = this.joinChat(chatRoomFields);
-        muc.writeMessage(muc.name, message, { system: true });
-      } else {
-        // Otherwise, just notify the user.
-        let conv = this.createConversation(invitation.from);
-        if (conv) {
-          conv.writeMessage(invitation.from, message, { system: true });
-        }
-      }
-    }
-
-    if (body) {
-      let date = _getDelay(aStanza);
-      if (
-        type == "groupchat" ||
-        (type == "error" && isMuc && !this._conv.has(convJid))
-      ) {
-        if (!isMuc) {
-          this.WARN(
-            "Received a groupchat message for unknown MUC " + normConvJid
-          );
-          return;
-        }
-        let muc = this._mucs.get(normConvJid);
-        muc.incomingMessage(body, aStanza, date);
-        return;
-      }
-
-      let conv = this.createConversation(convJid);
-      if (!conv) {
-        return;
-      }
-
-      if (isSent) {
-        _displaySentMsg(conv, body, date);
-        return;
-      }
-      conv.incomingMessage(body, aStanza, date);
-    } else if (type == "error") {
-      let conv = this.createConversation(convJid);
-      if (conv) {
-        conv.incomingMessage(null, aStanza);
-      }
-    } else if (x && x.uri == Stanza.NS.muc_user) {
-      let muc = this._mucs.get(normConvJid);
-      if (!muc) {
-        this.WARN(
-          "Received a groupchat message for unknown MUC " + normConvJid
-        );
-        return;
-      }
-      muc.onMessageStanza(aStanza);
-      return;
-    }
-
-    // If this is a sent message carbon, the user is typing on another client.
-    if (isSent) {
-      return;
-    }
-
-    // Don't create a conversation to only display the typing notifications.
-    if (!this._conv.has(normConvJid) && !this._conv.has(convJid)) {
-      return;
-    }
-
-    // Ignore errors while delivering typing notifications.
-    if (type == "error") {
-      return;
-    }
-
-    let typingState = Ci.prplIConvIM.NOT_TYPING;
-    let state;
-    let s = aStanza.getChildrenByNS(Stanza.NS.chatstates);
-    if (s.length > 0) {
-      state = s[0].localName;
-    }
-    if (state) {
-      this.DEBUG(state);
-      if (state == "composing") {
-        typingState = Ci.prplIConvIM.TYPING;
-      } else if (state == "paused") {
-        typingState = Ci.prplIConvIM.TYPED;
-      }
-    }
-    let convName = normConvJid;
-
-    // If the bare JID is a MUC that we have joined, use the full JID as this
-    // is a private message to a MUC participant.
-    if (isMuc) {
-      convName = convJid;
-    }
-
-    let conv = this._conv.get(convName);
-    if (!conv) {
-      return;
-    }
-    conv.updateTyping(typingState, conv.shortName);
-    conv.supportChatStateNotifications = !!state;
-  },
-
-  /* Called when there is an error in the xmpp session */
-  onError(aError, aException) {
-    if (aError === null || aError === undefined) {
-      aError = Ci.prplIAccount.ERROR_OTHER_ERROR;
-    }
-    this._disconnect(aError, aException.toString());
-  },
-
-  onVCard(aStanza) {
-    let jid = this._pendingVCardRequests.shift();
-    this._requestNextVCard();
-    if (!this._buddies.has(jid)) {
-      this.WARN("Received a vCard for unknown buddy " + jid);
-      return;
-    }
-
-    let vCard = aStanza.getElement(["vCard"]);
-    let error = this.parseError(aStanza);
-    if (
-      (error &&
-        (error.condition == "item-not-found" ||
-          error.condition == "service-unavailable")) ||
-      !vCard ||
-      !vCard.children.length
-    ) {
-      this.LOG("No vCard exists (or the user does not exist) for " + jid);
-      return;
-    } else if (error) {
-      this.WARN("Received unexpected vCard error " + error.condition);
-      return;
-    }
-
-    let buddy = this._buddies.get(jid);
-    let stanzaJid = this.normalize(aStanza.attributes.from);
-    if (jid && jid != stanzaJid) {
-      this.ERROR(
-        "Received vCard for a different jid (" +
-          stanzaJid +
-          ") " +
-          "than the requested " +
-          jid
-      );
-    }
-
-    let foundFormattedName = false;
-    let vCardInfo = this.parseVCard(vCard);
-    if (vCardInfo.fullName) {
-      buddy.vCardFormattedName = vCardInfo.fullName;
-      foundFormattedName = true;
-    }
-    if (vCardInfo.photo) {
-      buddy._saveIcon(vCardInfo.photo);
-    }
-    if (!foundFormattedName && buddy._vCardFormattedName) {
-      buddy.vCardFormattedName = "";
-    }
-    buddy._vCardReceived = true;
-  },
-
-  _requestNextVCard() {
-    if (!this._pendingVCardRequests.length) {
-      return;
-    }
-    let s = Stanza.iq(
-      "get",
-      null,
-      this._pendingVCardRequests[0],
-      Stanza.node("vCard", Stanza.NS.vcard)
-    );
-    this.sendStanza(s, this.onVCard, this);
-  },
-
-  _addVCardRequest(aJID) {
-    let requestPending = !!this._pendingVCardRequests.length;
-    this._pendingVCardRequests.push(aJID);
-    if (!requestPending) {
-      this._requestNextVCard();
-    }
-  },
-
-  // XEP-0029 (Section 2) and RFC 6122 (Section 2): The node and domain are
-  // lowercase, while resources are case sensitive and can contain spaces.
-  normalizeFullJid(aJID) {
-    return this._parseJID(aJID.trim()).jid;
-  },
-
-  // Standard normalization for XMPP removes the resource part of jids.
-  normalize(aJID) {
-    return aJID
-      .trim()
-      .split("/", 1)[0] // up to first slash
-      .toLowerCase();
-  },
-
-  // RFC 6122 (Section 2): [ localpart "@" ] domainpart [ "/" resourcepart ] is
-  // the form of jid.
-  // Localpart is parsed as node and optional.
-  // Domainpart is parsed as domain and required.
-  // resourcepart is parsed as resource and optional.
-  _parseJID(aJid) {
-    let match = /^(?:([^"&'/:<>@]+)@)?([^@/<>'\"]+)(?:\/(.*))?$/.exec(
-      aJid.trim()
-    );
-    if (!match) {
-      return null;
-    }
-
-    let result = {
-      node: match[1],
-      domain: match[2].toLowerCase(),
-      resource: match[3],
-    };
-    return this._setJID(result.domain, result.node, result.resource);
-  },
-
-  // Constructs jid as an object from domain, node and resource parts.
-  // The object has properties (node, domain, resource and jid).
-  // aDomain is required, but aNode and aResource are optional.
-  _setJID(aDomain, aNode = null, aResource = null) {
-    if (!aDomain) {
-      throw new Error("aDomain must have a value");
-    }
-
-    let result = {
-      node: aNode,
-      domain: aDomain.toLowerCase(),
-      resource: aResource,
-    };
-    let jid = result.domain;
-    if (result.node) {
-      result.node = result.node.toLowerCase();
-      jid = result.node + "@" + jid;
-    }
-    if (result.resource) {
-      jid += "/" + result.resource;
-    }
-    result.jid = jid;
-    return result;
-  },
-
-  _onRosterItem(aItem, aNotifyOfUpdates) {
-    let jid = aItem.attributes.jid;
-    if (!jid) {
-      this.WARN("Received a roster item without jid: " + aItem.getXML());
-      return "";
-    }
-    jid = this.normalize(jid);
-
-    let subscription = "";
-    if ("subscription" in aItem.attributes) {
-      subscription = aItem.attributes.subscription;
-    }
-    if (subscription == "remove") {
-      this._forgetRosterItem(jid);
-      return "";
-    }
-
-    let buddy;
-    if (this._buddies.has(jid)) {
-      buddy = this._buddies.get(jid);
-      let groups = aItem.getChildren("group");
-      if (groups.length) {
-        // If the server specified at least one group, ensure the group we use
-        // as the account buddy's tag is still a group on the server...
-        let tagName = buddy.tag.name;
-        if (!groups.some(g => g.innerText == tagName)) {
-          // ... otherwise we need to move our account buddy to a new group.
-          tagName = groups[0].innerText;
-          if (tagName) {
-            // Should always be true, but check just in case...
-            let oldTag = buddy.tag;
-            buddy._tag = Services.tags.createTag(tagName);
-            Services.contacts.accountBuddyMoved(buddy, oldTag, buddy._tag);
-          }
-        }
-      }
-    } else {
-      let tag;
-      for (let group of aItem.getChildren("group")) {
-        let name = group.innerText;
-        if (name) {
-          tag = Services.tags.createTag(name);
-          break; // TODO we should create an accountBuddy per group,
-          // but this._buddies would probably not like that...
-        }
-      }
-      buddy = new this._accountBuddyConstructor(
-        this,
-        null,
-        tag || Services.tags.defaultTag,
-        jid
-      );
-    }
-
-    // We request the vCard only if we haven't received it yet and are
-    // subscribed to presence for that contact.
-    if (
-      (subscription == "both" || subscription == "to") &&
-      !buddy._vCardReceived
-    ) {
-      this._addVCardRequest(jid);
-    }
-
-    let alias = "name" in aItem.attributes ? aItem.attributes.name : "";
-    if (alias) {
-      if (aNotifyOfUpdates && this._buddies.has(jid)) {
-        buddy.rosterAlias = alias;
-      } else {
-        buddy._rosterAlias = alias;
-      }
-    } else if (buddy._rosterAlias) {
-      buddy.rosterAlias = "";
-    }
-
-    if (subscription) {
-      buddy.subscription = subscription;
-    }
-    if (!this._buddies.has(jid)) {
-      this._buddies.set(jid, buddy);
-      Services.contacts.accountBuddyAdded(buddy);
-    } else if (aNotifyOfUpdates) {
-      buddy._notifyObservers("status-detail-changed");
-    }
-
-    // Keep the xml nodes of the item so that we don't have to
-    // recreate them when changing something (eg. the alias) in it.
-    buddy._rosterItem = aItem;
-
-    return jid;
-  },
-  _forgetRosterItem(aJID) {
-    Services.contacts.accountBuddyRemoved(this._buddies.get(aJID));
-    this._buddies.delete(aJID);
-  },
-
-  /* When the roster is received */
-  onRoster(aStanza) {
-    // For the first element that is a roster stanza.
-    for (let qe of aStanza.getChildren("query")) {
-      if (qe.uri != Stanza.NS.roster) {
-        continue;
-      }
-
-      // Find all the roster items in the new message.
-      let newRoster = new Set();
-      for (let item of qe.getChildren("item")) {
-        let jid = this._onRosterItem(item);
-        if (jid) {
-          newRoster.add(jid);
-        }
-      }
-      // If an item was in the old roster, but not in the new, forget it.
-      for (let jid of this._buddies.keys()) {
-        if (!newRoster.has(jid)) {
-          this._forgetRosterItem(jid);
-        }
-      }
-      break;
-    }
-
-    this._sendPresence();
-    this._buddies.forEach(b => {
-      if (b.subscription == "both" || b.subscription == "to") {
-        b.setStatus(Ci.imIStatusInfo.STATUS_OFFLINE, "");
-      }
-    });
-    this.reportConnected();
-    this._sendVCard();
-  },
-
-  /* Public methods */
-
-  sendStanza(aStanza, aCallback, aThis, aLogString) {
-    return this._connection.sendStanza(aStanza, aCallback, aThis, aLogString);
-  },
-
-  // Variations of the XMPP protocol can change these default constructors:
-  _conversationConstructor: XMPPConversation,
-  _MUCConversationConstructor: XMPPMUCConversation,
-  _accountBuddyConstructor: XMPPAccountBuddy,
-
-  /* Create a new conversation */
-  createConversation(aName) {
-    let convName = this.normalize(aName);
-
-    // Checks if conversation is with a participant of a MUC we are in. We do
-    // not want to strip the resource as it is of the form room@domain/nick.
-    let isMucParticipant = this._mucs.has(convName);
-    if (isMucParticipant) {
-      convName = this.normalizeFullJid(aName);
-    }
-
-    // Checking that the aName can be parsed and is not broken.
-    let jid = this._parseJID(convName);
-    if (
-      !jid ||
-      !jid.domain ||
-      (isMucParticipant && (!jid.node || !jid.resource))
-    ) {
-      this.ERROR("Could not create conversation as jid is broken: " + convName);
-      throw new Error("Invalid JID");
-    }
-
-    if (!this._conv.has(convName)) {
-      this._conv.set(
-        convName,
-        new this._conversationConstructor(this, convName, isMucParticipant)
-      );
-    }
-
-    return this._conv.get(convName);
-  },
-
-  /* Remove an existing conversation */
-  removeConversation(aNormalizedName) {
-    if (this._conv.has(aNormalizedName)) {
-      this._conv.delete(aNormalizedName);
-    } else if (this._mucs.has(aNormalizedName)) {
-      this._mucs.delete(aNormalizedName);
-    }
-  },
-
-  /* Private methods */
-
-  /* Disconnect from the server */
-  /* The aError and aErrorMessage parameters are passed to reportDisconnecting
-   * and used by the account manager.
-   * The aQuiet parameter is to avoid sending status change notifications
-   * during the uninitialization of the account. */
-  _disconnect(
-    aError = Ci.prplIAccount.NO_ERROR,
-    aErrorMessage = "",
-    aQuiet = false
-  ) {
-    if (!this._connection) {
-      return;
-    }
-
-    this.reportDisconnecting(aError, aErrorMessage);
-
-    this._buddies.forEach(b => {
-      if (!aQuiet) {
-        b.setStatus(Ci.imIStatusInfo.STATUS_UNKNOWN, "");
-      }
-      b.onAccountDisconnected();
-    });
-
-    this._mucs.forEach(muc => {
-      muc.joining = false; // In case we never finished joining.
-      muc.left = true;
-    });
-
-    this._connection.disconnect();
-    delete this._connection;
-
-    // We won't receive "user-icon-changed" notifications while the
-    // account isn't connected, so clear the cache to avoid keeping an
-    // obsolete icon.
-    delete this._cachedUserIcon;
-    // Also clear the cached user vCard, as we will want to redownload it
-    // after reconnecting.
-    delete this._userVCard;
-
-    // Clear vCard requests.
-    this._pendingVCardRequests = [];
-
-    this.reportDisconnected();
-  },
-
-  /* Set the user status on the server */
-  _sendPresence() {
-    delete this._shouldSendPresenceForIdlenessChange;
-
-    if (!this._connection) {
-      return;
-    }
-
-    let si = this.imAccount.statusInfo;
-    let statusType = si.statusType;
-    let show = "";
-    if (statusType == Ci.imIStatusInfo.STATUS_UNAVAILABLE) {
-      show = "dnd";
-    } else if (
-      statusType == Ci.imIStatusInfo.STATUS_AWAY ||
-      statusType == Ci.imIStatusInfo.STATUS_IDLE
-    ) {
-      show = "away";
-    }
-    let children = [];
-    if (show) {
-      children.push(Stanza.node("show", null, null, show));
-    }
-    let statusText = si.statusText;
-    if (statusText) {
-      children.push(Stanza.node("status", null, null, statusText));
-    }
-    if (this._idleSince) {
-      let time = Math.floor(Date.now() / 1000) - this._idleSince;
-      children.push(Stanza.node("query", Stanza.NS.last, { seconds: time }));
-    }
-    if (this.prefs.prefHasUserValue("priority")) {
-      let priority = Math.max(-128, Math.min(127, this.getInt("priority")));
-      if (priority) {
-        children.push(Stanza.node("priority", null, null, priority.toString()));
-      }
-    }
-    this.sendStanza(
-      Stanza.presence({ "xml:lang": "en" }, children),
-      aStanza => {
-        // As we are implicitly subscribed to our own presence (rfc6121#4), we
-        // will receive the presence stanza mirrored back to us. We don't need
-        // to do anything with this response.
-        return true;
-      }
-    );
-  },
-
-  _downloadingUserVCard: false,
-  _downloadUserVCard() {
-    // If a download is already in progress, don't start another one.
-    if (this._downloadingUserVCard) {
-      return;
-    }
-    this._downloadingUserVCard = true;
-    let s = Stanza.iq("get", null, null, Stanza.node("vCard", Stanza.NS.vcard));
-    this.sendStanza(s, this.onUserVCard, this);
-  },
-
-  onUserVCard(aStanza) {
-    delete this._downloadingUserVCard;
-    let userVCard = aStanza.getElement(["vCard"]) || null;
-    if (userVCard) {
-      // Strip any server-specific namespace off the incoming vcard
-      // before storing it.
-      this._userVCard = Stanza.node(
-        "vCard",
-        Stanza.NS.vcard,
-        null,
-        userVCard.children
-      );
-    }
-
-    // If a user icon exists in the vCard we received from the server,
-    // we need to ensure the line breaks in its binval are exactly the
-    // same as those we would include if we sent the icon, and that
-    // there isn't any other whitespace.
-    if (this._userVCard) {
-      let binval = this._userVCard.getElement(["PHOTO", "BINVAL"]);
-      if (binval && binval.children.length) {
-        binval = binval.children[0];
-        binval.text = binval.text
-          .replace(/[^A-Za-z0-9\+\/\=]/g, "")
-          .replace(/.{74}/g, "$&\n");
-      }
-    } else {
-      // Downloading the vCard failed.
-      if (
-        this.handleErrors({
-          itemNotFound: () => false, // OK, no vCard exists yet.
-          default: () => true,
-        })(aStanza)
-      ) {
-        this.WARN(
-          "Unexpected error retrieving the user's vcard, " +
-            "so we won't attempt to set it either."
-        );
-        return;
-      }
-      // Set this so that we don't get into an infinite loop trying to download
-      // the vcard again. The check in sendVCard is for hasOwnProperty.
-      this._userVCard = null;
-    }
-    this._sendVCard();
-  },
-
-  _cachingUserIcon: false,
-  _cacheUserIcon() {
-    if (this._cachingUserIcon) {
-      return;
-    }
-
-    let userIcon = this.imAccount.statusInfo.getUserIcon();
-    if (!userIcon) {
-      this._cachedUserIcon = null;
-      this._sendVCard();
-      return;
-    }
-
-    this._cachingUserIcon = true;
-    let channel = NetUtil.newChannel({
-      uri: userIcon,
-      loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
-      securityFlags: Ci.nsILoadInfo.SEC_REQUIRE_SAME_ORIGIN_DATA_INHERITS,
-      contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
-    });
-    NetUtil.asyncFetch(channel, (inputStream, resultCode) => {
-      if (!Components.isSuccessCode(resultCode)) {
-        return;
-      }
-      try {
-        let type = channel.contentType;
-        let buffer = NetUtil.readInputStreamToString(
-          inputStream,
-          inputStream.available()
-        );
-        let readImage = imgTools.decodeImageFromBuffer(
-          buffer,
-          buffer.length,
-          type
-        );
-        let scaledImage;
-        if (readImage.width <= 96 && readImage.height <= 96) {
-          scaledImage = imgTools.encodeImage(readImage, type);
-        } else {
-          if (type != "image/jpeg") {
-            type = "image/png";
-          }
-          scaledImage = imgTools.encodeScaledImage(readImage, type, 64, 64);
-        }
-
-        let bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
-          Ci.nsIBinaryInputStream
-        );
-        bstream.setInputStream(scaledImage);
-
-        let data = bstream.readBytes(bstream.available());
-        this._cachedUserIcon = {
-          type,
-          binval: btoa(data).replace(/.{74}/g, "$&\n"),
-        };
-      } catch (e) {
-        Cu.reportError(e);
-        this._cachedUserIcon = null;
-      }
-      delete this._cachingUserIcon;
-      this._sendVCard();
-    });
-  },
-  _sendVCard() {
-    if (!this._connection) {
-      return;
-    }
-
-    // We have to download the user's existing vCard before updating it.
-    // This lets us preserve the fields that we don't change or don't know.
-    // Some servers may reject a new vCard if we don't do this first.
-    if (!this.hasOwnProperty("_userVCard")) {
-      // The download of the vCard is asynchronous and will call _sendVCard back
-      // when the user's vCard has been received.
-      this._downloadUserVCard();
-      return;
-    }
-
-    // Read the local user icon asynchronously from the disk.
-    // _cacheUserIcon will call _sendVCard back once the icon is ready.
-    if (!this.hasOwnProperty("_cachedUserIcon")) {
-      this._cacheUserIcon();
-      return;
-    }
-
-    // If the user currently doesn't have any vCard on the server or
-    // the download failed, an empty new one.
-    if (!this._userVCard) {
-      this._userVCard = Stanza.node("vCard", Stanza.NS.vcard);
-    }
-
-    // Keep a serialized copy of the existing user vCard so that we
-    // can avoid resending identical data to the server.
-    let existingVCard = this._userVCard.getXML();
-
-    let fn = this._userVCard.getElement(["FN"]);
-    let displayName = this.imAccount.statusInfo.displayName;
-    if (displayName) {
-      // If a display name is set locally, update or add an FN field to the vCard.
-      if (!fn) {
-        this._userVCard.addChild(
-          Stanza.node("FN", Stanza.NS.vcard, null, displayName)
-        );
-      } else if (fn.children.length) {
-        fn.children[0].text = displayName;
-      } else {
-        fn.addText(displayName);
-      }
-    } else if ("_forceUserDisplayNameUpdate" in this) {
-      // We remove a display name stored on the server without replacing
-      // it with a new value only if this _sendVCard call is the result of
-      // a user action. This is to avoid removing data from the server each
-      // time the user connects from a new profile.
-      this._userVCard.children = this._userVCard.children.filter(
-        n => n.qName != "FN"
-      );
-    }
-    delete this._forceUserDisplayNameUpdate;
-
-    if (this._cachedUserIcon) {
-      // If we have a local user icon, update or add it in the PHOTO field.
-      let photoChildren = [
-        Stanza.node("TYPE", Stanza.NS.vcard, null, this._cachedUserIcon.type),
-        Stanza.node(
-          "BINVAL",
-          Stanza.NS.vcard,
-          null,
-          this._cachedUserIcon.binval
-        ),
-      ];
-      let photo = this._userVCard.getElement(["PHOTO"]);
-      if (photo) {
-        photo.children = photoChildren;
-      } else {
-        this._userVCard.addChild(
-          Stanza.node("PHOTO", Stanza.NS.vcard, null, photoChildren)
-        );
-      }
-    } else if ("_forceUserIconUpdate" in this) {
-      // Like for the display name, we remove a photo without
-      // replacing it only if the call is caused by a user action.
-      this._userVCard.children = this._userVCard.children.filter(
-        n => n.qName != "PHOTO"
-      );
-    }
-    delete this._forceUserIconUpdate;
-
-    // Send the vCard only if it has really changed.
-    // We handle the result response from the server (it does not require
-    // any further action).
-    if (this._userVCard.getXML() != existingVCard) {
-      this.sendStanza(
-        Stanza.iq("set", null, null, this._userVCard),
-        this._handleResult()
-      );
-    } else {
-      this.LOG(
-        "Not sending the vCard because the server stored vCard is identical."
-      );
-    }
-  },
 };
-function XMPPAccount(aProtocol, aImAccount) {
-  this._pendingVCardRequests = [];
-  this._init(aProtocol, aImAccount);
-}
-XMPPAccount.prototype = XMPPAccountPrototype;
deleted file mode 100644
--- a/chat/protocols/xmpp/xmpp.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {dde786d1-6f59-43d0-9bc8-b505a757fb30} xmpp.js
-contract @mozilla.org/chat/xmpp;1 {dde786d1-6f59-43d0-9bc8-b505a757fb30}
-category im-protocol-plugin prpl-jabber @mozilla.org/chat/xmpp;1
new file mode 100644
--- /dev/null
+++ b/chat/protocols/yahoo/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+  {
+    'cid': '{50ea817e-5d79-4657-91ae-aa0a52bdb98c}',
+    'contract_ids': ['@mozilla.org/chat/yahoo;1'],
+    'jsm': 'resource:///modules/yahoo.jsm',
+    'constructor': 'YahooProtocol',
+    'categories': {'im-protocol-plugin': 'prpl-yahoo'},
+  },
+]
--- a/chat/protocols/yahoo/moz.build
+++ b/chat/protocols/yahoo/moz.build
@@ -1,11 +1,14 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-EXTRA_COMPONENTS += [
-    'yahoo.js',
-    'yahoo.manifest',
+JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'yahoo.jsm',
 ]
 
-JAR_MANIFESTS += ['jar.mn']
+XPCOM_MANIFESTS += [
+    'components.conf',
+]
rename from chat/protocols/yahoo/yahoo.js
rename to chat/protocols/yahoo/yahoo.jsm
--- a/chat/protocols/yahoo/yahoo.js
+++ b/chat/protocols/yahoo/yahoo.jsm
@@ -1,12 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+var EXPORTED_SYMBOLS = ["YahooProtocol"];
+
 var { XPCOMUtils, l10nHelper } = ChromeUtils.import(
   "resource:///modules/imXPCOMUtils.jsm"
 );
 var { GenericAccountPrototype, GenericProtocolPrototype } = ChromeUtils.import(
   "resource:///modules/jsProtoHelper.jsm"
 );
 
 XPCOMUtils.defineLazyGetter(this, "_", () =>
@@ -46,12 +48,9 @@ YahooProtocol.prototype = {
     return "Yahoo";
   },
   get iconBaseURI() {
     return "chrome://prpl-yahoo/skin/";
   },
   getAccount(aImAccount) {
     return new YahooAccount(this, aImAccount);
   },
-  classID: Components.ID("{50ea817e-5d79-4657-91ae-aa0a52bdb98c}"),
 };
-
-var NSGetFactory = XPCOMUtils.generateNSGetFactory([YahooProtocol]);
deleted file mode 100644
--- a/chat/protocols/yahoo/yahoo.manifest
+++ /dev/null
@@ -1,3 +0,0 @@
-component {50ea817e-5d79-4657-91ae-aa0a52bdb98c} yahoo.js
-contract @mozilla.org/chat/yahoo;1 {50ea817e-5d79-4657-91ae-aa0a52bdb98c}
-category im-protocol-plugin prpl-yahoo @mozilla.org/chat/yahoo;1
--- a/mail/installer/package-manifest.in
+++ b/mail/installer/package-manifest.in
@@ -215,48 +215,16 @@
 #endif
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ; instant messaging
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 @RESPATH@/@PREF_DIR@/chat-prefs.js
 @RESPATH@/chrome/chat@JAREXT@
 @RESPATH@/chrome/chat.manifest
-@RESPATH@/components/imAccounts.js
-@RESPATH@/components/imAccounts.manifest
-@RESPATH@/components/imCommands.js
-@RESPATH@/components/imCommands.manifest
-@RESPATH@/components/imContacts.js
-@RESPATH@/components/imContacts.manifest
-@RESPATH@/components/imConversations.js
-@RESPATH@/components/imConversations.manifest
-@RESPATH@/components/imCore.js
-@RESPATH@/components/imCore.manifest
-@RESPATH@/components/facebook.js
-@RESPATH@/components/facebook.manifest
-@RESPATH@/components/gtalk.js
-@RESPATH@/components/gtalk.manifest
-@RESPATH@/components/irc.js
-@RESPATH@/components/irc.manifest
-@RESPATH@/components/matrix.js
-@RESPATH@/components/matrix.manifest
-@RESPATH@/components/odnoklassniki.js
-@RESPATH@/components/odnoklassniki.manifest
-@RESPATH@/components/skype.js
-@RESPATH@/components/skype.manifest
-@RESPATH@/components/twitter.js
-@RESPATH@/components/twitter.manifest
-@RESPATH@/components/xmpp.js
-@RESPATH@/components/xmpp.manifest
-@RESPATH@/components/yahoo.js
-@RESPATH@/components/yahoo.manifest
-@RESPATH@/components/smileProtocolHandler.js
-@RESPATH@/components/smileProtocolHandler.manifest
-@RESPATH@/components/logger.js
-@RESPATH@/components/logger.manifest
 
 ; Thunderbird specific
 @RESPATH@/@PREF_DIR@/all-im.js
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 ; Chrome Files
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;