summaryrefslogtreecommitdiffstats
path: root/comm/chat/components/src/imAccounts.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/components/src/imAccounts.sys.mjs')
-rw-r--r--comm/chat/components/src/imAccounts.sys.mjs1237
1 files changed, 1237 insertions, 0 deletions
diff --git a/comm/chat/components/src/imAccounts.sys.mjs b/comm/chat/components/src/imAccounts.sys.mjs
new file mode 100644
index 0000000000..f06b503fa6
--- /dev/null
+++ b/comm/chat/components/src/imAccounts.sys.mjs
@@ -0,0 +1,1237 @@
+/* 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/. */
+
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import {
+ ClassInfo,
+ executeSoon,
+ l10nHelper,
+} from "resource:///modules/imXPCOMUtils.sys.mjs";
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+import { IMServices } from "resource:///modules/IMServices.sys.mjs";
+import {
+ GenericAccountPrototype,
+ GenericAccountBuddyPrototype,
+} from "resource:///modules/jsProtoHelper.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyGetter(lazy, "_", () =>
+ l10nHelper("chrome://chat/locale/accounts.properties")
+);
+XPCOMUtils.defineLazyGetter(lazy, "_maxDebugMessages", () =>
+ Services.prefs.getIntPref("messenger.accounts.maxDebugMessages")
+);
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "HttpProtocolHandler",
+ "@mozilla.org/network/protocol;1?name=http",
+ "nsIHttpProtocolHandler"
+);
+
+var kPrefAutologinPending = "messenger.accounts.autoLoginPending";
+let kPrefAccountOrder = "mail.accountmanager.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 gUserCanceledPrimaryPasswordPrompt = 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.clearUserPref(kPrefAutologinPending);
+ SavePrefTimer.initTimer();
+ },
+};
+
+function UnknownProtocol(aPrplId) {
+ this.id = aPrplId;
+}
+UnknownProtocol.prototype = {
+ __proto__: ClassInfo("prplIProtocol", "Unknown protocol"),
+ get name() {
+ return "";
+ },
+ get normalizedName() {
+ // Use the ID, but remove the 'prpl-' prefix.
+ return this.id.replace(/^prpl-/, "");
+ },
+ get iconBaseURI() {
+ return "chrome://chat/skin/prpl-unknown/";
+ },
+ getOptions() {
+ return [];
+ },
+ get usernamePrefix() {
+ return "";
+ },
+ getUsernameSplit() {
+ return [];
+ },
+ get usernameEmptyText() {
+ return "";
+ },
+
+ getAccount(aKey, aName) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ accountExists() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ // false seems an acceptable default for all options
+ // (they should never be called anyway).
+ get chatHasTopic() {
+ return false;
+ },
+ get noPassword() {
+ return false;
+ },
+ get passwordOptional() {
+ return true;
+ },
+ get slashCommandsNative() {
+ return false;
+ },
+ get canEncrypt() {
+ 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;
+
+/**
+ * @param {string} aKey - Account key for preferences.
+ * @param {string} [aName] - Name of the account if it is new. Will be stored
+ * in account preferences. If not provided, the value from the account
+ * preferences is used instead.
+ * @param {string} [aPrplId] - Protocol ID for this account if it is new. Will
+ * be stored in account preferences. If not provided, the value from the
+ * account preferences is used instead.
+ */
+function imAccount(aKey, aName, aPrplId) {
+ if (!aKey.startsWith(kAccountKeyPrefix)) {
+ throw Components.Exception(`Invalid key: ${aKey}`, 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 = IMServices.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.
+ IMServices.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(() => {
+ // If protocol is falsy remove() was called on this instance while waiting
+ // for the promise to resolve. Since the instance was disposed there is
+ // nothing to do.
+ if (!this.protocol) {
+ return;
+ }
+
+ // 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 Components.Exception("", 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 (
+ lazy._maxDebugMessages &&
+ this._debugMessages.length >= lazy._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 ${lazy.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 || IMServices.core.globalUserStatus;
+ },
+
+ reconnectAttempt: 0,
+ timeOfLastConnect: 0,
+ timeOfNextReconnect: 0,
+ _reconnectTimer: null,
+ _startReconnectTimer() {
+ if (Services.io.offline) {
+ console.error("_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.clearUserPref(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.clearUserPref(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 primary password more than once at startup.
+ if (gUserCanceledPrimaryPasswordPrompt) {
+ return "";
+ }
+
+ let passwordURI = "im://" + this.protocol.id;
+ let logins;
+ try {
+ logins = Services.logins.findLogins(passwordURI, null, passwordURI);
+ } catch (e) {
+ this._handlePrimaryPasswordException(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 (gUserCanceledPrimaryPasswordPrompt) {
+ 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._handlePrimaryPasswordException(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();
+ },
+ _handlePrimaryPasswordException(aException) {
+ if (aException.result != Cr.NS_ERROR_ABORT) {
+ throw aException;
+ }
+
+ gUserCanceledPrimaryPasswordPrompt = true;
+ executeSoon(function () {
+ gUserCanceledPrimaryPasswordPrompt = 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) {
+ console.error(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();
+ IMServices.contacts.forgetAccount(this.numericId);
+ for (let prefName of this.prefBranch.getChildList("")) {
+ this.prefBranch.clearUserPref(prefName);
+ }
+ },
+ 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 Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ connect() {
+ if (!this.prplAccount) {
+ throw Components.Exception("", 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,
+ lazy._("passwordPromptTitle", this.name),
+ lazy._("passwordPromptText", this.name),
+ password,
+ lazy._("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 Components.Exception("", 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();
+ },
+
+ getSessions() {
+ return this._ensurePrplAccount.getSessions();
+ },
+ get encryptionStatus() {
+ return this._ensurePrplAccount.encryptionStatus;
+ },
+};
+
+var gAccountsService = null;
+
+export function AccountsService() {}
+AccountsService.prototype = {
+ initAccounts() {
+ this._initAutoLoginStatus();
+ this._accounts = [];
+ this._accountsById = {};
+ gAccountsService = this;
+ let accountIdArray = MailServices.accounts.accounts
+ .map(account => account.incomingServer.getCharValue("imAccount"))
+ .filter(accountKey => accountKey?.startsWith(kAccountKeyPrefix));
+ for (let account of accountIdArray) {
+ new imAccount(account);
+ }
+
+ this._prefObserver = this.observe.bind(this);
+ Services.prefs.addObserver(kPrefAccountOrder, this._prefObserver);
+ },
+
+ _prefObserver: null,
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "nsPref:changed" || aData != kPrefAccountOrder) {
+ return;
+ }
+
+ const imAccounts = MailServices.accounts.accounts
+ .map(account => account.incomingServer.getCharValue("imAccount"))
+ .filter(k => k?.startsWith(kAccountKeyPrefix))
+ .map(k =>
+ this.getAccountByNumericId(parseInt(k.substr(kAccountKeyPrefix.length)))
+ )
+ .filter(a => a);
+
+ // Only update _accounts if it's a reorder operation
+ if (imAccounts.length == this._accounts.length) {
+ this._accounts = imAccounts;
+ Services.obs.notifyObservers(this, "account-list-updated");
+ }
+ },
+
+ unInitAccounts() {
+ for (let account of this._accounts) {
+ account.unInit();
+ }
+ gAccountsService = null;
+ delete this._accounts;
+ delete this._accountsById;
+ Services.prefs.removeObserver(kPrefAccountOrder, 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 Components.Exception(
+ `Invalid id: ${aAccountId}`,
+ 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 this._accounts;
+ },
+
+ createAccount(aName, aPrpl) {
+ // Ensure an account with the same name and protocol doesn't already exist.
+ let prpl = IMServices.core.getProtocolById(aPrpl);
+ if (!prpl) {
+ throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
+ }
+ if (prpl.accountExists(aName)) {
+ console.error("Attempted to create a duplicate account!");
+ throw Components.Exception("", 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 (!IMServices.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);
+
+ Services.obs.notifyObservers(account, "account-added");
+ return account;
+ },
+
+ deleteAccount(aAccountId) {
+ let account = this.getAccountById(aAccountId);
+ if (!account) {
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let index = this._accounts.indexOf(account);
+ if (index == -1) {
+ throw Components.Exception("", 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");
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["imIAccountsService"]),
+ classDescription: "Accounts",
+};