/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- * 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/. */ /** * Here's how this dialog works: * The main dialog contains a tree on the left (id="accounttree") and an * iframe which loads a particular preference document (such as am-main.xhtml) * on the right. * * When the user clicks on items in the tree on the left, two things have * to be determined before the UI can be updated: * - the relevant account * - the relevant page * * When both of these are known, this is what happens: * - every form element of the previous page is saved in the account value * hashtable for the previous account * - the relevant page is loaded into the iframe * - each form element in the page is filled in with an appropriate value * from the current account's hashtable * - in the iframe inside the page, if there is an onInit() method, * it is called. The onInit method can further update this page based * on values set in the previous step. */ /* import-globals-from accountUtils.js */ /* import-globals-from am-prefs.js */ /* import-globals-from amUtils.js */ var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm"); var { MailServices } = ChromeUtils.import( "resource:///modules/MailServices.jsm" ); var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm"); ChromeUtils.defineModuleGetter( this, "FolderUtils", "resource:///modules/FolderUtils.jsm" ); var { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import( "resource:///modules/hostnameUtils.jsm" ); var { ChatIcons } = ChromeUtils.importESModule( "resource:///modules/chatIcons.sys.mjs" ); XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () { const { SubDialogManager } = ChromeUtils.importESModule( "resource://gre/modules/SubDialog.sys.mjs" ); return new SubDialogManager({ dialogStack: document.getElementById("dialogStack"), dialogTemplate: document.getElementById("dialogTemplate"), dialogOptions: { styleSheets: [ "chrome://messenger/skin/preferences/dialog.css", "chrome://messenger/skin/preferences/preferences.css", ], resizeCallback: ({ title, frame }) => { UIFontSize.registerWindow(frame.contentWindow); // Resize the dialog to fit the content with edited font size. requestAnimationFrame(() => { let dialogs = frame.ownerGlobal.gSubDialog._dialogs; let dialog = dialogs.find( d => d._frame.contentDocument == frame.contentDocument ); if (dialog) { UIFontSize.resizeSubDialog(dialog); } }); }, }, }); }); // If Local directory has changed the app needs to restart. Once this is set // a restart will be attempted at each attempt to close the Account manager with OK. var gRestartNeeded = false; // This is a hash-map for every account we've touched in the pane. Each entry // has additional maps of attribute-value pairs that we're going to want to save // when the user hits OK. var accountArray; var gGenericAttributeTypes; var currentAccount; var currentPageId; var pendingAccount; var pendingPageId; /** * This array contains filesystem folders that are deemed inappropriate * for use as the local directory pref for message storage. * It is global to allow extensions to add to/remove from it if needed. * Extensions adding new server types should first consider setting * nsIMsgProtocolInfo(of the server type).defaultLocalPath properly * so that the test will allow that directory automatically. * See the checkLocalDirectoryIsSafe function for description of the members. */ var gDangerousLocalStorageDirs = [ // profile folder { dirsvc: "ProfD", OS: null }, // GRE install folder { dirsvc: "GreD", OS: null }, // Application install folder { dirsvc: "CurProcD", OS: null }, // system temporary folder { dirsvc: "TmpD", OS: null }, // Windows system folder { dirsvc: "SysD", OS: "WINNT" }, // Windows folder { dirsvc: "WinD", OS: "WINNT" }, // Program Files folder { dirsvc: "ProgF", OS: "WINNT" }, // trash folder { dirsvc: "Trsh", OS: "Darwin" }, // Mac OS system folder { dir: "/System", OS: "Darwin" }, // devices folder { dir: "/dev", OS: "Darwin,Linux" }, // process info folder { dir: "/proc", OS: "Linux" }, // system state folder { dir: "/sys", OS: "Linux" }, ]; // This sets an attribute in a xul element so that we can later // know what value to substitute in a prefstring. Different // preference types set different attributes. We get the value // in the same way as the function getAccountValue() determines it. function updateElementWithKeys(account, element, type) { switch (type) { case "identity": element.identitykey = account.defaultIdentity.key; break; case "pop3": case "imap": case "nntp": case "server": element.serverkey = account.incomingServer.key; break; case "smtp": if (MailServices.smtp.defaultServer) { element.serverkey = MailServices.smtp.defaultServer.key; } break; default: // dump("unknown element type! "+type+"\n"); } } // called when the whole document loads // perform initialization here function onLoad() { let selectedServer = document.documentElement.server; let selectPage = document.documentElement.selectPage || null; // Arguments can have two properties: (1) "server," the nsIMsgIncomingServer // to select initially and (2) "selectPage," the page for that server to that // should be selected. accountArray = {}; gGenericAttributeTypes = {}; gAccountTree.load(); setTimeout(selectServer, 0, selectedServer, selectPage); let contentFrame = document.getElementById("contentFrame"); contentFrame.addEventListener("load", event => { let inputElements = contentFrame.contentDocument.querySelectorAll( "checkbox, input, menulist, textarea, radiogroup, richlistbox" ); contentFrame.contentDocument.addEventListener("prefchange", event => { onAccept(true); }); for (let input of inputElements) { if (input.localName == "input" || input.localName == "textarea") { input.addEventListener("change", event => { onAccept(true); }); } else { input.addEventListener("command", event => { onAccept(true); }); } } UIFontSize.registerWindow(contentFrame.contentWindow); }); UIDensity.registerWindow(window); UIFontSize.registerWindow(window); } function onUnload() { gAccountTree.unload(); } function selectServer(server, selectPageId) { let accountTree = document.getElementById("accounttree"); // Default to showing the first account. let accountRow = accountTree.rows[0]; // Find the tree-node for the account we want to select. if (server) { for (let row of accountTree.children) { let account = row._account; if (account && server == account.incomingServer) { accountRow = row; // Make sure all the panes of the account to be selected are shown. accountTree.expandRow(accountRow); break; } } } let pageToSelect = accountRow; if (selectPageId) { // Find the page that also corresponds to this server. // It either is the accountRow itself... let pageId = accountRow.getAttribute("PageTag"); if (pageId != selectPageId) { // ... or one of its children. pageToSelect = accountRow.querySelector( '[PageTag="' + selectPageId + '"]' ); } } accountTree.selectedIndex = accountTree.rows.indexOf(pageToSelect); } function replaceWithDefaultSmtpServer(deletedSmtpServerKey) { // First we replace the smtpserverkey in every identity. for (let identity of MailServices.accounts.allIdentities) { if (identity.smtpServerKey == deletedSmtpServerKey) { identity.smtpServerKey = ""; } } // When accounts have already been loaded in the panel then the first // replacement will be overwritten when the accountvalues are written out // from the pagedata. We get the loaded accounts and check to make sure // that the account exists for the accountid and that it has a default // identity associated with it (to exclude smtpservers and local folders) // Then we check only for the identity[type] and smtpServerKey[slot] and // replace that with the default smtpserverkey if necessary. for (var accountid in accountArray) { var account = accountArray[accountid]._account; if (account && account.defaultIdentity) { var accountValues = accountArray[accountid]; var smtpServerKey = getAccountValue( account, accountValues, "identity", "smtpServerKey", null, false ); if (smtpServerKey == deletedSmtpServerKey) { setAccountValue(accountValues, "identity", "smtpServerKey", ""); } } } } /** * Called when OK is clicked on the dialog. * * @param {boolean} aDoChecks - If true, execute checks on data, otherwise hope * they were already done elsewhere and proceed directly to saving the data. */ function onAccept(aDoChecks) { if (aDoChecks) { // Check if user/host have been modified correctly. if (!checkUserServerChanges(true)) { return false; } if (!checkAccountNameIsValid()) { return false; } } // Run checks as if the page was being left. if ("onLeave" in top.frames.contentFrame) { if (!top.frames.contentFrame.onLeave()) { // Prevent closing Account manager if user declined the changes. return false; } } if (!onSave()) { return false; } // hack hack - save the prefs file NOW in case we crash Services.prefs.savePrefFile(null); if (gRestartNeeded) { MailUtils.restartApplication(); // Prevent closing Account manager in case restart failed. If restart did not fail, // return value does not matter, as we are restarting. return false; } return true; } /** * See if the given path to a directory is usable on the current OS. * * aLocalPath the nsIFile of a directory to check. */ function checkDirectoryIsValid(aLocalPath) { // Any directory selected in the file picker already exists. // Any directory specified in prefs.js will be created at start if it does // not exist yet. // If at the time of entering Account Manager the directory does not exist, // it must be invalid in the current OS or not creatable due to permissions. // Even then, the backend sometimes tries to create a new one // under the current profile. if (!aLocalPath.exists() || !aLocalPath.isDirectory()) { return false; } if (Services.appinfo.OS == "WINNT") { // Do not allow some special filenames on Windows. // Taken from mozilla/widget/windows/nsDataObj.cpp::MangleTextToValidFilename() let dirLeafName = aLocalPath.leafName; const kForbiddenNames = [ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "CON", "PRN", "AUX", "NUL", "CLOCK$", ]; if (kForbiddenNames.includes(dirLeafName)) { return false; } } // The directory must be readable and writable to work as a mail store. if (!(aLocalPath.isReadable() && aLocalPath.isWritable())) { return false; } return true; } /** * Even if the local path is usable, there are some special folders we do not * want to allow for message storage as they cause problems (see e.g. bug 750781). * * aLocalPath The nsIFile of a directory to check. */ function checkDirectoryIsAllowed(aLocalPath) { /** * Check if the local path (aLocalPath) is 'safe' i.e. NOT a parent * or subdirectory of the given special system/app directory (aDirToCheck). * * @param {object} aDirToCheck - An object describing the special directory. * @param {string} aDirToCheck.dirsvc - A path keyword to retrieve from the * Directory service. * @param {string} aDirToCheck.dir - An absolute filesystem path. * @param {string} aDirToCheck.OS - A string of comma separated values defining on which. * Operating systems the folder is unusable: * - null = all * - WINNT = Windows * - Darwin = OS X * - Linux = Linux * @param {string} aDirToCheck.safeSubdirs - An array of directory names that * are allowed to be used under the tested directory. * @param {nsIFile} aLocalPath - An nsIFile of the directory to check, * intended for message storage. */ function checkLocalDirectoryIsSafe(aDirToCheck, aLocalPath) { if (aDirToCheck.OS) { if (!aDirToCheck.OS.split(",").includes(Services.appinfo.OS)) { return true; } } let testDir = null; if ("dirsvc" in aDirToCheck) { try { testDir = Services.dirsvc.get(aDirToCheck.dirsvc, Ci.nsIFile); } catch (e) { console.error( "The special folder " + aDirToCheck.dirsvc + " cannot be retrieved on this platform: " + e ); } if (!testDir) { return true; } } else if ("dir" in aDirToCheck) { testDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); testDir.initWithPath(aDirToCheck.dir); if (!testDir.exists()) { return true; } } else { console.error("No directory to check?"); return true; } testDir.normalize(); if (testDir.equals(aLocalPath) || aLocalPath.contains(testDir)) { return false; } if (testDir.contains(aLocalPath)) { if (!("safeSubdirs" in aDirToCheck)) { return false; } // While the tested directory may not be safe, // a subdirectory of some safe subdirectories may be fine. let isInSubdir = false; for (let subDir of aDirToCheck.safeSubdirs) { let checkDir = testDir.clone(); checkDir.append(subDir); if (checkDir.contains(aLocalPath)) { isInSubdir = true; break; } } return isInSubdir; } return true; } // end of checkDirectoryIsNotSpecial // If the server type has a nsIMsgProtocolInfo.defaultLocalPath set, // allow that directory. if (currentAccount.incomingServer) { try { let defaultPath = currentAccount.incomingServer.protocolInfo.defaultLocalPath; if (defaultPath) { defaultPath.normalize(); if (defaultPath.contains(aLocalPath)) { return true; } } } catch (e) { /* No problem if this fails. */ } } for (let tryDir of gDangerousLocalStorageDirs) { if (!checkLocalDirectoryIsSafe(tryDir, aLocalPath)) { return false; } } return true; } /** * Check if the specified directory does meet all the requirements * for safe mail storage. * * aLocalPath the nsIFile of a directory to check. */ function checkDirectoryIsUsable(aLocalPath) { const kAlertTitle = document .getElementById("bundle_prefs") .getString("prefPanel-server"); const originalPath = aLocalPath; let invalidPath = false; try { aLocalPath.normalize(); } catch (e) { invalidPath = true; } if (invalidPath || !checkDirectoryIsValid(aLocalPath)) { let alertString = document .getElementById("bundle_prefs") .getFormattedString("localDirectoryInvalid", [originalPath.path]); Services.prompt.alert(window, kAlertTitle, alertString); return false; } if (!checkDirectoryIsAllowed(aLocalPath)) { let alertNotAllowed = document .getElementById("bundle_prefs") .getFormattedString("localDirectoryNotAllowed", [originalPath.path]); Services.prompt.alert(window, kAlertTitle, alertNotAllowed); return false; } // Check that no other account has this same or dependent local directory. for (let server of MailServices.accounts.allServers) { if (server.key == currentAccount.incomingServer.key) { continue; } let serverPath = server.localPath; try { serverPath.normalize(); let alertStringID = null; if (serverPath.equals(aLocalPath)) { alertStringID = "directoryAlreadyUsedByOtherAccount"; } else if (serverPath.contains(aLocalPath)) { alertStringID = "directoryParentUsedByOtherAccount"; } else if (aLocalPath.contains(serverPath)) { alertStringID = "directoryChildUsedByOtherAccount"; } if (alertStringID) { let alertString = document .getElementById("bundle_prefs") .getFormattedString(alertStringID, [server.prettyName]); Services.prompt.alert(window, kAlertTitle, alertString); return false; } } catch (e) { // The other account's path is seriously broken, so we can't compare it. console.error( "The Local Directory path of the account " + server.prettyName + " seems invalid." ); } } return true; } /** * Check if the user and/or host names have been changed and if so check * if the new names already exists for an account or are empty. * Also check if the Local Directory path was changed. * * @param {boolean} showAlert - Show and alert if a problem with the host / user * name is found. */ function checkUserServerChanges(showAlert) { const prefBundle = document.getElementById("bundle_prefs"); const alertTitle = prefBundle.getString("prefPanel-server"); var alertText = null; var accountValues = getValueArrayFor(currentAccount); if (!accountValues) { return true; } let currentServer = currentAccount ? currentAccount.incomingServer : null; // If this type doesn't exist (just removed) then return. if (!("server" in accountValues) || !accountValues.server) { return true; } // Get the new username, hostname and type from the page. var typeElem = getPageFormElement("server.type"); var hostElem = getPageFormElement("server.hostName"); var userElem = getPageFormElement("server.username"); if (typeElem && userElem && hostElem) { var newType = getFormElementValue(typeElem); var oldHost = getAccountValue( currentAccount, accountValues, "server", "hostName", null, false ); var newHost = getFormElementValue(hostElem); var oldUser = getAccountValue( currentAccount, accountValues, "server", "username", null, false ); var newUser = getFormElementValue(userElem); var checkUser = true; // There is no username needed for e.g. news so reset it. if (currentServer && !currentServer.protocolInfo.requiresUsername) { oldUser = newUser = ""; checkUser = false; } alertText = null; // If something is changed then check if the new user/host already exists. if (oldUser != newUser || oldHost != newHost) { newUser = newUser.trim(); newHost = cleanUpHostName(newHost); if (checkUser && newUser == "") { alertText = prefBundle.getString("userNameEmpty"); } else if (!isLegalHostNameOrIP(newHost)) { alertText = prefBundle.getString("enterValidServerName"); } else { let sameServer = MailServices.accounts.findServer( newUser, newHost, newType ); if (sameServer && sameServer != currentServer) { alertText = prefBundle.getString("modifiedAccountExists"); } else { // New hostname passed all checks. We may have cleaned it up so set // the new value back into the input element. setFormElementValue(hostElem, newHost); } } if (alertText) { if (showAlert) { Services.prompt.alert(window, alertTitle, alertText); } // Restore the old values before return if (checkUser) { setFormElementValue(userElem, oldUser); } setFormElementValue(hostElem, oldHost); // If no message is shown to the user, silently revert the values // and consider the check a success. return !showAlert; } // If username is changed remind users to change Your Name and Email Address. // If server name is changed and has defined filters then remind users // to edit rules. if (showAlert) { let filterList; if (currentServer && checkUser) { filterList = currentServer.getEditableFilterList(null); } let changeText = ""; if ( oldHost != newHost && filterList != undefined && filterList.filterCount ) { changeText = prefBundle.getString("serverNameChanged"); } // In the event that oldHost == newHost or oldUser == newUser, // the \n\n will be trimmed off before the message is shown. if (oldUser != newUser) { changeText = changeText + "\n\n" + prefBundle.getString("userNameChanged"); } if (changeText != "") { Services.prompt.alert(window, alertTitle, changeText.trim()); } } let l10n = new Localization(["messenger/accountManager.ftl"], true); let cancel = Services.prompt.confirmEx( window, alertTitle, l10n.formatValueSync("server-change-restart-required"), Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL, prefBundle.getString("localDirectoryRestart"), null, null, null, {} ); if (cancel) { setFormElementValue(hostElem, oldHost); setFormElementValue(userElem, oldUser); return false; } gRestartNeeded = true; } } // Check the new value of the server.localPath field for validity. var pathElem = getPageFormElement("server.localPath"); if (!pathElem) { return true; } let dir = getFormElementValue(pathElem); if (!checkDirectoryIsUsable(dir)) { // return false; // Temporarily disable this. Just show warning but do not block. See bug 921371. console.error( `Local directory ${dir.path} of account ${currentAccount.key} is not safe to use. Consider changing it.` ); } // Warn if the Local directory path was changed. // This can be removed once bug 2654 is fixed. let oldLocalDir = getAccountValue( currentAccount, accountValues, "server", "localPath", null, false ); // both return nsIFile let newLocalDir = getFormElementValue(pathElem); if (oldLocalDir && newLocalDir && oldLocalDir.path != newLocalDir.path) { let brandName = document .getElementById("bundle_brand") .getString("brandShortName"); alertText = prefBundle.getFormattedString("localDirectoryChanged", [ brandName, ]); let cancel = Services.prompt.confirmEx( window, alertTitle, alertText, Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL, prefBundle.getString("localDirectoryRestart"), null, null, null, {} ); if (cancel) { setFormElementValue(pathElem, oldLocalDir); return false; } gRestartNeeded = true; } return true; } /** * If account name is not valid, alert the user. */ function checkAccountNameIsValid() { if (!currentAccount) { return true; } const prefBundle = document.getElementById("bundle_prefs"); let alertText = null; let serverNameElem = getPageFormElement("server.prettyName"); if (serverNameElem) { let accountName = getFormElementValue(serverNameElem); if (!accountName) { alertText = prefBundle.getString("accountNameEmpty"); } else if (accountNameExists(accountName, currentAccount.key)) { alertText = prefBundle.getString("accountNameExists"); // Change the account name to prevent UI freeze. let counter = 2; while ( accountNameExists(`${accountName}_${counter}`, currentAccount.key) ) { counter++; } serverNameElem.value = `${accountName}_${counter}`; } if (alertText) { const alertTitle = prefBundle.getString("accountWizard"); Services.prompt.alert(window, alertTitle, alertText); return false; } } return true; } function onSave() { if (pendingPageId) { dump("ERROR: " + pendingPageId + " hasn't loaded yet! Not saving.\n"); return false; } // make sure the current visible page is saved savePage(currentAccount); for (var accountid in accountArray) { var accountValues = accountArray[accountid]; var account = accountArray[accountid]._account; if (!saveAccount(accountValues, account)) { return false; } } return true; } /** * Highlight the default account row in the account tree, * optionally un-highlight the previous one. * * @param {?nsIMsgAccount} newDefault - The account that has become the new * default. Can be given as null if there is none. * @param {?nsIMsgAccount} oldDefault - The account that has stopped being the * default. Can be given as null if there was none. */ function markDefaultServer(newDefault, oldDefault) { if (oldDefault == newDefault) { return; } let accountTree = document.getElementById("accounttree"); for (let accountRow of accountTree.children) { if (newDefault && newDefault == accountRow._account) { accountRow.classList.add("isDefaultServer"); } if (oldDefault && oldDefault == accountRow._account) { accountRow.classList.remove("isDefaultServer"); } } } /** * Notify the UI to rebuild the account tree. */ function rebuildAccountTree() { // TODO: Reimplement or replace. } /** * Make currentAccount (currently selected in the account tree) the default one. */ function onSetDefault(event) { // Make sure this function was not called while the control item is disabled if (event.target.getAttribute("disabled") == "true") { return; } let previousDefault = MailServices.accounts.defaultAccount; MailServices.accounts.defaultAccount = currentAccount; markDefaultServer(currentAccount, previousDefault); // Update gloda's myContact with the new default account's default identity. Gloda._initMyIdentities(); gAccountTree.load(); } function onRemoveAccount(event) { if (event.target.getAttribute("disabled") == "true" || !currentAccount) { return; } let server = currentAccount.incomingServer; let canDelete = server.protocolInfo.canDelete || server.canDelete; if (!canDelete) { return; } let serverList = []; let accountTree = document.getElementById("accounttree"); // build the list of servers in the account tree (order is important) for (let row of accountTree.children) { if ("_account" in row) { let curServer = row._account.incomingServer; if (!serverList.includes(curServer)) { serverList.push(curServer); } } } // get position of the current server in the server list let serverIndex = serverList.indexOf(server); // After the current server is deleted, choose the next server/account, // or the previous one if the last one was deleted. if (serverIndex == serverList.length - 1) { serverIndex--; } else { serverIndex++; } // Need to save these before the account and its server is removed. let serverId = server.serverURI; // Confirm account deletion. let removeArgs = { server, account: currentAccount, result: false, }; let onCloseDialog = function () { // If result is true, the account was removed. if (!removeArgs.result) { return; } // clear cached data out of the account array currentAccount = currentPageId = null; if (serverId in accountArray) { delete accountArray[serverId]; } if (serverIndex >= 0 && serverIndex < serverList.length) { selectServer(serverList[serverIndex], null); } // Either the default account was deleted so there is a new one // or the default account was not changed. Either way, there is // no need to unmark the old one. markDefaultServer(MailServices.accounts.defaultAccount, null); }; gSubDialog.open( "chrome://messenger/content/removeAccount.xhtml", { features: "resizable=no", closingCallback: onCloseDialog, }, removeArgs ); } function saveAccount(accountValues, account) { var identity = null; var server = null; if (account) { identity = account.defaultIdentity; server = account.incomingServer; } for (var type in accountValues) { var dest; try { if (type == "identity") { dest = identity; } else if (type == "server") { dest = server; } else if (type == "pop3") { dest = server.QueryInterface(Ci.nsIPop3IncomingServer); } else if (type == "imap") { dest = server.QueryInterface(Ci.nsIImapIncomingServer); } else if (type == "none") { dest = server.QueryInterface(Ci.nsINoIncomingServer); } else if (type == "nntp") { dest = server.QueryInterface(Ci.nsINntpIncomingServer); } else if (type == "smtp") { dest = MailServices.smtp.defaultServer; } } catch (ex) { // don't do anything, just means we don't support that } if (dest == undefined) { continue; } var typeArray = accountValues[type]; for (var slot in typeArray) { if ( type in gGenericAttributeTypes && slot in gGenericAttributeTypes[type] ) { var methodName = "get"; switch (gGenericAttributeTypes[type][slot]) { case "int": methodName += "Int"; break; case "wstring": methodName += "Unichar"; break; case "string": methodName += "Char"; break; case "bool": // in some cases // like for radiogroups of type boolean // the value will be "false" instead of false // we need to convert it. if (typeArray[slot] == "false") { typeArray[slot] = false; } else if (typeArray[slot] == "true") { typeArray[slot] = true; } methodName += "Bool"; break; default: dump( "unexpected preftype: " + gGenericAttributeTypes[type][slot] + "\n" ); break; } methodName += methodName + "Value" in dest ? "Value" : "Attribute"; if (dest[methodName](slot) != typeArray[slot]) { methodName = methodName.replace("get", "set"); dest[methodName](slot, typeArray[slot]); } } else if ( slot in dest && typeArray[slot] != undefined && dest[slot] != typeArray[slot] ) { try { dest[slot] = typeArray[slot]; } catch (ex) { // hrm... need to handle special types here } } } } // if we made account changes to the spam settings, we'll need to re-initialize // our settings object if (server && server.spamSettings) { try { server.spamSettings.initialize(server); } catch (e) { let accountName = getAccountValue( account, getValueArrayFor(account), "server", "prettyName", null, false ); let alertText = document .getElementById("bundle_prefs") .getFormattedString("junkSettingsBroken", [accountName]); let review = Services.prompt.confirmEx( window, null, alertText, Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO, null, null, null, null, {} ); if (!review) { onAccountTreeSelect("am-junk.xhtml", account); return false; } } } return true; } /** * Set enabled/disabled state for the actions in the Account Actions menu. * Called only by Thunderbird. */ function initAccountActionsButtons(menupopup) { if (!Services.prefs.getBoolPref("mail.chat.enabled")) { document.getElementById("accountActionsAddIMAccount").hidden = true; } updateItems( document.getElementById("accounttree"), getCurrentAccount(), document.getElementById("accountActionsAddMailAccount"), document.getElementById("accountActionsDropdownSetDefault"), document.getElementById("accountActionsDropdownRemove") ); updateBlockedItems(menupopup.children, true); } /** * Determine enabled/disabled state for the passed in elements * representing account actions. */ function updateItems( tree, account, addAccountItem, setDefaultItem, removeItem ) { // Start with items disabled and then find out what can be enabled. let canSetDefault = false; let canDelete = false; if (account && tree.selectedIndex >= 0) { // Only try to check properties if there was anything selected in the tree // and it belongs to an account. // Otherwise we have either selected a SMTP server, or there is some // problem. Either way, we don't want the user to act on it. let server = account.incomingServer; if ( account != MailServices.accounts.defaultAccount && server.canBeDefaultServer && account.identities.length > 0 ) { canSetDefault = true; } canDelete = server.protocolInfo.canDelete || server.canDelete; } setEnabled(addAccountItem, true); setEnabled(setDefaultItem, canSetDefault); setEnabled(removeItem, canDelete); } /** * Disable buttons/menu items if their control preference is locked. * * @param {Node[]|NodeList} aItems - Elements to be checked. * @param {boolean} aMustBeTrue - If true then the pref must be boolean and set * to true to trigger the disabling. */ function updateBlockedItems(aItems, aMustBeTrue) { for (let item of aItems) { let prefstring = item.getAttribute("prefstring"); if (!prefstring) { continue; } if ( Services.prefs.prefIsLocked(prefstring) && (!aMustBeTrue || Services.prefs.getBoolPref(prefstring)) ) { item.setAttribute("disabled", true); } } } /** * Set enabled/disabled state for the control. */ function setEnabled(control, enabled) { if (!control) { return; } if (enabled) { control.removeAttribute("disabled"); } else { control.setAttribute("disabled", true); } } // Called when someone clicks on an account. Figure out context by what they // clicked on. This is also called when an account is removed. In this case, // nothing is selected. function onAccountTreeSelect(pageId, account) { let tree = document.getElementById("accounttree"); let changeView = pageId && account; if (!changeView) { if (tree.selectedIndex < 0) { return false; } let node = tree.rows[tree.selectedIndex]; account = "_account" in node ? node._account : null; pageId = node.getAttribute("PageTag"); } if (pageId == currentPageId && account == currentAccount) { return true; } if ( document .getElementById("contentFrame") .contentDocument.getElementById("server.localPath") ) { // Check if user/host names have been changed or the Local Directory is invalid. if (!checkUserServerChanges(false)) { changeView = true; account = currentAccount; pageId = currentPageId; } if (gRestartNeeded) { onAccept(false); } } if ( document .getElementById("contentFrame") .contentDocument.getElementById("server.prettyName") ) { // Check if account name is valid. if (!checkAccountNameIsValid()) { changeView = true; account = currentAccount; pageId = currentPageId; } } if (currentPageId) { // Change focus to the account tree first so that any 'onchange' handlers // on elements in the current page have a chance to run before the page // is saved and replaced by the new one. tree.focus(); } // Provide opportunity to do cleanups or checks when the current page is being left. if ("onLeave" in top.frames.contentFrame) { top.frames.contentFrame.onLeave(); } // save the previous page savePage(currentAccount); let changeAccount = account != currentAccount; if (changeView) { selectServer(account.incomingServer, pageId); } if (pageId != currentPageId) { // loading a complete different page // prevent overwriting with bad stuff currentAccount = currentPageId = null; pendingAccount = account; pendingPageId = pageId; loadPage(pageId); } else if (changeAccount) { // same page, different server restorePage(pageId, account); } return true; } // page has loaded function onPanelLoaded(pageId) { if (pageId != pendingPageId) { // if we're reloading the current page, we'll assume the // page has asked itself to be completely reloaded from // the prefs. to do this, clear out the the old entry in // the account data, and then restore theh page if (pageId == currentPageId) { var serverId = currentAccount ? currentAccount.incomingServer.serverURI : "global"; delete accountArray[serverId]; restorePage(currentPageId, currentAccount); } } else { restorePage(pendingPageId, pendingAccount); } // probably unnecessary, but useful for debugging pendingAccount = null; pendingPageId = null; } function pageURL(pageId) { // If we have a special non account manager pane (e.g. about:blank), // do not translate it into ChromePackageName URL. if (!pageId.startsWith("am-")) { return pageId; } let chromePackageName; try { // we could compare against "main","server","copies","offline","addressing", // "smtp" and "advanced" first to save the work, but don't, // as some of these might be turned into extensions (for thunderbird) let packageName = pageId.split("am-")[1].split(".xhtml")[0]; chromePackageName = MailServices.accounts.getChromePackageName(packageName); } catch (ex) { chromePackageName = "messenger"; } return "chrome://" + chromePackageName + "/content/" + pageId; } function loadPage(pageId) { const loadURIOptions = { triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), }; document .getElementById("contentFrame") .webNavigation.fixupAndLoadURIString(pageURL(pageId), loadURIOptions); } // save the values of the widgets to the given server function savePage(account) { if (!account) { return; } // tell the page that it's about to save if ("onSave" in top.frames.contentFrame) { top.frames.contentFrame.onSave(); } var accountValues = getValueArrayFor(account); if (!accountValues) { return; } // Reset accountArray so that only the current page will be saved. This is // needed to prevent resetting prefs unintentionally. An example is when // changing username/hostname, MsgIncomingServer.jsm will modify identities, // without this, identities changes may be reverted to old values in // accountArray. accountArray = {}; accountValues = {}; let serverId = account.incomingServer.serverURI; accountArray[serverId] = accountValues; accountArray[serverId]._account = account; var pageElements = getPageFormElements(); if (!pageElements) { return; } // store the value in the account for (let i = 0; i < pageElements.length; i++) { if (pageElements[i].id) { let vals = pageElements[i].id.split("."); if (vals.length >= 2) { let type = vals[0]; let slot = pageElements[i].id.slice(type.length + 1); setAccountValue( accountValues, type, slot, getFormElementValue(pageElements[i]) ); } } } } function setAccountValue(accountValues, type, slot, value) { if (!(type in accountValues)) { accountValues[type] = {}; } accountValues[type][slot] = value; } function getAccountValue( account, accountValues, type, slot, preftype, isGeneric ) { if (!(type in accountValues)) { accountValues[type] = {}; } // fill in the slot from the account if necessary if ( !(slot in accountValues[type]) || accountValues[type][slot] == undefined ) { var server; if (account) { server = account.incomingServer; } var source = null; try { if (type == "identity") { source = account.defaultIdentity; } else if (type == "server") { source = account.incomingServer; } else if (type == "pop3") { source = server.QueryInterface(Ci.nsIPop3IncomingServer); } else if (type == "imap") { source = server.QueryInterface(Ci.nsIImapIncomingServer); } else if (type == "none") { source = server.QueryInterface(Ci.nsINoIncomingServer); } else if (type == "nntp") { source = server.QueryInterface(Ci.nsINntpIncomingServer); } else if (type == "smtp") { source = MailServices.smtp.defaultServer; } } catch (ex) {} if (source) { if (isGeneric) { if (!(type in gGenericAttributeTypes)) { gGenericAttributeTypes[type] = {}; } // we need the preftype later, for setting when we save. gGenericAttributeTypes[type][slot] = preftype; var methodName = "get"; switch (preftype) { case "int": methodName += "Int"; break; case "wstring": methodName += "Unichar"; break; case "string": methodName += "Char"; break; case "bool": methodName += "Bool"; break; default: dump("unexpected preftype: " + preftype + "\n"); break; } methodName += methodName + "Value" in source ? "Value" : "Attribute"; accountValues[type][slot] = source[methodName](slot); } else if (slot in source) { accountValues[type][slot] = source[slot]; } else { accountValues[type][slot] = null; } } else { accountValues[type][slot] = null; } } return accountValues[type][slot]; } // restore the values of the widgets from the given server function restorePage(pageId, account) { if (!account) { return; } var accountValues = getValueArrayFor(account); if (!accountValues) { return; } if ("onPreInit" in top.frames.contentFrame) { top.frames.contentFrame.onPreInit(account, accountValues); } var pageElements = getPageFormElements(); if (!pageElements) { return; } // restore the value from the account for (let i = 0; i < pageElements.length; i++) { if (pageElements[i].id) { let vals = pageElements[i].id.split("."); if (vals.length >= 2) { let type = vals[0]; let slot = pageElements[i].id.slice(type.length + 1); // buttons are lockable, but don't have any data so we skip that part. // elements that do have data, we get the values at poke them in. if (pageElements[i].localName != "button") { var value = getAccountValue( account, accountValues, type, slot, pageElements[i].getAttribute("preftype"), pageElements[i].getAttribute("genericattr") == "true" ); setFormElementValue(pageElements[i], value); } var element = pageElements[i]; switch (type) { case "identity": element.identitykey = account.defaultIdentity.key; break; case "pop3": case "imap": case "nntp": case "server": element.serverkey = account.incomingServer.key; break; case "smtp": if (MailServices.smtp.defaultServer) { element.serverkey = MailServices.smtp.defaultServer.key; } break; } var isLocked = getAccountValueIsLocked(pageElements[i]); setEnabled(pageElements[i], !isLocked); } } } // tell the page that new values have been loaded if ("onInit" in top.frames.contentFrame) { top.frames.contentFrame.onInit(pageId, account.incomingServer.serverURI); } // everything has succeeded, vervied by setting currentPageId currentPageId = pageId; currentAccount = account; } /** * Gets the value of a widget in current the account settings page, * automatically setting the right property of it depending on element type. * * @param {HTMLInputElement} formElement - An input element. */ function getFormElementValue(formElement) { try { var type = formElement.localName; if (type == "checkbox") { if (formElement.getAttribute("reversed")) { return !formElement.checked; } return formElement.checked; } if (type == "input" && formElement.getAttribute("datatype") == "nsIFile") { if (formElement.value) { let localfile = Cc["@mozilla.org/file/local;1"].createInstance( Ci.nsIFile ); localfile.initWithPath(formElement.value); return localfile; } return null; } if (type == "input" || "value" in formElement) { return formElement.value.trim(); } return null; } catch (ex) { console.error("getFormElementValue failed, ex=" + ex + "\n"); } return null; } /** * Sets the value of a widget in current the account settings page, * automatically setting the right property of it depending on element type. * * @param {HTMLInputElement} formElement - An input element. * @param {string|nsIFile} value - The value to store in the element. */ function setFormElementValue(formElement, value) { var type = formElement.localName; if (type == "checkbox") { if (value == null) { formElement.checked = false; } else if (formElement.getAttribute("reversed")) { formElement.checked = !value; } else { formElement.checked = value; } } else if (type == "radiogroup" || type == "menulist") { if (value == null) { formElement.selectedIndex = 0; } else { formElement.value = value; } } else if ( type == "input" && formElement.getAttribute("datatype") == "nsIFile" ) { // handle nsIFile if (value) { let localfile = value.QueryInterface(Ci.nsIFile); try { formElement.value = localfile.path; } catch (ex) { dump("Still need to fix uninitialized nsIFile problem!\n"); } } else { formElement.value = ""; } } else if (type == "input") { if (value == null) { formElement.value = null; } else { formElement.value = value; } } else if (type == "label") { formElement.value = value || ""; } else if (value == null) { // let the form figure out what to do with it formElement.value = null; } else { formElement.value = value; } } // // conversion routines - get data associated // with a given pageId, serverId, etc // // helper routine for account manager panels to get the current account for the selected server function getCurrentAccount() { return currentAccount; } /** * Get the array of persisted form elements for the given page. */ function getPageFormElements() { // Uses getElementsByAttribute() which returns a live NodeList which is usually // faster than e.g. querySelector(). if ("getElementsByAttribute" in top.frames.contentFrame.document) { return top.frames.contentFrame.document.getElementsByAttribute( "wsm_persist", "true" ); } return null; } /** * Get a single persisted form element in the current page. * * @param {srtring} aId - ID of the element requested. */ function getPageFormElement(aId) { let elem = top.frames.contentFrame.document.getElementById(aId); if (elem && elem.getAttribute("wsm_persist") == "true") { return elem; } return null; } // get the value array for the given account function getValueArrayFor(account) { var serverId = account ? account.incomingServer.serverURI : "global"; if (!(serverId in accountArray)) { accountArray[serverId] = {}; accountArray[serverId]._account = account; } return accountArray[serverId]; } /** * Sets the name of the account rowitem in the tree pane. * * @param {string} aAccountKey - The key of the account to change. * @param {string} aLabel - The value of the label to set. */ function setAccountLabel(aAccountKey, aLabel) { let row = document.getElementById(aAccountKey); if (row) { row.setAttribute("aria-label", aLabel); row.title = aLabel; row.querySelector(".name").textContent = aLabel; } rebuildAccountTree(false); } var gAccountTree = { load() { this._build(); let mainTree = document.getElementById("accounttree"); mainTree.__defineGetter__("_orderableChildren", function () { let rows = [...this.children]; rows.pop(); return rows; }); mainTree.addEventListener("ordering", event => { if (!event.detail || event.detail.id == "smtp") { event.preventDefault(); } }); mainTree.addEventListener("ordered", event => { let accountKeyList = Array.from(mainTree.children, row => row.id); accountKeyList.pop(); // Remove SMTP. MailServices.accounts.reorderAccounts(accountKeyList); rebuildAccountTree(); }); mainTree.addEventListener("expanded", event => { this._dataStore.setValue( document.documentURI, event.target.id, "open", "true" ); }); mainTree.addEventListener("collapsed", event => { this._dataStore.setValue( document.documentURI, event.target.id, "open", "false" ); }); MailServices.accounts.addIncomingServerListener(this); }, unload() { MailServices.accounts.removeIncomingServerListener(this); }, onServerLoaded(server) { // We assume the newly appeared server was created by the user so we select // it in the tree. this._build(server); }, onServerUnloaded(aServer) { this._build(); }, onServerChanged(aServer) {}, _dataStore: Services.xulStore, /** * Retrieve from XULStore.json whether the account should be expanded (open) * in the account tree. * * @param {string} aAccountKey - Key of the account to check. */ _getAccountOpenState(aAccountKey) { if (!this._dataStore.hasValue(document.documentURI, aAccountKey, "open")) { // If there was no value stored, use opened state. return "true"; } // Retrieve the persisted value from XULStore.json. // It is stored under the URI of the current document and ID of the XUL element. return this._dataStore.getValue(document.documentURI, aAccountKey, "open"); }, _build(newServer) { var bundle = document.getElementById("bundle_prefs"); function getString(aString) { return bundle.getString(aString); } var panels = [ { string: getString("prefPanel-server"), src: "am-server.xhtml" }, { string: getString("prefPanel-copies"), src: "am-copies.xhtml" }, { string: getString("prefPanel-synchronization"), src: "am-offline.xhtml", }, { string: getString("prefPanel-diskspace"), src: "am-offline.xhtml" }, { string: getString("prefPanel-addressing"), src: "am-addressing.xhtml" }, { string: getString("prefPanel-junk"), src: "am-junk.xhtml" }, ]; let accounts = FolderUtils.allAccountsSorted(false); let mainTree = document.getElementById("accounttree"); // Clear off all children... while (mainTree.hasChildNodes()) { mainTree.lastChild.remove(); } for (let account of accounts) { let accountName = null; let accountKey = account.key; let amChrome = "about:blank"; let panelsToKeep = []; let server = null; // This "try {} catch {}" block is intentionally very long to catch // unknown exceptions and confine them to this single account. // This may happen from broken accounts. See e.g. bug 813929. // Other accounts can still be shown properly if they are valid. try { server = account.incomingServer; if ( server.type == "im" && !Services.prefs.getBoolPref("mail.chat.enabled") ) { continue; } accountName = server.prettyName; // Now add our panels. let idents = MailServices.accounts.getIdentitiesForServer(server); if (idents.length) { panelsToKeep.push(panels[0]); // The server panel is valid panelsToKeep.push(panels[1]); // also the copies panel panelsToKeep.push(panels[4]); // and addressing } // Everyone except News, RSS and IM has a junk panel // XXX: unextensible! // The existence of server.spamSettings can't currently be used for this. if ( server.type != "nntp" && server.type != "rss" && server.type != "im" ) { panelsToKeep.push(panels[5]); } // Check offline/diskspace support level. let diskspace = server.supportsDiskSpace; if (server.offlineSupportLevel >= 10 && diskspace) { panelsToKeep.push(panels[2]); } else if (diskspace) { panelsToKeep.push(panels[3]); } // extensions const CATEGORY = "mailnews-accountmanager-extensions"; for (let { data } of Services.catMan.enumerateCategory(CATEGORY)) { try { let svc = Cc[ Services.catMan.getCategoryEntry(CATEGORY, data) ].getService(Ci.nsIMsgAccountManagerExtension); if (svc.showPanel(server)) { let bundleName = "chrome://" + svc.chromePackageName + "/locale/am-" + svc.name + ".properties"; let bundle = Services.strings.createBundle(bundleName); let title = bundle.GetStringFromName("prefPanel-" + svc.name); panelsToKeep.push({ string: title, src: "am-" + svc.name + ".xhtml", }); } } catch (e) { // Fetching of this extension panel failed so do not show it, // just log error. let extName = data || "(unknown)"; console.error( "Error accessing panel from extension '" + extName + "': " + e ); } } amChrome = server.accountManagerChrome; } catch (e) { // Show only a placeholder in the account list saying this account // is broken, with no child panels. let accountID = accountName || accountKey; console.error("Error accessing account " + accountID + ": " + e); accountName = "Invalid account " + accountID; panelsToKeep.length = 0; } // Create the top level tree-item. let treeitem = document .getElementById("accountTreeItem") .content.firstElementChild.cloneNode(true); mainTree.appendChild(treeitem); treeitem.setAttribute("aria-label", accountName); treeitem.title = accountName; treeitem.querySelector(".name").textContent = accountName; treeitem.setAttribute("PageTag", amChrome); // Add icons based on account type. if (server) { treeitem.classList.add("serverType-" + server.type); if (server.isSecure) { treeitem.classList.add("isSecure"); } // For IM accounts, we can try to fetch a protocol specific icon. if (server.type == "im") { treeitem.querySelector(".icon").style.backgroundImage = "url(" + ChatIcons.getProtocolIconURI( server.wrappedJSObject.imAccount.protocol ) + ")"; treeitem.id = accountKey; } } if (panelsToKeep.length > 0) { let treekids = treeitem.querySelector("ul"); for (let panel of panelsToKeep) { let kidtreeitem = document.createElement("li"); kidtreeitem.title = panel.string; treekids.appendChild(kidtreeitem); let kidtreerow = document.createElement("div"); kidtreeitem.appendChild(kidtreerow); let kidtreecell = document.createElement("span"); kidtreecell.classList.add("name"); kidtreecell.tabIndex = -1; kidtreerow.appendChild(kidtreecell); kidtreecell.textContent = panel.string; kidtreeitem.setAttribute("PageTag", panel.src); kidtreeitem._account = account; kidtreeitem.id = `${accountKey}/${panel.src}`; } treeitem.id = accountKey; // Load the 'open' state of the account from XULStore.json. if (this._getAccountOpenState(accountKey) != "true") { treeitem.classList.add("collapsed"); } } treeitem._account = account; } markDefaultServer(MailServices.accounts.defaultAccount, null); // Now add the outgoing server node. let treeitem = document .getElementById("accountTreeItem") .content.firstElementChild.cloneNode(true); mainTree.appendChild(treeitem); treeitem.id = "smtp"; treeitem.querySelector(".name").textContent = getString("prefPanel-smtp"); treeitem.setAttribute("PageTag", "am-smtp.xhtml"); treeitem.classList.add("serverType-smtp"); // If a new server was created, select the server after rebuild of the tree. if (newServer) { setTimeout(selectServer, 0, newServer); } }, };