Backed out changeset faa7e3cc7838 (bug 1562313) for various issues missed in review. a=backout
authorPatrick Cloke <clokep@gmail.com>
Tue, 29 Oct 2019 11:07:44 -0400
changeset 37385 9ba26fa8d407d7466b79ab338b874e2460aa110e
parent 37384 b78bf453b765f4bcab77372f8b6a559f62b48c26
child 37386 5f93ed1690ab1ca2af7dd435d9db1d4310ba77ad
push id396
push userclokep@gmail.com
push dateMon, 06 Jan 2020 23:11:57 +0000
reviewersbackout
bugs1562313
backs outfaa7e3cc7838eea5d26bb4cb93ee0b940e6acbff
Backed out changeset faa7e3cc7838 (bug 1562313) for various issues missed in review. a=backout
chat/components/src/IMAccounts.jsm
chat/components/src/IMCommands.jsm
chat/components/src/IMContacts.jsm
chat/components/src/IMConversations.jsm
chat/components/src/IMCore.jsm
chat/components/src/Logger.jsm
chat/components/src/SmileProtocolHandler.jsm
chat/components/src/components.conf
chat/components/src/imAccounts.js
chat/components/src/imAccounts.manifest
chat/components/src/imCommands.js
chat/components/src/imCommands.manifest
chat/components/src/imContacts.js
chat/components/src/imContacts.manifest
chat/components/src/imConversations.js
chat/components/src/imConversations.manifest
chat/components/src/imCore.js
chat/components/src/imCore.manifest
chat/components/src/logger.js
chat/components/src/logger.manifest
chat/components/src/moz.build
chat/components/src/smileProtocolHandler.js
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/Facebook.jsm
chat/protocols/facebook/components.conf
chat/protocols/facebook/facebook.js
chat/protocols/facebook/facebook.manifest
chat/protocols/facebook/moz.build
chat/protocols/gtalk/Gtalk.jsm
chat/protocols/gtalk/components.conf
chat/protocols/gtalk/gtalk.js
chat/protocols/gtalk/gtalk.manifest
chat/protocols/gtalk/moz.build
chat/protocols/irc/IRC.jsm
chat/protocols/irc/components.conf
chat/protocols/irc/irc.js
chat/protocols/irc/irc.manifest
chat/protocols/irc/ircISUPPORT.jsm
chat/protocols/irc/moz.build
chat/protocols/irc/test/test_ctcpQuote.js
chat/protocols/irc/test/test_ircCAP.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/JSTestProtocol.jsm
chat/protocols/jsTest/components.conf
chat/protocols/jsTest/jsTestProtocol.js
chat/protocols/jsTest/jsTestProtocol.manifest
chat/protocols/jsTest/moz.build
chat/protocols/matrix/Matrix.jsm
chat/protocols/matrix/components.conf
chat/protocols/matrix/matrix.js
chat/protocols/matrix/matrix.manifest
chat/protocols/matrix/moz.build
chat/protocols/odnoklassniki/Odnoklassniki.jsm
chat/protocols/odnoklassniki/components.conf
chat/protocols/odnoklassniki/moz.build
chat/protocols/odnoklassniki/odnoklassniki.js
chat/protocols/odnoklassniki/odnoklassniki.manifest
chat/protocols/skype/Skype.jsm
chat/protocols/skype/components.conf
chat/protocols/skype/moz.build
chat/protocols/skype/skype.js
chat/protocols/skype/skype.manifest
chat/protocols/skype/test/test_MagicSha256.js
chat/protocols/skype/test/test_contactUrlToName.js
chat/protocols/twitter/Twitter.jsm
chat/protocols/twitter/components.conf
chat/protocols/twitter/moz.build
chat/protocols/twitter/twitter.js
chat/protocols/twitter/twitter.manifest
chat/protocols/xmpp/XMPPProtocol.jsm
chat/protocols/xmpp/components.conf
chat/protocols/xmpp/moz.build
chat/protocols/xmpp/xmpp.js
chat/protocols/xmpp/xmpp.manifest
chat/protocols/yahoo/Yahoo.jsm
chat/protocols/yahoo/components.conf
chat/protocols/yahoo/moz.build
chat/protocols/yahoo/yahoo.js
chat/protocols/yahoo/yahoo.manifest
mail/installer/package-manifest.in
deleted file mode 100644
--- a/chat/components/src/IMAccounts.jsm
+++ /dev/null
@@ -1,1300 +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 EXPORTED_SYMBOLS = ["AccountsService"];
-
-var {
-  ClassInfo,
-  EmptyEnumerator,
-  nsSimpleEnumerator,
-  XPCOMUtils,
-  setTimeout,
-  clearTimeout,
-  executeSoon,
-  l10nHelper,
-} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
-var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
-var {
-  GenericAccountPrototype,
-  GenericAccountBuddyPrototype,
-} = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
-
-var kPrefAutologinPending = "messenger.accounts.autoLoginPending";
-var kPrefMessengerAccounts = "messenger.accounts";
-var kPrefAccountPrefix = "messenger.account.";
-var kAccountKeyPrefix = "account";
-var kAccountOptionPrefPrefix = "options.";
-var kPrefAccountName = "name";
-var kPrefAccountPrpl = "prpl";
-var kPrefAccountAutoLogin = "autoLogin";
-var kPrefAccountAutoJoin = "autoJoin";
-var kPrefAccountAlias = "alias";
-var kPrefAccountFirstConnectionState = "firstConnectionState";
-
-var kPrefConvertOldPasswords = "messenger.accounts.convertOldPasswords";
-var kPrefAccountPassword = "password";
-
-XPCOMUtils.defineLazyGetter(this, "_", () =>
-  l10nHelper("chrome://chat/locale/accounts.properties")
-);
-
-XPCOMUtils.defineLazyGetter(this, "_maxDebugMessages", () =>
-  Services.prefs.getIntPref("messenger.accounts.maxDebugMessages")
-);
-
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "HttpProtocolHandler",
-  "@mozilla.org/network/protocol;1?name=http",
-  "nsIHttpProtocolHandler"
-);
-
-var gUserCanceledMasterPasswordPrompt = false;
-var gConvertingOldPasswords = false;
-
-var SavePrefTimer = {
-  saveNow() {
-    if (this._timer) {
-      clearTimeout(this._timer);
-      this._timer = null;
-    }
-    Services.prefs.savePrefFile(null);
-  },
-  _timer: null,
-  unInitTimer() {
-    if (this._timer) {
-      this.saveNow();
-    }
-  },
-  initTimer() {
-    if (!this._timer) {
-      this._timer = setTimeout(this.saveNow.bind(this), 5000);
-    }
-  },
-};
-
-var AutoLoginCounter = {
-  _count: 0,
-  startAutoLogin() {
-    ++this._count;
-    if (this._count != 1) {
-      return;
-    }
-    Services.prefs.setIntPref(kPrefAutologinPending, Date.now() / 1000);
-    SavePrefTimer.saveNow();
-  },
-  finishedAutoLogin() {
-    --this._count;
-    if (this._count != 0) {
-      return;
-    }
-    Services.prefs.deleteBranch(kPrefAutologinPending);
-    SavePrefTimer.initTimer();
-  },
-};
-
-function UnknownProtocol(aPrplId) {
-  this.id = aPrplId;
-}
-UnknownProtocol.prototype = {
-  __proto__: ClassInfo("prplIProtocol", "Unknown protocol"),
-  get name() {
-    return "";
-  },
-  get normalizedName() {
-    return this.name;
-  },
-  get iconBaseURI() {
-    return "chrome://chat/skin/prpl-unknown/";
-  },
-  getOptions() {
-    return EmptyEnumerator;
-  },
-  getUsernameSplit() {
-    return EmptyEnumerator;
-  },
-  get usernameEmptyText() {
-    return "";
-  },
-
-  getAccount(aKey, aName) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-  accountExists() {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-
-  // false seems an acceptable default for all options
-  // (they should never be called anyway).
-  get uniqueChatName() {
-    return false;
-  },
-  get chatHasTopic() {
-    return false;
-  },
-  get noPassword() {
-    return false;
-  },
-  get newMailNotification() {
-    return false;
-  },
-  get imagesInIM() {
-    return false;
-  },
-  get passwordOptional() {
-    return true;
-  },
-  get usePointSize() {
-    return true;
-  },
-  get registerNoScreenName() {
-    return false;
-  },
-  get slashCommandsNative() {
-    return false;
-  },
-  get usePurpleProxy() {
-    return false;
-  },
-};
-
-// An unknown prplIAccount.
-function UnknownAccount(aAccount) {
-  this._init(aAccount.protocol, aAccount);
-}
-UnknownAccount.prototype = GenericAccountPrototype;
-
-function UnknownAccountBuddy(aAccount, aBuddy, aTag) {
-  this._init(new UnknownAccount(aAccount), aBuddy, aTag);
-}
-UnknownAccountBuddy.prototype = GenericAccountBuddyPrototype;
-
-// aName and aPrplId are provided as parameter only if this is a new
-// account that doesn't exist in the preferences. In this case, these
-// 2 values should be stored.
-function imAccount(aKey, aName, aPrplId) {
-  if (!aKey.startsWith(kAccountKeyPrefix)) {
-    throw Cr.NS_ERROR_INVALID_ARG;
-  }
-
-  this.id = aKey;
-  this.numericId = parseInt(aKey.substr(kAccountKeyPrefix.length));
-  gAccountsService._keepAccount(this);
-  this.prefBranch = Services.prefs.getBranch(kPrefAccountPrefix + aKey + ".");
-
-  if (aName) {
-    this.name = aName;
-    this.prefBranch.setStringPref(kPrefAccountName, aName);
-
-    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
-  } else {
-    this.name = this.prefBranch.getStringPref(kPrefAccountName);
-  }
-
-  let prplId = aPrplId;
-  if (prplId) {
-    this.prefBranch.setCharPref(kPrefAccountPrpl, prplId);
-  } else {
-    prplId = this.prefBranch.getCharPref(kPrefAccountPrpl);
-  }
-
-  // Get the protocol plugin, or fallback to an UnknownProtocol instance.
-  this.protocol = Services.core.getProtocolById(prplId);
-  if (!this.protocol) {
-    this.protocol = new UnknownProtocol(prplId);
-    this._connectionErrorReason = Ci.imIAccount.ERROR_UNKNOWN_PRPL;
-    return;
-  }
-
-  // Ensure the account is correctly stored in blist.sqlite.
-  Services.contacts.storeAccount(this.numericId, this.name, prplId);
-
-  // Get the prplIAccount from the protocol plugin.
-  this.prplAccount = this.protocol.getAccount(this);
-
-  // Send status change notifications to the account.
-  this.observedStatusInfo = null; // (To execute the setter).
-
-  // If we have never finished the first connection attempt for this account,
-  // mark the account as having caused a crash.
-  if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
-    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_CRASHED;
-  }
-
-  Services.logins.initializationPromise.then(() => {
-    // Try to convert old passwords stored in the preferences.
-    // Don't try too hard if the user has canceled a master password prompt:
-    // we don't want to display several of theses prompts at startup.
-    if (gConvertingOldPasswords && !this.protocol.noPassword) {
-      try {
-        let password = this.prefBranch.getStringPref(kPrefAccountPassword);
-        if (password && !this.password) {
-          this.password = password;
-        }
-      } catch (e) {
-        /* No password saved in the prefs for this account. */
-      }
-    }
-
-    // Check for errors that should prevent connection attempts.
-    if (this._passwordRequired && !this.password) {
-      this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
-    } else if (
-      this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_CRASHED
-    ) {
-      this._connectionErrorReason = Ci.imIAccount.ERROR_CRASHED;
-    }
-  });
-}
-
-imAccount.prototype = {
-  __proto__: ClassInfo(["imIAccount", "prplIAccount"], "im account object"),
-
-  name: "",
-  id: "",
-  numericId: 0,
-  protocol: null,
-  prplAccount: null,
-  connectionState: Ci.imIAccount.STATE_DISCONNECTED,
-  connectionStateMsg: "",
-  connectionErrorMessage: "",
-  _connectionErrorReason: Ci.prplIAccount.NO_ERROR,
-  get connectionErrorReason() {
-    if (
-      this._connectionErrorReason != Ci.prplIAccount.NO_ERROR &&
-      (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
-        !this._password)
-    ) {
-      return this._connectionErrorReason;
-    }
-    return this.prplAccount.connectionErrorReason;
-  },
-
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "account-connect-progress") {
-      this.connectionStateMsg = aData;
-    } else if (aTopic == "account-connecting") {
-      if (this.prplAccount.connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
-        delete this.connectionErrorMessage;
-        if (this.timeOfNextReconnect - Date.now() > 1000) {
-          // This is a manual reconnection, reset the auto-reconnect stuff
-          this.timeOfLastConnect = 0;
-          this._cancelReconnection();
-        }
-      }
-      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) {
-        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_PENDING;
-      }
-      this.connectionState = Ci.imIAccount.STATE_CONNECTING;
-    } else if (aTopic == "account-connected") {
-      this.connectionState = Ci.imIAccount.STATE_CONNECTED;
-      this._finishedAutoLogin();
-      this.timeOfLastConnect = Date.now();
-      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) {
-        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_OK;
-      }
-      delete this.connectionStateMsg;
-
-      if (
-        this.canJoinChat &&
-        this.prefBranch.prefHasUserValue(kPrefAccountAutoJoin)
-      ) {
-        let autojoin = this.prefBranch.getStringPref(kPrefAccountAutoJoin);
-        if (autojoin) {
-          for (let room of autojoin.trim().split(/,\s*/)) {
-            if (room) {
-              this.joinChat(this.getChatRoomDefaultFieldValues(room));
-            }
-          }
-        }
-      }
-    } else if (aTopic == "account-disconnecting") {
-      this.connectionState = Ci.imIAccount.STATE_DISCONNECTING;
-      this.connectionErrorMessage = aData;
-      delete this.connectionStateMsg;
-      this._finishedAutoLogin();
-
-      let firstConnectionState = this.firstConnectionState;
-      if (
-        firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK &&
-        firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_CRASHED
-      ) {
-        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
-      }
-
-      let connectionErrorReason = this.prplAccount.connectionErrorReason;
-      if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
-        if (
-          connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR ||
-          connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR
-        ) {
-          this._startReconnectTimer();
-        }
-        this._sendNotification("account-connect-error");
-      }
-    } else if (aTopic == "account-disconnected") {
-      this.connectionState = Ci.imIAccount.STATE_DISCONNECTED;
-      let connectionErrorReason = this.prplAccount.connectionErrorReason;
-      if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
-        // If the account was disconnected with an error, save the debug messages.
-        this._omittedDebugMessagesBeforeError += this._omittedDebugMessages;
-        if (this._debugMessagesBeforeError) {
-          this._omittedDebugMessagesBeforeError += this._debugMessagesBeforeError.length;
-        }
-        this._debugMessagesBeforeError = this._debugMessages;
-      } else {
-        // After a clean disconnection, drop the debug messages that
-        // could have been left by a previous error.
-        delete this._omittedDebugMessagesBeforeError;
-        delete this._debugMessagesBeforeError;
-      }
-      delete this._omittedDebugMessages;
-      delete this._debugMessages;
-      if (
-        this._statusObserver &&
-        connectionErrorReason == Ci.prplIAccount.NO_ERROR &&
-        this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE
-      ) {
-        // If the status changed back to online while an account was still
-        // disconnecting, it was not reconnected automatically at that point,
-        // so we must do it now. (This happens for protocols like IRC where
-        // disconnection is not immediate.)
-        this._sendNotification(aTopic, aData);
-        this.connect();
-        return;
-      }
-    } else {
-      throw Cr.NS_ERROR_UNEXPECTED;
-    }
-    this._sendNotification(aTopic, aData);
-  },
-
-  _debugMessages: null,
-  _omittedDebugMessages: 0,
-  _debugMessagesBeforeError: null,
-  _omittedDebugMessagesBeforeError: 0,
-  logDebugMessage(aMessage, aLevel) {
-    if (!this._debugMessages) {
-      this._debugMessages = [];
-    }
-    if (_maxDebugMessages && this._debugMessages.length >= _maxDebugMessages) {
-      this._debugMessages.shift();
-      ++this._omittedDebugMessages;
-    }
-    this._debugMessages.push({ logLevel: aLevel, message: aMessage });
-  },
-  _createDebugMessage(aMessage) {
-    let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
-      Ci.nsIScriptError
-    );
-    scriptError.init(
-      aMessage,
-      "",
-      "",
-      0,
-      null,
-      Ci.nsIScriptError.warningFlag,
-      "component javascript"
-    );
-    return { logLevel: 0, message: scriptError };
-  },
-  getDebugMessages() {
-    let messages = [];
-    if (this._omittedDebugMessagesBeforeError) {
-      let text = this._omittedDebugMessagesBeforeError + " messages omitted";
-      messages.push(this._createDebugMessage(text));
-    }
-    if (this._debugMessagesBeforeError) {
-      messages = messages.concat(this._debugMessagesBeforeError);
-    }
-    if (this._omittedDebugMessages) {
-      let text = this._omittedDebugMessages + " messages omitted";
-      messages.push(this._createDebugMessage(text));
-    }
-    if (this._debugMessages) {
-      messages = messages.concat(this._debugMessages);
-    }
-    if (messages.length) {
-      let appInfo = Services.appinfo;
-      let header =
-        `${appInfo.name} ${appInfo.version} (${appInfo.appBuildID}), ` +
-        `Gecko ${appInfo.platformVersion} (${appInfo.platformBuildID}) ` +
-        `on ${HttpProtocolHandler.oscpu}`;
-      messages.unshift(this._createDebugMessage(header));
-    }
-
-    return messages;
-  },
-
-  _observedStatusInfo: null,
-  get observedStatusInfo() {
-    return this._observedStatusInfo;
-  },
-  _statusObserver: null,
-  set observedStatusInfo(aUserStatusInfo) {
-    if (!this.prplAccount) {
-      return;
-    }
-    if (this._statusObserver) {
-      this.statusInfo.removeObserver(this._statusObserver);
-    }
-    this._observedStatusInfo = aUserStatusInfo;
-    if (this._statusObserver) {
-      this.statusInfo.addObserver(this._statusObserver);
-    }
-  },
-  _removeStatusObserver() {
-    if (this._statusObserver) {
-      this.statusInfo.removeObserver(this._statusObserver);
-      delete this._statusObserver;
-    }
-  },
-  get statusInfo() {
-    return this._observedStatusInfo || Services.core.globalUserStatus;
-  },
-
-  reconnectAttempt: 0,
-  timeOfLastConnect: 0,
-  timeOfNextReconnect: 0,
-  _reconnectTimer: null,
-  _startReconnectTimer() {
-    if (Services.io.offline) {
-      Cu.reportError("_startReconnectTimer called while offline");
-      return;
-    }
-
-    /* If the last successful connection is older than 10 seconds, reset the
-       number of reconnection attempts. */
-    const kTimeBeforeSuccessfulConnection = 10;
-    if (
-      this.timeOfLastConnect &&
-      this.timeOfLastConnect + kTimeBeforeSuccessfulConnection * 1000 <
-        Date.now()
-    ) {
-      delete this.reconnectAttempt;
-      delete this.timeOfLastConnect;
-    }
-
-    let timers = Services.prefs
-      .getCharPref("messenger.accounts.reconnectTimer")
-      .split(",");
-    let delay = timers[Math.min(this.reconnectAttempt, timers.length - 1)];
-    let msDelay = parseInt(delay) * 1000;
-    ++this.reconnectAttempt;
-    this.timeOfNextReconnect = Date.now() + msDelay;
-    this._reconnectTimer = setTimeout(this.connect.bind(this), msDelay);
-  },
-
-  _sendNotification(aTopic, aData) {
-    Services.obs.notifyObservers(this, aTopic, aData);
-  },
-
-  get firstConnectionState() {
-    try {
-      return this.prefBranch.getIntPref(kPrefAccountFirstConnectionState);
-    } catch (e) {
-      return Ci.imIAccount.FIRST_CONNECTION_OK;
-    }
-  },
-  set firstConnectionState(aState) {
-    if (aState == Ci.imIAccount.FIRST_CONNECTION_OK) {
-      this.prefBranch.deleteBranch(kPrefAccountFirstConnectionState);
-    } else {
-      this.prefBranch.setIntPref(kPrefAccountFirstConnectionState, aState);
-      // We want to save this pref immediately when trying to connect.
-      if (aState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
-        SavePrefTimer.saveNow();
-      } else {
-        SavePrefTimer.initTimer();
-      }
-    }
-  },
-
-  _pendingReconnectForConnectionInfoChange: false,
-  _connectionInfoChanged() {
-    // The next connection will be the first connection with these parameters.
-    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
-
-    // We want to attempt to reconnect with the new settings only if a
-    // previous attempt failed or a connection attempt is currently
-    // pending (so we can return early if the account is currently
-    // connected or disconnected without error).
-    // The code doing the reconnection attempt is wrapped within an
-    // executeSoon call so that when multiple settings are changed at
-    // once we don't attempt to reconnect until they are all saved.
-    // If a reconnect attempt is already scheduled, we can also return early.
-    if (
-      this._pendingReconnectForConnectionInfoChange ||
-      this.connected ||
-      (this.disconnected &&
-        this.connectionErrorReason == Ci.prplIAccount.NO_ERROR)
-    ) {
-      return;
-    }
-
-    this._pendingReconnectForConnectionInfoChange = true;
-    executeSoon(
-      function() {
-        delete this._pendingReconnectForConnectionInfoChange;
-        // If the connection parameters have changed while we were
-        // trying to connect, cancel the ongoing connection attempt and
-        // try again with the new parameters.
-        if (this.connecting) {
-          this.disconnect();
-          this.connect();
-          return;
-        }
-        // If the account was disconnected because of a non-fatal
-        // connection error, retry now that we have new parameters.
-        let errorReason = this.connectionErrorReason;
-        if (
-          this.disconnected &&
-          errorReason != Ci.prplIAccount.NO_ERROR &&
-          errorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD &&
-          errorReason != Ci.imIAccount.ERROR_CRASHED &&
-          errorReason != Ci.imIAccount.ERROR_UNKNOWN_PRPL
-        ) {
-          this.connect();
-        }
-      }.bind(this)
-    );
-  },
-
-  // If the protocol plugin is missing, we can't access the normalizedName,
-  // but in lots of cases this.name is equivalent.
-  get normalizedName() {
-    return this.prplAccount ? this.prplAccount.normalizedName : this.name;
-  },
-  normalize(aName) {
-    return this.prplAccount ? this.prplAccount.normalize(aName) : aName;
-  },
-
-  _sendUpdateNotification() {
-    this._sendNotification("account-updated");
-  },
-
-  set alias(val) {
-    if (val) {
-      this.prefBranch.setStringPref(kPrefAccountAlias, val);
-    } else {
-      this.prefBranch.deleteBranch(kPrefAccountAlias);
-    }
-    this._sendUpdateNotification();
-  },
-  get alias() {
-    try {
-      return this.prefBranch.getStringPref(kPrefAccountAlias);
-    } catch (e) {
-      return "";
-    }
-  },
-
-  _password: "",
-  get password() {
-    if (this._password) {
-      return this._password;
-    }
-
-    // Avoid prompting the user for the master password more than once at startup.
-    if (gUserCanceledMasterPasswordPrompt) {
-      return "";
-    }
-
-    let passwordURI = "im://" + this.protocol.id;
-    let logins;
-    try {
-      logins = Services.logins.findLogins(passwordURI, null, passwordURI);
-    } catch (e) {
-      this._handleMasterPasswordException(e);
-      return "";
-    }
-    let normalizedName = this.normalizedName;
-    for (let login of logins) {
-      if (login.username == normalizedName) {
-        this._password = login.password;
-        if (
-          this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD
-        ) {
-          // We have found a password for an account marked as missing password,
-          // re-check all others accounts missing a password. But first,
-          // remove the error on our own account to avoid re-checking it.
-          delete this._connectionErrorReason;
-          gAccountsService._checkIfPasswordStillMissing();
-        }
-        return this._password;
-      }
-    }
-    return "";
-  },
-  _checkIfPasswordStillMissing() {
-    if (
-      this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
-      !this.password
-    ) {
-      return;
-    }
-
-    delete this._connectionErrorReason;
-    this._sendUpdateNotification();
-  },
-  get _passwordRequired() {
-    return !this.protocol.noPassword && !this.protocol.passwordOptional;
-  },
-  set password(aPassword) {
-    this._password = aPassword;
-    if (gUserCanceledMasterPasswordPrompt) {
-      return;
-    }
-    let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
-      Ci.nsILoginInfo
-    );
-    let passwordURI = "im://" + this.protocol.id;
-    newLogin.init(
-      passwordURI,
-      null,
-      passwordURI,
-      this.normalizedName,
-      aPassword,
-      "",
-      ""
-    );
-    try {
-      let logins = Services.logins.findLogins(passwordURI, null, passwordURI);
-      let saved = false;
-      for (let login of logins) {
-        if (newLogin.matches(login, true)) {
-          if (aPassword) {
-            Services.logins.modifyLogin(login, newLogin);
-          } else {
-            Services.logins.removeLogin(login);
-          }
-          saved = true;
-          break;
-        }
-      }
-      if (!saved && aPassword) {
-        Services.logins.addLogin(newLogin);
-      }
-    } catch (e) {
-      this._handleMasterPasswordException(e);
-    }
-
-    this._connectionInfoChanged();
-    if (
-      aPassword &&
-      this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD
-    ) {
-      this._connectionErrorReason = Ci.imIAccount.NO_ERROR;
-    } else if (!aPassword && this._passwordRequired) {
-      this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
-    }
-    this._sendUpdateNotification();
-  },
-  _handleMasterPasswordException(aException) {
-    if (aException.result != Cr.NS_ERROR_ABORT) {
-      throw aException;
-    }
-
-    gUserCanceledMasterPasswordPrompt = true;
-    executeSoon(function() {
-      gUserCanceledMasterPasswordPrompt = false;
-    });
-  },
-
-  get autoLogin() {
-    return this.prefBranch.getBoolPref(kPrefAccountAutoLogin, true);
-  },
-  set autoLogin(val) {
-    this.prefBranch.setBoolPref(kPrefAccountAutoLogin, val);
-    SavePrefTimer.initTimer();
-    this._sendUpdateNotification();
-  },
-  _autoLoginPending: false,
-  checkAutoLogin() {
-    // No auto-login if: the account has an error at the imIAccount level
-    // (unknown protocol, missing password, first connection crashed),
-    // the account is already connected or connecting, or autoLogin is off.
-    if (
-      this._connectionErrorReason != Ci.prplIAccount.NO_ERROR ||
-      this.connecting ||
-      this.connected ||
-      !this.autoLogin
-    ) {
-      return;
-    }
-
-    this._autoLoginPending = true;
-    AutoLoginCounter.startAutoLogin();
-    try {
-      this.connect();
-    } catch (e) {
-      Cu.reportError(e);
-      this._finishedAutoLogin();
-    }
-  },
-  _finishedAutoLogin() {
-    if (!this.hasOwnProperty("_autoLoginPending")) {
-      return;
-    }
-    delete this._autoLoginPending;
-    AutoLoginCounter.finishedAutoLogin();
-  },
-
-  // Delete the account (from the preferences, mozStorage, and call unInit).
-  remove() {
-    let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
-      Ci.nsILoginInfo
-    );
-    let passwordURI = "im://" + this.protocol.id;
-    // Note: the normalizedName may not be exactly right if the
-    // protocol plugin is missing.
-    login.init(passwordURI, null, passwordURI, this.normalizedName, "", "", "");
-    let logins = Services.logins.findLogins(passwordURI, null, passwordURI);
-    for (let l of logins) {
-      if (login.matches(l, true)) {
-        Services.logins.removeLogin(l);
-        break;
-      }
-    }
-    if (this.connected || this.connecting) {
-      this.disconnect();
-    }
-    if (this.prplAccount) {
-      this.prplAccount.remove();
-    }
-    this.unInit();
-    Services.contacts.forgetAccount(this.numericId);
-    this.prefBranch.deleteBranch("");
-  },
-  unInit() {
-    // remove any pending reconnection timer.
-    this._cancelReconnection();
-
-    // Keeping a status observer could cause an immediate reconnection.
-    this._removeStatusObserver();
-
-    // remove any pending autologin preference used for crash detection.
-    this._finishedAutoLogin();
-
-    // If the first connection was pending on quit, we set it back to unknown.
-    if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
-      this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
-    }
-
-    // and make sure we cleanup the save pref timer.
-    SavePrefTimer.unInitTimer();
-
-    if (this.prplAccount) {
-      this.prplAccount.unInit();
-    }
-
-    delete this.protocol;
-    delete this.prplAccount;
-  },
-
-  get _ensurePrplAccount() {
-    if (this.prplAccount) {
-      return this.prplAccount;
-    }
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-  connect() {
-    if (!this.prplAccount) {
-      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-    }
-
-    if (this._passwordRequired) {
-      // If the previous connection attempt failed because we have a wrong password,
-      // clear the passwor cache so that if there's no password in the password
-      // manager the user gets prompted again.
-      if (
-        this.connectionErrorReason ==
-        Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED
-      ) {
-        delete this._password;
-      }
-
-      let password = this.password;
-      if (!password) {
-        let prompts = Services.prompt;
-        let shouldSave = { value: false };
-        password = { value: "" };
-        if (
-          !prompts.promptPassword(
-            null,
-            _("passwordPromptTitle", this.name),
-            _("passwordPromptText", this.name),
-            password,
-            _("passwordPromptSaveCheckbox"),
-            shouldSave
-          )
-        ) {
-          return;
-        }
-
-        if (shouldSave.value) {
-          this.password = password.value;
-        } else {
-          this._password = password.value;
-        }
-      }
-    }
-
-    if (!this._statusObserver) {
-      this._statusObserver = {
-        observe: function(aSubject, aTopic, aData) {
-          // Disconnect or reconnect the account automatically, otherwise notify
-          // the prplAccount instance.
-          let statusType = aSubject.statusType;
-          let connectionErrorReason = this.connectionErrorReason;
-          if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE) {
-            if (this.connected || this.connecting) {
-              this.prplAccount.disconnect();
-            }
-            this._cancelReconnection();
-          } else if (
-            statusType > Ci.imIStatusInfo.STATUS_OFFLINE &&
-            this.disconnected &&
-            (connectionErrorReason == Ci.prplIAccount.NO_ERROR ||
-              connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR ||
-              connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR)
-          ) {
-            this.prplAccount.connect();
-          } else if (this.connected) {
-            this.prplAccount.observe(aSubject, aTopic, aData);
-          }
-        }.bind(this),
-      };
-
-      this.statusInfo.addObserver(this._statusObserver);
-    }
-
-    if (
-      !Services.io.offline &&
-      this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE &&
-      this.disconnected
-    ) {
-      this.prplAccount.connect();
-    }
-  },
-  disconnect() {
-    this._removeStatusObserver();
-    if (!this.disconnected) {
-      this._ensurePrplAccount.disconnect();
-    }
-  },
-
-  get disconnected() {
-    return this.connectionState == Ci.imIAccount.STATE_DISCONNECTED;
-  },
-  get connected() {
-    return this.connectionState == Ci.imIAccount.STATE_CONNECTED;
-  },
-  get connecting() {
-    return this.connectionState == Ci.imIAccount.STATE_CONNECTING;
-  },
-  get disconnecting() {
-    return this.connectionState == Ci.imIAccount.STATE_DISCONNECTING;
-  },
-
-  _cancelReconnection() {
-    if (this._reconnectTimer) {
-      clearTimeout(this._reconnectTimer);
-      delete this._reconnectTimer;
-    }
-    delete this.reconnectAttempt;
-    delete this.timeOfNextReconnect;
-  },
-  cancelReconnection() {
-    if (!this.disconnected) {
-      throw Cr.NS_ERROR_UNEXPECTED;
-    }
-
-    // Ensure we don't keep a status observer that could re-enable the
-    // auto-reconnect timers.
-    this.disconnect();
-
-    this._cancelReconnection();
-  },
-  createConversation(aName) {
-    return this._ensurePrplAccount.createConversation(aName);
-  },
-  addBuddy(aTag, aName) {
-    this._ensurePrplAccount.addBuddy(aTag, aName);
-  },
-  loadBuddy(aBuddy, aTag) {
-    if (this.prplAccount) {
-      return this.prplAccount.loadBuddy(aBuddy, aTag);
-    }
-    // Generate dummy account buddies for unknown protocols.
-    return new UnknownAccountBuddy(this, aBuddy, aTag);
-  },
-  requestBuddyInfo(aBuddyName) {
-    this._ensurePrplAccount.requestBuddyInfo(aBuddyName);
-  },
-  getChatRoomFields() {
-    return this._ensurePrplAccount.getChatRoomFields();
-  },
-  getChatRoomDefaultFieldValues(aDefaultChatName) {
-    return this._ensurePrplAccount.getChatRoomDefaultFieldValues(
-      aDefaultChatName
-    );
-  },
-  get canJoinChat() {
-    return this.prplAccount ? this.prplAccount.canJoinChat : false;
-  },
-  joinChat(aComponents) {
-    this._ensurePrplAccount.joinChat(aComponents);
-  },
-  setBool(aName, aVal) {
-    this.prefBranch.setBoolPref(kAccountOptionPrefPrefix + aName, aVal);
-    this._connectionInfoChanged();
-    if (this.prplAccount) {
-      this.prplAccount.setBool(aName, aVal);
-    }
-    SavePrefTimer.initTimer();
-  },
-  setInt(aName, aVal) {
-    this.prefBranch.setIntPref(kAccountOptionPrefPrefix + aName, aVal);
-    this._connectionInfoChanged();
-    if (this.prplAccount) {
-      this.prplAccount.setInt(aName, aVal);
-    }
-    SavePrefTimer.initTimer();
-  },
-  setString(aName, aVal) {
-    this.prefBranch.setStringPref(kAccountOptionPrefPrefix + aName, aVal);
-    this._connectionInfoChanged();
-    if (this.prplAccount) {
-      this.prplAccount.setString(aName, aVal);
-    }
-    SavePrefTimer.initTimer();
-  },
-  save() {
-    SavePrefTimer.saveNow();
-  },
-
-  get HTMLEnabled() {
-    return this._ensurePrplAccount.HTMLEnabled;
-  },
-  get HTMLEscapePlainText() {
-    return this._ensurePrplAccount.HTMLEscapePlainText;
-  },
-  get noBackgroundColors() {
-    return this._ensurePrplAccount.noBackgroundColors;
-  },
-  get autoResponses() {
-    return this._ensurePrplAccount.autoResponses;
-  },
-  get singleFormatting() {
-    return this._ensurePrplAccount.singleFormatting;
-  },
-  get noFontSizes() {
-    return this._ensurePrplAccount.noFontSizes;
-  },
-  get noUrlDesc() {
-    return this._ensurePrplAccount.noUrlDesc;
-  },
-  get noImages() {
-    return this._ensurePrplAccount.noImages;
-  },
-};
-
-var gAccountsService = null;
-
-function AccountsService() {}
-AccountsService.prototype = {
-  initAccounts() {
-    this._initAutoLoginStatus();
-    this._accounts = [];
-    this._accountsById = {};
-    gAccountsService = this;
-    gConvertingOldPasswords = Services.prefs.getBoolPref(
-      kPrefConvertOldPasswords
-    );
-    let accountList = this._accountList;
-    for (let account of accountList ? accountList.split(",") : []) {
-      try {
-        account.trim();
-        if (!account) {
-          throw Cr.NS_ERROR_INVALID_ARG;
-        }
-        new imAccount(account);
-      } catch (e) {
-        Cu.reportError(e);
-        dump(e + " " + e.toSource() + "\n");
-      }
-    }
-    // If the user has canceled a master password prompt, we haven't
-    // been able to save any password, so the old password conversion
-    // still needs to happen.
-    if (gConvertingOldPasswords && !gUserCanceledMasterPasswordPrompt) {
-      Services.prefs.setBoolPref(kPrefConvertOldPasswords, false);
-    }
-
-    this._prefObserver = this.observe.bind(this);
-    Services.prefs.addObserver(kPrefMessengerAccounts, this._prefObserver);
-  },
-
-  _observingAccountListChange: true,
-  _prefObserver: null,
-  observe(aSubject, aTopic, aData) {
-    if (
-      aTopic != "nsPref:changed" ||
-      aData != kPrefMessengerAccounts ||
-      !this._observingAccountListChange
-    ) {
-      return;
-    }
-
-    this._accounts = this._accountList
-      .split(",")
-      .map(account => account.trim())
-      .filter(k => k.startsWith(kAccountKeyPrefix))
-      .map(k => parseInt(k.substr(kAccountKeyPrefix.length)))
-      .map(this.getAccountByNumericId, this)
-      .filter(a => a);
-
-    Services.obs.notifyObservers(this, "account-list-updated");
-  },
-
-  get _accountList() {
-    return Services.prefs.getCharPref(kPrefMessengerAccounts);
-  },
-  set _accountList(aNewList) {
-    this._observingAccountListChange = false;
-    Services.prefs.setCharPref(kPrefMessengerAccounts, aNewList);
-    delete this._observingAccountListChange;
-  },
-
-  unInitAccounts() {
-    for (let account of this._accounts) {
-      account.unInit();
-    }
-    gAccountsService = null;
-    delete this._accounts;
-    delete this._accountsById;
-    Services.prefs.removeObserver(kPrefMessengerAccounts, this._prefObserver);
-    delete this._prefObserver;
-  },
-
-  autoLoginStatus: Ci.imIAccountsService.AUTOLOGIN_ENABLED,
-  _initAutoLoginStatus() {
-    /* If auto-login is already disabled, do nothing */
-    if (this.autoLoginStatus != Ci.imIAccountsService.AUTOLOGIN_ENABLED) {
-      return;
-    }
-
-    let prefs = Services.prefs;
-    if (!prefs.getIntPref("messenger.startup.action")) {
-      // the value 0 means that we start without connecting the accounts
-      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_USER_DISABLED;
-      return;
-    }
-
-    /* Disable auto-login if we are running in safe mode */
-    if (Services.appinfo.inSafeMode) {
-      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_SAFE_MODE;
-      return;
-    }
-
-    /* Check if we crashed at the last startup during autologin */
-    let autoLoginPending;
-    if (
-      prefs.getPrefType(kPrefAutologinPending) == prefs.PREF_INVALID ||
-      !(autoLoginPending = prefs.getIntPref(kPrefAutologinPending))
-    ) {
-      // if the pref isn't set, then we haven't crashed: keep autologin enabled
-      return;
-    }
-
-    // Last autologin hasn't finished properly.
-    // For now, assume it's because of a crash.
-    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_CRASH;
-    prefs.deleteBranch(kPrefAutologinPending);
-
-    // If the crash reporter isn't built, we can't know anything more.
-    if (!("nsICrashReporter" in Ci)) {
-      return;
-    }
-
-    try {
-      // Try to get more info with breakpad
-      let lastCrashTime = 0;
-
-      /* Locate the LastCrash file */
-      let lastCrash = Services.dirsvc.get("UAppData", Ci.nsIFile);
-      lastCrash.append("Crash Reports");
-      lastCrash.append("LastCrash");
-      if (lastCrash.exists()) {
-        /* Ok, the file exists, now let's try to read it */
-        let is = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
-          Ci.nsIFileInputStream
-        );
-        let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
-          Ci.nsIScriptableInputStream
-        );
-        is.init(lastCrash, -1, 0, 0);
-        sis.init(sis);
-
-        lastCrashTime = parseInt(sis.read(lastCrash.fileSize));
-
-        sis.close();
-      }
-      // The file not existing is totally acceptable, it just means that
-      // either we never crashed or breakpad is not enabled.
-      // In this case, lastCrashTime will keep its 0 initialization value.
-
-      /* dump("autoLoginPending = " + autoLoginPending +
-              ", lastCrash = " + lastCrashTime +
-              ", difference = " + lastCrashTime - autoLoginPending + "\n");*/
-
-      if (lastCrashTime < autoLoginPending) {
-        // the last crash caught by breakpad is older than our last autologin
-        // attempt.
-        // If breakpad is currently enabled, we can be confident that
-        // autologin was interrupted for an exterior reason
-        // (application killed by the user, power outage, ...)
-        try {
-          Services.appinfo
-            .QueryInterface(Ci.nsICrashReporter)
-            .annotateCrashReport("=", "");
-        } catch (e) {
-          // This should fail with NS_ERROR_INVALID_ARG if breakpad is enabled,
-          // and NS_ERROR_NOT_INITIALIZED if it is not.
-          if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) {
-            this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
-          }
-        }
-      }
-    } catch (e) {
-      // if we failed to get the last crash time, then keep the
-      // AUTOLOGIN_CRASH value in mAutoLoginStatus and return.
-    }
-  },
-
-  processAutoLogin() {
-    if (!this._accounts) {
-      // if we're already shutting down
-      return;
-    }
-
-    for (let account of this._accounts) {
-      account.checkAutoLogin();
-    }
-
-    // Make sure autologin is now enabled, so that we don't display a
-    // message stating that it is disabled and asking the user if it
-    // should be processed now.
-    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
-
-    // Notify observers so that any message stating that autologin is
-    // disabled can be removed
-    Services.obs.notifyObservers(this, "autologin-processed");
-  },
-
-  _checkingIfPasswordStillMissing: false,
-  _checkIfPasswordStillMissing() {
-    // Avoid recursion.
-    if (this._checkingIfPasswordStillMissing) {
-      return;
-    }
-
-    this._checkingIfPasswordStillMissing = true;
-    for (let account of this._accounts) {
-      account._checkIfPasswordStillMissing();
-    }
-    delete this._checkingIfPasswordStillMissing;
-  },
-
-  getAccountById(aAccountId) {
-    if (!aAccountId.startsWith(kAccountKeyPrefix)) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    let id = parseInt(aAccountId.substr(kAccountKeyPrefix.length));
-    return this.getAccountByNumericId(id);
-  },
-
-  _keepAccount(aAccount) {
-    this._accounts.push(aAccount);
-    this._accountsById[aAccount.numericId] = aAccount;
-  },
-  getAccountByNumericId(aAccountId) {
-    return this._accountsById[aAccountId];
-  },
-  getAccounts() {
-    return new nsSimpleEnumerator(this._accounts);
-  },
-
-  createAccount(aName, aPrpl) {
-    // Ensure an account with the same name and protocol doesn't already exist.
-    let prpl = Services.core.getProtocolById(aPrpl);
-    if (!prpl) {
-      throw Cr.NS_ERROR_UNEXPECTED;
-    }
-    if (prpl.accountExists(aName)) {
-      Cu.reportError("Attempted to create a duplicate account!");
-      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
-    }
-
-    /* First get a unique id for the new account. */
-    let id;
-    for (id = 1; ; ++id) {
-      if (this._accountsById.hasOwnProperty(id)) {
-        continue;
-      }
-
-      /* id isn't used by a known account, double check it isn't
-       already used in the sqlite database. This should never
-       happen, except if we have a corrupted profile. */
-      if (!Services.contacts.accountIdExists(id)) {
-        break;
-      }
-      Services.console.logStringMessage(
-        "No account " +
-          id +
-          " but there is some data in the buddy list for an account with this number. Your profile may be corrupted."
-      );
-    }
-
-    /* Actually create the new account. */
-    let key = kAccountKeyPrefix + id;
-    let account = new imAccount(key, aName, aPrpl);
-
-    /* Save the account list pref. */
-    let list = this._accountList;
-    this._accountList = list ? list + "," + key : key;
-
-    Services.obs.notifyObservers(account, "account-added");
-    return account;
-  },
-
-  deleteAccount(aAccountId) {
-    let account = this.getAccountById(aAccountId);
-    if (!account) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    let index = this._accounts.indexOf(account);
-    if (index == -1) {
-      throw Cr.NS_ERROR_UNEXPECTED;
-    }
-
-    let id = account.numericId;
-    account.remove();
-    this._accounts.splice(index, 1);
-    delete this._accountsById[id];
-    Services.obs.notifyObservers(account, "account-removed");
-
-    /* Update the account list pref. */
-    let list = this._accountList;
-    this._accountList = list
-      .split(",")
-      .filter(k => k.trim() != aAccountId)
-      .join(",");
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imIAccountsService]),
-};
deleted file mode 100644
--- a/chat/components/src/IMCommands.jsm
+++ /dev/null
@@ -1,289 +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 EXPORTED_SYMBOLS = ["IMCommands"];
-
-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")
-);
-
-function IMCommands() {}
-IMCommands.prototype = {
-  initCommands() {
-    this._commands = {};
-    // The say command is directly implemented in the UI layer, but has a
-    // dummy command registered here so it shows up as a command (e.g. when
-    // using the /help command).
-    this.registerCommand({
-      name: "say",
-      get helpString() {
-        return _("sayHelpString");
-      },
-      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
-      priority: Ci.imICommand.CMD_PRIORITY_HIGH,
-      run(aMsg, aConv) {
-        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-      },
-    });
-
-    this.registerCommand({
-      name: "raw",
-      get helpString() {
-        return _("rawHelpString");
-      },
-      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
-      priority: Ci.imICommand.CMD_PRIORITY_DEFAULT,
-      run(aMsg, aConv) {
-        let conv = Services.conversations.getUIConversation(aConv);
-        if (!conv) {
-          return false;
-        }
-        conv.sendMsg(aMsg);
-        return true;
-      },
-    });
-
-    this.registerCommand({
-      // Reference the command service so we can use the internal properties
-      // directly.
-      cmdSrv: this,
-
-      name: "help",
-      get helpString() {
-        return _("helpHelpString");
-      },
-      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
-      priority: Ci.imICommand.CMD_PRIORITY_DEFAULT,
-      run(aMsg, aConv) {
-        aMsg = aMsg.trim();
-        let conv = Services.conversations.getUIConversation(aConv);
-        if (!conv) {
-          return false;
-        }
-
-        // Handle when no command is given, list all possible commands that are
-        // available for this conversation (alphabetically).
-        if (!aMsg) {
-          let commands = this.cmdSrv.listCommandsForConversation(aConv);
-          if (!commands.length) {
-            return false;
-          }
-
-          // Concatenate the command names (separated by a comma and space).
-          let cmds = commands
-            .map(aCmd => aCmd.name)
-            .sort()
-            .join(", ");
-          let message = _("commands", cmds);
-
-          // Display the message
-          conv.systemMessage(message);
-          return true;
-        }
-
-        // A command name was given, find the commands that match.
-        let cmdArray = this.cmdSrv._findCommands(aConv, aMsg);
-
-        if (!cmdArray.length) {
-          // No command that matches.
-          let message = _("noCommand", aMsg);
-          conv.systemMessage(message);
-          return true;
-        }
-
-        // Only show the help for the one of the highest priority.
-        let cmd = cmdArray[0];
-
-        let text = cmd.helpString;
-        if (!text) {
-          text = _("noHelp", cmd.name);
-        }
-
-        // Display the message.
-        conv.systemMessage(text);
-        return true;
-      },
-    });
-
-    // Status commands
-    let status = {
-      back: "AVAILABLE",
-      away: "AWAY",
-      busy: "UNAVAILABLE",
-      dnd: "UNAVAILABLE",
-      offline: "OFFLINE",
-    };
-    for (let cmd in status) {
-      let statusValue = Ci.imIStatusInfo["STATUS_" + status[cmd]];
-      this.registerCommand({
-        name: cmd,
-        get helpString() {
-          return _("statusCommand", this.name, _(this.name));
-        },
-        usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
-        priority: Ci.imICommand.CMD_PRIORITY_HIGH,
-        run(aMsg) {
-          Services.core.globalUserStatus.setStatus(statusValue, aMsg);
-          return true;
-        },
-      });
-    }
-  },
-  unInitCommands() {
-    delete this._commands;
-  },
-
-  registerCommand(aCommand, aPrplId) {
-    let name = aCommand.name;
-    if (!name) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    if (!this._commands.hasOwnProperty(name)) {
-      this._commands[name] = {};
-    }
-    this._commands[name][aPrplId || ""] = aCommand;
-  },
-  unregisterCommand(aCommandName, aPrplId) {
-    if (this._commands.hasOwnProperty(aCommandName)) {
-      let prplId = aPrplId || "";
-      let commands = this._commands[aCommandName];
-      if (commands.hasOwnProperty(prplId)) {
-        delete commands[prplId];
-      }
-      if (!Object.keys(commands).length) {
-        delete this._commands[aCommandName];
-      }
-    }
-  },
-  listCommandsForConversation(aConversation) {
-    let result = [];
-    let prplId = aConversation && aConversation.account.protocol.id;
-    for (let name in this._commands) {
-      let commands = this._commands[name];
-      if (commands.hasOwnProperty("")) {
-        result.push(commands[""]);
-      }
-      if (prplId && commands.hasOwnProperty(prplId)) {
-        result.push(commands[prplId]);
-      }
-    }
-    if (aConversation) {
-      result = result.filter(this._usageContextFilter(aConversation));
-    }
-    return result;
-  },
-  // List only the commands for a protocol (excluding the global commands).
-  listCommandsForProtocol(aPrplId) {
-    if (!aPrplId) {
-      throw new Error("You must provide a prpl ID.");
-    }
-
-    let result = [];
-    for (let name in this._commands) {
-      let commands = this._commands[name];
-      if (commands.hasOwnProperty(aPrplId)) {
-        result.push(commands[aPrplId]);
-      }
-    }
-    return result;
-  },
-  _usageContextFilter(aConversation) {
-    let usageContext =
-      Ci.imICommand["CMD_CONTEXT_" + (aConversation.isChat ? "CHAT" : "IM")];
-    return c => c.usageContext & usageContext;
-  },
-  _findCommands(aConversation, aName) {
-    let prplId = null;
-    if (aConversation) {
-      let account = aConversation.account;
-      if (account.connected) {
-        prplId = account.protocol.id;
-      }
-    }
-
-    let commandNames;
-    // If there is an exact match for the given command name,
-    // don't look at any other commands.
-    if (this._commands.hasOwnProperty(aName)) {
-      commandNames = [aName];
-    } else {
-      // Otherwise, check if there is a partial match.
-      commandNames = Object.keys(this._commands).filter(command =>
-        command.startsWith(aName)
-      );
-    }
-
-    // If a single full command name matches the given (partial)
-    // command name, return the results for that command name. Otherwise,
-    // return an empty array (don't assume a certain command).
-    let cmdArray = [];
-    for (let commandName of commandNames) {
-      let matches = [];
-
-      // Get the 2 possible commands (the global and the proto specific).
-      let commands = this._commands[commandName];
-      if (commands.hasOwnProperty("")) {
-        matches.push(commands[""]);
-      }
-      if (prplId && commands.hasOwnProperty(prplId)) {
-        matches.push(commands[prplId]);
-      }
-
-      // Remove the commands that can't apply in this context.
-      if (aConversation) {
-        matches = matches.filter(this._usageContextFilter(aConversation));
-      }
-
-      if (!matches.length) {
-        continue;
-      }
-
-      // If we have found a second matching command name, return the empty array.
-      if (cmdArray.length) {
-        return [];
-      }
-
-      cmdArray = matches;
-    }
-
-    // Sort the matching commands by priority before returning the array.
-    return cmdArray.sort((a, b) => b.priority - a.priority);
-  },
-  executeCommand(aMessage, aConversation, aReturnedConv) {
-    if (!aMessage) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    let matchResult;
-    if (
-      aMessage[0] != "/" ||
-      !(matchResult = /^\/([a-z0-9]+)(?: |$)([\s\S]*)/.exec(aMessage))
-    ) {
-      return false;
-    }
-
-    let [, name, args] = matchResult;
-
-    let cmdArray = this._findCommands(aConversation, name);
-    if (!cmdArray.length) {
-      return false;
-    }
-
-    // cmdArray contains commands sorted by priority, attempt to apply
-    // them in order until one succeeds.
-    if (!cmdArray.some(aCmd => aCmd.run(args, aConversation, aReturnedConv))) {
-      // If they all failed, print help message.
-      this.executeCommand("/help " + name, aConversation);
-    }
-    return true;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imICommandsService]),
-};
deleted file mode 100644
--- a/chat/components/src/IMContacts.jsm
+++ /dev/null
@@ -1,1801 +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 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")
-);
-
-var gDBConnection = null;
-
-function executeAsyncThenFinalize(statement) {
-  statement.executeAsync();
-  statement.finalize();
-}
-
-function getDBConnection() {
-  const NS_APP_USER_PROFILE_50_DIR = "ProfD";
-  let dbFile = Services.dirsvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
-  dbFile.append("blist.sqlite");
-
-  let conn = Services.storage.openDatabase(dbFile);
-  if (!conn.connectionReady) {
-    throw Cr.NS_ERROR_UNEXPECTED;
-  }
-
-  // Grow blist db in 512KB increments.
-  try {
-    conn.setGrowthIncrement(512 * 1024, "");
-  } catch (e) {
-    if (e.result == Cr.NS_ERROR_FILE_TOO_BIG) {
-      Services.console.logStringMessage(
-        "Not setting growth increment on " +
-          "blist.sqlite because the available " +
-          "disk space is limited"
-      );
-    } else {
-      throw e;
-    }
-  }
-
-  // Create tables and indexes.
-  [
-    "CREATE TABLE IF NOT EXISTS accounts (" +
-      "id INTEGER PRIMARY KEY, " +
-      "name VARCHAR, " +
-      "prpl VARCHAR)",
-
-    "CREATE TABLE IF NOT EXISTS contacts (" +
-      "id INTEGER PRIMARY KEY, " +
-      "firstname VARCHAR, " +
-      "lastname VARCHAR, " +
-      "alias VARCHAR)",
-
-    "CREATE TABLE IF NOT EXISTS buddies (" +
-      "id INTEGER PRIMARY KEY, " +
-      "key VARCHAR NOT NULL, " +
-      "name VARCHAR NOT NULL, " +
-      "srv_alias VARCHAR, " +
-      "position INTEGER, " +
-      "icon BLOB, " +
-      "contact_id INTEGER)",
-    "CREATE INDEX IF NOT EXISTS buddies_contactindex " +
-      "ON buddies (contact_id)",
-
-    "CREATE TABLE IF NOT EXISTS tags (" +
-      "id INTEGER PRIMARY KEY, " +
-      "name VARCHAR UNIQUE NOT NULL, " +
-      "position INTEGER)",
-
-    "CREATE TABLE IF NOT EXISTS contact_tag (" +
-      "contact_id INTEGER NOT NULL, " +
-      "tag_id INTEGER NOT NULL)",
-    "CREATE INDEX IF NOT EXISTS contact_tag_contactindex " +
-      "ON contact_tag (contact_id)",
-    "CREATE INDEX IF NOT EXISTS contact_tag_tagindex " +
-      "ON contact_tag (tag_id)",
-
-    "CREATE TABLE IF NOT EXISTS account_buddy (" +
-      "account_id INTEGER NOT NULL, " +
-      "buddy_id INTEGER NOT NULL, " +
-      "status VARCHAR, " +
-      "tag_id INTEGER)",
-    "CREATE INDEX IF NOT EXISTS account_buddy_accountindex " +
-      "ON account_buddy (account_id)",
-    "CREATE INDEX IF NOT EXISTS account_buddy_buddyindex " +
-      "ON account_buddy (buddy_id)",
-  ].forEach(conn.executeSimpleSQL);
-
-  return conn;
-}
-
-// Wrap all the usage of DBConn inside a transaction that will be
-// committed automatically at the end of the event loop spin so that
-// we flush buddy list data to disk only once per event loop spin.
-var gDBConnWithPendingTransaction = null;
-Object.defineProperty(this, "DBConn", {
-  configurable: true,
-  enumerable: true,
-
-  get() {
-    if (gDBConnWithPendingTransaction) {
-      return gDBConnWithPendingTransaction;
-    }
-
-    if (!gDBConnection) {
-      gDBConnection = getDBConnection();
-      Services.obs.addObserver(function dbClose(aSubject, aTopic, aData) {
-        Services.obs.removeObserver(dbClose, aTopic);
-        if (gDBConnection) {
-          gDBConnection.asyncClose();
-          gDBConnection = null;
-        }
-      }, "profile-before-change");
-    }
-    gDBConnWithPendingTransaction = gDBConnection;
-    gDBConnection.beginTransaction();
-    executeSoon(function() {
-      gDBConnWithPendingTransaction.commitTransaction();
-      gDBConnWithPendingTransaction = null;
-    });
-    return gDBConnection;
-  },
-});
-
-function TagsService() {}
-TagsService.prototype = {
-  get wrappedJSObject() {
-    return this;
-  },
-  get defaultTag() {
-    return this.createTag(_("defaultGroup"));
-  },
-  createTag(aName) {
-    // If the tag already exists, we don't want to create a duplicate.
-    let tag = this.getTagByName(aName);
-    if (tag) {
-      return tag;
-    }
-
-    let statement = DBConn.createStatement(
-      "INSERT INTO tags (name, position) VALUES(:name, 0)"
-    );
-    try {
-      statement.params.name = aName;
-      statement.executeStep();
-    } finally {
-      statement.finalize();
-    }
-
-    tag = new Tag(DBConn.lastInsertRowID, aName);
-    Tags.push(tag);
-    return tag;
-  },
-  // Get an existing tag by (numeric) id. Returns null if not found.
-  getTagById: aId => TagsById[aId],
-  // Get an existing tag by name (will do an SQL query). Returns null
-  // if not found.
-  getTagByName(aName) {
-    let statement = DBConn.createStatement(
-      "SELECT id FROM tags where name = :name"
-    );
-    statement.params.name = aName;
-    try {
-      if (!statement.executeStep()) {
-        return null;
-      }
-      return this.getTagById(statement.row.id);
-    } finally {
-      statement.finalize();
-    }
-  },
-  // Get an array of all existing tags.
-  getTags() {
-    if (Tags.length) {
-      Tags.sort((a, b) =>
-        a.name.toLowerCase().localeCompare(b.name.toLowerCase())
-      );
-    } else {
-      this.defaultTag;
-    }
-
-    return Tags;
-  },
-
-  isTagHidden: aTag => aTag.id in otherContactsTag._hiddenTags,
-  hideTag(aTag) {
-    otherContactsTag.hideTag(aTag);
-  },
-  showTag(aTag) {
-    otherContactsTag.showTag(aTag);
-  },
-  get otherContactsTag() {
-    otherContactsTag._initContacts();
-    return otherContactsTag;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imITagsService]),
-};
-
-// TODO move into the tagsService
-var Tags = [];
-var TagsById = {};
-
-function Tag(aId, aName) {
-  this._id = aId;
-  this._name = aName;
-  this._contacts = [];
-  this._observers = [];
-
-  TagsById[this.id] = this;
-}
-Tag.prototype = {
-  __proto__: ClassInfo("imITag", "Tag"),
-  get id() {
-    return this._id;
-  },
-  get name() {
-    return this._name;
-  },
-  set name(aNewName) {
-    let statement = DBConn.createStatement(
-      "UPDATE tags SET name = :name WHERE id = :id"
-    );
-    try {
-      statement.params.name = aNewName;
-      statement.params.id = this._id;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-
-    // FIXME move the account buddies if some use this tag as their group
-    return aNewName;
-  },
-  getContacts() {
-    return this._contacts.filter(c => !c._empty);
-  },
-  _addContact(aContact) {
-    this._contacts.push(aContact);
-  },
-  _removeContact(aContact) {
-    let index = this._contacts.indexOf(aContact);
-    if (index != -1) {
-      this._contacts.splice(index, 1);
-    }
-  },
-
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  notifyObservers(aSubject, aTopic, aData) {
-    for (let observer of this._observers) {
-      observer.observe(aSubject, aTopic, aData);
-    }
-  },
-};
-
-var otherContactsTag = {
-  __proto__: ClassInfo(["nsIObserver", "imITag"], "Other Contacts Tag"),
-  hiddenTagsPref: "messenger.buddies.hiddenTags",
-  _hiddenTags: {},
-  _contactsInitialized: false,
-  _saveHiddenTagsPref() {
-    Services.prefs.setCharPref(
-      this.hiddenTagsPref,
-      Object.keys(this._hiddenTags).join(",")
-    );
-  },
-  showTag(aTag) {
-    let id = aTag.id;
-    delete this._hiddenTags[id];
-    let contacts = Object.keys(this._contacts).map(id => this._contacts[id]);
-    for (let contact of contacts) {
-      if (contact.getTags().some(t => t.id == id)) {
-        this._removeContact(contact);
-      }
-    }
-
-    aTag.notifyObservers(aTag, "tag-shown");
-    Services.obs.notifyObservers(aTag, "tag-shown");
-    this._saveHiddenTagsPref();
-  },
-  hideTag(aTag) {
-    if (aTag.id < 0 || aTag.id in otherContactsTag._hiddenTags) {
-      return;
-    }
-
-    this._hiddenTags[aTag.id] = aTag;
-    if (this._contactsInitialized) {
-      this._hideTag(aTag);
-    }
-
-    aTag.notifyObservers(aTag, "tag-hidden");
-    Services.obs.notifyObservers(aTag, "tag-hidden");
-    this._saveHiddenTagsPref();
-  },
-  _hideTag(aTag) {
-    for (let contact of aTag.getContacts()) {
-      if (
-        !(contact.id in this._contacts) &&
-        contact.getTags().every(t => t.id in this._hiddenTags)
-      ) {
-        this._addContact(contact);
-      }
-    }
-  },
-  observe(aSubject, aTopic, aData) {
-    aSubject.QueryInterface(Ci.imIContact);
-    if (aTopic == "contact-tag-removed" || aTopic == "contact-added") {
-      if (
-        !(aSubject.id in this._contacts) &&
-        !(parseInt(aData) in this._hiddenTags) &&
-        aSubject.getTags().every(t => t.id in this._hiddenTags)
-      ) {
-        this._addContact(aSubject);
-      }
-    } else if (
-      aSubject.id in this._contacts &&
-      (aTopic == "contact-removed" ||
-        (aTopic == "contact-tag-added" &&
-          !(parseInt(aData) in this._hiddenTags)))
-    ) {
-      this._removeContact(aSubject);
-    }
-  },
-
-  _initHiddenTags() {
-    let pref = Services.prefs.getCharPref(this.hiddenTagsPref);
-    if (!pref) {
-      return;
-    }
-    for (let tagId of pref.split(",")) {
-      this._hiddenTags[tagId] = TagsById[tagId];
-    }
-  },
-  _initContacts() {
-    if (this._contactsInitialized) {
-      return;
-    }
-    this._observers = [];
-    this._observer = {
-      self: this,
-      observe(aSubject, aTopic, aData) {
-        if (aTopic == "contact-moved-in" && !(aSubject instanceof Contact)) {
-          return;
-        }
-
-        this.self.notifyObservers(aSubject, aTopic, aData);
-      },
-    };
-    this._contacts = {};
-    this._contactsInitialized = true;
-    for (let id in this._hiddenTags) {
-      let tag = this._hiddenTags[id];
-      this._hideTag(tag);
-    }
-    Services.obs.addObserver(this, "contact-tag-added");
-    Services.obs.addObserver(this, "contact-tag-removed");
-    Services.obs.addObserver(this, "contact-added");
-    Services.obs.addObserver(this, "contact-removed");
-  },
-
-  // imITag implementation
-  get id() {
-    return -1;
-  },
-  get name() {
-    return "__others__";
-  },
-  set name(aNewName) {
-    throw Cr.NS_ERROR_NOT_AVAILABLE;
-  },
-  getContacts() {
-    return Object.keys(this._contacts).map(id => this._contacts[id]);
-  },
-  _addContact(aContact) {
-    this._contacts[aContact.id] = aContact;
-    this.notifyObservers(aContact, "contact-moved-in");
-    for (let observer of ContactsById[aContact.id]._observers) {
-      observer.observe(this, "contact-moved-in", null);
-    }
-    aContact.addObserver(this._observer);
-  },
-  _removeContact(aContact) {
-    delete this._contacts[aContact.id];
-    aContact.removeObserver(this._observer);
-    this.notifyObservers(aContact, "contact-moved-out");
-    for (let observer of ContactsById[aContact.id]._observers) {
-      observer.observe(this, "contact-moved-out", null);
-    }
-  },
-
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  notifyObservers(aSubject, aTopic, aData) {
-    for (let observer of this._observers) {
-      observer.observe(aSubject, aTopic, aData);
-    }
-  },
-};
-
-var ContactsById = {};
-var LastDummyContactId = 0;
-function Contact(aId, aAlias) {
-  // Assign a negative id to dummy contacts that have a single buddy
-  this._id = aId || --LastDummyContactId;
-  this._alias = aAlias;
-  this._tags = [];
-  this._buddies = [];
-  this._observers = [];
-
-  ContactsById[this._id] = this;
-}
-Contact.prototype = {
-  __proto__: ClassInfo("imIContact", "Contact"),
-  _id: 0,
-  get id() {
-    return this._id;
-  },
-  get alias() {
-    return this._alias;
-  },
-  set alias(aNewAlias) {
-    this._ensureNotDummy();
-
-    let statement = DBConn.createStatement(
-      "UPDATE contacts SET alias = :alias WHERE id = :id"
-    );
-    statement.params.alias = aNewAlias;
-    statement.params.id = this._id;
-    executeAsyncThenFinalize(statement);
-
-    let oldDisplayName = this.displayName;
-    this._alias = aNewAlias;
-    this._notifyObservers("display-name-changed", oldDisplayName);
-    for (let buddy of this._buddies) {
-      for (let accountBuddy of buddy._accounts) {
-        accountBuddy.serverAlias = aNewAlias;
-      }
-    }
-    return aNewAlias;
-  },
-  _ensureNotDummy() {
-    if (this._id >= 0) {
-      return;
-    }
-
-    // Create a real contact for this dummy contact
-    let statement = DBConn.createStatement(
-      "INSERT INTO contacts DEFAULT VALUES"
-    );
-    try {
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-    delete ContactsById[this._id];
-    let oldId = this._id;
-    this._id = DBConn.lastInsertRowID;
-    ContactsById[this._id] = this;
-    this._notifyObservers("no-longer-dummy", oldId.toString());
-    // Update the contact_id for the single existing buddy of this contact
-    statement = DBConn.createStatement(
-      "UPDATE buddies SET contact_id = :id WHERE id = :buddy_id"
-    );
-    statement.params.id = this._id;
-    statement.params.buddy_id = this._buddies[0].id;
-    executeAsyncThenFinalize(statement);
-  },
-
-  getTags() {
-    return this._tags;
-  },
-  addTag(aTag, aInherited) {
-    if (this.hasTag(aTag)) {
-      return;
-    }
-
-    if (!aInherited) {
-      this._ensureNotDummy();
-      let statement = DBConn.createStatement(
-        "INSERT INTO contact_tag (contact_id, tag_id) " +
-          "VALUES(:contactId, :tagId)"
-      );
-      statement.params.contactId = this.id;
-      statement.params.tagId = aTag.id;
-      executeAsyncThenFinalize(statement);
-    }
-
-    aTag = TagsById[aTag.id];
-    this._tags.push(aTag);
-    aTag._addContact(this);
-
-    aTag.notifyObservers(this, "contact-moved-in");
-    for (let observer of this._observers) {
-      observer.observe(aTag, "contact-moved-in", null);
-    }
-    Services.obs.notifyObservers(this, "contact-tag-added", aTag.id);
-  },
-  /* Remove a tag from the local tags of the contact. */
-  _removeTag(aTag) {
-    if (!this.hasTag(aTag) || this._isTagInherited(aTag)) {
-      return;
-    }
-
-    this._removeContactTagRow(aTag);
-
-    this._tags = this._tags.filter(tag => tag.id != aTag.id);
-    aTag = TagsById[aTag.id];
-    aTag._removeContact(this);
-
-    aTag.notifyObservers(this, "contact-moved-out");
-    for (let observer of this._observers) {
-      observer.observe(aTag, "contact-moved-out", null);
-    }
-    Services.obs.notifyObservers(this, "contact-tag-removed", aTag.id);
-  },
-  _removeContactTagRow(aTag) {
-    let statement = DBConn.createStatement(
-      "DELETE FROM contact_tag " +
-        "WHERE contact_id = :contactId " +
-        "AND tag_id = :tagId"
-    );
-    statement.params.contactId = this.id;
-    statement.params.tagId = aTag.id;
-    executeAsyncThenFinalize(statement);
-  },
-  hasTag(aTag) {
-    return this._tags.some(t => t.id == aTag.id);
-  },
-  _massMove: false,
-  removeTag(aTag) {
-    if (!this.hasTag(aTag)) {
-      throw new Error(
-        "Attempting to remove a tag that the contact doesn't have"
-      );
-    }
-    if (this._tags.length == 1) {
-      throw new Error("Attempting to remove the last tag of a contact");
-    }
-
-    this._massMove = true;
-    let hasTag = this.hasTag.bind(this);
-    let newTag = this._tags[this._tags[0].id != aTag.id ? 0 : 1];
-    let moved = false;
-    this._buddies.forEach(function(aBuddy) {
-      aBuddy._accounts.forEach(function(aAccountBuddy) {
-        if (aAccountBuddy.tag.id == aTag.id) {
-          if (
-            aBuddy._accounts.some(
-              ab =>
-                ab.account.numericId == aAccountBuddy.account.numericId &&
-                ab.tag.id != aTag.id &&
-                hasTag(ab.tag)
-            )
-          ) {
-            // A buddy that already has an accountBuddy of the same
-            // account with another tag of the contact shouldn't be
-            // moved to newTag, just remove the accountBuddy
-            // associated to the tag we are removing.
-            aAccountBuddy.remove();
-            moved = true;
-          } else {
-            try {
-              aAccountBuddy.tag = newTag;
-              moved = true;
-            } catch (e) {
-              // Ignore failures. Some protocol plugins may not implement this.
-            }
-          }
-        }
-      });
-    });
-    this._massMove = false;
-    if (moved) {
-      this._moved(aTag, newTag);
-    } else {
-      // If we are here, the old tag is not inherited from a buddy, so
-      // just remove the local tag.
-      this._removeTag(aTag);
-    }
-  },
-  _isTagInherited(aTag) {
-    for (let buddy of this._buddies) {
-      for (let accountBuddy of buddy._accounts) {
-        if (accountBuddy.tag.id == aTag.id) {
-          return true;
-        }
-      }
-    }
-    return false;
-  },
-  _moved(aOldTag, aNewTag) {
-    if (this._massMove) {
-      return;
-    }
-
-    // Avoid xpconnect wrappers.
-    aNewTag = aNewTag && TagsById[aNewTag.id];
-    aOldTag = aOldTag && TagsById[aOldTag.id];
-
-    // Decide what we need to do. Return early if nothing to do.
-    let shouldRemove =
-      aOldTag && this.hasTag(aOldTag) && !this._isTagInherited(aOldTag);
-    let shouldAdd =
-      aNewTag && !this.hasTag(aNewTag) && this._isTagInherited(aNewTag);
-    if (!shouldRemove && !shouldAdd) {
-      return;
-    }
-
-    // Apply the changes.
-    let tags = this._tags;
-    if (shouldRemove) {
-      tags = tags.filter(aTag => aTag.id != aOldTag.id);
-      aOldTag._removeContact(this);
-    }
-    if (shouldAdd) {
-      tags.push(aNewTag);
-      aNewTag._addContact(this);
-    }
-    this._tags = tags;
-
-    // Finally, notify of the changes.
-    if (shouldRemove) {
-      aOldTag.notifyObservers(this, "contact-moved-out");
-      for (let observer of this._observers) {
-        observer.observe(aOldTag, "contact-moved-out", null);
-      }
-      Services.obs.notifyObservers(this, "contact-tag-removed", aOldTag.id);
-    }
-    if (shouldAdd) {
-      aNewTag.notifyObservers(this, "contact-moved-in");
-      for (let observer of this._observers) {
-        observer.observe(aNewTag, "contact-moved-in", null);
-      }
-      Services.obs.notifyObservers(this, "contact-tag-added", aNewTag.id);
-    }
-    Services.obs.notifyObservers(this, "contact-moved");
-  },
-
-  getBuddies() {
-    return this._buddies;
-  },
-  get _empty() {
-    return this._buddies.length == 0 || this._buddies.every(b => b._empty);
-  },
-
-  mergeContact(aContact) {
-    // Avoid merging the contact with itself or merging into an
-    // already removed contact.
-    if (aContact.id == this.id || !(this.id in ContactsById)) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    this._ensureNotDummy();
-    let contact = ContactsById[aContact.id]; // remove XPConnect wrapper
-
-    // Copy all the contact-only tags first, otherwise they would be lost.
-    for (let tag of contact.getTags()) {
-      if (!contact._isTagInherited(tag)) {
-        this.addTag(tag);
-      }
-    }
-
-    // Adopt each buddy. Removing the last one will delete the contact.
-    for (let buddy of contact.getBuddies()) {
-      buddy.contact = this;
-    }
-    this._updatePreferredBuddy();
-  },
-  moveBuddyBefore(aBuddy, aBeforeBuddy) {
-    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
-    let oldPosition = this._buddies.indexOf(buddy);
-    if (oldPosition == -1) {
-      throw new Error("aBuddy isn't attached to this contact");
-    }
-
-    let newPosition = -1;
-    if (aBeforeBuddy) {
-      newPosition = this._buddies.indexOf(BuddiesById[aBeforeBuddy.id]);
-    }
-    if (newPosition == -1) {
-      newPosition = this._buddies.length - 1;
-    }
-
-    if (oldPosition == newPosition) {
-      return;
-    }
-
-    this._buddies.splice(oldPosition, 1);
-    this._buddies.splice(newPosition, 0, buddy);
-    this._updatePositions(
-      Math.min(oldPosition, newPosition),
-      Math.max(oldPosition, newPosition)
-    );
-    buddy._notifyObservers("position-changed", String(newPosition));
-    this._updatePreferredBuddy(buddy);
-  },
-  adoptBuddy(aBuddy) {
-    if (aBuddy.contact.id == this.id) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
-    buddy.contact = this;
-    this._updatePreferredBuddy(buddy);
-  },
-  _massRemove: false,
-  _removeBuddy(aBuddy) {
-    if (this._buddies.length == 1) {
-      if (this._id > 0) {
-        let statement = DBConn.createStatement(
-          "DELETE FROM contacts WHERE id = :id"
-        );
-        statement.params.id = this._id;
-        executeAsyncThenFinalize(statement);
-      }
-      this._notifyObservers("removed");
-      delete ContactsById[this._id];
-
-      for (let tag of this._tags) {
-        tag._removeContact(this);
-      }
-      let statement = DBConn.createStatement(
-        "DELETE FROM contact_tag WHERE contact_id = :id"
-      );
-      statement.params.id = this._id;
-      executeAsyncThenFinalize(statement);
-
-      delete this._tags;
-      delete this._buddies;
-      delete this._observers;
-    } else {
-      let index = this._buddies.indexOf(aBuddy);
-      if (index == -1) {
-        throw new Error("Removing an unknown buddy from contact " + this._id);
-      }
-
-      this._buddies = this._buddies.filter(b => b !== aBuddy);
-
-      // If we are actually removing the whole contact, don't bother updating
-      // the positions or the preferred buddy.
-      if (this._massRemove) {
-        return;
-      }
-
-      // No position to update if the removed buddy is at the last position.
-      if (index < this._buddies.length) {
-        this._updatePositions(index);
-      }
-
-      if (this._preferredBuddy.id == aBuddy.id) {
-        this._updatePreferredBuddy();
-      }
-    }
-  },
-  _updatePositions(aIndexBegin, aIndexEnd) {
-    if (aIndexEnd === undefined) {
-      aIndexEnd = this._buddies.length - 1;
-    }
-    if (aIndexBegin > aIndexEnd) {
-      throw new Error("_updatePositions: Invalid indexes");
-    }
-
-    let statement = DBConn.createStatement(
-      "UPDATE buddies SET position = :position WHERE id = :buddyId"
-    );
-    for (let i = aIndexBegin; i <= aIndexEnd; ++i) {
-      statement.params.position = i;
-      statement.params.buddyId = this._buddies[i].id;
-      statement.executeAsync();
-    }
-    statement.finalize();
-  },
-
-  detachBuddy(aBuddy) {
-    // Should return a new contact with the same list of tags.
-    let buddy = BuddiesById[aBuddy.id];
-    if (buddy.contact.id != this.id) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-    if (buddy.contact._buddies.length == 1) {
-      throw Cr.NS_ERROR_UNEXPECTED;
-    }
-
-    // Save the list of tags, it may be destoyed if the buddy was the last one.
-    let tags = buddy.contact.getTags();
-
-    // Create a new dummy contact and use it for the detached buddy.
-    buddy.contact = new Contact();
-    buddy.contact._notifyObservers("added");
-
-    // The first tag was inherited during the contact setter.
-    // This will copy the remaining tags.
-    for (let tag of tags) {
-      buddy.contact.addTag(tag);
-    }
-
-    return buddy.contact;
-  },
-  remove() {
-    this._massRemove = true;
-    for (let buddy of this._buddies) {
-      buddy.remove();
-    }
-  },
-
-  // imIStatusInfo implementation
-  _preferredBuddy: null,
-  get preferredBuddy() {
-    if (!this._preferredBuddy) {
-      this._updatePreferredBuddy();
-    }
-    return this._preferredBuddy;
-  },
-  set preferredBuddy(aBuddy) {
-    let shouldNotify = this._preferredBuddy != null;
-    let oldDisplayName =
-      this._preferredBuddy && this._preferredBuddy.displayName;
-    this._preferredBuddy = aBuddy;
-    if (shouldNotify) {
-      this._notifyObservers("preferred-buddy-changed");
-    }
-    if (oldDisplayName && this._preferredBuddy.displayName != oldDisplayName) {
-      this._notifyObservers("display-name-changed", oldDisplayName);
-    }
-    this._updateStatus();
-  },
-  // aBuddy indicate which buddy's availability has changed.
-  _updatePreferredBuddy(aBuddy) {
-    if (aBuddy) {
-      aBuddy = BuddiesById[aBuddy.id]; // remove potential XPConnect wrapper
-
-      if (!this._preferredBuddy) {
-        this.preferredBuddy = aBuddy;
-        return;
-      }
-
-      if (aBuddy.id == this._preferredBuddy.id) {
-        // The suggested buddy is already preferred, check if its
-        // availability has changed.
-        if (
-          aBuddy.statusType > this._statusType ||
-          (aBuddy.statusType == this._statusType &&
-            aBuddy.availabilityDetails >= this._availabilityDetails)
-        ) {
-          // keep the currently preferred buddy, only update the status.
-          this._updateStatus();
-          return;
-        }
-        // We aren't sure that the currently preferred buddy should
-        // still be preferred. Let's go through the list!
-      } else {
-        // The suggested buddy is not currently preferred. If it is
-        // more available or at a better position, prefer it!
-        if (
-          aBuddy.statusType > this._statusType ||
-          (aBuddy.statusType == this._statusType &&
-            (aBuddy.availabilityDetails > this._availabilityDetails ||
-              (aBuddy.availabilityDetails == this._availabilityDetails &&
-                this._buddies.indexOf(aBuddy) <
-                  this._buddies.indexOf(this.preferredBuddy))))
-        ) {
-          this.preferredBuddy = aBuddy;
-        }
-        return;
-      }
-    }
-
-    let preferred;
-    // |this._buddies| is ordered by user preference, so in case of
-    // equal availability, keep the current value of |preferred|.
-    for (let buddy of this._buddies) {
-      if (
-        !preferred ||
-        preferred.statusType < buddy.statusType ||
-        (preferred.statusType == buddy.statusType &&
-          preferred.availabilityDetails < buddy.availabilityDetails)
-      ) {
-        preferred = buddy;
-      }
-    }
-    if (
-      preferred &&
-      (!this._preferredBuddy || preferred.id != this._preferredBuddy.id)
-    ) {
-      this.preferredBuddy = preferred;
-    }
-  },
-  _updateStatus() {
-    let buddy = this._preferredBuddy; // for convenience
-
-    // Decide which notifications should be fired.
-    let notifications = [];
-    if (
-      this._statusType != buddy.statusType ||
-      this._availabilityDetails != buddy.availabilityDetails
-    ) {
-      notifications.push("availability-changed");
-    }
-    if (
-      this._statusType != buddy.statusType ||
-      this._statusText != buddy.statusText
-    ) {
-      notifications.push("status-changed");
-      if (this.online && buddy.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE) {
-        notifications.push("signed-off");
-      }
-      if (!this.online && buddy.statusType > Ci.imIStatusInfo.STATUS_OFFLINE) {
-        notifications.push("signed-on");
-      }
-    }
-
-    // Actually change the stored status.
-    [this._statusType, this._statusText, this._availabilityDetails] = [
-      buddy.statusType,
-      buddy.statusText,
-      buddy.availabilityDetails,
-    ];
-
-    // Fire the notifications.
-    notifications.forEach(function(aTopic) {
-      this._notifyObservers(aTopic);
-    }, this);
-  },
-  get displayName() {
-    return this._alias || this.preferredBuddy.displayName;
-  },
-  get buddyIconFilename() {
-    return this.preferredBuddy.buddyIconFilename;
-  },
-  _statusType: 0,
-  get statusType() {
-    return this._statusType;
-  },
-  get online() {
-    return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
-  },
-  get available() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
-  },
-  get idle() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
-  },
-  get mobile() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
-  },
-  _statusText: "",
-  get statusText() {
-    return this._statusText;
-  },
-  _availabilityDetails: 0,
-  get availabilityDetails() {
-    return this._availabilityDetails;
-  },
-  get canSendMessage() {
-    return this.preferredBuddy.canSendMessage;
-  },
-  // XXX should we list the buddies in the tooltip?
-  getTooltipInfo() {
-    return this.preferredBuddy.getTooltipInfo();
-  },
-  createConversation() {
-    let uiConv = Services.conversations.getUIConversationByContactId(this.id);
-    if (uiConv) {
-      return uiConv.target;
-    }
-    return this.preferredBuddy.createConversation();
-  },
-
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    if (!this.hasOwnProperty("_observers")) {
-      return;
-    }
-
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  // internal calls + calls from add-ons
-  notifyObservers(aSubject, aTopic, aData) {
-    for (let observer of this._observers) {
-      if ("observe" in observer) {
-        // avoid failing on destructed XBL bindings...
-        observer.observe(aSubject, aTopic, aData);
-      }
-    }
-    for (let tag of this._tags) {
-      tag.notifyObservers(aSubject, aTopic, aData);
-    }
-    Services.obs.notifyObservers(aSubject, aTopic, aData);
-  },
-  _notifyObservers(aTopic, aData) {
-    this.notifyObservers(this, "contact-" + aTopic, aData);
-  },
-
-  // This is called by the imIBuddy implementations.
-  _observe(aSubject, aTopic, aData) {
-    // Forward the notification.
-    this.notifyObservers(aSubject, aTopic, aData);
-
-    let isPreferredBuddy =
-      aSubject instanceof Buddy && aSubject.id == this.preferredBuddy.id;
-    switch (aTopic) {
-      case "buddy-availability-changed":
-        this._updatePreferredBuddy(aSubject);
-        break;
-      case "buddy-status-changed":
-        if (isPreferredBuddy) {
-          this._updateStatus();
-        }
-        break;
-      case "buddy-display-name-changed":
-        if (isPreferredBuddy && !this._alias) {
-          this._notifyObservers("display-name-changed", aData);
-        }
-        break;
-      case "buddy-icon-changed":
-        if (isPreferredBuddy) {
-          this._notifyObservers("icon-changed");
-        }
-        break;
-      case "buddy-added":
-        // Currently buddies are always added in dummy empty contacts,
-        // later we may want to check this._buddies.length == 1.
-        this._notifyObservers("added");
-        break;
-      case "buddy-removed":
-        this._removeBuddy(aSubject);
-    }
-  },
-};
-
-var BuddiesById = {};
-function Buddy(aId, aKey, aName, aSrvAlias, aContactId) {
-  this._id = aId;
-  this._key = aKey;
-  this._name = aName;
-  if (aSrvAlias) {
-    this._srvAlias = aSrvAlias;
-  }
-  this._accounts = [];
-  this._observers = [];
-
-  if (aContactId) {
-    this._contact = ContactsById[aContactId];
-  }
-  // Avoid failure if aContactId was invalid.
-  if (!this._contact) {
-    this._contact = new Contact(null, null);
-  }
-
-  this._contact._buddies.push(this);
-
-  BuddiesById[this._id] = this;
-}
-Buddy.prototype = {
-  __proto__: ClassInfo("imIBuddy", "Buddy"),
-  get id() {
-    return this._id;
-  },
-  destroy() {
-    for (let ab of this._accounts) {
-      ab.unInit();
-    }
-    delete this._accounts;
-    delete this._observers;
-    delete this._preferredAccount;
-  },
-  get protocol() {
-    return this._accounts[0].account.protocol;
-  },
-  get userName() {
-    return this._name;
-  },
-  get normalizedName() {
-    return this._key;
-  },
-  _srvAlias: "",
-  _contact: null,
-  get contact() {
-    return this._contact;
-  },
-  set contact(aContact) /* not in imIBuddy */ {
-    if (aContact.id == this._contact.id) {
-      throw Cr.NS_ERROR_INVALID_ARG;
-    }
-
-    this._notifyObservers("moved-out-of-contact");
-    this._contact._removeBuddy(this);
-
-    this._contact = aContact;
-    this._contact._buddies.push(this);
-
-    // Ensure all the inherited tags are in the new contact.
-    for (let accountBuddy of this._accounts) {
-      this._contact.addTag(TagsById[accountBuddy.tag.id], true);
-    }
-
-    let statement = DBConn.createStatement(
-      "UPDATE buddies SET contact_id = :contactId, " +
-        "position = :position " +
-        "WHERE id = :buddyId"
-    );
-    statement.params.contactId = aContact.id > 0 ? aContact.id : 0;
-    statement.params.position = aContact._buddies.length - 1;
-    statement.params.buddyId = this.id;
-    executeAsyncThenFinalize(statement);
-
-    this._notifyObservers("moved-into-contact");
-    return aContact;
-  },
-  _hasAccountBuddy(aAccountId, aTagId) {
-    for (let ab of this._accounts) {
-      if (ab.account.numericId == aAccountId && ab.tag.id == aTagId) {
-        return true;
-      }
-    }
-    return false;
-  },
-  getAccountBuddies() {
-    return this._accounts;
-  },
-
-  _addAccount(aAccountBuddy, aTag) {
-    this._accounts.push(aAccountBuddy);
-    let contact = this._contact;
-    if (!this._contact._tags.includes(aTag)) {
-      this._contact._tags.push(aTag);
-      aTag._addContact(contact);
-    }
-
-    if (!this._preferredAccount) {
-      this._preferredAccount = aAccountBuddy;
-    }
-  },
-  get _empty() {
-    return this._accounts.length == 0;
-  },
-
-  remove() {
-    for (let account of this._accounts) {
-      account.remove();
-    }
-  },
-
-  // imIStatusInfo implementation
-  _preferredAccount: null,
-  get preferredAccountBuddy() {
-    return this._preferredAccount;
-  },
-  _isPreferredAccount(aAccountBuddy) {
-    if (
-      aAccountBuddy.account.numericId !=
-      this._preferredAccount.account.numericId
-    ) {
-      return false;
-    }
-
-    // In case we have more than one accountBuddy for the same buddy
-    // and account (possible if the buddy is in several groups on the
-    // server), the protocol plugin may be broken and not update all
-    // instances, so ensure we handle the notifications on the instance
-    // that is currently being notified of a change:
-    this._preferredAccount = aAccountBuddy;
-
-    return true;
-  },
-  set preferredAccount(aAccount) {
-    let oldDisplayName =
-      this._preferredAccount && this._preferredAccount.displayName;
-    this._preferredAccount = aAccount;
-    this._notifyObservers("preferred-account-changed");
-    if (
-      oldDisplayName &&
-      this._preferredAccount.displayName != oldDisplayName
-    ) {
-      this._notifyObservers("display-name-changed", oldDisplayName);
-    }
-    this._updateStatus();
-  },
-  // aAccount indicate which account's availability has changed.
-  _updatePreferredAccount(aAccount) {
-    if (aAccount) {
-      if (
-        aAccount.account.numericId == this._preferredAccount.account.numericId
-      ) {
-        // The suggested account is already preferred, check if its
-        // availability has changed.
-        if (
-          aAccount.statusType > this._statusType ||
-          (aAccount.statusType == this._statusType &&
-            aAccount.availabilityDetails >= this._availabilityDetails)
-        ) {
-          // keep the currently preferred account, only update the status.
-          this._updateStatus();
-          return;
-        }
-        // We aren't sure that the currently preferred account should
-        // still be preferred. Let's go through the list!
-      } else {
-        // The suggested account is not currently preferred. If it is
-        // more available, prefer it!
-        if (
-          aAccount.statusType > this._statusType ||
-          (aAccount.statusType == this._statusType &&
-            aAccount.availabilityDetails > this._availabilityDetails)
-        ) {
-          this.preferredAccount = aAccount;
-        }
-        return;
-      }
-    }
-
-    let preferred;
-    // TODO take into account the order of the account-manager list.
-    for (let account of this._accounts) {
-      if (
-        !preferred ||
-        preferred.statusType < account.statusType ||
-        (preferred.statusType == account.statusType &&
-          preferred.availabilityDetails < account.availabilityDetails)
-      ) {
-        preferred = account;
-      }
-    }
-    if (!this._preferredAccount) {
-      if (preferred) {
-        this.preferredAccount = preferred;
-      }
-      return;
-    }
-    if (
-      preferred.account.numericId != this._preferredAccount.account.numericId
-    ) {
-      this.preferredAccount = preferred;
-    } else {
-      this._updateStatus();
-    }
-  },
-  _updateStatus() {
-    let account = this._preferredAccount; // for convenience
-
-    // Decide which notifications should be fired.
-    let notifications = [];
-    if (
-      this._statusType != account.statusType ||
-      this._availabilityDetails != account.availabilityDetails
-    ) {
-      notifications.push("availability-changed");
-    }
-    if (
-      this._statusType != account.statusType ||
-      this._statusText != account.statusText
-    ) {
-      notifications.push("status-changed");
-      if (
-        this.online &&
-        account.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE
-      ) {
-        notifications.push("signed-off");
-      }
-      if (
-        !this.online &&
-        account.statusType > Ci.imIStatusInfo.STATUS_OFFLINE
-      ) {
-        notifications.push("signed-on");
-      }
-    }
-
-    // Actually change the stored status.
-    [this._statusType, this._statusText, this._availabilityDetails] = [
-      account.statusType,
-      account.statusText,
-      account.availabilityDetails,
-    ];
-
-    // Fire the notifications.
-    notifications.forEach(function(aTopic) {
-      this._notifyObservers(aTopic);
-    }, this);
-  },
-  get displayName() {
-    return (
-      (this._preferredAccount && this._preferredAccount.displayName) ||
-      this._srvAlias ||
-      this._name
-    );
-  },
-  get buddyIconFilename() {
-    return this._preferredAccount.buddyIconFilename;
-  },
-  _statusType: 0,
-  get statusType() {
-    return this._statusType;
-  },
-  get online() {
-    return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
-  },
-  get available() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
-  },
-  get idle() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
-  },
-  get mobile() {
-    return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
-  },
-  _statusText: "",
-  get statusText() {
-    return this._statusText;
-  },
-  _availabilityDetails: 0,
-  get availabilityDetails() {
-    return this._availabilityDetails;
-  },
-  get canSendMessage() {
-    return this._preferredAccount.canSendMessage;
-  },
-  // XXX should we list the accounts in the tooltip?
-  getTooltipInfo() {
-    return this._preferredAccount.getTooltipInfo();
-  },
-  createConversation() {
-    return this._preferredAccount.createConversation();
-  },
-
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    if (!this._observers) {
-      return;
-    }
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  // internal calls + calls from add-ons
-  notifyObservers(aSubject, aTopic, aData) {
-    try {
-      for (let observer of this._observers) {
-        observer.observe(aSubject, aTopic, aData);
-      }
-      this._contact._observe(aSubject, aTopic, aData);
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  },
-  _notifyObservers(aTopic, aData) {
-    this.notifyObservers(this, "buddy-" + aTopic, aData);
-  },
-
-  // This is called by the prplIAccountBuddy implementations.
-  observe(aSubject, aTopic, aData) {
-    // Forward the notification.
-    this.notifyObservers(aSubject, aTopic, aData);
-
-    switch (aTopic) {
-      case "account-buddy-availability-changed":
-        this._updatePreferredAccount(aSubject);
-        break;
-      case "account-buddy-status-changed":
-        if (this._isPreferredAccount(aSubject)) {
-          this._updateStatus();
-        }
-        break;
-      case "account-buddy-display-name-changed":
-        if (this._isPreferredAccount(aSubject)) {
-          this._srvAlias =
-            this.displayName != this.userName ? this.displayName : "";
-          let statement = DBConn.createStatement(
-            "UPDATE buddies SET srv_alias = :srvAlias WHERE id = :buddyId"
-          );
-          statement.params.buddyId = this.id;
-          statement.params.srvAlias = this._srvAlias;
-          executeAsyncThenFinalize(statement);
-          this._notifyObservers("display-name-changed", aData);
-        }
-        break;
-      case "account-buddy-icon-changed":
-        if (this._isPreferredAccount(aSubject)) {
-          this._notifyObservers("icon-changed");
-        }
-        break;
-      case "account-buddy-added":
-        if (this._accounts.length == 0) {
-          // Add the new account in the empty buddy instance.
-          // The TagsById hack is to bypass the xpconnect wrapper.
-          this._addAccount(aSubject, TagsById[aSubject.tag.id]);
-          this._updateStatus();
-          this._notifyObservers("added");
-        } else {
-          this._accounts.push(aSubject);
-          this.contact._moved(null, aSubject.tag);
-          this._updatePreferredAccount(aSubject);
-        }
-        break;
-      case "account-buddy-removed":
-        if (this._accounts.length == 1) {
-          let statement = DBConn.createStatement(
-            "DELETE FROM buddies WHERE id = :id"
-          );
-          try {
-            statement.params.id = this.id;
-            statement.execute();
-          } finally {
-            statement.finalize();
-          }
-          this._notifyObservers("removed");
-
-          delete BuddiesById[this._id];
-          this.destroy();
-        } else {
-          this._accounts = this._accounts.filter(function(ab) {
-            return (
-              ab.account.numericId != aSubject.account.numericId ||
-              ab.tag.id != aSubject.tag.id
-            );
-          });
-          if (
-            this._preferredAccount.account.numericId ==
-              aSubject.account.numericId &&
-            this._preferredAccount.tag.id == aSubject.tag.id
-          ) {
-            this._preferredAccount = null;
-            this._updatePreferredAccount();
-          }
-          this.contact._moved(aSubject.tag);
-        }
-        break;
-    }
-  },
-};
-
-function ContactsService() {}
-ContactsService.prototype = {
-  initContacts() {
-    let statement = DBConn.createStatement("SELECT id, name FROM tags");
-    try {
-      while (statement.executeStep()) {
-        Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1)));
-      }
-    } finally {
-      statement.finalize();
-    }
-
-    statement = DBConn.createStatement("SELECT id, alias FROM contacts");
-    try {
-      while (statement.executeStep()) {
-        new Contact(statement.getInt32(0), statement.getUTF8String(1));
-      }
-    } finally {
-      statement.finalize();
-    }
-
-    statement = DBConn.createStatement(
-      "SELECT contact_id, tag_id FROM contact_tag"
-    );
-    try {
-      while (statement.executeStep()) {
-        let contact = ContactsById[statement.getInt32(0)];
-        let tag = TagsById[statement.getInt32(1)];
-        contact._tags.push(tag);
-        tag._addContact(contact);
-      }
-    } finally {
-      statement.finalize();
-    }
-
-    statement = DBConn.createStatement(
-      "SELECT id, key, name, srv_alias, contact_id FROM buddies ORDER BY position"
-    );
-    try {
-      while (statement.executeStep()) {
-        new Buddy(
-          statement.getInt32(0),
-          statement.getUTF8String(1),
-          statement.getUTF8String(2),
-          statement.getUTF8String(3),
-          statement.getInt32(4)
-        );
-        // FIXME is there a way to enforce that all AccountBuddies of a Buddy have the same protocol?
-      }
-    } finally {
-      statement.finalize();
-    }
-
-    statement = DBConn.createStatement(
-      "SELECT account_id, buddy_id, tag_id FROM account_buddy"
-    );
-    try {
-      while (statement.executeStep()) {
-        let accountId = statement.getInt32(0);
-        let buddyId = statement.getInt32(1);
-        let tagId = statement.getInt32(2);
-
-        if (!BuddiesById.hasOwnProperty(buddyId)) {
-          Cu.reportError(
-            "Corrupted database: account_buddy entry for account " +
-              accountId +
-              " and tag " +
-              tagId +
-              " references unknown buddy with id " +
-              buddyId
-          );
-          continue;
-        }
-
-        let buddy = BuddiesById[buddyId];
-        if (buddy._hasAccountBuddy(accountId, tagId)) {
-          Cu.reportError(
-            "Corrupted database: duplicated account_buddy entry: " +
-              "account_id = " +
-              accountId +
-              ", buddy_id = " +
-              buddyId +
-              ", tag_id = " +
-              tagId
-          );
-          continue;
-        }
-
-        let account = Services.accounts.getAccountByNumericId(accountId);
-        let tag = TagsById[tagId];
-        try {
-          buddy._addAccount(account.loadBuddy(buddy, tag), tag);
-        } catch (e) {
-          Cu.reportError(e);
-          dump(e + "\n");
-        }
-      }
-    } finally {
-      statement.finalize();
-    }
-    otherContactsTag._initHiddenTags();
-  },
-  unInitContacts() {
-    Tags = [];
-    TagsById = {};
-    // Avoid shutdown leaks caused by references to native components
-    // implementing prplIAccountBuddy.
-    for (let buddyId in BuddiesById) {
-      let buddy = BuddiesById[buddyId];
-      buddy.destroy();
-    }
-    BuddiesById = {};
-    ContactsById = {};
-  },
-
-  getContactById: aId => ContactsById[aId],
-  // Get an array of all existing contacts.
-  getContacts() {
-    return Object.keys(ContactsById)
-      .filter(id => !ContactsById[id]._empty)
-      .map(id => ContactsById[id]);
-  },
-  getBuddyById: aId => BuddiesById[aId],
-  getBuddyByNameAndProtocol(aNormalizedName, aPrpl) {
-    let statement = DBConn.createStatement(
-      "SELECT b.id FROM buddies b " +
-        "JOIN account_buddy ab ON buddy_id = b.id " +
-        "JOIN accounts a ON account_id = a.id " +
-        "WHERE b.key = :buddyName and a.prpl = :prplId"
-    );
-    statement.params.buddyName = aNormalizedName;
-    statement.params.prplId = aPrpl.id;
-    try {
-      if (!statement.executeStep()) {
-        return null;
-      }
-      return BuddiesById[statement.row.id];
-    } finally {
-      statement.finalize();
-    }
-  },
-  getAccountBuddyByNameAndAccount(aNormalizedName, aAccount) {
-    let buddy = this.getBuddyByNameAndProtocol(
-      aNormalizedName,
-      aAccount.protocol
-    );
-    if (buddy) {
-      let id = aAccount.id;
-      for (let accountBuddy of buddy.getAccountBuddies()) {
-        if (accountBuddy.account.id == id) {
-          return accountBuddy;
-        }
-      }
-    }
-    return null;
-  },
-
-  accountBuddyAdded(aAccountBuddy) {
-    let account = aAccountBuddy.account;
-    let normalizedName = aAccountBuddy.normalizedName;
-    let buddy = this.getBuddyByNameAndProtocol(
-      normalizedName,
-      account.protocol
-    );
-    if (!buddy) {
-      let statement = DBConn.createStatement(
-        "INSERT INTO buddies " +
-          "(key, name, srv_alias, position) " +
-          "VALUES(:key, :name, :srvAlias, 0)"
-      );
-      try {
-        let name = aAccountBuddy.userName;
-        let srvAlias = aAccountBuddy.serverAlias;
-        statement.params.key = normalizedName;
-        statement.params.name = name;
-        statement.params.srvAlias = srvAlias;
-        statement.execute();
-        buddy = new Buddy(
-          DBConn.lastInsertRowID,
-          normalizedName,
-          name,
-          srvAlias,
-          0
-        );
-      } finally {
-        statement.finalize();
-      }
-    }
-
-    // Initialize the 'buddy' field of the prplIAccountBuddy instance.
-    aAccountBuddy.buddy = buddy;
-
-    // Ensure we aren't storing a duplicate entry.
-    let accountId = account.numericId;
-    let tagId = aAccountBuddy.tag.id;
-    if (buddy._hasAccountBuddy(accountId, tagId)) {
-      Cu.reportError(
-        "Attempting to store a duplicate account buddy " +
-          normalizedName +
-          ", account id = " +
-          accountId +
-          ", tag id = " +
-          tagId
-      );
-      return;
-    }
-
-    // Store the new account buddy.
-    let statement = DBConn.createStatement(
-      "INSERT INTO account_buddy " +
-        "(account_id, buddy_id, tag_id) " +
-        "VALUES(:accountId, :buddyId, :tagId)"
-    );
-    try {
-      statement.params.accountId = accountId;
-      statement.params.buddyId = buddy.id;
-      statement.params.tagId = tagId;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-
-    // Fire the notifications.
-    buddy.observe(aAccountBuddy, "account-buddy-added");
-  },
-  accountBuddyRemoved(aAccountBuddy) {
-    let buddy = aAccountBuddy.buddy;
-    let statement = DBConn.createStatement(
-      "DELETE FROM account_buddy " +
-        "WHERE account_id = :accountId AND " +
-        "buddy_id = :buddyId AND " +
-        "tag_id = :tagId"
-    );
-    try {
-      statement.params.accountId = aAccountBuddy.account.numericId;
-      statement.params.buddyId = buddy.id;
-      statement.params.tagId = aAccountBuddy.tag.id;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-
-    buddy.observe(aAccountBuddy, "account-buddy-removed");
-  },
-
-  accountBuddyMoved(aAccountBuddy, aOldTag, aNewTag) {
-    let buddy = aAccountBuddy.buddy;
-    let statement = DBConn.createStatement(
-      "UPDATE account_buddy " +
-        "SET tag_id = :newTagId " +
-        "WHERE account_id = :accountId AND " +
-        "buddy_id = :buddyId AND " +
-        "tag_id = :oldTagId"
-    );
-    try {
-      statement.params.accountId = aAccountBuddy.account.numericId;
-      statement.params.buddyId = buddy.id;
-      statement.params.oldTagId = aOldTag.id;
-      statement.params.newTagId = aNewTag.id;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-
-    let contact = ContactsById[buddy.contact.id];
-
-    // aNewTag is now inherited by the contact from an account buddy, so avoid
-    // keeping direct tag <-> contact links in the contact_tag table.
-    contact._removeContactTagRow(aNewTag);
-
-    buddy.observe(aAccountBuddy, "account-buddy-moved");
-    contact._moved(aOldTag, aNewTag);
-  },
-
-  storeAccount(aId, aUserName, aPrplId) {
-    let statement = DBConn.createStatement(
-      "SELECT name, prpl FROM accounts WHERE id = :id"
-    );
-    statement.params.id = aId;
-    try {
-      if (statement.executeStep()) {
-        if (
-          statement.getUTF8String(0) == aUserName &&
-          statement.getUTF8String(1) == aPrplId
-        ) {
-          // The account is already stored correctly.
-          return;
-        }
-        throw Cr.NS_ERROR_UNEXPECTED; // Corrupted database?!?
-      }
-    } finally {
-      statement.finalize();
-    }
-
-    // Actually store the account.
-    statement = DBConn.createStatement(
-      "INSERT INTO accounts (id, name, prpl) " +
-        "VALUES(:id, :userName, :prplId)"
-    );
-    try {
-      statement.params.id = aId;
-      statement.params.userName = aUserName;
-      statement.params.prplId = aPrplId;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-  },
-  accountIdExists(aId) {
-    let statement = DBConn.createStatement(
-      "SELECT id FROM accounts WHERE id = :id"
-    );
-    try {
-      statement.params.id = aId;
-      return statement.executeStep();
-    } finally {
-      statement.finalize();
-    }
-  },
-  forgetAccount(aId) {
-    let statement = DBConn.createStatement(
-      "DELETE FROM accounts WHERE id = :accountId"
-    );
-    try {
-      statement.params.accountId = aId;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-
-    // removing the account from the accounts table is not enough,
-    // we need to remove all the associated account_buddy entries too
-    statement = DBConn.createStatement(
-      "DELETE FROM account_buddy WHERE account_id = :accountId"
-    );
-    try {
-      statement.params.accountId = aId;
-      statement.execute();
-    } finally {
-      statement.finalize();
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imIContactsService]),
-};
deleted file mode 100644
--- a/chat/components/src/IMConversations.jsm
+++ /dev/null
@@ -1,836 +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 EXPORTED_SYMBOLS = ["ConversationsService", "IMMessage", "UIConversation"];
-
-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;
-var gLastPrplConvId = 0;
-
-XPCOMUtils.defineLazyGetter(this, "bundle", () =>
-  Services.strings.createBundle("chrome://chat/locale/conversations.properties")
-);
-
-function OutgoingMessage(aMsg, aConversation) {
-  this.message = aMsg;
-  this.conversation = aConversation;
-}
-OutgoingMessage.prototype = {
-  __proto__: ClassInfo("imIOutgoingMessage", "Outgoing Message"),
-  cancelled: false,
-  action: false,
-};
-
-function IMMessage(aPrplMessage) {
-  this.prplMessage = aPrplMessage;
-}
-IMMessage.prototype = {
-  __proto__: ClassInfo(["imIMessage", "prplIMessage"], "IM Message"),
-  cancelled: false,
-  color: "",
-  _displayMessage: null,
-  encrypted: false,
-
-  get displayMessage() {
-    // Explicitly test for null so that blank messages don't fall back to
-    // the original. Especially problematic in encryption extensions like OTR.
-    return this._displayMessage !== null
-      ? this._displayMessage
-      : this.prplMessage.originalMessage;
-  },
-  set displayMessage(aMsg) {
-    this._displayMessage = aMsg;
-  },
-
-  get message() {
-    return this.prplMessage.message;
-  },
-  set message(aMsg) {
-    this.prplMessage.message = aMsg;
-  },
-
-  // from prplIMessage
-  get who() {
-    return this.prplMessage.who;
-  },
-  get time() {
-    return this.prplMessage.time;
-  },
-  get id() {
-    return this.prplMessage.id;
-  },
-  get alias() {
-    return this.prplMessage.alias;
-  },
-  get iconURL() {
-    return this.prplMessage.iconURL;
-  },
-  get conversation() {
-    return this.prplMessage.conversation;
-  },
-  set conversation(aConv) {
-    this.prplMessage.conversation = aConv;
-  },
-  get outgoing() {
-    return this.prplMessage.outgoing;
-  },
-  get incoming() {
-    return this.prplMessage.incoming;
-  },
-  get system() {
-    return this.prplMessage.system;
-  },
-  get autoResponse() {
-    return this.prplMessage.autoResponse;
-  },
-  get containsNick() {
-    return this.prplMessage.containsNick;
-  },
-  get noLog() {
-    return this.prplMessage.noLog;
-  },
-  get error() {
-    return this.prplMessage.error;
-  },
-  get delayed() {
-    return this.prplMessage.delayed;
-  },
-  get noFormat() {
-    return this.prplMessage.noFormat;
-  },
-  get containsImages() {
-    return this.prplMessage.containsImages;
-  },
-  get notification() {
-    return this.prplMessage.notification;
-  },
-  get noLinkification() {
-    return this.prplMessage.noLinkification;
-  },
-  get originalMessage() {
-    return this.prplMessage.originalMessage;
-  },
-  getActions() {
-    return this.prplMessage.getActions();
-  },
-};
-
-function UIConversation(aPrplConversation) {
-  this._prplConv = {};
-  this.id = ++gLastUIConvId;
-  this._observers = [];
-  this._messages = [];
-  this.changeTargetTo(aPrplConversation);
-  let iface = Ci["prplIConv" + (aPrplConversation.isChat ? "Chat" : "IM")];
-  this._interfaces = this._interfaces.concat(iface);
-  // XPConnect will create a wrapper around 'this' after here,
-  // so the list of exposed interfaces shouldn't change anymore.
-  this.updateContactObserver();
-  Services.obs.notifyObservers(this, "new-ui-conversation");
-}
-
-UIConversation.prototype = {
-  __proto__: ClassInfo(
-    ["imIConversation", "prplIConversation", "nsIObserver"],
-    "UI conversation"
-  ),
-  _observedContact: null,
-  get contact() {
-    let target = this.target;
-    if (!target.isChat && target.buddy) {
-      return target.buddy.buddy.contact;
-    }
-    return null;
-  },
-  updateContactObserver() {
-    let contact = this.contact;
-    if (contact && !this._observedContact) {
-      contact.addObserver(this);
-      this._observedContact = contact;
-    } else if (!contact && this.observedContact) {
-      this._observedContact.removeObserver(this);
-      delete this._observedContact;
-    }
-  },
-  get target() {
-    return this._prplConv[this._currentTargetId];
-  },
-  set target(aPrplConversation) {
-    this.changeTargetTo(aPrplConversation);
-  },
-  get hasMultipleTargets() {
-    return Object.keys(this._prplConv).length > 1;
-  },
-  getTargetByAccount(aAccount) {
-    let accountId = aAccount.id;
-    for (let id in this._prplConv) {
-      let prplConv = this._prplConv[id];
-      if (prplConv.account.id == accountId) {
-        return prplConv;
-      }
-    }
-    return null;
-  },
-  _currentTargetId: 0,
-  changeTargetTo(aPrplConversation) {
-    let id = aPrplConversation.id;
-    if (this._currentTargetId == id) {
-      return;
-    }
-
-    if (!(id in this._prplConv)) {
-      this._prplConv[id] = aPrplConversation;
-      aPrplConversation.addObserver(this.observeConv.bind(this, id));
-    }
-
-    let shouldNotify = this._currentTargetId;
-    this._currentTargetId = id;
-    if (!this.isChat) {
-      let buddy = this.buddy;
-      if (buddy) {
-        ({ statusType: this.statusType, statusText: this.statusText } = buddy);
-      }
-    }
-    if (shouldNotify) {
-      this.notifyObservers(this, "target-prpl-conversation-changed");
-      let target = this.target;
-      let params = [target.title, target.account.protocol.name];
-      this.systemMessage(bundle.formatStringFromName("targetChanged", params));
-    }
-  },
-  // Returns a boolean indicating if the ui-conversation was closed.
-  // If the conversation was closed, aContactId.value is set to the contact id
-  // or 0 if no contact was associated with the conversation.
-  removeTarget(aPrplConversation, aContactId) {
-    let id = aPrplConversation.id;
-    if (!(id in this._prplConv)) {
-      throw new Error("unknown prpl conversation");
-    }
-
-    delete this._prplConv[id];
-    if (this._currentTargetId != id) {
-      return false;
-    }
-
-    for (let newId in this._prplConv) {
-      this.changeTargetTo(this._prplConv[newId]);
-      return false;
-    }
-
-    if (this._observedContact) {
-      this._observedContact.removeObserver(this);
-      aContactId.value = this._observedContact.id;
-      delete this._observedContact;
-    } else {
-      aContactId.value = 0;
-    }
-
-    delete this._currentTargetId;
-    this.notifyObservers(this, "ui-conversation-closed");
-    return true;
-  },
-
-  _unreadMessageCount: 0,
-  get unreadMessageCount() {
-    return this._unreadMessageCount;
-  },
-  _unreadTargetedMessageCount: 0,
-  get unreadTargetedMessageCount() {
-    return this._unreadTargetedMessageCount;
-  },
-  _unreadIncomingMessageCount: 0,
-  get unreadIncomingMessageCount() {
-    return this._unreadIncomingMessageCount;
-  },
-  markAsRead() {
-    delete this._unreadMessageCount;
-    delete this._unreadTargetedMessageCount;
-    delete this._unreadIncomingMessageCount;
-    this._notifyUnreadCountChanged();
-  },
-  _lastNotifiedUnreadCount: 0,
-  _notifyUnreadCountChanged() {
-    if (this._unreadIncomingMessageCount == this._lastNotifiedUnreadCount) {
-      return;
-    }
-
-    this._lastNotifiedUnreadCount = this._unreadIncomingMessageCount;
-    for (let observer of this._observers) {
-      observer.observe(
-        this,
-        "unread-message-count-changed",
-        this._unreadIncomingMessageCount.toString()
-      );
-    }
-  },
-  getMessages() {
-    return this._messages;
-  },
-  checkClose() {
-    if (!this._currentTargetId) {
-      // Already closed.
-      return true;
-    }
-
-    if (
-      !Services.prefs.getBoolPref("messenger.conversations.alwaysClose") &&
-      ((this.isChat && !this.left) ||
-        (!this.isChat &&
-          (this.unreadIncomingMessageCount != 0 ||
-            Services.prefs.getBoolPref(
-              "messenger.conversations.holdByDefault"
-            ))))
-    ) {
-      return false;
-    }
-
-    this.close();
-    return true;
-  },
-
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "contact-no-longer-dummy") {
-      let oldId = parseInt(aData);
-      // gConversationsService is ugly... :(
-      delete gConversationsService._uiConvByContactId[oldId];
-      gConversationsService._uiConvByContactId[aSubject.id] = this;
-    } else if (aTopic == "account-buddy-status-changed") {
-      if (
-        !this._statusUpdatePending &&
-        aSubject.account.id == this.account.id &&
-        aSubject.buddy.id == this.buddy.buddy.id
-      ) {
-        this._statusUpdatePending = true;
-        Services.tm.mainThread.dispatch(
-          this.updateBuddyStatus.bind(this),
-          Ci.nsIEventTarget.DISPATCH_NORMAL
-        );
-      }
-    } else if (aTopic == "account-buddy-icon-changed") {
-      if (
-        !this._statusUpdatePending &&
-        aSubject.account.id == this.account.id &&
-        aSubject.buddy.id == this.buddy.buddy.id
-      ) {
-        this._iconUpdatePending = true;
-        Services.tm.mainThread.dispatch(
-          this.updateIcon.bind(this),
-          Ci.nsIEventTarget.DISPATCH_NORMAL
-        );
-      }
-    } else if (
-      aTopic == "account-buddy-display-name-changed" &&
-      aSubject.account.id == this.account.id &&
-      aSubject.buddy.id == this.buddy.buddy.id
-    ) {
-      this.notifyObservers(this, "update-buddy-display-name");
-    }
-  },
-
-  _iconUpdatePending: false,
-  updateIcon() {
-    delete this._iconUpdatePending;
-    this.notifyObservers(this, "update-buddy-icon");
-  },
-
-  _statusUpdatePending: false,
-  updateBuddyStatus() {
-    delete this._statusUpdatePending;
-    let { statusType: statusType, statusText: statusText } = this.buddy;
-
-    if (
-      "statusType" in this &&
-      this.statusType == statusType &&
-      this.statusText == statusText
-    ) {
-      return;
-    }
-
-    let wasUnknown = this.statusType == Ci.imIStatusInfo.STATUS_UNKNOWN;
-    this.statusType = statusType;
-    this.statusText = statusText;
-
-    this.notifyObservers(this, "update-buddy-status");
-
-    let msg;
-    if (statusType == Ci.imIStatusInfo.STATUS_UNKNOWN) {
-      msg = bundle.formatStringFromName("statusUnknown", [this.title]);
-    } else {
-      let status = Status.toLabel(statusType);
-      let stringId = wasUnknown ? "statusChangedFromUnknown" : "statusChanged";
-      if (this._justReconnected) {
-        stringId = "statusKnown";
-        delete this._justReconnected;
-      }
-      if (statusText) {
-        msg = bundle.formatStringFromName(stringId + "WithStatusText", [
-          this.title,
-          status,
-          statusText,
-        ]);
-      } else {
-        msg = bundle.formatStringFromName(stringId, [this.title, status]);
-      }
-    }
-    this.systemMessage(msg);
-  },
-
-  _disconnected: false,
-  disconnecting() {
-    if (this._disconnected) {
-      return;
-    }
-
-    this._disconnected = true;
-    if (this.contact) {
-      // Handled by the contact observer.
-      return;
-    }
-
-    if (this.isChat && this.left) {
-      this._wasLeft = true;
-    } else {
-      this.systemMessage(bundle.GetStringFromName("accountDisconnected"));
-    }
-    this.notifyObservers(this, "update-buddy-status");
-  },
-  connected() {
-    if (this._disconnected) {
-      delete this._disconnected;
-      let msg = bundle.GetStringFromName("accountReconnected");
-      if (this.isChat) {
-        if (!this._wasLeft) {
-          this.systemMessage(msg);
-          // Reconnect chat if possible.
-          let chatRoomFields = this.target.chatRoomFields;
-          if (chatRoomFields) {
-            this.account.joinChat(chatRoomFields);
-          }
-        }
-        delete this._wasLeft;
-      } else {
-        this._justReconnected = true;
-        // Exclude convs with contacts, these receive presence info updates
-        // (and therefore a reconnected message).
-        if (!this.contact) {
-          this.systemMessage(msg);
-        }
-      }
-    }
-    this.notifyObservers(this, "update-buddy-status");
-  },
-
-  observeConv(aTargetId, aSubject, aTopic, aData) {
-    if (
-      aTargetId != this._currentTargetId &&
-      (aTopic == "new-text" ||
-        (aTopic == "update-typing" &&
-          this._prplConv[aTargetId].typingState == Ci.prplIConvIM.TYPING))
-    ) {
-      this.target = this._prplConv[aTargetId];
-    }
-
-    this.notifyObservers(aSubject, aTopic, aData);
-  },
-
-  systemMessage(aText, aIsError) {
-    let flags = { system: true, noLog: true, error: !!aIsError };
-    new Message("system", aText, flags).conversation = this;
-  },
-
-  // prplIConversation
-  get isChat() {
-    return this.target.isChat;
-  },
-  get account() {
-    return this.target.account;
-  },
-  get name() {
-    return this.target.name;
-  },
-  get normalizedName() {
-    return this.target.normalizedName;
-  },
-  get title() {
-    return this.target.title;
-  },
-  get startDate() {
-    return this.target.startDate;
-  },
-  sendMsg(aMsg) {
-    // Add-ons (eg. pastebin) have an opportunity to cancel the message at this
-    // point, or change the text content of the message.
-    // If an add-on wants to split a message, it should truncate the first
-    // message, and insert new messages using the conversation's sendMsg method.
-    let om = new OutgoingMessage(aMsg, this);
-    this.notifyObservers(om, "preparing-message");
-    if (om.cancelled) {
-      return;
-    }
-
-    // Protocols have an opportunity here to preprocess messages before they are
-    // sent (eg. split long messages). If a message is split here, the split
-    // will be visible in the UI.
-    let messages = this.target.prepareForSending(om);
-
-    // Protocols can return null if they don't need to make any changes.
-    // (nb. passing null with retval array results in an empty array)
-    if (!messages || !messages.length) {
-      messages = [om.message];
-    }
-
-    for (let msg of messages) {
-      // Add-ons (eg. OTR) have an opportunity to tweak or cancel the message
-      // at this point.
-      om = new OutgoingMessage(msg, this.target);
-      this.notifyObservers(om, "sending-message");
-      if (om.cancelled) {
-        continue;
-      }
-      this.target.sendMsg(om.message);
-    }
-  },
-  unInit() {
-    for (let id in this._prplConv) {
-      let conv = this._prplConv[id];
-      gConversationsService.forgetConversation(conv);
-    }
-    if (this._observedContact) {
-      this._observedContact.removeObserver(this);
-      delete this._observedContact;
-    }
-    this._prplConv = {}; // Prevent .close from failing.
-    delete this._currentTargetId;
-    this.notifyObservers(this, "ui-conversation-destroyed");
-  },
-  close() {
-    for (let id in this._prplConv) {
-      let conv = this._prplConv[id];
-      conv.close();
-    }
-    if (!this.hasOwnProperty("_currentTargetId")) {
-      return;
-    }
-    delete this._currentTargetId;
-    this.notifyObservers(this, "ui-conversation-closed");
-    Services.obs.notifyObservers(this, "ui-conversation-closed");
-  },
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  notifyObservers(aSubject, aTopic, aData) {
-    if (aTopic == "new-text") {
-      aSubject = new IMMessage(aSubject);
-      this.notifyObservers(aSubject, "received-message");
-      if (aSubject.cancelled) {
-        return;
-      }
-      if (!aSubject.system) {
-        aSubject.conversation.prepareForDisplaying(aSubject);
-      }
-
-      this._messages.push(aSubject);
-      ++this._unreadMessageCount;
-      if (aSubject.incoming && !aSubject.system) {
-        ++this._unreadIncomingMessageCount;
-        if (!this.isChat || aSubject.containsNick) {
-          ++this._unreadTargetedMessageCount;
-        }
-      }
-    }
-
-    for (let observer of this._observers) {
-      if (!observer.observe && !this._observers.includes(observer)) {
-        // Observer removed by a previous call to another observer.
-        continue;
-      }
-      observer.observe(aSubject, aTopic, aData);
-    }
-    this._notifyUnreadCountChanged();
-
-    if (aTopic == "new-text") {
-      Services.obs.notifyObservers(aSubject, aTopic, aData);
-      if (
-        aSubject.incoming &&
-        !aSubject.system &&
-        (!this.isChat || aSubject.containsNick)
-      ) {
-        this.notifyObservers(aSubject, "new-directed-incoming-message", aData);
-        Services.obs.notifyObservers(
-          aSubject,
-          "new-directed-incoming-message",
-          aData
-        );
-      }
-    }
-  },
-
-  // Used above when notifying of new-texts originating in the
-  // UIConversation. This happens when this.systemMessage() is called. The
-  // conversation for the message is set as the UIConversation.
-  prepareForDisplaying(aMsg) {},
-
-  // prplIConvIM
-  get buddy() {
-    return this.target.buddy;
-  },
-  get typingState() {
-    return this.target.typingState;
-  },
-  sendTyping(aString) {
-    return this.target.sendTyping(aString);
-  },
-
-  // Chat only
-  getParticipants() {
-    return this.target.getParticipants();
-  },
-  get topic() {
-    return this.target.topic;
-  },
-  set topic(aTopic) {
-    this.target.topic = aTopic;
-  },
-  get topicSetter() {
-    return this.target.topicSetter;
-  },
-  get topicSettable() {
-    return this.target.topicSettable;
-  },
-  get noTopicString() {
-    return bundle.GetStringFromName("noTopic");
-  },
-  get nick() {
-    return this.target.nick;
-  },
-  get left() {
-    return this.target.left;
-  },
-  get joining() {
-    return this.target.joining;
-  },
-};
-
-var gConversationsService;
-function ConversationsService() {
-  gConversationsService = this;
-}
-ConversationsService.prototype = {
-  get wrappedJSObject() {
-    return this;
-  },
-
-  initConversations() {
-    this._uiConv = {};
-    this._uiConvByContactId = {};
-    this._prplConversations = [];
-    Services.obs.addObserver(this, "account-disconnecting");
-    Services.obs.addObserver(this, "account-connected");
-    Services.obs.addObserver(this, "account-buddy-added");
-    Services.obs.addObserver(this, "account-buddy-removed");
-  },
-
-  unInitConversations() {
-    let UIConvs = this.getUIConversations();
-    for (let UIConv of UIConvs) {
-      UIConv.unInit();
-    }
-    delete this._uiConv;
-    delete this._uiConvByContactId;
-    // This should already be empty, but just to be sure...
-    for (let prplConv of this._prplConversations) {
-      prplConv.unInit();
-    }
-    delete this._prplConversations;
-    Services.obs.removeObserver(this, "account-disconnecting");
-    Services.obs.removeObserver(this, "account-connected");
-    Services.obs.removeObserver(this, "account-buddy-added");
-    Services.obs.removeObserver(this, "account-buddy-removed");
-  },
-
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "account-connected") {
-      for (let id in this._uiConv) {
-        let conv = this._uiConv[id];
-        if (conv.account.id == aSubject.id) {
-          conv.connected();
-        }
-      }
-    } else if (aTopic == "account-disconnecting") {
-      for (let id in this._uiConv) {
-        let conv = this._uiConv[id];
-        if (conv.account.id == aSubject.id) {
-          conv.disconnecting();
-        }
-      }
-    } else if (aTopic == "account-buddy-added") {
-      let accountBuddy = aSubject;
-      let prplConversation = this.getConversationByNameAndAccount(
-        accountBuddy.normalizedName,
-        accountBuddy.account,
-        false
-      );
-      if (!prplConversation) {
-        return;
-      }
-
-      let uiConv = this.getUIConversation(prplConversation);
-      let contactId = accountBuddy.buddy.contact.id;
-      if (contactId in this._uiConvByContactId) {
-        // Trouble! There is an existing uiConv for this contact.
-        // We should avoid having two uiConvs with the same contact.
-        // This is ugly UX, but at least can only happen if there is
-        // already an accountBuddy with the same name for the same
-        // protocol on a different account, which should be rare.
-        this.removeConversation(prplConversation);
-        return;
-      }
-      // Link the existing uiConv to the contact.
-      this._uiConvByContactId[contactId] = uiConv;
-      uiConv.updateContactObserver();
-      uiConv.notifyObservers(uiConv, "update-conv-buddy");
-    } else if (aTopic == "account-buddy-removed") {
-      let accountBuddy = aSubject;
-      let contactId = accountBuddy.buddy.contact.id;
-      if (!(contactId in this._uiConvByContactId)) {
-        return;
-      }
-      let uiConv = this._uiConvByContactId[contactId];
-
-      // If there is more than one target on the uiConv, close the
-      // prplConv as we can't dissociate the uiConv from the contact.
-      // The conversation with the contact will continue with a different
-      // target.
-      if (uiConv.hasMultipleTargets) {
-        let prplConversation = uiConv.getTargetByAccount(accountBuddy.account);
-        if (prplConversation) {
-          this.removeConversation(prplConversation);
-        }
-        return;
-      }
-
-      delete this._uiConvByContactId[contactId];
-      uiConv.updateContactObserver();
-      uiConv.notifyObservers(uiConv, "update-conv-buddy");
-    }
-  },
-
-  addConversation(aPrplConversation) {
-    // Give an id to the new conversation.
-    aPrplConversation.id = ++gLastPrplConvId;
-    this._prplConversations.push(aPrplConversation);
-
-    // Notify observers.
-    Services.obs.notifyObservers(aPrplConversation, "new-conversation");
-
-    // Update or create the corresponding UI conversation.
-    let contactId;
-    if (!aPrplConversation.isChat) {
-      let accountBuddy = aPrplConversation.buddy;
-      if (accountBuddy) {
-        contactId = accountBuddy.buddy.contact.id;
-      }
-    }
-
-    if (contactId) {
-      if (contactId in this._uiConvByContactId) {
-        let uiConv = this._uiConvByContactId[contactId];
-        uiConv.target = aPrplConversation;
-        this._uiConv[aPrplConversation.id] = uiConv;
-        return;
-      }
-    }
-
-    let newUIConv = new UIConversation(aPrplConversation);
-    this._uiConv[aPrplConversation.id] = newUIConv;
-    if (contactId) {
-      this._uiConvByContactId[contactId] = newUIConv;
-    }
-  },
-  removeConversation(aPrplConversation) {
-    Services.obs.notifyObservers(aPrplConversation, "conversation-closed");
-
-    let uiConv = this.getUIConversation(aPrplConversation);
-    delete this._uiConv[aPrplConversation.id];
-    let contactId = {};
-    if (uiConv.removeTarget(aPrplConversation, contactId)) {
-      if (contactId.value) {
-        delete this._uiConvByContactId[contactId.value];
-      }
-      Services.obs.notifyObservers(uiConv, "ui-conversation-closed");
-    }
-    this.forgetConversation(aPrplConversation);
-  },
-  forgetConversation(aPrplConversation) {
-    aPrplConversation.unInit();
-
-    this._prplConversations = this._prplConversations.filter(
-      c => c !== aPrplConversation
-    );
-  },
-
-  getUIConversations() {
-    let rv = [];
-    if (this._uiConv) {
-      for (let prplConvId in this._uiConv) {
-        // Since an UIConversation may be linked to multiple prplConversations,
-        // we must ensure we don't return the same UIConversation twice,
-        // by checking the id matches that of the active prplConversation.
-        let uiConv = this._uiConv[prplConvId];
-        if (prplConvId == uiConv.target.id) {
-          rv.push(uiConv);
-        }
-      }
-    }
-    return rv;
-  },
-  getUIConversation(aPrplConversation) {
-    let id = aPrplConversation.id;
-    if (this._uiConv && id in this._uiConv) {
-      return this._uiConv[id];
-    }
-    throw new Error("Unknown conversation");
-  },
-  getUIConversationByContactId(aId) {
-    return aId in this._uiConvByContactId ? this._uiConvByContactId[aId] : null;
-  },
-
-  getConversations() {
-    return new nsSimpleEnumerator(this._prplConversations);
-  },
-  getConversationById(aId) {
-    for (let conv of this._prplConversations) {
-      if (conv.id == aId) {
-        return conv;
-      }
-    }
-    return null;
-  },
-  getConversationByNameAndAccount(aName, aAccount, aIsChat) {
-    let normalizedName = aAccount.normalize(aName);
-    for (let conv of this._prplConversations) {
-      if (
-        aAccount.normalize(conv.name) == normalizedName &&
-        aAccount.numericId == conv.account.numericId &&
-        conv.isChat == aIsChat
-      ) {
-        return conv;
-      }
-    }
-    return null;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imIConversationsService]),
-};
deleted file mode 100644
--- a/chat/components/src/IMCore.jsm
+++ /dev/null
@@ -1,414 +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 EXPORTED_SYMBOLS = ["IMCore"];
-
-var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
-var {
-  XPCOMUtils,
-  ClassInfo,
-  initLogModule,
-  nsSimpleEnumerator,
-} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(
-  this,
-  "categoryManager",
-  "@mozilla.org/categorymanager;1",
-  "nsICategoryManager"
-);
-
-var kQuitApplicationGranted = "quit-application-granted";
-var kProtocolPluginCategory = "im-protocol-plugin";
-
-var kPrefReportIdle = "messenger.status.reportIdle";
-var kPrefUserIconFilename = "messenger.status.userIconFileName";
-var kPrefUserDisplayname = "messenger.status.userDisplayName";
-var kPrefTimeBeforeIdle = "messenger.status.timeBeforeIdle";
-var kPrefAwayWhenIdle = "messenger.status.awayWhenIdle";
-var kPrefDefaultMessage = "messenger.status.defaultIdleAwayMessage";
-
-var NS_IOSERVICE_GOING_OFFLINE_TOPIC = "network:offline-about-to-go-offline";
-var NS_IOSERVICE_OFFLINE_STATUS_TOPIC = "network:offline-status-changed";
-
-function UserStatus() {
-  this._observers = [];
-
-  if (Services.prefs.getBoolPref(kPrefReportIdle)) {
-    this._addIdleObserver();
-  }
-  Services.prefs.addObserver(kPrefReportIdle, this);
-
-  if (Services.io.offline) {
-    this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE;
-  }
-  Services.obs.addObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC);
-  Services.obs.addObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
-}
-UserStatus.prototype = {
-  __proto__: ClassInfo("imIUserStatusInfo", "User status info"),
-
-  unInit() {
-    this._observers = [];
-    Services.prefs.removeObserver(kPrefReportIdle, this);
-    if (this._observingIdleness) {
-      this._removeIdleObserver();
-    }
-    Services.obs.removeObserver(this, NS_IOSERVICE_GOING_OFFLINE_TOPIC);
-    Services.obs.removeObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
-  },
-  _observingIdleness: false,
-  _addIdleObserver() {
-    this._observingIdleness = true;
-    this._idleService = Cc["@mozilla.org/widget/idleservice;1"].getService(
-      Ci.nsIIdleService
-    );
-    Services.obs.addObserver(this, "im-sent");
-
-    this._timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle);
-    if (this._timeBeforeIdle < 0) {
-      this._timeBeforeIdle = 0;
-    }
-    Services.prefs.addObserver(kPrefTimeBeforeIdle, this);
-    if (this._timeBeforeIdle) {
-      this._idleService.addIdleObserver(this, this._timeBeforeIdle);
-    }
-  },
-  _removeIdleObserver() {
-    if (this._timeBeforeIdle) {
-      this._idleService.removeIdleObserver(this, this._timeBeforeIdle);
-    }
-
-    Services.prefs.removeObserver(kPrefTimeBeforeIdle, this);
-    delete this._timeBeforeIdle;
-
-    Services.obs.removeObserver(this, "im-sent");
-    delete this._idleService;
-    delete this._observingIdleness;
-  },
-
-  observe(aSubject, aTopic, aData) {
-    if (aTopic == "nsPref:changed") {
-      if (aData == kPrefReportIdle) {
-        let reportIdle = Services.prefs.getBoolPref(kPrefReportIdle);
-        if (reportIdle && !this._observingIdleness) {
-          this._addIdleObserver();
-        } else if (!reportIdle && this._observingIdleness) {
-          this._removeIdleObserver();
-        }
-      } else if (aData == kPrefTimeBeforeIdle) {
-        let timeBeforeIdle = Services.prefs.getIntPref(kPrefTimeBeforeIdle);
-        if (timeBeforeIdle != this._timeBeforeIdle) {
-          if (this._timeBeforeIdle) {
-            this._idleService.removeIdleObserver(this, this._timeBeforeIdle);
-          }
-          this._timeBeforeIdle = timeBeforeIdle;
-          if (this._timeBeforeIdle) {
-            this._idleService.addIdleObserver(this, this._timeBeforeIdle);
-          }
-        }
-      } else {
-        throw Cr.NS_ERROR_UNEXPECTED;
-      }
-    } else if (aTopic == NS_IOSERVICE_GOING_OFFLINE_TOPIC) {
-      this.offline = true;
-    } else if (
-      aTopic == NS_IOSERVICE_OFFLINE_STATUS_TOPIC &&
-      aData == "online"
-    ) {
-      this.offline = false;
-    } else {
-      this._checkIdle();
-    }
-  },
-
-  _offlineStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
-  set offline(aOffline) {
-    let statusType = this.statusType;
-    let statusText = this.statusText;
-    if (aOffline) {
-      this._offlineStatusType = Ci.imIStatusInfo.STATUS_OFFLINE;
-    } else {
-      delete this._offlineStatusType;
-    }
-    if (this.statusType != statusType || this.statusText != statusText) {
-      this._notifyObservers("status-changed", this.statusText);
-    }
-  },
-
-  _idleTime: 0,
-  get idleTime() {
-    return this._idleTime;
-  },
-  set idleTime(aIdleTime) {
-    this._idleTime = aIdleTime;
-    this._notifyObservers("idle-time-changed", aIdleTime);
-  },
-  _idle: false,
-  _idleStatusText: "",
-  _idleStatusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
-  _checkIdle() {
-    let idleTime = Math.floor(this._idleService.idleTime / 1000);
-    let idle = this._timeBeforeIdle && idleTime >= this._timeBeforeIdle;
-    if (idle == this._idle) {
-      return;
-    }
-
-    let statusType = this.statusType;
-    let statusText = this.statusText;
-    this._idle = idle;
-    if (idle) {
-      this.idleTime = idleTime;
-      if (Services.prefs.getBoolPref(kPrefAwayWhenIdle)) {
-        this._idleStatusType = Ci.imIStatusInfo.STATUS_AWAY;
-        this._idleStatusText = Services.prefs.getComplexValue(
-          kPrefDefaultMessage,
-          Ci.nsIPrefLocalizedString
-        ).data;
-      }
-    } else {
-      this.idleTime = 0;
-      delete this._idleStatusType;
-      delete this._idleStatusText;
-    }
-    if (this.statusType != statusType || this.statusText != statusText) {
-      this._notifyObservers("status-changed", this.statusText);
-    }
-  },
-
-  _statusText: "",
-  get statusText() {
-    return this._statusText || this._idleStatusText;
-  },
-  _statusType: Ci.imIStatusInfo.STATUS_AVAILABLE,
-  get statusType() {
-    return Math.min(
-      this._statusType,
-      this._idleStatusType,
-      this._offlineStatusType
-    );
-  },
-  setStatus(aStatus, aMessage) {
-    if (aStatus != Ci.imIStatusInfo.STATUS_UNKNOWN) {
-      this._statusType = aStatus;
-    }
-    if (aStatus != Ci.imIStatusInfo.STATUS_OFFLINE) {
-      this._statusText = aMessage;
-    }
-    this._notifyObservers("status-changed", aMessage);
-  },
-
-  _getProfileDir: () => Services.dirsvc.get("ProfD", Ci.nsIFile),
-  setUserIcon(aIconFile) {
-    let folder = this._getProfileDir();
-
-    let newName = "";
-    if (aIconFile) {
-      // Get the extension (remove trailing dots - invalid Windows extension).
-      let ext = aIconFile.leafName.replace(/.*(\.[a-z0-9]+)\.*/i, "$1");
-      // newName = userIcon-<timestamp(now)>.<aIconFile.extension>
-      newName = "userIcon-" + Math.floor(Date.now() / 1000) + ext;
-
-      // Copy the new icon file to newName in the profile folder.
-      aIconFile.copyTo(folder, newName);
-    }
-
-    // Get the previous file name before saving the new file name.
-    let oldFileName = Services.prefs.getCharPref(kPrefUserIconFilename);
-    Services.prefs.setCharPref(kPrefUserIconFilename, newName);
-
-    // Now that the new icon has been copied to the profile directory
-    // and the pref value changed, we can remove the old icon. Ignore
-    // failures so that we always fire the user-icon-changed notification.
-    try {
-      if (oldFileName) {
-        folder.append(oldFileName);
-        if (folder.exists()) {
-          folder.remove(false);
-        }
-      }
-    } catch (e) {
-      Cu.reportError(e);
-    }
-
-    this._notifyObservers("user-icon-changed", newName);
-  },
-  getUserIcon() {
-    let filename = Services.prefs.getCharPref(kPrefUserIconFilename);
-    if (!filename) {
-      // No icon has been set.
-      return null;
-    }
-
-    let file = this._getProfileDir();
-    file.append(filename);
-
-    if (!file.exists()) {
-      Services.console.logStringMessage("Invalid userIconFileName preference");
-      return null;
-    }
-
-    return Services.io.newFileURI(file);
-  },
-
-  get displayName() {
-    return Services.prefs.getStringPref(kPrefUserDisplayname);
-  },
-  set displayName(aDisplayName) {
-    Services.prefs.setStringPref(kPrefUserDisplayname, aDisplayName);
-    this._notifyObservers("user-display-name-changed", aDisplayName);
-  },
-
-  addObserver(aObserver) {
-    if (!this._observers.includes(aObserver)) {
-      this._observers.push(aObserver);
-    }
-  },
-  removeObserver(aObserver) {
-    this._observers = this._observers.filter(o => o !== aObserver);
-  },
-  _notifyObservers(aTopic, aData) {
-    for (let observer of this._observers) {
-      observer.observe(this, aTopic, aData);
-    }
-  },
-};
-
-function IMCore() {}
-IMCore.prototype = {
-  globalUserStatus: null,
-
-  _initialized: false,
-  get initialized() {
-    return this._initialized;
-  },
-  init() {
-    if (this._initialized) {
-      return;
-    }
-
-    initLogModule("core", this);
-
-    Services.obs.addObserver(this, kQuitApplicationGranted);
-    this._initialized = true;
-
-    Services.cmd.initCommands();
-    this._protos = {};
-
-    this.globalUserStatus = new UserStatus();
-    this.globalUserStatus.addObserver({
-      observe(aSubject, aTopic, aData) {
-        Services.obs.notifyObservers(aSubject, aTopic, aData);
-      },
-    });
-
-    let accounts = Services.accounts;
-    accounts.initAccounts();
-    Services.contacts.initContacts();
-    Services.conversations.initConversations();
-    Services.obs.notifyObservers(this, "prpl-init");
-
-    // Wait with automatic connections until the password service
-    // is available.
-    if (accounts.autoLoginStatus == Ci.imIAccountsService.AUTOLOGIN_ENABLED) {
-      Services.logins.initializationPromise.then(() => {
-        Services.accounts.processAutoLogin();
-      });
-    }
-  },
-  observe(aObject, aTopic, aData) {
-    if (aTopic == kQuitApplicationGranted) {
-      this.quit();
-    }
-  },
-  quit() {
-    if (!this._initialized) {
-      throw Cr.NS_ERROR_NOT_INITIALIZED;
-    }
-
-    Services.obs.removeObserver(this, kQuitApplicationGranted);
-    Services.obs.notifyObservers(this, "prpl-quit");
-
-    Services.conversations.unInitConversations();
-    Services.accounts.unInitAccounts();
-    Services.contacts.unInitContacts();
-    Services.cmd.unInitCommands();
-
-    this.globalUserStatus.unInit();
-    delete this.globalUserStatus;
-    delete this._protos;
-    delete this._initialized;
-  },
-
-  getProtocols() {
-    if (!this._initialized) {
-      throw Cr.NS_ERROR_NOT_INITIALIZED;
-    }
-
-    let protocols = [];
-    let entries = categoryManager.enumerateCategory(kProtocolPluginCategory);
-    while (entries.hasMoreElements()) {
-      let id = entries.getNext().QueryInterface(Ci.nsISupportsCString).data;
-
-      // If the preference is set to disable this prpl, don't show it in the
-      // full list of protocols.
-      let pref = "chat.prpls." + id + ".disable";
-      if (
-        Services.prefs.getPrefType(pref) == Services.prefs.PREF_BOOL &&
-        Services.prefs.getBoolPref(pref)
-      ) {
-        this.LOG("Disabling prpl: " + id);
-        continue;
-      }
-
-      let proto = this.getProtocolById(id);
-      if (proto) {
-        protocols.push(proto);
-      }
-    }
-    return new nsSimpleEnumerator(protocols);
-  },
-
-  getProtocolById(aPrplId) {
-    if (!this._initialized) {
-      throw Cr.NS_ERROR_NOT_INITIALIZED;
-    }
-
-    if (this._protos.hasOwnProperty(aPrplId)) {
-      return this._protos[aPrplId];
-    }
-
-    let cid;
-    try {
-      cid = categoryManager.getCategoryEntry(kProtocolPluginCategory, aPrplId);
-    } catch (e) {
-      return null; // no protocol registered for this id.
-    }
-
-    let proto = null;
-    try {
-      proto = Cc[cid].createInstance(Ci.prplIProtocol);
-    } catch (e) {
-      // This is a real error, the protocol is registered and failed to init.
-      let error = "failed to create an instance of " + cid + ": " + e;
-      dump(error + "\n");
-      Cu.reportError(error);
-    }
-    if (!proto) {
-      return null;
-    }
-
-    try {
-      proto.init(aPrplId);
-    } catch (e) {
-      Cu.reportError("Could not initialize protocol " + aPrplId + ": " + e);
-      return null;
-    }
-
-    this._protos[aPrplId] = proto;
-    return proto;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.imICoreService]),
-};
deleted file mode 100644
--- a/chat/components/src/Logger.jsm
+++ /dev/null
@@ -1,1144 +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 EXPORTED_SYMBOLS = [
-  "Logger",
-  "encodeName",
-  "convIsRealMUC",
-  "getLogFolderPathForAccount",
-  "getLogFilePathForConversation",
-  "getNewLogFileName",
-  "queueFileOperation",
-  "fileOperations",
-  "appendToFile",
-  "getLogWriter",
-  "closeLogWriter",
-];
-
-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"
-);
-var { ClassInfo } = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
-var { ToLocaleFormat } = ChromeUtils.import(
-  "resource:///modules/ToLocaleFormat.jsm"
-);
-var { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
-var { getHiddenHTMLWindow } = ChromeUtils.import(
-  "resource:///modules/hiddenWindow.jsm"
-);
-
-XPCOMUtils.defineLazyGetter(this, "_", () =>
-  l10nHelper("chrome://chat/locale/logger.properties")
-);
-
-var kLineBreak = "@mozilla.org/windows-registry-key;1" in Cc ? "\r\n" : "\n";
-
-/*
- * Maps file paths to promises returned by ongoing OS.File operations on them.
- * This is so that a file can be read after a pending write operation completes
- * and vice versa (opening a file multiple times concurrently may fail on Windows).
- */
-var fileOperations = new Map();
-
-// Uses above map to queue operations on a file.
-function queueFileOperation(aPath, aOperation) {
-  // Ensure the operation is queued regardless of whether the last one succeeded.
-  // This is safe since the promise is returned and consumers are expected to
-  // handle any errors. If there's no promise existing for the given path already,
-  // queue the operation on a dummy pre-resolved promise.
-  let promise = (fileOperations.get(aPath) || Promise.resolve()).then(
-    aOperation,
-    aOperation
-  );
-  fileOperations.set(aPath, promise);
-
-  let cleanup = () => {
-    // If no further operations have been queued, remove the reference from the map.
-    if (fileOperations.get(aPath) === promise) {
-      fileOperations.delete(aPath);
-    }
-  };
-  // Ensure we clear unused promises whether they resolved or rejected.
-  promise.then(cleanup, cleanup);
-
-  return promise;
-}
-
-/*
- * Convenience method to append to a file using the above queue system. If any of
- * the I/O operations reject, the returned promise will reject with the same reason.
- * We open the file, append, and close it immediately. The alternative is to keep
- * it open and append as required, but we want to make sure we don't open a file
- * for reading while it's already open for writing, so we close it every time
- * (opening a file multiple times concurrently may fail on Windows).
- * Note: This function creates parent directories if required.
- */
-function appendToFile(aPath, aEncodedString, aCreate) {
-  return queueFileOperation(aPath, async function() {
-    await OS.File.makeDir(OS.Path.dirname(aPath), {
-      ignoreExisting: true,
-      from: OS.Constants.Path.profileDir,
-    });
-    let file = await OS.File.open(aPath, { write: true, create: aCreate });
-    try {
-      await file.write(aEncodedString);
-    } finally {
-      /*
-       * If both the write() above and the close() below throw, and we don't
-       * handle the close error here, the promise will be rejected with the close
-       * error and the write error will be dropped. To avoid this, we log any
-       * close error here so that any write error will be propagated.
-       */
-      await file.close().catch(Cu.reportError);
-    }
-  });
-}
-
-OS.File.profileBeforeChange.addBlocker(
-  "Chat logger: writing all pending messages",
-  async function() {
-    for (let promise of fileOperations.values()) {
-      try {
-        await promise;
-      } catch (aError) {
-        // Ignore the error, whatever queued the operation will take care of it.
-      }
-    }
-  }
-);
-
-// This function checks names against OS naming conventions and alters them
-// accordingly so that they can be used as file/folder names.
-function encodeName(aName) {
-  // Reserved device names by Windows (prefixing "%").
-  let reservedNames = /^(CON|PRN|AUX|NUL|COM\d|LPT\d)$/i;
-  if (reservedNames.test(aName)) {
-    return "%" + aName;
-  }
-
-  // "." and " " must not be at the end of a file or folder name (appending "_").
-  if (/[\. _]/.test(aName.slice(-1))) {
-    aName += "_";
-  }
-
-  // Reserved characters are replaced by %[hex value]. encodeURIComponent() is
-  // not sufficient, nevertheless decodeURIComponent() can be used to decode.
-  function encodeReservedChars(match) {
-    return "%" + match.charCodeAt(0).toString(16);
-  }
-  return aName.replace(/[<>:"\/\\|?*&%]/g, encodeReservedChars);
-}
-
-function getLogFolderPathForAccount(aAccount) {
-  return OS.Path.join(
-    OS.Constants.Path.profileDir,
-    "logs",
-    aAccount.protocol.normalizedName,
-    encodeName(aAccount.normalizedName)
-  );
-}
-
-function getLogFilePathForConversation(aConv, aFormat, aStartTime) {
-  if (!aStartTime) {
-    aStartTime = aConv.startDate / 1000;
-  }
-  let path = getLogFolderPathForAccount(aConv.account);
-  let name = aConv.normalizedName;
-  if (convIsRealMUC(aConv)) {
-    name += ".chat";
-  }
-  return OS.Path.join(
-    path,
-    encodeName(name),
-    getNewLogFileName(aFormat, aStartTime)
-  );
-}
-
-function getNewLogFileName(aFormat, aStartTime) {
-  let date = aStartTime ? new Date(aStartTime) : new Date();
-  let dateTime = ToLocaleFormat("%Y-%m-%d.%H%M%S", date);
-  let offset = date.getTimezoneOffset();
-  if (offset < 0) {
-    dateTime += "+";
-    offset *= -1;
-  } else {
-    dateTime += "-";
-  }
-  let minutes = offset % 60;
-  offset = (offset - minutes) / 60;
-  function twoDigits(aNumber) {
-    if (aNumber == 0) {
-      return "00";
-    }
-    return aNumber < 10 ? "0" + aNumber : aNumber;
-  }
-  if (!aFormat) {
-    aFormat = "txt";
-  }
-  return dateTime + twoDigits(offset) + twoDigits(minutes) + "." + aFormat;
-}
-
-// One of these is maintained for every conversation being logged. It initializes
-// a log file and appends to it as required.
-function LogWriter(aConversation) {
-  this._conv = aConversation;
-  if (Services.prefs.getCharPref("purple.logging.format") == "json") {
-    this.format = "json";
-  }
-  this.paths = [];
-  this.startNewFile(this._conv.startDate / 1000);
-}
-LogWriter.prototype = {
-  // All log file paths used by this LogWriter.
-  paths: [],
-  // Path of the log file that is currently being written to.
-  get currentPath() {
-    return this.paths[this.paths.length - 1];
-  },
-  // Constructor sets this to a promise that will resolve when the log header
-  // has been written.
-  _initialized: null,
-  _startTime: null,
-  _lastMessageTime: null,
-  _messageCount: 0,
-  format: "txt",
-  encoder: new TextEncoder(),
-  startNewFile(aStartTime, aContinuedSession) {
-    // We start a new log file every 1000 messages. The start time of this new
-    // log file is the time of the next message. Since message times are in seconds,
-    // if we receive 1000 messages within a second after starting the new file,
-    // we will create another file, using the same start time - and so the same
-    // file name. To avoid this, ensure the new start time is at least one second
-    // greater than the current one. This is ugly, but should rarely be needed.
-    aStartTime = Math.max(aStartTime, this._startTime + 1000);
-    this._startTime = this._lastMessageTime = aStartTime;
-    this._messageCount = 0;
-    this.paths.push(
-      getLogFilePathForConversation(this._conv, this.format, aStartTime)
-    );
-    let account = this._conv.account;
-    let header;
-    if (this.format == "json") {
-      header = {
-        date: new Date(this._startTime),
-        name: this._conv.name,
-        title: this._conv.title,
-        account: account.normalizedName,
-        protocol: account.protocol.normalizedName,
-        isChat: this._conv.isChat,
-        normalizedName: this._conv.normalizedName,
-      };
-      if (aContinuedSession) {
-        header.continuedSession = true;
-      }
-      header = JSON.stringify(header) + "\n";
-    } else {
-      const dateTimeFormatter = new Services.intl.DateTimeFormat("en-US", {
-        dateStyle: "full",
-        timeStyle: "long",
-      });
-      header =
-        "Conversation with " +
-        this._conv.name +
-        " at " +
-        dateTimeFormatter.format(new Date(this._conv.startDate / 1000)) +
-        " on " +
-        account.name +
-        " (" +
-        account.protocol.normalizedName +
-        ")" +
-        kLineBreak;
-    }
-    this._initialized = appendToFile(
-      this.currentPath,
-      this.encoder.encode(header),
-      true
-    );
-    // Catch the error separately so that _initialized will stay rejected if
-    // writing the header failed.
-    this._initialized.catch(aError =>
-      Cu.reportError("Failed to initialize log file:\n" + aError)
-    );
-  },
-  _serialize(aString) {
-    // TODO cleanup once bug 102699 is fixed
-    let doc = getHiddenHTMLWindow().document;
-    let div = doc.createElementNS("http://www.w3.org/1999/xhtml", "div");
-    // eslint-disable-next-line no-unsanitized/property
-    div.innerHTML = aString
-      .replace(/\r?\n/g, "<br/>")
-      .replace(/<br>/gi, "<br/>");
-    const type = "text/plain";
-    let encoder = Cu.createDocumentEncoder(type);
-    encoder.init(doc, type, 0);
-    encoder.setContainerNode(div);
-    encoder.setNodeFixup({
-      fixupNode(aNode, aSerializeKids) {
-        if (aNode.localName == "a" && aNode.hasAttribute("href")) {
-          let url = aNode.getAttribute("href");
-          let content = aNode.textContent;
-          if (url != content) {
-            aNode.textContent = content + " (" + url + ")";
-          }
-        }
-        return null;
-      },
-    });
-    return encoder.encodeToString();
-  },
-  // We start a new log file in the following cases:
-  // - If it has been 30 minutes since the last message.
-  kInactivityLimit: 30 * 60 * 1000,
-  // - If at midnight, it's been longer than 3 hours since we started the file.
-  kDayOverlapLimit: 3 * 60 * 60 * 1000,
-  // - After every 1000 messages.
-  kMessageCountLimit: 1000,
-  logMessage(aMessage) {
-    // aMessage.time is in seconds, we need it in milliseconds.
-    let messageTime = aMessage.time * 1000;
-    let messageMidnight = new Date(messageTime).setHours(0, 0, 0, 0);
-
-    let inactivityLimitExceeded =
-      !aMessage.delayed &&
-      messageTime - this._lastMessageTime > this.kInactivityLimit;
-    let dayOverlapLimitExceeded =
-      !aMessage.delayed &&
-      messageMidnight - this._startTime > this.kDayOverlapLimit;
-
-    if (
-      inactivityLimitExceeded ||
-      dayOverlapLimitExceeded ||
-      this._messageCount == this.kMessageCountLimit
-    ) {
-      // We start a new session if the inactivity limit was exceeded.
-      this.startNewFile(messageTime, !inactivityLimitExceeded);
-    }
-    ++this._messageCount;
-
-    if (!aMessage.delayed) {
-      this._lastMessageTime = messageTime;
-    }
-
-    let lineToWrite;
-    if (this.format == "json") {
-      let msg = {
-        date: new Date(messageTime),
-        who: aMessage.who,
-        text: aMessage.displayMessage,
-        flags: [
-          "outgoing",
-          "incoming",
-          "system",
-          "autoResponse",
-          "containsNick",
-          "error",
-          "delayed",
-          "noFormat",
-          "containsImages",
-          "notification",
-          "noLinkification",
-        ].filter(f => aMessage[f]),
-      };
-      let alias = aMessage.alias;
-      if (alias && alias != msg.who) {
-        msg.alias = alias;
-      }
-      lineToWrite = JSON.stringify(msg) + "\n";
-    } else {
-      // Text log.
-      let date = new Date(messageTime);
-      let line = "(" + date.toLocaleTimeString() + ") ";
-      let msg = this._serialize(aMessage.displayMessage);
-      if (aMessage.system) {
-        line += msg;
-      } else {
-        let sender = aMessage.alias || aMessage.who;
-        if (aMessage.autoResponse) {
-          line += sender + " <AUTO-REPLY>: " + msg;
-        } else if (msg.startsWith("/me ")) {
-          line += "***" + sender + " " + msg.substr(4);
-        } else {
-          line += sender + ": " + msg;
-        }
-      }
-      lineToWrite = line + kLineBreak;
-    }
-    lineToWrite = this.encoder.encode(lineToWrite);
-    this._initialized.then(() => {
-      appendToFile(this.currentPath, lineToWrite).catch(aError =>
-        Cu.reportError("Failed to log message:\n" + aError)
-      );
-    });
-  },
-};
-
-var dummyLogWriter = {
-  paths: null,
-  currentPath: null,
-  logMessage() {},
-};
-
-var gLogWritersById = new Map();
-function getLogWriter(aConversation) {
-  let id = aConversation.id;
-  if (!gLogWritersById.has(id)) {
-    let prefName =
-      "purple.logging.log_" + (aConversation.isChat ? "chats" : "ims");
-    if (Services.prefs.getBoolPref(prefName)) {
-      gLogWritersById.set(id, new LogWriter(aConversation));
-    } else {
-      gLogWritersById.set(id, dummyLogWriter);
-    }
-  }
-  return gLogWritersById.get(id);
-}
-
-function closeLogWriter(aConversation) {
-  gLogWritersById.delete(aConversation.id);
-}
-
-// LogWriter for system logs.
-function SystemLogWriter(aAccount) {
-  this._account = aAccount;
-  this.path = OS.Path.join(
-    getLogFolderPathForAccount(aAccount),
-    ".system",
-    getNewLogFileName()
-  );
-  const dateTimeFormatter = new Services.intl.DateTimeFormat("en-US", {
-    dateStyle: "full",
-    timeStyle: "long",
-  });
-  let header =
-    "System log for account " +
-    aAccount.name +
-    " (" +
-    aAccount.protocol.normalizedName +
-    ") connected at " +
-    dateTimeFormatter.format(new Date()) +
-    kLineBreak;
-  this._initialized = appendToFile(
-    this.path,
-    this.encoder.encode(header),
-    true
-  );
-  // Catch the error separately so that _initialized will stay rejected if
-  // writing the header failed.
-  this._initialized.catch(aError =>
-    Cu.reportError("Error initializing system log:\n" + aError)
-  );
-}
-SystemLogWriter.prototype = {
-  encoder: new TextEncoder(),
-  // Constructor sets this to a promise that will resolve when the log header
-  // has been written.
-  _initialized: null,
-  path: null,
-  logEvent(aString) {
-    let date = ToLocaleFormat("%x %X", new Date());
-    let lineToWrite = this.encoder.encode(
-      "---- " + aString + " @ " + date + " ----" + kLineBreak
-    );
-    this._initialized.then(() => {
-      appendToFile(this.path, lineToWrite).catch(aError =>
-        Cu.reportError("Failed to log event:\n" + aError)
-      );
-    });
-  },
-};
-
-var dummySystemLogWriter = {
-  path: null,
-  logEvent() {},
-};
-
-var gSystemLogWritersById = new Map();
-function getSystemLogWriter(aAccount, aCreate) {
-  let id = aAccount.id;
-  if (aCreate) {
-    if (!Services.prefs.getBoolPref("purple.logging.log_system")) {
-      return dummySystemLogWriter;
-    }
-    let writer = new SystemLogWriter(aAccount);
-    gSystemLogWritersById.set(id, writer);
-    return writer;
-  }
-
-  return (
-    (gSystemLogWritersById.has(id) && gSystemLogWritersById.get(id)) ||
-    dummySystemLogWriter
-  );
-}
-
-function closeSystemLogWriter(aAccount) {
-  gSystemLogWritersById.delete(aAccount.id);
-}
-
-/**
- * Takes a properly formatted log file name and extracts the date information
- * and filetype, returning the results as an Array.
- *
- * Filenames are expected to be formatted as:
- *
- * YYYY-MM-DD.HHmmSS+ZZzz.format
- *
- * @param aFilename the name of the file
- * @returns an Array, where the first element is a Date object for the date
- *          that the log file represents, and the file type as a string.
- */
-function getDateFromFilename(aFilename) {
-  const kRegExp = /([\d]{4})-([\d]{2})-([\d]{2}).([\d]{2})([\d]{2})([\d]{2})([+-])([\d]{2})([\d]{2}).*\.([A-Za-z]+)$/;
-
-  let r = aFilename.match(kRegExp);
-  if (!r) {
-    Cu.reportError(
-      "Found log file with name not matching YYYY-MM-DD.HHmmSS+ZZzz.format: " +
-        aFilename
-    );
-    return [];
-  }
-
-  // We ignore the timezone offset for now (FIXME)
-  return [new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]), r[10]];
-}
-
-/**
- * Returns true if a Conversation is both a chat conversation, and not
- * a Twitter conversation.
- */
-function convIsRealMUC(aConversation) {
-  return (
-    aConversation.isChat && aConversation.account.protocol.id != "prpl-twitter"
-  );
-}
-
-function LogMessage(aData, aConversation) {
-  this._init(aData.who, aData.text);
-  this._conversation = aConversation;
-  this.time = Math.round(new Date(aData.date) / 1000);
-  if ("alias" in aData) {
-    this._alias = aData.alias;
-  }
-  for (let flag of aData.flags) {
-    this[flag] = true;
-  }
-}
-
-LogMessage.prototype = {
-  __proto__: GenericMessagePrototype,
-  _interfaces: [Ci.imIMessage, Ci.prplIMessage],
-  get displayMessage() {
-    return this.originalMessage;
-  },
-};
-
-function LogConversation(aMessages, aProperties) {
-  this._messages = aMessages;
-  for (let property in aProperties) {
-    this[property] = aProperties[property];
-  }
-}
-LogConversation.prototype = {
-  __proto__: ClassInfo("imILogConversation", "Log conversation object"),
-  get isChat() {
-    return this._isChat;
-  },
-  get buddy() {
-    return null;
-  },
-  get account() {
-    return {
-      alias: "",
-      name: this._accountName,
-      normalizedName: this._accountName,
-      protocol: { name: this._protocolName },
-      statusInfo: Services.core.globalUserStatus,
-    };
-  },
-  getMessages() {
-    return this._messages.map(m => new LogMessage(m, this));
-  },
-  getMessagesEnumerator(aMessageCount) {
-    if (aMessageCount) {
-      aMessageCount.value = this._messages.length;
-    }
-    let enumerator = {
-      _index: 0,
-      _conv: this,
-      _messages: this._messages,
-      hasMoreElements() {
-        return this._index < this._messages.length;
-      },
-      getNext() {
-        return new LogMessage(this._messages[this._index++], this._conv);
-      },
-      QueryInterface: ChromeUtils.generateQI([Ci.nsISimpleEnumerator]),
-      *[Symbol.iterator]() {
-        while (this.hasMoreElements()) {
-          yield this.getNext();
-        }
-      },
-    };
-    return enumerator;
-  },
-};
-
-/**
- * A Log object represents one or more log files. The constructor expects one
- * argument, which is either a single path to a (json or txt) log file or an
- * array of objects each having two properties:
- *   path: The full path of the (json only) log file it represents.
- *   time: The Date object extracted from the filename of the logfile.
- *
- * The returned Log object's time property will be:
- *   For a single file - exact time extracted from the name of the log file.
- *   For a set of files - the time extracted, reduced to the day.
- */
-function Log(aEntries) {
-  if (typeof aEntries == "string") {
-    // Assume that aEntries is a single path.
-    let path = aEntries;
-    this.path = path;
-    let [date, format] = getDateFromFilename(OS.Path.basename(path));
-    if (!date || !format) {
-      this.format = "invalid";
-      this.time = 0;
-      return;
-    }
-    this.time = date.valueOf() / 1000;
-    this.format = format;
-    // Wrap the path in an array
-    this._entryPaths = [path];
-    return;
-  }
-
-  if (!aEntries.length) {
-    throw new Error(
-      "Log was passed an invalid argument, " +
-        "expected a non-empty array or a string."
-    );
-  }
-
-  // Assume aEntries is an array of objects.
-  // Sort our list of entries for this day in increasing order.
-  aEntries.sort((aLeft, aRight) => aLeft.time - aRight.time);
-
-  this._entryPaths = aEntries.map(entry => entry.path);
-  // Calculate the timestamp for the first entry down to the day.
-  let timestamp = new Date(aEntries[0].time);
-  timestamp.setHours(0);
-  timestamp.setMinutes(0);
-  timestamp.setSeconds(0);
-  this.time = timestamp.valueOf() / 1000;
-  // Path is used to uniquely identify a Log, and sometimes used to
-  // quickly determine which directory a log file is from.  We'll use
-  // the first file's path.
-  this.path = aEntries[0].path;
-}
-Log.prototype = {
-  __proto__: ClassInfo("imILog", "Log object"),
-  _entryPaths: null,
-  format: "json",
-  async getConversation() {
-    /*
-     * Read the set of log files asynchronously and return a promise that
-     * resolves to a LogConversation instance. Even if a file contains some
-     * junk (invalid JSON), messages that are valid will be read. If the first
-     * line of metadata is corrupt however, the data isn't useful and the
-     * promise will resolve to null.
-     */
-    if (this.format != "json") {
-      return null;
-    }
-    let messages = [];
-    let properties = {};
-    let firstFile = true;
-    let decoder = new TextDecoder();
-    for (let path of this._entryPaths) {
-      let lines;
-      try {
-        let contents = await queueFileOperation(path, () => OS.File.read(path));
-        lines = decoder.decode(contents).split("\n");
-      } catch (aError) {
-        Cu.reportError('Error reading log file "' + path + '":\n' + aError);
-        continue;
-      }
-      let nextLine = lines.shift();
-      let filename = OS.Path.basename(path);
-
-      let data;
-      try {
-        // This will fail if either nextLine is undefined, or not valid JSON.
-        data = JSON.parse(nextLine);
-      } catch (aError) {
-        messages.push({
-          who: "sessionstart",
-          date: getDateFromFilename(filename)[0],
-          text: _("badLogfile", filename),
-          flags: ["noLog", "notification", "error", "system"],
-        });
-        continue;
-      }
-
-      if (firstFile || !data.continuedSession) {
-        messages.push({
-          who: "sessionstart",
-          date: getDateFromFilename(filename)[0],
-          text: "",
-          flags: ["noLog", "notification"],
-        });
-      }
-
-      if (firstFile) {
-        properties.startDate = new Date(data.date) * 1000;
-        properties.name = data.name;
-        properties.title = data.title;
-        properties._accountName = data.account;
-        properties._protocolName = data.protocol;
-        properties._isChat = data.isChat;
-        properties.normalizedName = data.normalizedName;
-        firstFile = false;
-      }
-
-      while (lines.length) {
-        nextLine = lines.shift();
-        if (!nextLine) {
-          break;
-        }
-        try {
-          messages.push(JSON.parse(nextLine));
-        } catch (e) {
-          // If a message line contains junk, just ignore the error and
-          // continue reading the conversation.
-        }
-      }
-    }
-
-    if (firstFile) {
-      // All selected log files are invalid.
-      return null;
-    }
-
-    return new LogConversation(messages, properties);
-  },
-};
-
-/**
- * Log enumerators provide lists of log files ("entries"). aEntries is an array
- * of the OS.File.DirectoryIterator.Entry instances which represent the log
- * files to be parsed.
- *
- * DailyLogEnumerator organizes entries by date, and enumerates them in order.
- * LogEnumerator enumerates logs in the same order as the input array.
- */
-function DailyLogEnumerator(aEntries) {
-  this._entries = {};
-
-  for (let entry of aEntries) {
-    let path = entry.path;
-
-    let [logDate, logFormat] = getDateFromFilename(OS.Path.basename(path));
-    if (!logDate) {
-      // We'll skip this one, since it's got a busted filename.
-      continue;
-    }
-
-    let dateForID = new Date(logDate);
-    let dayID;
-    if (logFormat == "json") {
-      // We want to cluster all of the logs that occur on the same day
-      // into the same Arrays. We clone the date for the log, reset it to
-      // the 0th hour/minute/second, and use that to construct an ID for the
-      // Array we'll put the log in.
-      dateForID.setHours(0);
-      dateForID.setMinutes(0);
-      dateForID.setSeconds(0);
-      dayID = dateForID.toISOString();
-
-      if (!(dayID in this._entries)) {
-        this._entries[dayID] = [];
-      }
-
-      this._entries[dayID].push({
-        path,
-        time: logDate,
-      });
-    } else {
-      // Add legacy text logs as individual paths.
-      dayID = dateForID.toISOString() + "txt";
-      this._entries[dayID] = path;
-    }
-  }
-
-  this._days = Object.keys(this._entries).sort();
-  this._index = 0;
-}
-DailyLogEnumerator.prototype = {
-  _entries: {},
-  _days: [],
-  _index: 0,
-  hasMoreElements() {
-    return this._index < this._days.length;
-  },
-  getNext() {
-    let dayID = this._days[this._index++];
-    return new Log(this._entries[dayID]);
-  },
-  QueryInterface: ChromeUtils.generateQI([Ci.nsISimpleEnumerator]),
-  *[Symbol.iterator]() {
-    while (this.hasMoreElements()) {
-      yield this.getNext();
-    }
-  },
-};
-
-function LogEnumerator(aEntries) {
-  this._entries = aEntries;
-  this._entries.sort((a, b) => a.name > b.name);
-}
-LogEnumerator.prototype = {
-  _entries: [],
-  hasMoreElements() {
-    return this._entries.length > 0;
-  },
-  getNext() {
-    // Create and return a log from the first entry.
-    return new Log(this._entries.shift().path);
-  },
-  QueryInterface: ChromeUtils.generateQI([Ci.nsISimpleEnumerator]),
-  *[Symbol.iterator]() {
-    while (this.hasMoreElements()) {
-      yield this.getNext();
-    }
-  },
-};
-
-function Logger() {}
-Logger.prototype = {
-  // Returned Promise resolves to an array of entries for the
-  // log folder if it exists, otherwise null.
-  async _getLogArray(aAccount, aNormalizedName) {
-    let iterator, path;
-    try {
-      path = OS.Path.join(
-        getLogFolderPathForAccount(aAccount),
-        encodeName(aNormalizedName)
-      );
-      if (await queueFileOperation(path, () => OS.File.exists(path))) {
-        iterator = new OS.File.DirectoryIterator(path);
-        let entries = await iterator.nextBatch();
-        iterator.close();
-        return entries;
-      }
-    } catch (aError) {
-      if (iterator) {
-        iterator.close();
-      }
-      Cu.reportError(
-        'Error getting directory entries for "' + path + '":\n' + aError
-      );
-    }
-    return [];
-  },
-  getLogFromFile(aFilePath, aGroupByDay) {
-    if (!aGroupByDay) {
-      return Promise.resolve(new Log(aFilePath));
-    }
-    let [targetDate] = getDateFromFilename(OS.Path.basename(aFilePath));
-    if (!targetDate) {
-      return null;
-    }
-
-    targetDate = targetDate.toDateString();
-
-    // We'll assume that the files relevant to our interests are
-    // in the same folder as the one provided.
-    let iterator = new OS.File.DirectoryIterator(OS.Path.dirname(aFilePath));
-    let relevantEntries = [];
-    return iterator
-      .forEach(function(aEntry) {
-        if (aEntry.isDir) {
-          return;
-        }
-        let path = aEntry.path;
-        let [logTime] = getDateFromFilename(OS.Path.basename(path));
-
-        // If someone placed a 'foreign' file into the logs directory,
-        // pattern matching fails and getDateFromFilename() returns [].
-        if (logTime && targetDate == logTime.toDateString()) {
-          relevantEntries.push({
-            path,
-            time: logTime,
-          });
-        }
-      })
-      .then(
-        () => {
-          iterator.close();
-          return new Log(relevantEntries);
-        },
-        aError => {
-          iterator.close();
-          throw aError;
-        }
-      );
-  },
-  // Creates and returns the appropriate LogEnumerator for the given log array
-  // depending on aGroupByDay, or an EmptyEnumerator if the input array is empty.
-  _getEnumerator(aLogArray, aGroupByDay) {
-    let enumerator = aGroupByDay ? DailyLogEnumerator : LogEnumerator;
-    return aLogArray.length ? new enumerator(aLogArray) : EmptyEnumerator;
-  },
-  async getLogPathsForConversation(aConversation) {
-    let writer = gLogWritersById.get(aConversation.id);
-    // Resolve to null if we haven't created a LogWriter yet for this conv, or
-    // if logging is disabled (paths will be null).
-    if (!writer || !writer.paths) {
-      return null;
-    }
-    let paths = writer.paths;
-    // Wait for any pending file operations to finish, then resolve to the paths
-    // regardless of whether these operations succeeded.
-    for (let path of paths) {
-      await fileOperations.get(path);
-    }
-    return paths;
-  },
-  getLogsForAccountAndName(aAccount, aNormalizedName, aGroupByDay) {
-    return this._getLogArray(aAccount, aNormalizedName).then(aEntries =>
-      this._getEnumerator(aEntries, aGroupByDay)
-    );
-  },
-  getLogsForAccountBuddy(aAccountBuddy, aGroupByDay) {
-    return this.getLogsForAccountAndName(
-      aAccountBuddy.account,
-      aAccountBuddy.normalizedName,
-      aGroupByDay
-    );
-  },
-  async getLogsForBuddy(aBuddy, aGroupByDay) {
-    let entries = [];
-    for (let accountBuddy of aBuddy.getAccountBuddies()) {
-      entries = entries.concat(
-        await this._getLogArray(
-          accountBuddy.account,
-          accountBuddy.normalizedName
-        )
-      );
-    }
-    return this._getEnumerator(entries, aGroupByDay);
-  },
-  async getLogsForContact(aContact, aGroupByDay) {
-    let entries = [];
-    for (let buddy of aContact.getBuddies()) {
-      for (let accountBuddy of buddy.getAccountBuddies()) {
-        entries = entries.concat(
-          await this._getLogArray(
-            accountBuddy.account,
-            accountBuddy.normalizedName
-          )
-        );
-      }
-    }
-    return this._getEnumerator(entries, aGroupByDay);
-  },
-  getLogsForConversation(aConversation, aGroupByDay) {
-    let name = aConversation.normalizedName;
-    if (convIsRealMUC(aConversation)) {
-      name += ".chat";
-    }
-    return this.getLogsForAccountAndName(
-      aConversation.account,
-      name,
-      aGroupByDay
-    );
-  },
-  getSystemLogsForAccount(aAccount) {
-    return this.getLogsForAccountAndName(aAccount, ".system");
-  },
-  async getSimilarLogs(aLog, aGroupByDay) {
-    let iterator = new OS.File.DirectoryIterator(OS.Path.dirname(aLog.path));
-    let entries;
-    try {
-      entries = await iterator.nextBatch();
-    } catch (aError) {
-      Cu.reportError(
-        'Error getting similar logs for "' + aLog.path + '":\n' + aError
-      );
-    }
-    // If there was an error, this will return an EmptyEnumerator.
-    return this._getEnumerator(entries, aGroupByDay);
-  },
-
-  getLogFolderPathForAccount(aAccount) {
-    return getLogFolderPathForAccount(aAccount);
-  },
-
-  deleteLogFolderForAccount(aAccount) {
-    if (!aAccount.disconnecting && !aAccount.disconnected) {
-      throw new Error(
-        "Account must be disconnected first before deleting logs."
-      );
-    }
-
-    if (aAccount.disconnecting) {
-      Cu.reportError(
-        "Account is still disconnecting while we attempt to remove logs."
-      );
-    }
-
-    let logPath = this.getLogFolderPathForAccount(aAccount);
-    // Find all operations on files inside the log folder.
-    let pendingPromises = [];
-    function checkLogFiles(promiseOperation, filePath) {
-      if (filePath.startsWith(logPath)) {
-        pendingPromises.push(promiseOperation);
-      }
-    }
-    fileOperations.forEach(checkLogFiles);
-    // After all operations finish, remove the whole log folder.
-    return Promise.all(pendingPromises)
-      .then(values => {
-        OS.File.removeDir(logPath, { ignoreAbsent: true });
-      })
-      .catch(aError =>
-        Cu.reportError("Failed to remove log folders:\n" + aError)
-      );
-  },
-
-  async forEach(aCallback) {
-    let getAllSubdirs = async function(aPaths, aErrorMsg) {
-      let entries = [];
-      for (let path of aPaths) {
-        let iterator = new OS.File.DirectoryIterator(path);
-        try {
-          entries = entries.concat(await iterator.nextBatch());
-        } catch (aError) {
-          if (aErrorMsg) {
-            Cu.reportError(aErrorMsg + "\n" + aError);
-          }
-        } finally {
-          iterator.close();
-        }
-      }
-      entries = entries
-        .filter(aEntry => aEntry.isDir)
-        .map(aEntry => aEntry.path);
-      return entries;
-    };
-
-    let logsPath = OS.Path.join(OS.Constants.Path.profileDir, "logs");
-    let prpls = await getAllSubdirs([logsPath]);
-    let accounts = await getAllSubdirs(
-      prpls,
-      "Error while sweeping prpl folder:"
-    );
-    let logFolders = await getAllSubdirs(
-      accounts,
-      "Error while sweeping account folder:"
-    );
-    for (let folder of logFolders) {
-      let iterator = new OS.File.DirectoryIterator(folder);
-      try {
-        await iterator.forEach(aEntry => {
-          if (aEntry.isDir || !aEntry.name.endsWith(".json")) {
-            return null;
-          }
-          return aCallback.processLog(aEntry.path);
-        });
-      } catch (aError) {
-        // If the callback threw, reject the promise and let the caller handle it.
-        if (!(aError instanceof OS.File.Error)) {
-          throw aError;
-        }
-        Cu.reportError("Error sweeping log folder:\n" + aError);
-      } finally {
-        iterator.close();
-      }
-    }
-  },
-
-  observe(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "profile-after-change":
-        Services.obs.addObserver(this, "final-ui-startup");
-        break;
-      case "final-ui-startup":
-        Services.obs.removeObserver(this, "final-ui-startup");
-        [
-          "new-text",
-          "conversation-closed",
-          "conversation-left-chat",
-          "account-connected",
-          "account-disconnected",
-          "account-buddy-status-changed",
-        ].forEach(function(aEvent) {
-          Services.obs.addObserver(this, aEvent);
-        }, this);
-        break;
-      case "new-text":
-        let excludeBecauseEncrypted = false;
-        if (aSubject.encrypted) {
-          excludeBecauseEncrypted = !Services.prefs.getBoolPref(
-            "messenger.account." +
-              aSubject.conversation.account.id +
-              ".options.otrAllowMsgLog",
-            Services.prefs.getBoolPref("chat.otr.default.allowMsgLog")
-          );
-        }
-        if (!aSubject.noLog && !excludeBecauseEncrypted) {
-          let log = getLogWriter(aSubject.conversation);
-          log.logMessage(aSubject);
-        }
-        break;
-      case "conversation-closed":
-      case "conversation-left-chat":
-        closeLogWriter(aSubject);
-        break;
-      case "account-connected":
-        getSystemLogWriter(aSubject, true).logEvent(
-          "+++ " + aSubject.name + " signed on"
-        );
-        break;
-      case "account-disconnected":
-        getSystemLogWriter(aSubject).logEvent(
-          "+++ " + aSubject.name + " signed off"
-        );
-        closeSystemLogWriter(aSubject);
-        break;
-      case "account-buddy-status-changed":
-        let status;
-        if (!aSubject.online) {
-          status = "Offline";
-        } else if (aSubject.mobile) {
-          status = "Mobile";
-        } else if (aSubject.idle) {
-          status = "Idle";
-        } else if (aSubject.available) {
-          status = "Available";
-        } else {
-          status = "Unavailable";
-        }
-
-        let statusText = aSubject.statusText;
-        if (statusText) {
-          status += ' ("' + statusText + '")';
-        }
-
-        let nameText = aSubject.displayName + " (" + aSubject.userName + ")";
-        getSystemLogWriter(aSubject.account).logEvent(
-          nameText + " is now " + status
-        );
-        break;
-      default:
-        throw new Error("Unexpected notification " + aTopic);
-    }
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver, Ci.imILogger]),
-};
deleted file mode 100644
--- a/chat/components/src/SmileProtocolHandler.jsm
+++ /dev/null
@@ -1,36 +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 EXPORTED_SYMBOLS = ["SmileProtocolHandler"];
-
-var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-var { getSmileRealURI } = ChromeUtils.import(
-  "resource:///modules/imSmileys.jsm"
-);
-
-var kSmileRegexp = /^smile:\/\//;
-
-function smileProtocolHandler() {}
-
-smileProtocolHandler.prototype = {
-  scheme: "smile",
-  defaultPort: -1,
-  protocolFlags:
-    Ci.nsIProtocolHandler.URI_NORELATIVE |
-    Ci.nsIProtocolHandler.URI_NOAUTH |
-    Ci.nsIProtocolHandler.URI_IS_UI_RESOURCE |
-    Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE,
-  newChannel(aURI, aLoadInfo) {
-    let smile = aURI.spec.replace(kSmileRegexp, "");
-    let uri = Services.io.newURI(getSmileRealURI(smile));
-    let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
-    channel.originalURI = aURI;
-    return channel;
-  },
-  allowPort(aPort, aScheme) {
-    return false;
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIProtocolHandler]),
-};
deleted file mode 100644
--- a/chat/components/src/components.conf
+++ /dev/null
@@ -1,57 +0,0 @@
-# -*- 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': 'IMCommands',
-  },
-  {
-    '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': 'IMCore',
-  },
-  {
-    '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',
-  },
-]
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imAccounts.js
@@ -0,0 +1,1302 @@
+/* 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 {
+  ClassInfo,
+  EmptyEnumerator,
+  nsSimpleEnumerator,
+  XPCOMUtils,
+  setTimeout,
+  clearTimeout,
+  executeSoon,
+  l10nHelper,
+} = ChromeUtils.import("resource:///modules/imXPCOMUtils.jsm");
+var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
+var {
+  GenericAccountPrototype,
+  GenericAccountBuddyPrototype,
+} = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
+
+var kPrefAutologinPending = "messenger.accounts.autoLoginPending";
+var kPrefMessengerAccounts = "messenger.accounts";
+var kPrefAccountPrefix = "messenger.account.";
+var kAccountKeyPrefix = "account";
+var kAccountOptionPrefPrefix = "options.";
+var kPrefAccountName = "name";
+var kPrefAccountPrpl = "prpl";
+var kPrefAccountAutoLogin = "autoLogin";
+var kPrefAccountAutoJoin = "autoJoin";
+var kPrefAccountAlias = "alias";
+var kPrefAccountFirstConnectionState = "firstConnectionState";
+
+var kPrefConvertOldPasswords = "messenger.accounts.convertOldPasswords";
+var kPrefAccountPassword = "password";
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/locale/accounts.properties")
+);
+
+XPCOMUtils.defineLazyGetter(this, "_maxDebugMessages", () =>
+  Services.prefs.getIntPref("messenger.accounts.maxDebugMessages")
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+  this,
+  "HttpProtocolHandler",
+  "@mozilla.org/network/protocol;1?name=http",
+  "nsIHttpProtocolHandler"
+);
+
+var gUserCanceledMasterPasswordPrompt = false;
+var gConvertingOldPasswords = false;
+
+var SavePrefTimer = {
+  saveNow() {
+    if (this._timer) {
+      clearTimeout(this._timer);
+      this._timer = null;
+    }
+    Services.prefs.savePrefFile(null);
+  },
+  _timer: null,
+  unInitTimer() {
+    if (this._timer) {
+      this.saveNow();
+    }
+  },
+  initTimer() {
+    if (!this._timer) {
+      this._timer = setTimeout(this.saveNow.bind(this), 5000);
+    }
+  },
+};
+
+var AutoLoginCounter = {
+  _count: 0,
+  startAutoLogin() {
+    ++this._count;
+    if (this._count != 1) {
+      return;
+    }
+    Services.prefs.setIntPref(kPrefAutologinPending, Date.now() / 1000);
+    SavePrefTimer.saveNow();
+  },
+  finishedAutoLogin() {
+    --this._count;
+    if (this._count != 0) {
+      return;
+    }
+    Services.prefs.deleteBranch(kPrefAutologinPending);
+    SavePrefTimer.initTimer();
+  },
+};
+
+function UnknownProtocol(aPrplId) {
+  this.id = aPrplId;
+}
+UnknownProtocol.prototype = {
+  __proto__: ClassInfo("prplIProtocol", "Unknown protocol"),
+  get name() {
+    return "";
+  },
+  get normalizedName() {
+    return this.name;
+  },
+  get iconBaseURI() {
+    return "chrome://chat/skin/prpl-unknown/";
+  },
+  getOptions() {
+    return EmptyEnumerator;
+  },
+  getUsernameSplit() {
+    return EmptyEnumerator;
+  },
+  get usernameEmptyText() {
+    return "";
+  },
+
+  getAccount(aKey, aName) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  accountExists() {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  // false seems an acceptable default for all options
+  // (they should never be called anyway).
+  get uniqueChatName() {
+    return false;
+  },
+  get chatHasTopic() {
+    return false;
+  },
+  get noPassword() {
+    return false;
+  },
+  get newMailNotification() {
+    return false;
+  },
+  get imagesInIM() {
+    return false;
+  },
+  get passwordOptional() {
+    return true;
+  },
+  get usePointSize() {
+    return true;
+  },
+  get registerNoScreenName() {
+    return false;
+  },
+  get slashCommandsNative() {
+    return false;
+  },
+  get usePurpleProxy() {
+    return false;
+  },
+};
+
+// An unknown prplIAccount.
+function UnknownAccount(aAccount) {
+  this._init(aAccount.protocol, aAccount);
+}
+UnknownAccount.prototype = GenericAccountPrototype;
+
+function UnknownAccountBuddy(aAccount, aBuddy, aTag) {
+  this._init(new UnknownAccount(aAccount), aBuddy, aTag);
+}
+UnknownAccountBuddy.prototype = GenericAccountBuddyPrototype;
+
+// aName and aPrplId are provided as parameter only if this is a new
+// account that doesn't exist in the preferences. In this case, these
+// 2 values should be stored.
+function imAccount(aKey, aName, aPrplId) {
+  if (!aKey.startsWith(kAccountKeyPrefix)) {
+    throw Cr.NS_ERROR_INVALID_ARG;
+  }
+
+  this.id = aKey;
+  this.numericId = parseInt(aKey.substr(kAccountKeyPrefix.length));
+  gAccountsService._keepAccount(this);
+  this.prefBranch = Services.prefs.getBranch(kPrefAccountPrefix + aKey + ".");
+
+  if (aName) {
+    this.name = aName;
+    this.prefBranch.setStringPref(kPrefAccountName, aName);
+
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+  } else {
+    this.name = this.prefBranch.getStringPref(kPrefAccountName);
+  }
+
+  let prplId = aPrplId;
+  if (prplId) {
+    this.prefBranch.setCharPref(kPrefAccountPrpl, prplId);
+  } else {
+    prplId = this.prefBranch.getCharPref(kPrefAccountPrpl);
+  }
+
+  // Get the protocol plugin, or fallback to an UnknownProtocol instance.
+  this.protocol = Services.core.getProtocolById(prplId);
+  if (!this.protocol) {
+    this.protocol = new UnknownProtocol(prplId);
+    this._connectionErrorReason = Ci.imIAccount.ERROR_UNKNOWN_PRPL;
+    return;
+  }
+
+  // Ensure the account is correctly stored in blist.sqlite.
+  Services.contacts.storeAccount(this.numericId, this.name, prplId);
+
+  // Get the prplIAccount from the protocol plugin.
+  this.prplAccount = this.protocol.getAccount(this);
+
+  // Send status change notifications to the account.
+  this.observedStatusInfo = null; // (To execute the setter).
+
+  // If we have never finished the first connection attempt for this account,
+  // mark the account as having caused a crash.
+  if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_CRASHED;
+  }
+
+  Services.logins.initializationPromise.then(() => {
+    // Try to convert old passwords stored in the preferences.
+    // Don't try too hard if the user has canceled a master password prompt:
+    // we don't want to display several of theses prompts at startup.
+    if (gConvertingOldPasswords && !this.protocol.noPassword) {
+      try {
+        let password = this.prefBranch.getStringPref(kPrefAccountPassword);
+        if (password && !this.password) {
+          this.password = password;
+        }
+      } catch (e) {
+        /* No password saved in the prefs for this account. */
+      }
+    }
+
+    // Check for errors that should prevent connection attempts.
+    if (this._passwordRequired && !this.password) {
+      this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
+    } else if (
+      this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_CRASHED
+    ) {
+      this._connectionErrorReason = Ci.imIAccount.ERROR_CRASHED;
+    }
+  });
+}
+
+imAccount.prototype = {
+  __proto__: ClassInfo(["imIAccount", "prplIAccount"], "im account object"),
+
+  name: "",
+  id: "",
+  numericId: 0,
+  protocol: null,
+  prplAccount: null,
+  connectionState: Ci.imIAccount.STATE_DISCONNECTED,
+  connectionStateMsg: "",
+  connectionErrorMessage: "",
+  _connectionErrorReason: Ci.prplIAccount.NO_ERROR,
+  get connectionErrorReason() {
+    if (
+      this._connectionErrorReason != Ci.prplIAccount.NO_ERROR &&
+      (this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
+        !this._password)
+    ) {
+      return this._connectionErrorReason;
+    }
+    return this.prplAccount.connectionErrorReason;
+  },
+
+  observe(aSubject, aTopic, aData) {
+    if (aTopic == "account-connect-progress") {
+      this.connectionStateMsg = aData;
+    } else if (aTopic == "account-connecting") {
+      if (this.prplAccount.connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
+        delete this.connectionErrorMessage;
+        if (this.timeOfNextReconnect - Date.now() > 1000) {
+          // This is a manual reconnection, reset the auto-reconnect stuff
+          this.timeOfLastConnect = 0;
+          this._cancelReconnection();
+        }
+      }
+      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) {
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_PENDING;
+      }
+      this.connectionState = Ci.imIAccount.STATE_CONNECTING;
+    } else if (aTopic == "account-connected") {
+      this.connectionState = Ci.imIAccount.STATE_CONNECTED;
+      this._finishedAutoLogin();
+      this.timeOfLastConnect = Date.now();
+      if (this.firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK) {
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_OK;
+      }
+      delete this.connectionStateMsg;
+
+      if (
+        this.canJoinChat &&
+        this.prefBranch.prefHasUserValue(kPrefAccountAutoJoin)
+      ) {
+        let autojoin = this.prefBranch.getStringPref(kPrefAccountAutoJoin);
+        if (autojoin) {
+          for (let room of autojoin.trim().split(/,\s*/)) {
+            if (room) {
+              this.joinChat(this.getChatRoomDefaultFieldValues(room));
+            }
+          }
+        }
+      }
+    } else if (aTopic == "account-disconnecting") {
+      this.connectionState = Ci.imIAccount.STATE_DISCONNECTING;
+      this.connectionErrorMessage = aData;
+      delete this.connectionStateMsg;
+      this._finishedAutoLogin();
+
+      let firstConnectionState = this.firstConnectionState;
+      if (
+        firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_OK &&
+        firstConnectionState != Ci.imIAccount.FIRST_CONNECTION_CRASHED
+      ) {
+        this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+      }
+
+      let connectionErrorReason = this.prplAccount.connectionErrorReason;
+      if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
+        if (
+          connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR ||
+          connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR
+        ) {
+          this._startReconnectTimer();
+        }
+        this._sendNotification("account-connect-error");
+      }
+    } else if (aTopic == "account-disconnected") {
+      this.connectionState = Ci.imIAccount.STATE_DISCONNECTED;
+      let connectionErrorReason = this.prplAccount.connectionErrorReason;
+      if (connectionErrorReason != Ci.prplIAccount.NO_ERROR) {
+        // If the account was disconnected with an error, save the debug messages.
+        this._omittedDebugMessagesBeforeError += this._omittedDebugMessages;
+        if (this._debugMessagesBeforeError) {
+          this._omittedDebugMessagesBeforeError += this._debugMessagesBeforeError.length;
+        }
+        this._debugMessagesBeforeError = this._debugMessages;
+      } else {
+        // After a clean disconnection, drop the debug messages that
+        // could have been left by a previous error.
+        delete this._omittedDebugMessagesBeforeError;
+        delete this._debugMessagesBeforeError;
+      }
+      delete this._omittedDebugMessages;
+      delete this._debugMessages;
+      if (
+        this._statusObserver &&
+        connectionErrorReason == Ci.prplIAccount.NO_ERROR &&
+        this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE
+      ) {
+        // If the status changed back to online while an account was still
+        // disconnecting, it was not reconnected automatically at that point,
+        // so we must do it now. (This happens for protocols like IRC where
+        // disconnection is not immediate.)
+        this._sendNotification(aTopic, aData);
+        this.connect();
+        return;
+      }
+    } else {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+    this._sendNotification(aTopic, aData);
+  },
+
+  _debugMessages: null,
+  _omittedDebugMessages: 0,
+  _debugMessagesBeforeError: null,
+  _omittedDebugMessagesBeforeError: 0,
+  logDebugMessage(aMessage, aLevel) {
+    if (!this._debugMessages) {
+      this._debugMessages = [];
+    }
+    if (_maxDebugMessages && this._debugMessages.length >= _maxDebugMessages) {
+      this._debugMessages.shift();
+      ++this._omittedDebugMessages;
+    }
+    this._debugMessages.push({ logLevel: aLevel, message: aMessage });
+  },
+  _createDebugMessage(aMessage) {
+    let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+      Ci.nsIScriptError
+    );
+    scriptError.init(
+      aMessage,
+      "",
+      "",
+      0,
+      null,
+      Ci.nsIScriptError.warningFlag,
+      "component javascript"
+    );
+    return { logLevel: 0, message: scriptError };
+  },
+  getDebugMessages() {
+    let messages = [];
+    if (this._omittedDebugMessagesBeforeError) {
+      let text = this._omittedDebugMessagesBeforeError + " messages omitted";
+      messages.push(this._createDebugMessage(text));
+    }
+    if (this._debugMessagesBeforeError) {
+      messages = messages.concat(this._debugMessagesBeforeError);
+    }
+    if (this._omittedDebugMessages) {
+      let text = this._omittedDebugMessages + " messages omitted";
+      messages.push(this._createDebugMessage(text));
+    }
+    if (this._debugMessages) {
+      messages = messages.concat(this._debugMessages);
+    }
+    if (messages.length) {
+      let appInfo = Services.appinfo;
+      let header =
+        `${appInfo.name} ${appInfo.version} (${appInfo.appBuildID}), ` +
+        `Gecko ${appInfo.platformVersion} (${appInfo.platformBuildID}) ` +
+        `on ${HttpProtocolHandler.oscpu}`;
+      messages.unshift(this._createDebugMessage(header));
+    }
+
+    return messages;
+  },
+
+  _observedStatusInfo: null,
+  get observedStatusInfo() {
+    return this._observedStatusInfo;
+  },
+  _statusObserver: null,
+  set observedStatusInfo(aUserStatusInfo) {
+    if (!this.prplAccount) {
+      return;
+    }
+    if (this._statusObserver) {
+      this.statusInfo.removeObserver(this._statusObserver);
+    }
+    this._observedStatusInfo = aUserStatusInfo;
+    if (this._statusObserver) {
+      this.statusInfo.addObserver(this._statusObserver);
+    }
+  },
+  _removeStatusObserver() {
+    if (this._statusObserver) {
+      this.statusInfo.removeObserver(this._statusObserver);
+      delete this._statusObserver;
+    }
+  },
+  get statusInfo() {
+    return this._observedStatusInfo || Services.core.globalUserStatus;
+  },
+
+  reconnectAttempt: 0,
+  timeOfLastConnect: 0,
+  timeOfNextReconnect: 0,
+  _reconnectTimer: null,
+  _startReconnectTimer() {
+    if (Services.io.offline) {
+      Cu.reportError("_startReconnectTimer called while offline");
+      return;
+    }
+
+    /* If the last successful connection is older than 10 seconds, reset the
+       number of reconnection attempts. */
+    const kTimeBeforeSuccessfulConnection = 10;
+    if (
+      this.timeOfLastConnect &&
+      this.timeOfLastConnect + kTimeBeforeSuccessfulConnection * 1000 <
+        Date.now()
+    ) {
+      delete this.reconnectAttempt;
+      delete this.timeOfLastConnect;
+    }
+
+    let timers = Services.prefs
+      .getCharPref("messenger.accounts.reconnectTimer")
+      .split(",");
+    let delay = timers[Math.min(this.reconnectAttempt, timers.length - 1)];
+    let msDelay = parseInt(delay) * 1000;
+    ++this.reconnectAttempt;
+    this.timeOfNextReconnect = Date.now() + msDelay;
+    this._reconnectTimer = setTimeout(this.connect.bind(this), msDelay);
+  },
+
+  _sendNotification(aTopic, aData) {
+    Services.obs.notifyObservers(this, aTopic, aData);
+  },
+
+  get firstConnectionState() {
+    try {
+      return this.prefBranch.getIntPref(kPrefAccountFirstConnectionState);
+    } catch (e) {
+      return Ci.imIAccount.FIRST_CONNECTION_OK;
+    }
+  },
+  set firstConnectionState(aState) {
+    if (aState == Ci.imIAccount.FIRST_CONNECTION_OK) {
+      this.prefBranch.deleteBranch(kPrefAccountFirstConnectionState);
+    } else {
+      this.prefBranch.setIntPref(kPrefAccountFirstConnectionState, aState);
+      // We want to save this pref immediately when trying to connect.
+      if (aState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
+        SavePrefTimer.saveNow();
+      } else {
+        SavePrefTimer.initTimer();
+      }
+    }
+  },
+
+  _pendingReconnectForConnectionInfoChange: false,
+  _connectionInfoChanged() {
+    // The next connection will be the first connection with these parameters.
+    this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+
+    // We want to attempt to reconnect with the new settings only if a
+    // previous attempt failed or a connection attempt is currently
+    // pending (so we can return early if the account is currently
+    // connected or disconnected without error).
+    // The code doing the reconnection attempt is wrapped within an
+    // executeSoon call so that when multiple settings are changed at
+    // once we don't attempt to reconnect until they are all saved.
+    // If a reconnect attempt is already scheduled, we can also return early.
+    if (
+      this._pendingReconnectForConnectionInfoChange ||
+      this.connected ||
+      (this.disconnected &&
+        this.connectionErrorReason == Ci.prplIAccount.NO_ERROR)
+    ) {
+      return;
+    }
+
+    this._pendingReconnectForConnectionInfoChange = true;
+    executeSoon(
+      function() {
+        delete this._pendingReconnectForConnectionInfoChange;
+        // If the connection parameters have changed while we were
+        // trying to connect, cancel the ongoing connection attempt and
+        // try again with the new parameters.
+        if (this.connecting) {
+          this.disconnect();
+          this.connect();
+          return;
+        }
+        // If the account was disconnected because of a non-fatal
+        // connection error, retry now that we have new parameters.
+        let errorReason = this.connectionErrorReason;
+        if (
+          this.disconnected &&
+          errorReason != Ci.prplIAccount.NO_ERROR &&
+          errorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD &&
+          errorReason != Ci.imIAccount.ERROR_CRASHED &&
+          errorReason != Ci.imIAccount.ERROR_UNKNOWN_PRPL
+        ) {
+          this.connect();
+        }
+      }.bind(this)
+    );
+  },
+
+  // If the protocol plugin is missing, we can't access the normalizedName,
+  // but in lots of cases this.name is equivalent.
+  get normalizedName() {
+    return this.prplAccount ? this.prplAccount.normalizedName : this.name;
+  },
+  normalize(aName) {
+    return this.prplAccount ? this.prplAccount.normalize(aName) : aName;
+  },
+
+  _sendUpdateNotification() {
+    this._sendNotification("account-updated");
+  },
+
+  set alias(val) {
+    if (val) {
+      this.prefBranch.setStringPref(kPrefAccountAlias, val);
+    } else {
+      this.prefBranch.deleteBranch(kPrefAccountAlias);
+    }
+    this._sendUpdateNotification();
+  },
+  get alias() {
+    try {
+      return this.prefBranch.getStringPref(kPrefAccountAlias);
+    } catch (e) {
+      return "";
+    }
+  },
+
+  _password: "",
+  get password() {
+    if (this._password) {
+      return this._password;
+    }
+
+    // Avoid prompting the user for the master password more than once at startup.
+    if (gUserCanceledMasterPasswordPrompt) {
+      return "";
+    }
+
+    let passwordURI = "im://" + this.protocol.id;
+    let logins;
+    try {
+      logins = Services.logins.findLogins(passwordURI, null, passwordURI);
+    } catch (e) {
+      this._handleMasterPasswordException(e);
+      return "";
+    }
+    let normalizedName = this.normalizedName;
+    for (let login of logins) {
+      if (login.username == normalizedName) {
+        this._password = login.password;
+        if (
+          this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD
+        ) {
+          // We have found a password for an account marked as missing password,
+          // re-check all others accounts missing a password. But first,
+          // remove the error on our own account to avoid re-checking it.
+          delete this._connectionErrorReason;
+          gAccountsService._checkIfPasswordStillMissing();
+        }
+        return this._password;
+      }
+    }
+    return "";
+  },
+  _checkIfPasswordStillMissing() {
+    if (
+      this._connectionErrorReason != Ci.imIAccount.ERROR_MISSING_PASSWORD ||
+      !this.password
+    ) {
+      return;
+    }
+
+    delete this._connectionErrorReason;
+    this._sendUpdateNotification();
+  },
+  get _passwordRequired() {
+    return !this.protocol.noPassword && !this.protocol.passwordOptional;
+  },
+  set password(aPassword) {
+    this._password = aPassword;
+    if (gUserCanceledMasterPasswordPrompt) {
+      return;
+    }
+    let newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+      Ci.nsILoginInfo
+    );
+    let passwordURI = "im://" + this.protocol.id;
+    newLogin.init(
+      passwordURI,
+      null,
+      passwordURI,
+      this.normalizedName,
+      aPassword,
+      "",
+      ""
+    );
+    try {
+      let logins = Services.logins.findLogins(passwordURI, null, passwordURI);
+      let saved = false;
+      for (let login of logins) {
+        if (newLogin.matches(login, true)) {
+          if (aPassword) {
+            Services.logins.modifyLogin(login, newLogin);
+          } else {
+            Services.logins.removeLogin(login);
+          }
+          saved = true;
+          break;
+        }
+      }
+      if (!saved && aPassword) {
+        Services.logins.addLogin(newLogin);
+      }
+    } catch (e) {
+      this._handleMasterPasswordException(e);
+    }
+
+    this._connectionInfoChanged();
+    if (
+      aPassword &&
+      this._connectionErrorReason == Ci.imIAccount.ERROR_MISSING_PASSWORD
+    ) {
+      this._connectionErrorReason = Ci.imIAccount.NO_ERROR;
+    } else if (!aPassword && this._passwordRequired) {
+      this._connectionErrorReason = Ci.imIAccount.ERROR_MISSING_PASSWORD;
+    }
+    this._sendUpdateNotification();
+  },
+  _handleMasterPasswordException(aException) {
+    if (aException.result != Cr.NS_ERROR_ABORT) {
+      throw aException;
+    }
+
+    gUserCanceledMasterPasswordPrompt = true;
+    executeSoon(function() {
+      gUserCanceledMasterPasswordPrompt = false;
+    });
+  },
+
+  get autoLogin() {
+    return this.prefBranch.getBoolPref(kPrefAccountAutoLogin, true);
+  },
+  set autoLogin(val) {
+    this.prefBranch.setBoolPref(kPrefAccountAutoLogin, val);
+    SavePrefTimer.initTimer();
+    this._sendUpdateNotification();
+  },
+  _autoLoginPending: false,
+  checkAutoLogin() {
+    // No auto-login if: the account has an error at the imIAccount level
+    // (unknown protocol, missing password, first connection crashed),
+    // the account is already connected or connecting, or autoLogin is off.
+    if (
+      this._connectionErrorReason != Ci.prplIAccount.NO_ERROR ||
+      this.connecting ||
+      this.connected ||
+      !this.autoLogin
+    ) {
+      return;
+    }
+
+    this._autoLoginPending = true;
+    AutoLoginCounter.startAutoLogin();
+    try {
+      this.connect();
+    } catch (e) {
+      Cu.reportError(e);
+      this._finishedAutoLogin();
+    }
+  },
+  _finishedAutoLogin() {
+    if (!this.hasOwnProperty("_autoLoginPending")) {
+      return;
+    }
+    delete this._autoLoginPending;
+    AutoLoginCounter.finishedAutoLogin();
+  },
+
+  // Delete the account (from the preferences, mozStorage, and call unInit).
+  remove() {
+    let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+      Ci.nsILoginInfo
+    );
+    let passwordURI = "im://" + this.protocol.id;
+    // Note: the normalizedName may not be exactly right if the
+    // protocol plugin is missing.
+    login.init(passwordURI, null, passwordURI, this.normalizedName, "", "", "");
+    let logins = Services.logins.findLogins(passwordURI, null, passwordURI);
+    for (let l of logins) {
+      if (login.matches(l, true)) {
+        Services.logins.removeLogin(l);
+        break;
+      }
+    }
+    if (this.connected || this.connecting) {
+      this.disconnect();
+    }
+    if (this.prplAccount) {
+      this.prplAccount.remove();
+    }
+    this.unInit();
+    Services.contacts.forgetAccount(this.numericId);
+    this.prefBranch.deleteBranch("");
+  },
+  unInit() {
+    // remove any pending reconnection timer.
+    this._cancelReconnection();
+
+    // Keeping a status observer could cause an immediate reconnection.
+    this._removeStatusObserver();
+
+    // remove any pending autologin preference used for crash detection.
+    this._finishedAutoLogin();
+
+    // If the first connection was pending on quit, we set it back to unknown.
+    if (this.firstConnectionState == Ci.imIAccount.FIRST_CONNECTION_PENDING) {
+      this.firstConnectionState = Ci.imIAccount.FIRST_CONNECTION_UNKNOWN;
+    }
+
+    // and make sure we cleanup the save pref timer.
+    SavePrefTimer.unInitTimer();
+
+    if (this.prplAccount) {
+      this.prplAccount.unInit();
+    }
+
+    delete this.protocol;
+    delete this.prplAccount;
+  },
+
+  get _ensurePrplAccount() {
+    if (this.prplAccount) {
+      return this.prplAccount;
+    }
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+  connect() {
+    if (!this.prplAccount) {
+      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+    }
+
+    if (this._passwordRequired) {
+      // If the previous connection attempt failed because we have a wrong password,
+      // clear the passwor cache so that if there's no password in the password
+      // manager the user gets prompted again.
+      if (
+        this.connectionErrorReason ==
+        Ci.prplIAccount.ERROR_AUTHENTICATION_FAILED
+      ) {
+        delete this._password;
+      }
+
+      let password = this.password;
+      if (!password) {
+        let prompts = Services.prompt;
+        let shouldSave = { value: false };
+        password = { value: "" };
+        if (
+          !prompts.promptPassword(
+            null,
+            _("passwordPromptTitle", this.name),
+            _("passwordPromptText", this.name),
+            password,
+            _("passwordPromptSaveCheckbox"),
+            shouldSave
+          )
+        ) {
+          return;
+        }
+
+        if (shouldSave.value) {
+          this.password = password.value;
+        } else {
+          this._password = password.value;
+        }
+      }
+    }
+
+    if (!this._statusObserver) {
+      this._statusObserver = {
+        observe: function(aSubject, aTopic, aData) {
+          // Disconnect or reconnect the account automatically, otherwise notify
+          // the prplAccount instance.
+          let statusType = aSubject.statusType;
+          let connectionErrorReason = this.connectionErrorReason;
+          if (statusType == Ci.imIStatusInfo.STATUS_OFFLINE) {
+            if (this.connected || this.connecting) {
+              this.prplAccount.disconnect();
+            }
+            this._cancelReconnection();
+          } else if (
+            statusType > Ci.imIStatusInfo.STATUS_OFFLINE &&
+            this.disconnected &&
+            (connectionErrorReason == Ci.prplIAccount.NO_ERROR ||
+              connectionErrorReason == Ci.prplIAccount.ERROR_NETWORK_ERROR ||
+              connectionErrorReason == Ci.prplIAccount.ERROR_ENCRYPTION_ERROR)
+          ) {
+            this.prplAccount.connect();
+          } else if (this.connected) {
+            this.prplAccount.observe(aSubject, aTopic, aData);
+          }
+        }.bind(this),
+      };
+
+      this.statusInfo.addObserver(this._statusObserver);
+    }
+
+    if (
+      !Services.io.offline &&
+      this.statusInfo.statusType > Ci.imIStatusInfo.STATUS_OFFLINE &&
+      this.disconnected
+    ) {
+      this.prplAccount.connect();
+    }
+  },
+  disconnect() {
+    this._removeStatusObserver();
+    if (!this.disconnected) {
+      this._ensurePrplAccount.disconnect();
+    }
+  },
+
+  get disconnected() {
+    return this.connectionState == Ci.imIAccount.STATE_DISCONNECTED;
+  },
+  get connected() {
+    return this.connectionState == Ci.imIAccount.STATE_CONNECTED;
+  },
+  get connecting() {
+    return this.connectionState == Ci.imIAccount.STATE_CONNECTING;
+  },
+  get disconnecting() {
+    return this.connectionState == Ci.imIAccount.STATE_DISCONNECTING;
+  },
+
+  _cancelReconnection() {
+    if (this._reconnectTimer) {
+      clearTimeout(this._reconnectTimer);
+      delete this._reconnectTimer;
+    }
+    delete this.reconnectAttempt;
+    delete this.timeOfNextReconnect;
+  },
+  cancelReconnection() {
+    if (!this.disconnected) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    // Ensure we don't keep a status observer that could re-enable the
+    // auto-reconnect timers.
+    this.disconnect();
+
+    this._cancelReconnection();
+  },
+  createConversation(aName) {
+    return this._ensurePrplAccount.createConversation(aName);
+  },
+  addBuddy(aTag, aName) {
+    this._ensurePrplAccount.addBuddy(aTag, aName);
+  },
+  loadBuddy(aBuddy, aTag) {
+    if (this.prplAccount) {
+      return this.prplAccount.loadBuddy(aBuddy, aTag);
+    }
+    // Generate dummy account buddies for unknown protocols.
+    return new UnknownAccountBuddy(this, aBuddy, aTag);
+  },
+  requestBuddyInfo(aBuddyName) {
+    this._ensurePrplAccount.requestBuddyInfo(aBuddyName);
+  },
+  getChatRoomFields() {
+    return this._ensurePrplAccount.getChatRoomFields();
+  },
+  getChatRoomDefaultFieldValues(aDefaultChatName) {
+    return this._ensurePrplAccount.getChatRoomDefaultFieldValues(
+      aDefaultChatName
+    );
+  },
+  get canJoinChat() {
+    return this.prplAccount ? this.prplAccount.canJoinChat : false;
+  },
+  joinChat(aComponents) {
+    this._ensurePrplAccount.joinChat(aComponents);
+  },
+  setBool(aName, aVal) {
+    this.prefBranch.setBoolPref(kAccountOptionPrefPrefix + aName, aVal);
+    this._connectionInfoChanged();
+    if (this.prplAccount) {
+      this.prplAccount.setBool(aName, aVal);
+    }
+    SavePrefTimer.initTimer();
+  },
+  setInt(aName, aVal) {
+    this.prefBranch.setIntPref(kAccountOptionPrefPrefix + aName, aVal);
+    this._connectionInfoChanged();
+    if (this.prplAccount) {
+      this.prplAccount.setInt(aName, aVal);
+    }
+    SavePrefTimer.initTimer();
+  },
+  setString(aName, aVal) {
+    this.prefBranch.setStringPref(kAccountOptionPrefPrefix + aName, aVal);
+    this._connectionInfoChanged();
+    if (this.prplAccount) {
+      this.prplAccount.setString(aName, aVal);
+    }
+    SavePrefTimer.initTimer();
+  },
+  save() {
+    SavePrefTimer.saveNow();
+  },
+
+  get HTMLEnabled() {
+    return this._ensurePrplAccount.HTMLEnabled;
+  },
+  get HTMLEscapePlainText() {
+    return this._ensurePrplAccount.HTMLEscapePlainText;
+  },
+  get noBackgroundColors() {
+    return this._ensurePrplAccount.noBackgroundColors;
+  },
+  get autoResponses() {
+    return this._ensurePrplAccount.autoResponses;
+  },
+  get singleFormatting() {
+    return this._ensurePrplAccount.singleFormatting;
+  },
+  get noFontSizes() {
+    return this._ensurePrplAccount.noFontSizes;
+  },
+  get noUrlDesc() {
+    return this._ensurePrplAccount.noUrlDesc;
+  },
+  get noImages() {
+    return this._ensurePrplAccount.noImages;
+  },
+};
+
+var gAccountsService = null;
+
+function AccountsService() {}
+AccountsService.prototype = {
+  initAccounts() {
+    this._initAutoLoginStatus();
+    this._accounts = [];
+    this._accountsById = {};
+    gAccountsService = this;
+    gConvertingOldPasswords = Services.prefs.getBoolPref(
+      kPrefConvertOldPasswords
+    );
+    let accountList = this._accountList;
+    for (let account of accountList ? accountList.split(",") : []) {
+      try {
+        account.trim();
+        if (!account) {
+          throw Cr.NS_ERROR_INVALID_ARG;
+        }
+        new imAccount(account);
+      } catch (e) {
+        Cu.reportError(e);
+        dump(e + " " + e.toSource() + "\n");
+      }
+    }
+    // If the user has canceled a master password prompt, we haven't
+    // been able to save any password, so the old password conversion
+    // still needs to happen.
+    if (gConvertingOldPasswords && !gUserCanceledMasterPasswordPrompt) {
+      Services.prefs.setBoolPref(kPrefConvertOldPasswords, false);
+    }
+
+    this._prefObserver = this.observe.bind(this);
+    Services.prefs.addObserver(kPrefMessengerAccounts, this._prefObserver);
+  },
+
+  _observingAccountListChange: true,
+  _prefObserver: null,
+  observe(aSubject, aTopic, aData) {
+    if (
+      aTopic != "nsPref:changed" ||
+      aData != kPrefMessengerAccounts ||
+      !this._observingAccountListChange
+    ) {
+      return;
+    }
+
+    this._accounts = this._accountList
+      .split(",")
+      .map(account => account.trim())
+      .filter(k => k.startsWith(kAccountKeyPrefix))
+      .map(k => parseInt(k.substr(kAccountKeyPrefix.length)))
+      .map(this.getAccountByNumericId, this)
+      .filter(a => a);
+
+    Services.obs.notifyObservers(this, "account-list-updated");
+  },
+
+  get _accountList() {
+    return Services.prefs.getCharPref(kPrefMessengerAccounts);
+  },
+  set _accountList(aNewList) {
+    this._observingAccountListChange = false;
+    Services.prefs.setCharPref(kPrefMessengerAccounts, aNewList);
+    delete this._observingAccountListChange;
+  },
+
+  unInitAccounts() {
+    for (let account of this._accounts) {
+      account.unInit();
+    }
+    gAccountsService = null;
+    delete this._accounts;
+    delete this._accountsById;
+    Services.prefs.removeObserver(kPrefMessengerAccounts, this._prefObserver);
+    delete this._prefObserver;
+  },
+
+  autoLoginStatus: Ci.imIAccountsService.AUTOLOGIN_ENABLED,
+  _initAutoLoginStatus() {
+    /* If auto-login is already disabled, do nothing */
+    if (this.autoLoginStatus != Ci.imIAccountsService.AUTOLOGIN_ENABLED) {
+      return;
+    }
+
+    let prefs = Services.prefs;
+    if (!prefs.getIntPref("messenger.startup.action")) {
+      // the value 0 means that we start without connecting the accounts
+      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_USER_DISABLED;
+      return;
+    }
+
+    /* Disable auto-login if we are running in safe mode */
+    if (Services.appinfo.inSafeMode) {
+      this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_SAFE_MODE;
+      return;
+    }
+
+    /* Check if we crashed at the last startup during autologin */
+    let autoLoginPending;
+    if (
+      prefs.getPrefType(kPrefAutologinPending) == prefs.PREF_INVALID ||
+      !(autoLoginPending = prefs.getIntPref(kPrefAutologinPending))
+    ) {
+      // if the pref isn't set, then we haven't crashed: keep autologin enabled
+      return;
+    }
+
+    // Last autologin hasn't finished properly.
+    // For now, assume it's because of a crash.
+    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_CRASH;
+    prefs.deleteBranch(kPrefAutologinPending);
+
+    // If the crash reporter isn't built, we can't know anything more.
+    if (!("nsICrashReporter" in Ci)) {
+      return;
+    }
+
+    try {
+      // Try to get more info with breakpad
+      let lastCrashTime = 0;
+
+      /* Locate the LastCrash file */
+      let lastCrash = Services.dirsvc.get("UAppData", Ci.nsIFile);
+      lastCrash.append("Crash Reports");
+      lastCrash.append("LastCrash");
+      if (lastCrash.exists()) {
+        /* Ok, the file exists, now let's try to read it */
+        let is = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+          Ci.nsIFileInputStream
+        );
+        let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+          Ci.nsIScriptableInputStream
+        );
+        is.init(lastCrash, -1, 0, 0);
+        sis.init(sis);
+
+        lastCrashTime = parseInt(sis.read(lastCrash.fileSize));
+
+        sis.close();
+      }
+      // The file not existing is totally acceptable, it just means that
+      // either we never crashed or breakpad is not enabled.
+      // In this case, lastCrashTime will keep its 0 initialization value.
+
+      /* dump("autoLoginPending = " + autoLoginPending +
+              ", lastCrash = " + lastCrashTime +
+              ", difference = " + lastCrashTime - autoLoginPending + "\n");*/
+
+      if (lastCrashTime < autoLoginPending) {
+        // the last crash caught by breakpad is older than our last autologin
+        // attempt.
+        // If breakpad is currently enabled, we can be confident that
+        // autologin was interrupted for an exterior reason
+        // (application killed by the user, power outage, ...)
+        try {
+          Services.appinfo
+            .QueryInterface(Ci.nsICrashReporter)
+            .annotateCrashReport("=", "");
+        } catch (e) {
+          // This should fail with NS_ERROR_INVALID_ARG if breakpad is enabled,
+          // and NS_ERROR_NOT_INITIALIZED if it is not.
+          if (e.result != Cr.NS_ERROR_NOT_INITIALIZED) {
+            this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
+          }
+        }
+      }
+    } catch (e) {
+      // if we failed to get the last crash time, then keep the
+      // AUTOLOGIN_CRASH value in mAutoLoginStatus and return.
+    }
+  },
+
+  processAutoLogin() {
+    if (!this._accounts) {
+      // if we're already shutting down
+      return;
+    }
+
+    for (let account of this._accounts) {
+      account.checkAutoLogin();
+    }
+
+    // Make sure autologin is now enabled, so that we don't display a
+    // message stating that it is disabled and asking the user if it
+    // should be processed now.
+    this.autoLoginStatus = Ci.imIAccountsService.AUTOLOGIN_ENABLED;
+
+    // Notify observers so that any message stating that autologin is
+    // disabled can be removed
+    Services.obs.notifyObservers(this, "autologin-processed");
+  },
+
+  _checkingIfPasswordStillMissing: false,
+  _checkIfPasswordStillMissing() {
+    // Avoid recursion.
+    if (this._checkingIfPasswordStillMissing) {
+      return;
+    }
+
+    this._checkingIfPasswordStillMissing = true;
+    for (let account of this._accounts) {
+      account._checkIfPasswordStillMissing();
+    }
+    delete this._checkingIfPasswordStillMissing;
+  },
+
+  getAccountById(aAccountId) {
+    if (!aAccountId.startsWith(kAccountKeyPrefix)) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    let id = parseInt(aAccountId.substr(kAccountKeyPrefix.length));
+    return this.getAccountByNumericId(id);
+  },
+
+  _keepAccount(aAccount) {
+    this._accounts.push(aAccount);
+    this._accountsById[aAccount.numericId] = aAccount;
+  },
+  getAccountByNumericId(aAccountId) {
+    return this._accountsById[aAccountId];
+  },
+  getAccounts() {
+    return new nsSimpleEnumerator(this._accounts);
+  },
+
+  createAccount(aName, aPrpl) {
+    // Ensure an account with the same name and protocol doesn't already exist.
+    let prpl = Services.core.getProtocolById(aPrpl);
+    if (!prpl) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+    if (prpl.accountExists(aName)) {
+      Cu.reportError("Attempted to create a duplicate account!");
+      throw Cr.NS_ERROR_ALREADY_INITIALIZED;
+    }
+
+    /* First get a unique id for the new account. */
+    let id;
+    for (id = 1; ; ++id) {
+      if (this._accountsById.hasOwnProperty(id)) {
+        continue;
+      }
+
+      /* id isn't used by a known account, double check it isn't
+       already used in the sqlite database. This should never
+       happen, except if we have a corrupted profile. */
+      if (!Services.contacts.accountIdExists(id)) {
+        break;
+      }
+      Services.console.logStringMessage(
+        "No account " +
+          id +
+          " but there is some data in the buddy list for an account with this number. Your profile may be corrupted."
+      );
+    }
+
+    /* Actually create the new account. */
+    let key = kAccountKeyPrefix + id;
+    let account = new imAccount(key, aName, aPrpl);
+
+    /* Save the account list pref. */
+    let list = this._accountList;
+    this._accountList = list ? list + "," + key : key;
+
+    Services.obs.notifyObservers(account, "account-added");
+    return account;
+  },
+
+  deleteAccount(aAccountId) {
+    let account = this.getAccountById(aAccountId);
+    if (!account) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    let index = this._accounts.indexOf(account);
+    if (index == -1) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    let id = account.numericId;
+    account.remove();
+    this._accounts.splice(index, 1);
+    delete this._accountsById[id];
+    Services.obs.notifyObservers(account, "account-removed");
+
+    /* Update the account list pref. */
+    let list = this._accountList;
+    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]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imAccounts.manifest
@@ -0,0 +1,2 @@
+component {a94b5427-cd8d-40cf-b47e-b67671953e70} imAccounts.js
+contract @mozilla.org/chat/accounts-service;1 {a94b5427-cd8d-40cf-b47e-b67671953e70}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCommands.js
@@ -0,0 +1,292 @@
+/* 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 { XPCOMUtils, l10nHelper } = ChromeUtils.import(
+  "resource:///modules/imXPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/locale/commands.properties")
+);
+
+function CommandsService() {}
+CommandsService.prototype = {
+  initCommands() {
+    this._commands = {};
+    // The say command is directly implemented in the UI layer, but has a
+    // dummy command registered here so it shows up as a command (e.g. when
+    // using the /help command).
+    this.registerCommand({
+      name: "say",
+      get helpString() {
+        return _("sayHelpString");
+      },
+      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
+      priority: Ci.imICommand.CMD_PRIORITY_HIGH,
+      run(aMsg, aConv) {
+        throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+      },
+    });
+
+    this.registerCommand({
+      name: "raw",
+      get helpString() {
+        return _("rawHelpString");
+      },
+      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
+      priority: Ci.imICommand.CMD_PRIORITY_DEFAULT,
+      run(aMsg, aConv) {
+        let conv = Services.conversations.getUIConversation(aConv);
+        if (!conv) {
+          return false;
+        }
+        conv.sendMsg(aMsg);
+        return true;
+      },
+    });
+
+    this.registerCommand({
+      // Reference the command service so we can use the internal properties
+      // directly.
+      cmdSrv: this,
+
+      name: "help",
+      get helpString() {
+        return _("helpHelpString");
+      },
+      usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
+      priority: Ci.imICommand.CMD_PRIORITY_DEFAULT,
+      run(aMsg, aConv) {
+        aMsg = aMsg.trim();
+        let conv = Services.conversations.getUIConversation(aConv);
+        if (!conv) {
+          return false;
+        }
+
+        // Handle when no command is given, list all possible commands that are
+        // available for this conversation (alphabetically).
+        if (!aMsg) {
+          let commands = this.cmdSrv.listCommandsForConversation(aConv);
+          if (!commands.length) {
+            return false;
+          }
+
+          // Concatenate the command names (separated by a comma and space).
+          let cmds = commands
+            .map(aCmd => aCmd.name)
+            .sort()
+            .join(", ");
+          let message = _("commands", cmds);
+
+          // Display the message
+          conv.systemMessage(message);
+          return true;
+        }
+
+        // A command name was given, find the commands that match.
+        let cmdArray = this.cmdSrv._findCommands(aConv, aMsg);
+
+        if (!cmdArray.length) {
+          // No command that matches.
+          let message = _("noCommand", aMsg);
+          conv.systemMessage(message);
+          return true;
+        }
+
+        // Only show the help for the one of the highest priority.
+        let cmd = cmdArray[0];
+
+        let text = cmd.helpString;
+        if (!text) {
+          text = _("noHelp", cmd.name);
+        }
+
+        // Display the message.
+        conv.systemMessage(text);
+        return true;
+      },
+    });
+
+    // Status commands
+    let status = {
+      back: "AVAILABLE",
+      away: "AWAY",
+      busy: "UNAVAILABLE",
+      dnd: "UNAVAILABLE",
+      offline: "OFFLINE",
+    };
+    for (let cmd in status) {
+      let statusValue = Ci.imIStatusInfo["STATUS_" + status[cmd]];
+      this.registerCommand({
+        name: cmd,
+        get helpString() {
+          return _("statusCommand", this.name, _(this.name));
+        },
+        usageContext: Ci.imICommand.CMD_CONTEXT_ALL,
+        priority: Ci.imICommand.CMD_PRIORITY_HIGH,
+        run(aMsg) {
+          Services.core.globalUserStatus.setStatus(statusValue, aMsg);
+          return true;
+        },
+      });
+    }
+  },
+  unInitCommands() {
+    delete this._commands;
+  },
+
+  registerCommand(aCommand, aPrplId) {
+    let name = aCommand.name;
+    if (!name) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    if (!this._commands.hasOwnProperty(name)) {
+      this._commands[name] = {};
+    }
+    this._commands[name][aPrplId || ""] = aCommand;
+  },
+  unregisterCommand(aCommandName, aPrplId) {
+    if (this._commands.hasOwnProperty(aCommandName)) {
+      let prplId = aPrplId || "";
+      let commands = this._commands[aCommandName];
+      if (commands.hasOwnProperty(prplId)) {
+        delete commands[prplId];
+      }
+      if (!Object.keys(commands).length) {
+        delete this._commands[aCommandName];
+      }
+    }
+  },
+  listCommandsForConversation(aConversation) {
+    let result = [];
+    let prplId = aConversation && aConversation.account.protocol.id;
+    for (let name in this._commands) {
+      let commands = this._commands[name];
+      if (commands.hasOwnProperty("")) {
+        result.push(commands[""]);
+      }
+      if (prplId && commands.hasOwnProperty(prplId)) {
+        result.push(commands[prplId]);
+      }
+    }
+    if (aConversation) {
+      result = result.filter(this._usageContextFilter(aConversation));
+    }
+    return result;
+  },
+  // List only the commands for a protocol (excluding the global commands).
+  listCommandsForProtocol(aPrplId) {
+    if (!aPrplId) {
+      throw new Error("You must provide a prpl ID.");
+    }
+
+    let result = [];
+    for (let name in this._commands) {
+      let commands = this._commands[name];
+      if (commands.hasOwnProperty(aPrplId)) {
+        result.push(commands[aPrplId]);
+      }
+    }
+    return result;
+  },
+  _usageContextFilter(aConversation) {
+    let usageContext =
+      Ci.imICommand["CMD_CONTEXT_" + (aConversation.isChat ? "CHAT" : "IM")];
+    return c => c.usageContext & usageContext;
+  },
+  _findCommands(aConversation, aName) {
+    let prplId = null;
+    if (aConversation) {
+      let account = aConversation.account;
+      if (account.connected) {
+        prplId = account.protocol.id;
+      }
+    }
+
+    let commandNames;
+    // If there is an exact match for the given command name,
+    // don't look at any other commands.
+    if (this._commands.hasOwnProperty(aName)) {
+      commandNames = [aName];
+    } else {
+      // Otherwise, check if there is a partial match.
+      commandNames = Object.keys(this._commands).filter(command =>
+        command.startsWith(aName)
+      );
+    }
+
+    // If a single full command name matches the given (partial)
+    // command name, return the results for that command name. Otherwise,
+    // return an empty array (don't assume a certain command).
+    let cmdArray = [];
+    for (let commandName of commandNames) {
+      let matches = [];
+
+      // Get the 2 possible commands (the global and the proto specific).
+      let commands = this._commands[commandName];
+      if (commands.hasOwnProperty("")) {
+        matches.push(commands[""]);
+      }
+      if (prplId && commands.hasOwnProperty(prplId)) {
+        matches.push(commands[prplId]);
+      }
+
+      // Remove the commands that can't apply in this context.
+      if (aConversation) {
+        matches = matches.filter(this._usageContextFilter(aConversation));
+      }
+
+      if (!matches.length) {
+        continue;
+      }
+
+      // If we have found a second matching command name, return the empty array.
+      if (cmdArray.length) {
+        return [];
+      }
+
+      cmdArray = matches;
+    }
+
+    // Sort the matching commands by priority before returning the array.
+    return cmdArray.sort((a, b) => b.priority - a.priority);
+  },
+  executeCommand(aMessage, aConversation, aReturnedConv) {
+    if (!aMessage) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    let matchResult;
+    if (
+      aMessage[0] != "/" ||
+      !(matchResult = /^\/([a-z0-9]+)(?: |$)([\s\S]*)/.exec(aMessage))
+    ) {
+      return false;
+    }
+
+    let [, name, args] = matchResult;
+
+    let cmdArray = this._findCommands(aConversation, name);
+    if (!cmdArray.length) {
+      return false;
+    }
+
+    // cmdArray contains commands sorted by priority, attempt to apply
+    // them in order until one succeeds.
+    if (!cmdArray.some(aCmd => aCmd.run(args, aConversation, aReturnedConv))) {
+      // 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]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imCommands.manifest
@@ -0,0 +1,2 @@
+component {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23} imCommands.js
+contract @mozilla.org/chat/commands-service;1 {7cb20c68-ccc8-4a79-b6f1-0b4771ed6c23}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imContacts.js
@@ -0,0 +1,1810 @@
+/* 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 { XPCOMUtils, executeSoon, ClassInfo, l10nHelper } = ChromeUtils.import(
+  "resource:///modules/imXPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyGetter(this, "_", () =>
+  l10nHelper("chrome://chat/locale/contacts.properties")
+);
+
+var gDBConnection = null;
+
+function executeAsyncThenFinalize(statement) {
+  statement.executeAsync();
+  statement.finalize();
+}
+
+function getDBConnection() {
+  const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+  let dbFile = Services.dirsvc.get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
+  dbFile.append("blist.sqlite");
+
+  let conn = Services.storage.openDatabase(dbFile);
+  if (!conn.connectionReady) {
+    throw Cr.NS_ERROR_UNEXPECTED;
+  }
+
+  // Grow blist db in 512KB increments.
+  try {
+    conn.setGrowthIncrement(512 * 1024, "");
+  } catch (e) {
+    if (e.result == Cr.NS_ERROR_FILE_TOO_BIG) {
+      Services.console.logStringMessage(
+        "Not setting growth increment on " +
+          "blist.sqlite because the available " +
+          "disk space is limited"
+      );
+    } else {
+      throw e;
+    }
+  }
+
+  // Create tables and indexes.
+  [
+    "CREATE TABLE IF NOT EXISTS accounts (" +
+      "id INTEGER PRIMARY KEY, " +
+      "name VARCHAR, " +
+      "prpl VARCHAR)",
+
+    "CREATE TABLE IF NOT EXISTS contacts (" +
+      "id INTEGER PRIMARY KEY, " +
+      "firstname VARCHAR, " +
+      "lastname VARCHAR, " +
+      "alias VARCHAR)",
+
+    "CREATE TABLE IF NOT EXISTS buddies (" +
+      "id INTEGER PRIMARY KEY, " +
+      "key VARCHAR NOT NULL, " +
+      "name VARCHAR NOT NULL, " +
+      "srv_alias VARCHAR, " +
+      "position INTEGER, " +
+      "icon BLOB, " +
+      "contact_id INTEGER)",
+    "CREATE INDEX IF NOT EXISTS buddies_contactindex " +
+      "ON buddies (contact_id)",
+
+    "CREATE TABLE IF NOT EXISTS tags (" +
+      "id INTEGER PRIMARY KEY, " +
+      "name VARCHAR UNIQUE NOT NULL, " +
+      "position INTEGER)",
+
+    "CREATE TABLE IF NOT EXISTS contact_tag (" +
+      "contact_id INTEGER NOT NULL, " +
+      "tag_id INTEGER NOT NULL)",
+    "CREATE INDEX IF NOT EXISTS contact_tag_contactindex " +
+      "ON contact_tag (contact_id)",
+    "CREATE INDEX IF NOT EXISTS contact_tag_tagindex " +
+      "ON contact_tag (tag_id)",
+
+    "CREATE TABLE IF NOT EXISTS account_buddy (" +
+      "account_id INTEGER NOT NULL, " +
+      "buddy_id INTEGER NOT NULL, " +
+      "status VARCHAR, " +
+      "tag_id INTEGER)",
+    "CREATE INDEX IF NOT EXISTS account_buddy_accountindex " +
+      "ON account_buddy (account_id)",
+    "CREATE INDEX IF NOT EXISTS account_buddy_buddyindex " +
+      "ON account_buddy (buddy_id)",
+  ].forEach(conn.executeSimpleSQL);
+
+  return conn;
+}
+
+// Wrap all the usage of DBConn inside a transaction that will be
+// committed automatically at the end of the event loop spin so that
+// we flush buddy list data to disk only once per event loop spin.
+var gDBConnWithPendingTransaction = null;
+Object.defineProperty(this, "DBConn", {
+  configurable: true,
+  enumerable: true,
+
+  get() {
+    if (gDBConnWithPendingTransaction) {
+      return gDBConnWithPendingTransaction;
+    }
+
+    if (!gDBConnection) {
+      gDBConnection = getDBConnection();
+      Services.obs.addObserver(function dbClose(aSubject, aTopic, aData) {
+        Services.obs.removeObserver(dbClose, aTopic);
+        if (gDBConnection) {
+          gDBConnection.asyncClose();
+          gDBConnection = null;
+        }
+      }, "profile-before-change");
+    }
+    gDBConnWithPendingTransaction = gDBConnection;
+    gDBConnection.beginTransaction();
+    executeSoon(function() {
+      gDBConnWithPendingTransaction.commitTransaction();
+      gDBConnWithPendingTransaction = null;
+    });
+    return gDBConnection;
+  },
+});
+
+function TagsService() {}
+TagsService.prototype = {
+  get wrappedJSObject() {
+    return this;
+  },
+  get defaultTag() {
+    return this.createTag(_("defaultGroup"));
+  },
+  createTag(aName) {
+    // If the tag already exists, we don't want to create a duplicate.
+    let tag = this.getTagByName(aName);
+    if (tag) {
+      return tag;
+    }
+
+    let statement = DBConn.createStatement(
+      "INSERT INTO tags (name, position) VALUES(:name, 0)"
+    );
+    try {
+      statement.params.name = aName;
+      statement.executeStep();
+    } finally {
+      statement.finalize();
+    }
+
+    tag = new Tag(DBConn.lastInsertRowID, aName);
+    Tags.push(tag);
+    return tag;
+  },
+  // Get an existing tag by (numeric) id. Returns null if not found.
+  getTagById: aId => TagsById[aId],
+  // Get an existing tag by name (will do an SQL query). Returns null
+  // if not found.
+  getTagByName(aName) {
+    let statement = DBConn.createStatement(
+      "SELECT id FROM tags where name = :name"
+    );
+    statement.params.name = aName;
+    try {
+      if (!statement.executeStep()) {
+        return null;
+      }
+      return this.getTagById(statement.row.id);
+    } finally {
+      statement.finalize();
+    }
+  },
+  // Get an array of all existing tags.
+  getTags() {
+    if (Tags.length) {
+      Tags.sort((a, b) =>
+        a.name.toLowerCase().localeCompare(b.name.toLowerCase())
+      );
+    } else {
+      this.defaultTag;
+    }
+
+    return Tags;
+  },
+
+  isTagHidden: aTag => aTag.id in otherContactsTag._hiddenTags,
+  hideTag(aTag) {
+    otherContactsTag.hideTag(aTag);
+  },
+  showTag(aTag) {
+    otherContactsTag.showTag(aTag);
+  },
+  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;
+  this._name = aName;
+  this._contacts = [];
+  this._observers = [];
+
+  TagsById[this.id] = this;
+}
+Tag.prototype = {
+  __proto__: ClassInfo("imITag", "Tag"),
+  get id() {
+    return this._id;
+  },
+  get name() {
+    return this._name;
+  },
+  set name(aNewName) {
+    let statement = DBConn.createStatement(
+      "UPDATE tags SET name = :name WHERE id = :id"
+    );
+    try {
+      statement.params.name = aNewName;
+      statement.params.id = this._id;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+
+    // FIXME move the account buddies if some use this tag as their group
+    return aNewName;
+  },
+  getContacts() {
+    return this._contacts.filter(c => !c._empty);
+  },
+  _addContact(aContact) {
+    this._contacts.push(aContact);
+  },
+  _removeContact(aContact) {
+    let index = this._contacts.indexOf(aContact);
+    if (index != -1) {
+      this._contacts.splice(index, 1);
+    }
+  },
+
+  addObserver(aObserver) {
+    if (!this._observers.includes(aObserver)) {
+      this._observers.push(aObserver);
+    }
+  },
+  removeObserver(aObserver) {
+    this._observers = this._observers.filter(o => o !== aObserver);
+  },
+  notifyObservers(aSubject, aTopic, aData) {
+    for (let observer of this._observers) {
+      observer.observe(aSubject, aTopic, aData);
+    }
+  },
+};
+
+var otherContactsTag = {
+  __proto__: ClassInfo(["nsIObserver", "imITag"], "Other Contacts Tag"),
+  hiddenTagsPref: "messenger.buddies.hiddenTags",
+  _hiddenTags: {},
+  _contactsInitialized: false,
+  _saveHiddenTagsPref() {
+    Services.prefs.setCharPref(
+      this.hiddenTagsPref,
+      Object.keys(this._hiddenTags).join(",")
+    );
+  },
+  showTag(aTag) {
+    let id = aTag.id;
+    delete this._hiddenTags[id];
+    let contacts = Object.keys(this._contacts).map(id => this._contacts[id]);
+    for (let contact of contacts) {
+      if (contact.getTags().some(t => t.id == id)) {
+        this._removeContact(contact);
+      }
+    }
+
+    aTag.notifyObservers(aTag, "tag-shown");
+    Services.obs.notifyObservers(aTag, "tag-shown");
+    this._saveHiddenTagsPref();
+  },
+  hideTag(aTag) {
+    if (aTag.id < 0 || aTag.id in otherContactsTag._hiddenTags) {
+      return;
+    }
+
+    this._hiddenTags[aTag.id] = aTag;
+    if (this._contactsInitialized) {
+      this._hideTag(aTag);
+    }
+
+    aTag.notifyObservers(aTag, "tag-hidden");
+    Services.obs.notifyObservers(aTag, "tag-hidden");
+    this._saveHiddenTagsPref();
+  },
+  _hideTag(aTag) {
+    for (let contact of aTag.getContacts()) {
+      if (
+        !(contact.id in this._contacts) &&
+        contact.getTags().every(t => t.id in this._hiddenTags)
+      ) {
+        this._addContact(contact);
+      }
+    }
+  },
+  observe(aSubject, aTopic, aData) {
+    aSubject.QueryInterface(Ci.imIContact);
+    if (aTopic == "contact-tag-removed" || aTopic == "contact-added") {
+      if (
+        !(aSubject.id in this._contacts) &&
+        !(parseInt(aData) in this._hiddenTags) &&
+        aSubject.getTags().every(t => t.id in this._hiddenTags)
+      ) {
+        this._addContact(aSubject);
+      }
+    } else if (
+      aSubject.id in this._contacts &&
+      (aTopic == "contact-removed" ||
+        (aTopic == "contact-tag-added" &&
+          !(parseInt(aData) in this._hiddenTags)))
+    ) {
+      this._removeContact(aSubject);
+    }
+  },
+
+  _initHiddenTags() {
+    let pref = Services.prefs.getCharPref(this.hiddenTagsPref);
+    if (!pref) {
+      return;
+    }
+    for (let tagId of pref.split(",")) {
+      this._hiddenTags[tagId] = TagsById[tagId];
+    }
+  },
+  _initContacts() {
+    if (this._contactsInitialized) {
+      return;
+    }
+    this._observers = [];
+    this._observer = {
+      self: this,
+      observe(aSubject, aTopic, aData) {
+        if (aTopic == "contact-moved-in" && !(aSubject instanceof Contact)) {
+          return;
+        }
+
+        this.self.notifyObservers(aSubject, aTopic, aData);
+      },
+    };
+    this._contacts = {};
+    this._contactsInitialized = true;
+    for (let id in this._hiddenTags) {
+      let tag = this._hiddenTags[id];
+      this._hideTag(tag);
+    }
+    Services.obs.addObserver(this, "contact-tag-added");
+    Services.obs.addObserver(this, "contact-tag-removed");
+    Services.obs.addObserver(this, "contact-added");
+    Services.obs.addObserver(this, "contact-removed");
+  },
+
+  // imITag implementation
+  get id() {
+    return -1;
+  },
+  get name() {
+    return "__others__";
+  },
+  set name(aNewName) {
+    throw Cr.NS_ERROR_NOT_AVAILABLE;
+  },
+  getContacts() {
+    return Object.keys(this._contacts).map(id => this._contacts[id]);
+  },
+  _addContact(aContact) {
+    this._contacts[aContact.id] = aContact;
+    this.notifyObservers(aContact, "contact-moved-in");
+    for (let observer of ContactsById[aContact.id]._observers) {
+      observer.observe(this, "contact-moved-in", null);
+    }
+    aContact.addObserver(this._observer);
+  },
+  _removeContact(aContact) {
+    delete this._contacts[aContact.id];
+    aContact.removeObserver(this._observer);
+    this.notifyObservers(aContact, "contact-moved-out");
+    for (let observer of ContactsById[aContact.id]._observers) {
+      observer.observe(this, "contact-moved-out", null);
+    }
+  },
+
+  addObserver(aObserver) {
+    if (!this._observers.includes(aObserver)) {
+      this._observers.push(aObserver);
+    }
+  },
+  removeObserver(aObserver) {
+    this._observers = this._observers.filter(o => o !== aObserver);
+  },
+  notifyObservers(aSubject, aTopic, aData) {
+    for (let observer of this._observers) {
+      observer.observe(aSubject, aTopic, aData);
+    }
+  },
+};
+
+var ContactsById = {};
+var LastDummyContactId = 0;
+function Contact(aId, aAlias) {
+  // Assign a negative id to dummy contacts that have a single buddy
+  this._id = aId || --LastDummyContactId;
+  this._alias = aAlias;
+  this._tags = [];
+  this._buddies = [];
+  this._observers = [];
+
+  ContactsById[this._id] = this;
+}
+Contact.prototype = {
+  __proto__: ClassInfo("imIContact", "Contact"),
+  _id: 0,
+  get id() {
+    return this._id;
+  },
+  get alias() {
+    return this._alias;
+  },
+  set alias(aNewAlias) {
+    this._ensureNotDummy();
+
+    let statement = DBConn.createStatement(
+      "UPDATE contacts SET alias = :alias WHERE id = :id"
+    );
+    statement.params.alias = aNewAlias;
+    statement.params.id = this._id;
+    executeAsyncThenFinalize(statement);
+
+    let oldDisplayName = this.displayName;
+    this._alias = aNewAlias;
+    this._notifyObservers("display-name-changed", oldDisplayName);
+    for (let buddy of this._buddies) {
+      for (let accountBuddy of buddy._accounts) {
+        accountBuddy.serverAlias = aNewAlias;
+      }
+    }
+    return aNewAlias;
+  },
+  _ensureNotDummy() {
+    if (this._id >= 0) {
+      return;
+    }
+
+    // Create a real contact for this dummy contact
+    let statement = DBConn.createStatement(
+      "INSERT INTO contacts DEFAULT VALUES"
+    );
+    try {
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+    delete ContactsById[this._id];
+    let oldId = this._id;
+    this._id = DBConn.lastInsertRowID;
+    ContactsById[this._id] = this;
+    this._notifyObservers("no-longer-dummy", oldId.toString());
+    // Update the contact_id for the single existing buddy of this contact
+    statement = DBConn.createStatement(
+      "UPDATE buddies SET contact_id = :id WHERE id = :buddy_id"
+    );
+    statement.params.id = this._id;
+    statement.params.buddy_id = this._buddies[0].id;
+    executeAsyncThenFinalize(statement);
+  },
+
+  getTags() {
+    return this._tags;
+  },
+  addTag(aTag, aInherited) {
+    if (this.hasTag(aTag)) {
+      return;
+    }
+
+    if (!aInherited) {
+      this._ensureNotDummy();
+      let statement = DBConn.createStatement(
+        "INSERT INTO contact_tag (contact_id, tag_id) " +
+          "VALUES(:contactId, :tagId)"
+      );
+      statement.params.contactId = this.id;
+      statement.params.tagId = aTag.id;
+      executeAsyncThenFinalize(statement);
+    }
+
+    aTag = TagsById[aTag.id];
+    this._tags.push(aTag);
+    aTag._addContact(this);
+
+    aTag.notifyObservers(this, "contact-moved-in");
+    for (let observer of this._observers) {
+      observer.observe(aTag, "contact-moved-in", null);
+    }
+    Services.obs.notifyObservers(this, "contact-tag-added", aTag.id);
+  },
+  /* Remove a tag from the local tags of the contact. */
+  _removeTag(aTag) {
+    if (!this.hasTag(aTag) || this._isTagInherited(aTag)) {
+      return;
+    }
+
+    this._removeContactTagRow(aTag);
+
+    this._tags = this._tags.filter(tag => tag.id != aTag.id);
+    aTag = TagsById[aTag.id];
+    aTag._removeContact(this);
+
+    aTag.notifyObservers(this, "contact-moved-out");
+    for (let observer of this._observers) {
+      observer.observe(aTag, "contact-moved-out", null);
+    }
+    Services.obs.notifyObservers(this, "contact-tag-removed", aTag.id);
+  },
+  _removeContactTagRow(aTag) {
+    let statement = DBConn.createStatement(
+      "DELETE FROM contact_tag " +
+        "WHERE contact_id = :contactId " +
+        "AND tag_id = :tagId"
+    );
+    statement.params.contactId = this.id;
+    statement.params.tagId = aTag.id;
+    executeAsyncThenFinalize(statement);
+  },
+  hasTag(aTag) {
+    return this._tags.some(t => t.id == aTag.id);
+  },
+  _massMove: false,
+  removeTag(aTag) {
+    if (!this.hasTag(aTag)) {
+      throw new Error(
+        "Attempting to remove a tag that the contact doesn't have"
+      );
+    }
+    if (this._tags.length == 1) {
+      throw new Error("Attempting to remove the last tag of a contact");
+    }
+
+    this._massMove = true;
+    let hasTag = this.hasTag.bind(this);
+    let newTag = this._tags[this._tags[0].id != aTag.id ? 0 : 1];
+    let moved = false;
+    this._buddies.forEach(function(aBuddy) {
+      aBuddy._accounts.forEach(function(aAccountBuddy) {
+        if (aAccountBuddy.tag.id == aTag.id) {
+          if (
+            aBuddy._accounts.some(
+              ab =>
+                ab.account.numericId == aAccountBuddy.account.numericId &&
+                ab.tag.id != aTag.id &&
+                hasTag(ab.tag)
+            )
+          ) {
+            // A buddy that already has an accountBuddy of the same
+            // account with another tag of the contact shouldn't be
+            // moved to newTag, just remove the accountBuddy
+            // associated to the tag we are removing.
+            aAccountBuddy.remove();
+            moved = true;
+          } else {
+            try {
+              aAccountBuddy.tag = newTag;
+              moved = true;
+            } catch (e) {
+              // Ignore failures. Some protocol plugins may not implement this.
+            }
+          }
+        }
+      });
+    });
+    this._massMove = false;
+    if (moved) {
+      this._moved(aTag, newTag);
+    } else {
+      // If we are here, the old tag is not inherited from a buddy, so
+      // just remove the local tag.
+      this._removeTag(aTag);
+    }
+  },
+  _isTagInherited(aTag) {
+    for (let buddy of this._buddies) {
+      for (let accountBuddy of buddy._accounts) {
+        if (accountBuddy.tag.id == aTag.id) {
+          return true;
+        }
+      }
+    }
+    return false;
+  },
+  _moved(aOldTag, aNewTag) {
+    if (this._massMove) {
+      return;
+    }
+
+    // Avoid xpconnect wrappers.
+    aNewTag = aNewTag && TagsById[aNewTag.id];
+    aOldTag = aOldTag && TagsById[aOldTag.id];
+
+    // Decide what we need to do. Return early if nothing to do.
+    let shouldRemove =
+      aOldTag && this.hasTag(aOldTag) && !this._isTagInherited(aOldTag);
+    let shouldAdd =
+      aNewTag && !this.hasTag(aNewTag) && this._isTagInherited(aNewTag);
+    if (!shouldRemove && !shouldAdd) {
+      return;
+    }
+
+    // Apply the changes.
+    let tags = this._tags;
+    if (shouldRemove) {
+      tags = tags.filter(aTag => aTag.id != aOldTag.id);
+      aOldTag._removeContact(this);
+    }
+    if (shouldAdd) {
+      tags.push(aNewTag);
+      aNewTag._addContact(this);
+    }
+    this._tags = tags;
+
+    // Finally, notify of the changes.
+    if (shouldRemove) {
+      aOldTag.notifyObservers(this, "contact-moved-out");
+      for (let observer of this._observers) {
+        observer.observe(aOldTag, "contact-moved-out", null);
+      }
+      Services.obs.notifyObservers(this, "contact-tag-removed", aOldTag.id);
+    }
+    if (shouldAdd) {
+      aNewTag.notifyObservers(this, "contact-moved-in");
+      for (let observer of this._observers) {
+        observer.observe(aNewTag, "contact-moved-in", null);
+      }
+      Services.obs.notifyObservers(this, "contact-tag-added", aNewTag.id);
+    }
+    Services.obs.notifyObservers(this, "contact-moved");
+  },
+
+  getBuddies() {
+    return this._buddies;
+  },
+  get _empty() {
+    return this._buddies.length == 0 || this._buddies.every(b => b._empty);
+  },
+
+  mergeContact(aContact) {
+    // Avoid merging the contact with itself or merging into an
+    // already removed contact.
+    if (aContact.id == this.id || !(this.id in ContactsById)) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    this._ensureNotDummy();
+    let contact = ContactsById[aContact.id]; // remove XPConnect wrapper
+
+    // Copy all the contact-only tags first, otherwise they would be lost.
+    for (let tag of contact.getTags()) {
+      if (!contact._isTagInherited(tag)) {
+        this.addTag(tag);
+      }
+    }
+
+    // Adopt each buddy. Removing the last one will delete the contact.
+    for (let buddy of contact.getBuddies()) {
+      buddy.contact = this;
+    }
+    this._updatePreferredBuddy();
+  },
+  moveBuddyBefore(aBuddy, aBeforeBuddy) {
+    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+    let oldPosition = this._buddies.indexOf(buddy);
+    if (oldPosition == -1) {
+      throw new Error("aBuddy isn't attached to this contact");
+    }
+
+    let newPosition = -1;
+    if (aBeforeBuddy) {
+      newPosition = this._buddies.indexOf(BuddiesById[aBeforeBuddy.id]);
+    }
+    if (newPosition == -1) {
+      newPosition = this._buddies.length - 1;
+    }
+
+    if (oldPosition == newPosition) {
+      return;
+    }
+
+    this._buddies.splice(oldPosition, 1);
+    this._buddies.splice(newPosition, 0, buddy);
+    this._updatePositions(
+      Math.min(oldPosition, newPosition),
+      Math.max(oldPosition, newPosition)
+    );
+    buddy._notifyObservers("position-changed", String(newPosition));
+    this._updatePreferredBuddy(buddy);
+  },
+  adoptBuddy(aBuddy) {
+    if (aBuddy.contact.id == this.id) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    let buddy = BuddiesById[aBuddy.id]; // remove XPConnect wrapper
+    buddy.contact = this;
+    this._updatePreferredBuddy(buddy);
+  },
+  _massRemove: false,
+  _removeBuddy(aBuddy) {
+    if (this._buddies.length == 1) {
+      if (this._id > 0) {
+        let statement = DBConn.createStatement(
+          "DELETE FROM contacts WHERE id = :id"
+        );
+        statement.params.id = this._id;
+        executeAsyncThenFinalize(statement);
+      }
+      this._notifyObservers("removed");
+      delete ContactsById[this._id];
+
+      for (let tag of this._tags) {
+        tag._removeContact(this);
+      }
+      let statement = DBConn.createStatement(
+        "DELETE FROM contact_tag WHERE contact_id = :id"
+      );
+      statement.params.id = this._id;
+      executeAsyncThenFinalize(statement);
+
+      delete this._tags;
+      delete this._buddies;
+      delete this._observers;
+    } else {
+      let index = this._buddies.indexOf(aBuddy);
+      if (index == -1) {
+        throw new Error("Removing an unknown buddy from contact " + this._id);
+      }
+
+      this._buddies = this._buddies.filter(b => b !== aBuddy);
+
+      // If we are actually removing the whole contact, don't bother updating
+      // the positions or the preferred buddy.
+      if (this._massRemove) {
+        return;
+      }
+
+      // No position to update if the removed buddy is at the last position.
+      if (index < this._buddies.length) {
+        this._updatePositions(index);
+      }
+
+      if (this._preferredBuddy.id == aBuddy.id) {
+        this._updatePreferredBuddy();
+      }
+    }
+  },
+  _updatePositions(aIndexBegin, aIndexEnd) {
+    if (aIndexEnd === undefined) {
+      aIndexEnd = this._buddies.length - 1;
+    }
+    if (aIndexBegin > aIndexEnd) {
+      throw new Error("_updatePositions: Invalid indexes");
+    }
+
+    let statement = DBConn.createStatement(
+      "UPDATE buddies SET position = :position WHERE id = :buddyId"
+    );
+    for (let i = aIndexBegin; i <= aIndexEnd; ++i) {
+      statement.params.position = i;
+      statement.params.buddyId = this._buddies[i].id;
+      statement.executeAsync();
+    }
+    statement.finalize();
+  },
+
+  detachBuddy(aBuddy) {
+    // Should return a new contact with the same list of tags.
+    let buddy = BuddiesById[aBuddy.id];
+    if (buddy.contact.id != this.id) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+    if (buddy.contact._buddies.length == 1) {
+      throw Cr.NS_ERROR_UNEXPECTED;
+    }
+
+    // Save the list of tags, it may be destoyed if the buddy was the last one.
+    let tags = buddy.contact.getTags();
+
+    // Create a new dummy contact and use it for the detached buddy.
+    buddy.contact = new Contact();
+    buddy.contact._notifyObservers("added");
+
+    // The first tag was inherited during the contact setter.
+    // This will copy the remaining tags.
+    for (let tag of tags) {
+      buddy.contact.addTag(tag);
+    }
+
+    return buddy.contact;
+  },
+  remove() {
+    this._massRemove = true;
+    for (let buddy of this._buddies) {
+      buddy.remove();
+    }
+  },
+
+  // imIStatusInfo implementation
+  _preferredBuddy: null,
+  get preferredBuddy() {
+    if (!this._preferredBuddy) {
+      this._updatePreferredBuddy();
+    }
+    return this._preferredBuddy;
+  },
+  set preferredBuddy(aBuddy) {
+    let shouldNotify = this._preferredBuddy != null;
+    let oldDisplayName =
+      this._preferredBuddy && this._preferredBuddy.displayName;
+    this._preferredBuddy = aBuddy;
+    if (shouldNotify) {
+      this._notifyObservers("preferred-buddy-changed");
+    }
+    if (oldDisplayName && this._preferredBuddy.displayName != oldDisplayName) {
+      this._notifyObservers("display-name-changed", oldDisplayName);
+    }
+    this._updateStatus();
+  },
+  // aBuddy indicate which buddy's availability has changed.
+  _updatePreferredBuddy(aBuddy) {
+    if (aBuddy) {
+      aBuddy = BuddiesById[aBuddy.id]; // remove potential XPConnect wrapper
+
+      if (!this._preferredBuddy) {
+        this.preferredBuddy = aBuddy;
+        return;
+      }
+
+      if (aBuddy.id == this._preferredBuddy.id) {
+        // The suggested buddy is already preferred, check if its
+        // availability has changed.
+        if (
+          aBuddy.statusType > this._statusType ||
+          (aBuddy.statusType == this._statusType &&
+            aBuddy.availabilityDetails >= this._availabilityDetails)
+        ) {
+          // keep the currently preferred buddy, only update the status.
+          this._updateStatus();
+          return;
+        }
+        // We aren't sure that the currently preferred buddy should
+        // still be preferred. Let's go through the list!
+      } else {
+        // The suggested buddy is not currently preferred. If it is
+        // more available or at a better position, prefer it!
+        if (
+          aBuddy.statusType > this._statusType ||
+          (aBuddy.statusType == this._statusType &&
+            (aBuddy.availabilityDetails > this._availabilityDetails ||
+              (aBuddy.availabilityDetails == this._availabilityDetails &&
+                this._buddies.indexOf(aBuddy) <
+                  this._buddies.indexOf(this.preferredBuddy))))
+        ) {
+          this.preferredBuddy = aBuddy;
+        }
+        return;
+      }
+    }
+
+    let preferred;
+    // |this._buddies| is ordered by user preference, so in case of
+    // equal availability, keep the current value of |preferred|.
+    for (let buddy of this._buddies) {
+      if (
+        !preferred ||
+        preferred.statusType < buddy.statusType ||
+        (preferred.statusType == buddy.statusType &&
+          preferred.availabilityDetails < buddy.availabilityDetails)
+      ) {
+        preferred = buddy;
+      }
+    }
+    if (
+      preferred &&
+      (!this._preferredBuddy || preferred.id != this._preferredBuddy.id)
+    ) {
+      this.preferredBuddy = preferred;
+    }
+  },
+  _updateStatus() {
+    let buddy = this._preferredBuddy; // for convenience
+
+    // Decide which notifications should be fired.
+    let notifications = [];
+    if (
+      this._statusType != buddy.statusType ||
+      this._availabilityDetails != buddy.availabilityDetails
+    ) {
+      notifications.push("availability-changed");
+    }
+    if (
+      this._statusType != buddy.statusType ||
+      this._statusText != buddy.statusText
+    ) {
+      notifications.push("status-changed");
+      if (this.online && buddy.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE) {
+        notifications.push("signed-off");
+      }
+      if (!this.online && buddy.statusType > Ci.imIStatusInfo.STATUS_OFFLINE) {
+        notifications.push("signed-on");
+      }
+    }
+
+    // Actually change the stored status.
+    [this._statusType, this._statusText, this._availabilityDetails] = [
+      buddy.statusType,
+      buddy.statusText,
+      buddy.availabilityDetails,
+    ];
+
+    // Fire the notifications.
+    notifications.forEach(function(aTopic) {
+      this._notifyObservers(aTopic);
+    }, this);
+  },
+  get displayName() {
+    return this._alias || this.preferredBuddy.displayName;
+  },
+  get buddyIconFilename() {
+    return this.preferredBuddy.buddyIconFilename;
+  },
+  _statusType: 0,
+  get statusType() {
+    return this._statusType;
+  },
+  get online() {
+    return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
+  },
+  get available() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
+  },
+  get idle() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
+  },
+  get mobile() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
+  },
+  _statusText: "",
+  get statusText() {
+    return this._statusText;
+  },
+  _availabilityDetails: 0,
+  get availabilityDetails() {
+    return this._availabilityDetails;
+  },
+  get canSendMessage() {
+    return this.preferredBuddy.canSendMessage;
+  },
+  // XXX should we list the buddies in the tooltip?
+  getTooltipInfo() {
+    return this.preferredBuddy.getTooltipInfo();
+  },
+  createConversation() {
+    let uiConv = Services.conversations.getUIConversationByContactId(this.id);
+    if (uiConv) {
+      return uiConv.target;
+    }
+    return this.preferredBuddy.createConversation();
+  },
+
+  addObserver(aObserver) {
+    if (!this._observers.includes(aObserver)) {
+      this._observers.push(aObserver);
+    }
+  },
+  removeObserver(aObserver) {
+    if (!this.hasOwnProperty("_observers")) {
+      return;
+    }
+
+    this._observers = this._observers.filter(o => o !== aObserver);
+  },
+  // internal calls + calls from add-ons
+  notifyObservers(aSubject, aTopic, aData) {
+    for (let observer of this._observers) {
+      if ("observe" in observer) {
+        // avoid failing on destructed XBL bindings...
+        observer.observe(aSubject, aTopic, aData);
+      }
+    }
+    for (let tag of this._tags) {
+      tag.notifyObservers(aSubject, aTopic, aData);
+    }
+    Services.obs.notifyObservers(aSubject, aTopic, aData);
+  },
+  _notifyObservers(aTopic, aData) {
+    this.notifyObservers(this, "contact-" + aTopic, aData);
+  },
+
+  // This is called by the imIBuddy implementations.
+  _observe(aSubject, aTopic, aData) {
+    // Forward the notification.
+    this.notifyObservers(aSubject, aTopic, aData);
+
+    let isPreferredBuddy =
+      aSubject instanceof Buddy && aSubject.id == this.preferredBuddy.id;
+    switch (aTopic) {
+      case "buddy-availability-changed":
+        this._updatePreferredBuddy(aSubject);
+        break;
+      case "buddy-status-changed":
+        if (isPreferredBuddy) {
+          this._updateStatus();
+        }
+        break;
+      case "buddy-display-name-changed":
+        if (isPreferredBuddy && !this._alias) {
+          this._notifyObservers("display-name-changed", aData);
+        }
+        break;
+      case "buddy-icon-changed":
+        if (isPreferredBuddy) {
+          this._notifyObservers("icon-changed");
+        }
+        break;
+      case "buddy-added":
+        // Currently buddies are always added in dummy empty contacts,
+        // later we may want to check this._buddies.length == 1.
+        this._notifyObservers("added");
+        break;
+      case "buddy-removed":
+        this._removeBuddy(aSubject);
+    }
+  },
+};
+
+var BuddiesById = {};
+function Buddy(aId, aKey, aName, aSrvAlias, aContactId) {
+  this._id = aId;
+  this._key = aKey;
+  this._name = aName;
+  if (aSrvAlias) {
+    this._srvAlias = aSrvAlias;
+  }
+  this._accounts = [];
+  this._observers = [];
+
+  if (aContactId) {
+    this._contact = ContactsById[aContactId];
+  }
+  // Avoid failure if aContactId was invalid.
+  if (!this._contact) {
+    this._contact = new Contact(null, null);
+  }
+
+  this._contact._buddies.push(this);
+
+  BuddiesById[this._id] = this;
+}
+Buddy.prototype = {
+  __proto__: ClassInfo("imIBuddy", "Buddy"),
+  get id() {
+    return this._id;
+  },
+  destroy() {
+    for (let ab of this._accounts) {
+      ab.unInit();
+    }
+    delete this._accounts;
+    delete this._observers;
+    delete this._preferredAccount;
+  },
+  get protocol() {
+    return this._accounts[0].account.protocol;
+  },
+  get userName() {
+    return this._name;
+  },
+  get normalizedName() {
+    return this._key;
+  },
+  _srvAlias: "",
+  _contact: null,
+  get contact() {
+    return this._contact;
+  },
+  set contact(aContact) /* not in imIBuddy */ {
+    if (aContact.id == this._contact.id) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    this._notifyObservers("moved-out-of-contact");
+    this._contact._removeBuddy(this);
+
+    this._contact = aContact;
+    this._contact._buddies.push(this);
+
+    // Ensure all the inherited tags are in the new contact.
+    for (let accountBuddy of this._accounts) {
+      this._contact.addTag(TagsById[accountBuddy.tag.id], true);
+    }
+
+    let statement = DBConn.createStatement(
+      "UPDATE buddies SET contact_id = :contactId, " +
+        "position = :position " +
+        "WHERE id = :buddyId"
+    );
+    statement.params.contactId = aContact.id > 0 ? aContact.id : 0;
+    statement.params.position = aContact._buddies.length - 1;
+    statement.params.buddyId = this.id;
+    executeAsyncThenFinalize(statement);
+
+    this._notifyObservers("moved-into-contact");
+    return aContact;
+  },
+  _hasAccountBuddy(aAccountId, aTagId) {
+    for (let ab of this._accounts) {
+      if (ab.account.numericId == aAccountId && ab.tag.id == aTagId) {
+        return true;
+      }
+    }
+    return false;
+  },
+  getAccountBuddies() {
+    return this._accounts;
+  },
+
+  _addAccount(aAccountBuddy, aTag) {
+    this._accounts.push(aAccountBuddy);
+    let contact = this._contact;
+    if (!this._contact._tags.includes(aTag)) {
+      this._contact._tags.push(aTag);
+      aTag._addContact(contact);
+    }
+
+    if (!this._preferredAccount) {
+      this._preferredAccount = aAccountBuddy;
+    }
+  },
+  get _empty() {
+    return this._accounts.length == 0;
+  },
+
+  remove() {
+    for (let account of this._accounts) {
+      account.remove();
+    }
+  },
+
+  // imIStatusInfo implementation
+  _preferredAccount: null,
+  get preferredAccountBuddy() {
+    return this._preferredAccount;
+  },
+  _isPreferredAccount(aAccountBuddy) {
+    if (
+      aAccountBuddy.account.numericId !=
+      this._preferredAccount.account.numericId
+    ) {
+      return false;
+    }
+
+    // In case we have more than one accountBuddy for the same buddy
+    // and account (possible if the buddy is in several groups on the
+    // server), the protocol plugin may be broken and not update all
+    // instances, so ensure we handle the notifications on the instance
+    // that is currently being notified of a change:
+    this._preferredAccount = aAccountBuddy;
+
+    return true;
+  },
+  set preferredAccount(aAccount) {
+    let oldDisplayName =
+      this._preferredAccount && this._preferredAccount.displayName;
+    this._preferredAccount = aAccount;
+    this._notifyObservers("preferred-account-changed");
+    if (
+      oldDisplayName &&
+      this._preferredAccount.displayName != oldDisplayName
+    ) {
+      this._notifyObservers("display-name-changed", oldDisplayName);
+    }
+    this._updateStatus();
+  },
+  // aAccount indicate which account's availability has changed.
+  _updatePreferredAccount(aAccount) {
+    if (aAccount) {
+      if (
+        aAccount.account.numericId == this._preferredAccount.account.numericId
+      ) {
+        // The suggested account is already preferred, check if its
+        // availability has changed.
+        if (
+          aAccount.statusType > this._statusType ||
+          (aAccount.statusType == this._statusType &&
+            aAccount.availabilityDetails >= this._availabilityDetails)
+        ) {
+          // keep the currently preferred account, only update the status.
+          this._updateStatus();
+          return;
+        }
+        // We aren't sure that the currently preferred account should
+        // still be preferred. Let's go through the list!
+      } else {
+        // The suggested account is not currently preferred. If it is
+        // more available, prefer it!
+        if (
+          aAccount.statusType > this._statusType ||
+          (aAccount.statusType == this._statusType &&
+            aAccount.availabilityDetails > this._availabilityDetails)
+        ) {
+          this.preferredAccount = aAccount;
+        }
+        return;
+      }
+    }
+
+    let preferred;
+    // TODO take into account the order of the account-manager list.
+    for (let account of this._accounts) {
+      if (
+        !preferred ||
+        preferred.statusType < account.statusType ||
+        (preferred.statusType == account.statusType &&
+          preferred.availabilityDetails < account.availabilityDetails)
+      ) {
+        preferred = account;
+      }
+    }
+    if (!this._preferredAccount) {
+      if (preferred) {
+        this.preferredAccount = preferred;
+      }
+      return;
+    }
+    if (
+      preferred.account.numericId != this._preferredAccount.account.numericId
+    ) {
+      this.preferredAccount = preferred;
+    } else {
+      this._updateStatus();
+    }
+  },
+  _updateStatus() {
+    let account = this._preferredAccount; // for convenience
+
+    // Decide which notifications should be fired.
+    let notifications = [];
+    if (
+      this._statusType != account.statusType ||
+      this._availabilityDetails != account.availabilityDetails
+    ) {
+      notifications.push("availability-changed");
+    }
+    if (
+      this._statusType != account.statusType ||
+      this._statusText != account.statusText
+    ) {
+      notifications.push("status-changed");
+      if (
+        this.online &&
+        account.statusType <= Ci.imIStatusInfo.STATUS_OFFLINE
+      ) {
+        notifications.push("signed-off");
+      }
+      if (
+        !this.online &&
+        account.statusType > Ci.imIStatusInfo.STATUS_OFFLINE
+      ) {
+        notifications.push("signed-on");
+      }
+    }
+
+    // Actually change the stored status.
+    [this._statusType, this._statusText, this._availabilityDetails] = [
+      account.statusType,
+      account.statusText,
+      account.availabilityDetails,
+    ];
+
+    // Fire the notifications.
+    notifications.forEach(function(aTopic) {
+      this._notifyObservers(aTopic);
+    }, this);
+  },
+  get displayName() {
+    return (
+      (this._preferredAccount && this._preferredAccount.displayName) ||
+      this._srvAlias ||
+      this._name
+    );
+  },
+  get buddyIconFilename() {
+    return this._preferredAccount.buddyIconFilename;
+  },
+  _statusType: 0,
+  get statusType() {
+    return this._statusType;
+  },
+  get online() {
+    return this.statusType > Ci.imIStatusInfo.STATUS_OFFLINE;
+  },
+  get available() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_AVAILABLE;
+  },
+  get idle() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_IDLE;
+  },
+  get mobile() {
+    return this.statusType == Ci.imIStatusInfo.STATUS_MOBILE;
+  },
+  _statusText: "",
+  get statusText() {
+    return this._statusText;
+  },
+  _availabilityDetails: 0,
+  get availabilityDetails() {
+    return this._availabilityDetails;
+  },
+  get canSendMessage() {
+    return this._preferredAccount.canSendMessage;
+  },
+  // XXX should we list the accounts in the tooltip?
+  getTooltipInfo() {
+    return this._preferredAccount.getTooltipInfo();
+  },
+  createConversation() {
+    return this._preferredAccount.createConversation();
+  },
+
+  addObserver(aObserver) {
+    if (!this._observers.includes(aObserver)) {
+      this._observers.push(aObserver);
+    }
+  },
+  removeObserver(aObserver) {
+    if (!this._observers) {
+      return;
+    }
+    this._observers = this._observers.filter(o => o !== aObserver);
+  },
+  // internal calls + calls from add-ons
+  notifyObservers(aSubject, aTopic, aData) {
+    try {
+      for (let observer of this._observers) {
+        observer.observe(aSubject, aTopic, aData);
+      }
+      this._contact._observe(aSubject, aTopic, aData);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+  _notifyObservers(aTopic, aData) {
+    this.notifyObservers(this, "buddy-" + aTopic, aData);
+  },
+
+  // This is called by the prplIAccountBuddy implementations.
+  observe(aSubject, aTopic, aData) {
+    // Forward the notification.
+    this.notifyObservers(aSubject, aTopic, aData);
+
+    switch (aTopic) {
+      case "account-buddy-availability-changed":
+        this._updatePreferredAccount(aSubject);
+        break;
+      case "account-buddy-status-changed":
+        if (this._isPreferredAccount(aSubject)) {
+          this._updateStatus();
+        }
+        break;
+      case "account-buddy-display-name-changed":
+        if (this._isPreferredAccount(aSubject)) {
+          this._srvAlias =
+            this.displayName != this.userName ? this.displayName : "";
+          let statement = DBConn.createStatement(
+            "UPDATE buddies SET srv_alias = :srvAlias WHERE id = :buddyId"
+          );
+          statement.params.buddyId = this.id;
+          statement.params.srvAlias = this._srvAlias;
+          executeAsyncThenFinalize(statement);
+          this._notifyObservers("display-name-changed", aData);
+        }
+        break;
+      case "account-buddy-icon-changed":
+        if (this._isPreferredAccount(aSubject)) {
+          this._notifyObservers("icon-changed");
+        }
+        break;
+      case "account-buddy-added":
+        if (this._accounts.length == 0) {
+          // Add the new account in the empty buddy instance.
+          // The TagsById hack is to bypass the xpconnect wrapper.
+          this._addAccount(aSubject, TagsById[aSubject.tag.id]);
+          this._updateStatus();
+          this._notifyObservers("added");
+        } else {
+          this._accounts.push(aSubject);
+          this.contact._moved(null, aSubject.tag);
+          this._updatePreferredAccount(aSubject);
+        }
+        break;
+      case "account-buddy-removed":
+        if (this._accounts.length == 1) {
+          let statement = DBConn.createStatement(
+            "DELETE FROM buddies WHERE id = :id"
+          );
+          try {
+            statement.params.id = this.id;
+            statement.execute();
+          } finally {
+            statement.finalize();
+          }
+          this._notifyObservers("removed");
+
+          delete BuddiesById[this._id];
+          this.destroy();
+        } else {
+          this._accounts = this._accounts.filter(function(ab) {
+            return (
+              ab.account.numericId != aSubject.account.numericId ||
+              ab.tag.id != aSubject.tag.id
+            );
+          });
+          if (
+            this._preferredAccount.account.numericId ==
+              aSubject.account.numericId &&
+            this._preferredAccount.tag.id == aSubject.tag.id
+          ) {
+            this._preferredAccount = null;
+            this._updatePreferredAccount();
+          }
+          this.contact._moved(aSubject.tag);
+        }
+        break;
+    }
+  },
+};
+
+function ContactsService() {}
+ContactsService.prototype = {
+  initContacts() {
+    let statement = DBConn.createStatement("SELECT id, name FROM tags");
+    try {
+      while (statement.executeStep()) {
+        Tags.push(new Tag(statement.getInt32(0), statement.getUTF8String(1)));
+      }
+    } finally {
+      statement.finalize();
+    }
+
+    statement = DBConn.createStatement("SELECT id, alias FROM contacts");
+    try {
+      while (statement.executeStep()) {
+        new Contact(statement.getInt32(0), statement.getUTF8String(1));
+      }
+    } finally {
+      statement.finalize();
+    }
+
+    statement = DBConn.createStatement(
+      "SELECT contact_id, tag_id FROM contact_tag"
+    );
+    try {
+      while (statement.executeStep()) {
+        let contact = ContactsById[statement.getInt32(0)];
+        let tag = TagsById[statement.getInt32(1)];
+        contact._tags.push(tag);
+        tag._addContact(contact);
+      }
+    } finally {
+      statement.finalize();
+    }
+
+    statement = DBConn.createStatement(
+      "SELECT id, key, name, srv_alias, contact_id FROM buddies ORDER BY position"
+    );
+    try {
+      while (statement.executeStep()) {
+        new Buddy(
+          statement.getInt32(0),
+          statement.getUTF8String(1),
+          statement.getUTF8String(2),
+          statement.getUTF8String(3),
+          statement.getInt32(4)
+        );
+        // FIXME is there a way to enforce that all AccountBuddies of a Buddy have the same protocol?
+      }
+    } finally {
+      statement.finalize();
+    }
+
+    statement = DBConn.createStatement(
+      "SELECT account_id, buddy_id, tag_id FROM account_buddy"
+    );
+    try {
+      while (statement.executeStep()) {
+        let accountId = statement.getInt32(0);
+        let buddyId = statement.getInt32(1);
+        let tagId = statement.getInt32(2);
+
+        if (!BuddiesById.hasOwnProperty(buddyId)) {
+          Cu.reportError(
+            "Corrupted database: account_buddy entry for account " +
+              accountId +
+              " and tag " +
+              tagId +
+              " references unknown buddy with id " +
+              buddyId
+          );
+          continue;
+        }
+
+        let buddy = BuddiesById[buddyId];
+        if (buddy._hasAccountBuddy(accountId, tagId)) {
+          Cu.reportError(
+            "Corrupted database: duplicated account_buddy entry: " +
+              "account_id = " +
+              accountId +
+              ", buddy_id = " +
+              buddyId +
+              ", tag_id = " +
+              tagId
+          );
+          continue;
+        }
+
+        let account = Services.accounts.getAccountByNumericId(accountId);
+        let tag = TagsById[tagId];
+        try {
+          buddy._addAccount(account.loadBuddy(buddy, tag), tag);
+        } catch (e) {
+          Cu.reportError(e);
+          dump(e + "\n");
+        }
+      }
+    } finally {
+      statement.finalize();
+    }
+    otherContactsTag._initHiddenTags();
+  },
+  unInitContacts() {
+    Tags = [];
+    TagsById = {};
+    // Avoid shutdown leaks caused by references to native components
+    // implementing prplIAccountBuddy.
+    for (let buddyId in BuddiesById) {
+      let buddy = BuddiesById[buddyId];
+      buddy.destroy();
+    }
+    BuddiesById = {};
+    ContactsById = {};
+  },
+
+  getContactById: aId => ContactsById[aId],
+  // Get an array of all existing contacts.
+  getContacts() {
+    return Object.keys(ContactsById)
+      .filter(id => !ContactsById[id]._empty)
+      .map(id => ContactsById[id]);
+  },
+  getBuddyById: aId => BuddiesById[aId],
+  getBuddyByNameAndProtocol(aNormalizedName, aPrpl) {
+    let statement = DBConn.createStatement(
+      "SELECT b.id FROM buddies b " +
+        "JOIN account_buddy ab ON buddy_id = b.id " +
+        "JOIN accounts a ON account_id = a.id " +
+        "WHERE b.key = :buddyName and a.prpl = :prplId"
+    );
+    statement.params.buddyName = aNormalizedName;
+    statement.params.prplId = aPrpl.id;
+    try {
+      if (!statement.executeStep()) {
+        return null;
+      }
+      return BuddiesById[statement.row.id];
+    } finally {
+      statement.finalize();
+    }
+  },
+  getAccountBuddyByNameAndAccount(aNormalizedName, aAccount) {
+    let buddy = this.getBuddyByNameAndProtocol(
+      aNormalizedName,
+      aAccount.protocol
+    );
+    if (buddy) {
+      let id = aAccount.id;
+      for (let accountBuddy of buddy.getAccountBuddies()) {
+        if (accountBuddy.account.id == id) {
+          return accountBuddy;
+        }
+      }
+    }
+    return null;
+  },
+
+  accountBuddyAdded(aAccountBuddy) {
+    let account = aAccountBuddy.account;
+    let normalizedName = aAccountBuddy.normalizedName;
+    let buddy = this.getBuddyByNameAndProtocol(
+      normalizedName,
+      account.protocol
+    );
+    if (!buddy) {
+      let statement = DBConn.createStatement(
+        "INSERT INTO buddies " +
+          "(key, name, srv_alias, position) " +
+          "VALUES(:key, :name, :srvAlias, 0)"
+      );
+      try {
+        let name = aAccountBuddy.userName;
+        let srvAlias = aAccountBuddy.serverAlias;
+        statement.params.key = normalizedName;
+        statement.params.name = name;
+        statement.params.srvAlias = srvAlias;
+        statement.execute();
+        buddy = new Buddy(
+          DBConn.lastInsertRowID,
+          normalizedName,
+          name,
+          srvAlias,
+          0
+        );
+      } finally {
+        statement.finalize();
+      }
+    }
+
+    // Initialize the 'buddy' field of the prplIAccountBuddy instance.
+    aAccountBuddy.buddy = buddy;
+
+    // Ensure we aren't storing a duplicate entry.
+    let accountId = account.numericId;
+    let tagId = aAccountBuddy.tag.id;
+    if (buddy._hasAccountBuddy(accountId, tagId)) {
+      Cu.reportError(
+        "Attempting to store a duplicate account buddy " +
+          normalizedName +
+          ", account id = " +
+          accountId +
+          ", tag id = " +
+          tagId
+      );
+      return;
+    }
+
+    // Store the new account buddy.
+    let statement = DBConn.createStatement(
+      "INSERT INTO account_buddy " +
+        "(account_id, buddy_id, tag_id) " +
+        "VALUES(:accountId, :buddyId, :tagId)"
+    );
+    try {
+      statement.params.accountId = accountId;
+      statement.params.buddyId = buddy.id;
+      statement.params.tagId = tagId;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+
+    // Fire the notifications.
+    buddy.observe(aAccountBuddy, "account-buddy-added");
+  },
+  accountBuddyRemoved(aAccountBuddy) {
+    let buddy = aAccountBuddy.buddy;
+    let statement = DBConn.createStatement(
+      "DELETE FROM account_buddy " +
+        "WHERE account_id = :accountId AND " +
+        "buddy_id = :buddyId AND " +
+        "tag_id = :tagId"
+    );
+    try {
+      statement.params.accountId = aAccountBuddy.account.numericId;
+      statement.params.buddyId = buddy.id;
+      statement.params.tagId = aAccountBuddy.tag.id;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+
+    buddy.observe(aAccountBuddy, "account-buddy-removed");
+  },
+
+  accountBuddyMoved(aAccountBuddy, aOldTag, aNewTag) {
+    let buddy = aAccountBuddy.buddy;
+    let statement = DBConn.createStatement(
+      "UPDATE account_buddy " +
+        "SET tag_id = :newTagId " +
+        "WHERE account_id = :accountId AND " +
+        "buddy_id = :buddyId AND " +
+        "tag_id = :oldTagId"
+    );
+    try {
+      statement.params.accountId = aAccountBuddy.account.numericId;
+      statement.params.buddyId = buddy.id;
+      statement.params.oldTagId = aOldTag.id;
+      statement.params.newTagId = aNewTag.id;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+
+    let contact = ContactsById[buddy.contact.id];
+
+    // aNewTag is now inherited by the contact from an account buddy, so avoid
+    // keeping direct tag <-> contact links in the contact_tag table.
+    contact._removeContactTagRow(aNewTag);
+
+    buddy.observe(aAccountBuddy, "account-buddy-moved");
+    contact._moved(aOldTag, aNewTag);
+  },
+
+  storeAccount(aId, aUserName, aPrplId) {
+    let statement = DBConn.createStatement(
+      "SELECT name, prpl FROM accounts WHERE id = :id"
+    );
+    statement.params.id = aId;
+    try {
+      if (statement.executeStep()) {
+        if (
+          statement.getUTF8String(0) == aUserName &&
+          statement.getUTF8String(1) == aPrplId
+        ) {
+          // The account is already stored correctly.
+          return;
+        }
+        throw Cr.NS_ERROR_UNEXPECTED; // Corrupted database?!?
+      }
+    } finally {
+      statement.finalize();
+    }
+
+    // Actually store the account.
+    statement = DBConn.createStatement(
+      "INSERT INTO accounts (id, name, prpl) " +
+        "VALUES(:id, :userName, :prplId)"
+    );
+    try {
+      statement.params.id = aId;
+      statement.params.userName = aUserName;
+      statement.params.prplId = aPrplId;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+  },
+  accountIdExists(aId) {
+    let statement = DBConn.createStatement(
+      "SELECT id FROM accounts WHERE id = :id"
+    );
+    try {
+      statement.params.id = aId;
+      return statement.executeStep();
+    } finally {
+      statement.finalize();
+    }
+  },
+  forgetAccount(aId) {
+    let statement = DBConn.createStatement(
+      "DELETE FROM accounts WHERE id = :accountId"
+    );
+    try {
+      statement.params.accountId = aId;
+      statement.execute();
+    } finally {
+      statement.finalize();
+    }
+
+    // removing the account from the accounts table is not enough,
+    // we need to remove all the associated account_buddy entries too
+    statement = DBConn.createStatement(
+      "DELETE FROM account_buddy WHERE account_id = :accountId"
+    );
+    try {
+      statement.params.accountId = aId;
+      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,
+]);
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imContacts.manifest
@@ -0,0 +1,4 @@
+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}
new file mode 100644
--- /dev/null
+++ b/chat/components/src/imConversations.js
@@ -0,0 +1,839 @@
+/* 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 { 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;
+var gLastPrplConvId = 0;
+
+XPCOMUtils.defineLazyGetter(this, "bundle", () =>
+  Services.strings.createBundle("chrome://chat/locale/conversations.properties")
+);
+
+function OutgoingMessage(aMsg, aConversation) {
+  this.message = aMsg;
+  this.conversation = aConversation;
+}
+OutgoingMessage.prototype = {
+  __proto__: ClassInfo("imIOutgoingMessage", "Outgoing Message"),
+  cancelled: false,
+  action: false,
+};
+
+function imMessage(aPrplMessage) {
+  this.prplMessage = aPrplMessage;
+}
+imMessage.prototype = {
+  __proto__: ClassInfo(["imIMessage", "prplIMessage"], "IM Message"),
+  cancelled: false,
+  color: "",
+  _displayMessage: null,
+  encrypted: false,
+
+  get displayMessage() {
+    // Explicitly test for null so that blank messages don't fall back to
+    // the original. Especially problematic in encryption extensions like OTR.
+    return this._displayMessage !== null
+      ? this._displayMessage
+      : this.prplMessage.originalMessage;
+  },
+  set displayMessage(aMsg) {
+    this._displayMessage = aMsg;
+  },
+
+  get message() {
+    return this.prplMessage.message;
+  },
+  set message(aMsg) {
+    this.prplMessage.message = aMsg;
+  },
+
+  // from prplIMessage
+  get who() {
+    return this.prplMessage.who;
+  },
+  get time() {
+    return this.prplMessage.time;
+  },
+  get id() {
+    return this.prplMessage.id;
+  },
+  get alias() {
+    return this.prplMessage.alias;
+  },
+  get iconURL() {
+    return this.prplMessage.iconURL;
+  },
+  get conversation() {
+    return this.prplMessage.conversation;
+  },
+  set conversation(aConv) {
+    this.prplMessage.conversation = aConv;
+  },
+  get outgoing() {
+    return this.prplMessage.outgoing;
+  },
+  get incoming() {
+    return this.prplMessage.incoming;
+  },
+  get system() {
+    return this.prplMessage.system;
+  },
+  get autoResponse() {
+    return this.prplMessage.autoResponse;
+  },
+  get containsNick() {
+    return this.prplMessage.containsNick;
+  },
+  get noLog() {
+    return this.prplMessage.noLog;
+  },
+  get error() {
+    return this.prplMessage.error;
+  },
+  get delayed() {
+    return this.prplMessage.delayed;
+  },
+  get noFormat() {
+    return this.prplMessage.noFormat;
+  },
+  get containsImages() {
+    return this.prplMessage.containsImages;
+  },
+  get notification() {
+    return this.prplMessage.notification;
+  },
+  get noLinkification() {
+    return this.prplMessage.noLinkification;
+  },
+  get originalMessage() {
+    return this.prplMessage.originalMessage;
+  },
+  getActions() {
+    return this.prplMessage.getActions();
+  },
+};
+
+function UIConversation(aPrplConversation) {
+  this._prplConv = {};
+  this.id = ++gLastUIConvId;
+  this._observers = [];
+  this._messages = [];
+  this.changeTargetTo(aPrplConversation);
+  let iface = Ci["prplIConv" + (aPrplConversation.isChat ? "Chat" : "IM")];
+  this._interfaces = this._interfaces.concat(iface);
+  // XPConnect will create a wrapper around 'this' after here,
+  // so the list of exposed interfaces shouldn't change anymore.
+  this.updateContactObserver();
+  Services.obs.notifyObservers(this, "new-ui-conversation");
+}
+
+UIConversation.prototype = {
+  __proto__: ClassInfo(
+    ["imIConversation", "prplIConversation", "nsIObserver"],
+    "UI conversation"
+  ),
+  _observedContact: null,
+  get contact() {
+    let target = this.target;
+    if (!target.isChat && target.buddy) {
+      return target.buddy.buddy.contact;
+    }
+    return null;
+  },
+  updateContactObserver() {
+    let contact = this.contact;
+    if (contact && !this._observedContact) {
+      contact.addObserver(this);
+      this._observedContact = contact;
+    } else if (!contact && this.observedContact) {
+      this._observedContact.removeObserver(this);
+      delete this._observedContact;
+    }
+  },
+  get target() {
+    return this._prplConv[this._currentTargetId];
+  },
+  set target(aPrplConversation) {
+    this.changeTargetTo(aPrplConversation);
+  },
+  get hasMultipleTargets() {
+    return Object.keys(this._prplConv).length > 1;
+  },
+  getTargetByAccount(aAccount) {
+    let accountId = aAccount.id;
+    for (let id in this._prplConv) {
+      let prplConv = this._prplConv[id];
+      if (prplConv.account.id == accountId) {
+        return prplConv;
+      }
+    }
+    return null;
+  },
+  _currentTargetId: 0,
+  changeTargetTo(aPrplConversation) {
+    let id = aPrplConversation.id;
+    if (this._currentTargetId == id) {
+      return;
+    }
+
+    if (!(id in this._prplConv)) {
+      this._prplConv[id] = aPrplConversation;
+      aPrplConversation.addObserver(this.observeConv.bind(this, id));
+    }
+
+    let shouldNotify = this._currentTargetId;
+    this._currentTargetId = id;
+    if (!this.isChat) {
+      let buddy = this.buddy;
+      if (buddy) {
+        ({ statusType: this.statusType, statusText: this.statusText } = buddy);
+      }
+    }
+    if (shouldNotify) {
+      this.notifyObservers(this, "target-prpl-conversation-changed");
+      let target = this.target;
+      let params = [target.title, target.account.protocol.name];
+      this.systemMessage(bundle.formatStringFromName("targetChanged", params));
+    }
+  },
+  // Returns a boolean indicating if the ui-conversation was closed.
+  // If the conversation was closed, aContactId.value is set to the contact id
+  // or 0 if no contact was associated with the conversation.
+  removeTarget(aPrplConversation, aContactId) {
+    let id = aPrplConversation.id;
+    if (!(id in this._prplConv)) {
+      throw new