Revision control

Other Tools

/* 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"];
const { clearTimeout, setTimeout } = ChromeUtils.import(
);
var { ClassInfo, XPCOMUtils, executeSoon, l10nHelper } = ChromeUtils.import(
);
var { Services } = ChromeUtils.import("resource:///modules/imServices.jsm");
var {
GenericAccountPrototype,
GenericAccountBuddyPrototype,
} = ChromeUtils.import("resource:///modules/jsProtoHelper.jsm");
ChromeUtils.defineModuleGetter(
this,
"MailServices",
);
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";
XPCOMUtils.defineLazyGetter(this, "_", () =>
);
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 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() {
},
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("", 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(() => {
// 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 (_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.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 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);
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,
_("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 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();
},
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;
},
getSessions() {
return this._ensurePrplAccount.getSessions();
},
get encryptionStatus() {
return this._ensurePrplAccount.encryptionStatus;
},
};
var gAccountsService = null;
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) {
try {
if (!account) {
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
}
new imAccount(account);
} catch (e) {
Cu.reportError(e);
dump(e + " " + e.toSource() + "\n");
}
}
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("", 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 = Services.core.getProtocolById(aPrpl);
if (!prpl) {
throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED);
}
if (prpl.accountExists(aName)) {
Cu.reportError("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 (!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);
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",
};