/* -*- Mode: C++; 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 (accounttree) and a * deck on the right. Each card in the deck on the right contains an * IFRAME which loads a particular preference document (such as am-main.xul) * * when the user clicks on items in the tree on the right, 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 card containing the relevant page is brought to the front * - 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. */ Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); Components.utils.import("resource:///modules/iteratorUtils.jsm"); Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource:///modules/folderUtils.jsm"); Components.utils.import("resource:///modules/hostnameUtils.jsm"); // 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. * Extentions 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"); } } function hideShowControls(serverType) { let controls = document.querySelectorAll("[hidefor]"); for (let controlNo = 0; controlNo < controls.length; controlNo++) { let control = controls[controlNo]; let hideFor = control.getAttribute("hidefor"); // Hide unsupported server types using hideFor="servertype1,servertype2". let hide = false; let hideForTokens = hideFor.split(","); for (let tokenNo = 0; tokenNo < hideForTokens.length; tokenNo++) { if (hideForTokens[tokenNo] == serverType) { hide = true; break; } } if (hide) control.setAttribute("hidden", "true"); else control.removeAttribute("hidden"); } } // called when the whole document loads // perform initialization here function onLoad() { var selectedServer; var 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. if ("arguments" in window && window.arguments[0]) { selectedServer = window.arguments[0].server; selectPage = window.arguments[0].selectPage; } accountArray = new Object(); gGenericAttributeTypes = new Object(); gAccountTree.load(); setTimeout(selectServer, 0, selectedServer, selectPage); // Make sure the account manager window fits the screen. document.getElementById("accountManager").style.maxHeight = (window.screen.availHeight - 30) + "px"; } function onUnload() { gAccountTree.unload(); } function selectServer(server, selectPageId) { let childrenNode = document.getElementById("account-tree-children"); // Default to showing the first account. let accountNode = childrenNode.firstChild; // Find the tree-node for the account we want to select if (server) { for (let i = 0; i < childrenNode.childNodes.length; i++) { let account = childrenNode.childNodes[i]._account; if (account && server == account.incomingServer) { accountNode = childrenNode.childNodes[i]; // Make sure all the panes of the account to be selected are shown. accountNode.setAttribute("open", "true"); break; } } } let pageToSelect = accountNode; if (selectPageId) { // Find the page that also corresponds to this server. // It either is the accountNode itself... let pageId = accountNode.getAttribute("PageTag"); if (pageId != selectPageId) { // ... or one of its children. pageToSelect = accountNode.querySelector('[PageTag="' + selectPageId + '"]'); } } let accountTree = document.getElementById("accounttree"); let index = accountTree.contentView.getIndexOfItem(pageToSelect); accountTree.view.selection.select(index); accountTree.treeBoxObject.ensureRowIsVisible(index); let lastItem = accountNode.lastChild.lastChild; if (lastItem.localName == "treeitem") index = accountTree.contentView.getIndexOfItem(lastItem); accountTree.treeBoxObject.ensureRowIsVisible(index); } function replaceWithDefaultSmtpServer(deletedSmtpServerKey) { // First we replace the smtpserverkey in every identity. let am = MailServices.accounts; for (let identity in fixIterator(am.allIdentities, Components.interfaces.nsIMsgIdentity)) { 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 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; } if (!onSave()) return false; // hack hack - save the prefs file NOW in case we crash Services.prefs.savePrefFile(null); if (gRestartNeeded) { gRestartNeeded = !BrowserUtils.restartApplication(); // returns false so that Account manager is not exited when restart failed return !gRestartNeeded; } 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.indexOf(dirLeafName) != -1) 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 aDirToCheck An object describing the special directory. * The object has the following members: * dirsvc : A path keyword to retrieve from the Directory service. * dir : An absolute filesystem path. * Only one of 'dirsvc' or 'dir' can be specified. * 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 * safeSubdirs : An array of directory names that are allowed to be used * under the tested directory. * @param aLocalPath An nsIFile of the directory to check, intended for message storage. */ function checkLocalDirectoryIsSafe(aDirToCheck, aLocalPath) { if (aDirToCheck.OS) { if (aDirToCheck.OS.split(",").indexOf(Services.appinfo.OS) == -1) return true; } let testDir = null; if ("dirsvc" in aDirToCheck) { try { testDir = Services.dirsvc.get(aDirToCheck.dirsvc, Components.interfaces.nsIFile); } catch (e) { Components.utils.reportError("The special folder " + aDirToCheck.dirsvc + " cannot be retrieved on this platform: " + e); } if (!testDir) return true; } else if ("dir" in aDirToCheck) { testDir = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsIFile); testDir.initWithPath(aDirToCheck.dir); if (!testDir.exists()) return true; } else { Components.utils.reportError("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. let allServers = MailServices.accounts.allServers; for (let server in fixIterator(allServers, Components.interfaces.nsIMsgIncomingServer)) { 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. Components.utils.reportError("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 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.realHostName"); var userElem = getPageFormElement("server.realUsername"); if (typeElem && userElem && hostElem) { var newType = getFormElementValue(typeElem); var oldHost = getAccountValue(currentAccount, accountValues, "server", "realHostName", null, false); var newHost = getFormElementValue(hostElem); var oldUser = getAccountValue(currentAccount, accountValues, "server", "realUsername", 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 .findRealServer(newUser, newHost, newType, 0); 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()); } } } // Check the new value of the server.localPath field for validity. var pathElem = getPageFormElement("server.localPath"); if (!pathElem) return true; if (!checkDirectoryIsUsable(getFormElementValue(pathElem))) { // return false; // Temporarily disable this. Just show warning but do not block. See bug 921371. Components.utils.reportError("Local directory '" + getFormElementValue(pathElem).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"); 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; } function onAddAccount() { MsgAccountWizard(); } function AddMailAccount() { NewMailAccount(MailServices.mailSession.topmostMsgWindow); } /** * Highlight the default account row in the account tree, * optionally un-highlight the previous one. * * @param newDefault The account that has become the new default. * Can be given as null if there is none. * @param 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 accountTreeNodes = document.getElementById("account-tree-children") .childNodes; for (let i = 0; i < accountTreeNodes.length; i++) { let accountNode = accountTreeNodes[i]; if (newDefault && newDefault == accountNode._account) { let props = accountNode.firstChild.firstChild.getAttribute("properties"); accountNode.firstChild.firstChild .setAttribute("properties", props + " isDefaultServer-true"); } if (oldDefault && oldDefault == accountNode._account) { let props = accountNode.firstChild.firstChild.getAttribute("properties"); props = props.replace(/isDefaultServer-true/, ""); accountNode.firstChild.firstChild.setAttribute("properties", props); } } } /** * 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 = getDefaultAccount(); MailServices.accounts.defaultAccount = currentAccount; markDefaultServer(currentAccount, previousDefault); // This is only needed on Seamonkey which has this button. setEnabled(document.getElementById("setDefaultButton"), false); } 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 accountTreeNode = document.getElementById("account-tree-children"); // build the list of servers in the account tree (order is important) for (let i = 0; i < accountTreeNode.childNodes.length; i++) { if ("_account" in accountTreeNode.childNodes[i]) { let curServer = accountTreeNode.childNodes[i]._account.incomingServer; if (serverList.indexOf(curServer) == -1) 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: server, account: currentAccount, result: false }; window.openDialog("chrome://messenger/content/removeAccount.xul", "removeAccount", "chrome,titlebar,modal,centerscreen,resizable=no", removeArgs); // 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(getDefaultAccount(), null); } function saveAccount(accountValues, account) { var identity = null; var server = null; if (account) { identity = account.defaultIdentity; server = account.incomingServer; } for (var type in accountValues) { var typeArray = accountValues[type]; for (var slot in typeArray) { var dest; try { if (type == "identity") dest = identity; else if (type == "server") dest = server; else if (type == "pop3") dest = server.QueryInterface(Components.interfaces.nsIPop3IncomingServer); else if (type == "imap") dest = server.QueryInterface(Components.interfaces.nsIImapIncomingServer); else if (type == "none") dest = server.QueryInterface(Components.interfaces.nsINoIncomingServer); else if (type == "nntp") dest = server.QueryInterface(Components.interfaces.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; 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: " + preftype + "\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.xul", account); return false; } } } return true; } /** * Set enabled/disabled state for account actions buttons. * Called by all apps, but if the buttons do not exist, exits early. */ function updateButtons(tree, account) { let addAccountButton = document.getElementById("addAccountButton"); let removeButton = document.getElementById("removeButton"); let setDefaultButton = document.getElementById("setDefaultButton"); if (!addAccountButton && !removeButton && !setDefaultButton) return; // Thunderbird isn't using these. updateItems(tree, account, addAccountButton, setDefaultButton, removeButton); updateBlockedItems([addAccountButton, setDefaultButton, removeButton], false); } /** * 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.childNodes, 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.view.selection.count >= 1)) { // 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 != getDefaultAccount() && 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. * SeaMonkey: Not currently handled by WSM or the main loop yet * since these buttons aren't under the IFRAME. * * @param aItems an array or NodeList of elements to be checked * @param aMustBeTrue if true then the pref must be boolean and set to true * to trigger the disabling (TB requires this, SM not) */ 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.view.selection.count < 1) return false; let node = tree.contentView.getItemAtIndex(tree.currentIndex); 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(); } // 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); } if (changeAccount) updateButtons(tree, 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(".xul")[0]; chromePackageName = MailServices.accounts.getChromePackageName(packageName); } catch (ex) { chromePackageName = "messenger"; } return "chrome://" + chromePackageName + "/content/" + pageId; } function loadPage(pageId) { const LOAD_FLAGS_NONE = Components.interfaces.nsIWebNavigation.LOAD_FLAGS_NONE; document.getElementById("contentFrame").webNavigation.loadURI(pageURL(pageId), LOAD_FLAGS_NONE, null, null, null); } // 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; 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] = new Object(); accountValues[type][slot] = value; } function getAccountValue(account, accountValues, type, slot, preftype, isGeneric) { if (!(type in accountValues)) accountValues[type] = new Object(); // 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(Components.interfaces.nsIPop3IncomingServer); else if (type == "imap") source = server.QueryInterface(Components.interfaces.nsIImapIncomingServer); else if (type == "none") source = server.QueryInterface(Components.interfaces.nsINoIncomingServer); else if (type == "nntp") source = server.QueryInterface(Components.interfaces.nsINntpIncomingServer); else if (type == "smtp") source = MailServices.smtp.defaultServer; } catch (ex) { } if (source) { if (isGeneric) { if (!(type in gGenericAttributeTypes)) gGenericAttributeTypes[type] = new Object(); // 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 formElement A XUL 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 == "textbox" && formElement.getAttribute("datatype") == "nsIFile") { if (formElement.value) { let localfile = Components.classes["@mozilla.org/file/local;1"] .createInstance(Components.interfaces.nsIFile); localfile.initWithPath(formElement.value); return localfile; } return null; } if ((type == "textbox") || ("value" in formElement)) { return formElement.value; } return null; } catch (ex) { Components.utils.reportError("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 formElement A XUL input element. * @param 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; } // handle nsIFile else if (type == "textbox" && formElement.getAttribute("datatype") == "nsIFile") { if (value) { let localfile = value.QueryInterface(Components.interfaces.nsIFile); try { formElement.value = localfile.path; } catch (ex) { dump("Still need to fix uninitialized nsIFile problem!\n"); } } else { formElement.value = ""; } } else if (type == "textbox") { if (value == null) formElement.value = null; else formElement.value = value; } else if (type == "label") { formElement.value = value || ""; } // let the form figure out what to do with it else { if (value == null) 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; } /** * Returns the default account without throwing exception if there is none. * The account manager can be opened even if there are no account yet. */ function getDefaultAccount() { try { return MailServices.accounts.defaultAccount; } catch (e) { return null; // No default account yet. } } /** * 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 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] = new Object(); accountArray[serverId]._account = account; } return accountArray[serverId]; } var gAccountTree = { load: function at_load() { this._build(); MailServices.accounts.addIncomingServerListener(this); }, unload: function at_unload() { MailServices.accounts.removeIncomingServerListener(this); }, onServerLoaded: function at_onServerLoaded(aServer) { this._build(); }, onServerUnloaded: function at_onServerUnloaded(aServer) { this._build(); }, onServerChanged: function at_onServerChanged(aServer) {}, _dataStore: Components.classes["@mozilla.org/xul/xulstore;1"] .getService(Components.interfaces.nsIXULStore), /** * Retrieve from XULStore.json whether the account should be expanded (open) * in the account tree. * * @param aAccountKey key of the account to check */ _getAccountOpenState: function at_getAccountOpenState(aAccountKey) { if (!this._dataStore.hasValue(document.documentURI, aAccountKey, "open")) { // If there was no value stored, use opened state. return "true"; } else { // 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: function at_build() { const Ci = Components.interfaces; var bundle = document.getElementById("bundle_prefs"); function getString(aString) { return bundle.getString(aString); } var panels = [{string: getString("prefPanel-server"), src: "am-server.xul"}, {string: getString("prefPanel-copies"), src: "am-copies.xul"}, {string: getString("prefPanel-synchronization"), src: "am-offline.xul"}, {string: getString("prefPanel-diskspace"), src: "am-offline.xul"}, {string: getString("prefPanel-addressing"), src: "am-addressing.xul"}, {string: getString("prefPanel-junk"), src: "am-junk.xul"}]; let accounts = allAccountsSorted(false); let mainTree = document.getElementById("account-tree-children"); // 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 addresssing } // 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 let catMan = Components.classes["@mozilla.org/categorymanager;1"] .getService(Ci.nsICategoryManager); const CATEGORY = "mailnews-accountmanager-extensions"; let catEnum = catMan.enumerateCategory(CATEGORY); while (catEnum.hasMoreElements()) { let entryName = null; try { entryName = catEnum.getNext().QueryInterface(Ci.nsISupportsCString).data; let svc = Components.classes[catMan.getCategoryEntry(CATEGORY, entryName)] .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 + ".xul"}); } } catch(e) { // Fetching of this extension panel failed so do not show it, // just log error. let extName = entryName || "(unknown)"; Components.utils.reportError("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); Components.utils.reportError("Error accessing account " + accountID + ": " + e); accountName = "Invalid account " + accountID; panelsToKeep.length = 0; } // Create the top level tree-item. var treeitem = document.createElement("treeitem"); mainTree.appendChild(treeitem); var treerow = document.createElement("treerow"); treeitem.appendChild(treerow); var treecell = document.createElement("treecell"); treerow.appendChild(treecell); treecell.setAttribute("label", accountName); treeitem.setAttribute("PageTag", amChrome); // Add icons based on account type. if (server) { treecell.setAttribute("properties", "folderNameCol isServer-true" + " serverType-" + server.type); // For IM accounts, we can try to fetch a protocol specific icon. if (server.type == "im") { treecell.setAttribute("src", server.wrappedJSObject.imAccount .protocol.iconBaseURI + "icon.png"); } } if (panelsToKeep.length > 0) { var treekids = document.createElement("treechildren"); treeitem.appendChild(treekids); for (let panel of panelsToKeep) { var kidtreeitem = document.createElement("treeitem"); treekids.appendChild(kidtreeitem); var kidtreerow = document.createElement("treerow"); kidtreeitem.appendChild(kidtreerow); var kidtreecell = document.createElement("treecell"); kidtreerow.appendChild(kidtreecell); kidtreecell.setAttribute("label", panel.string); kidtreeitem.setAttribute("PageTag", panel.src); kidtreeitem._account = account; } treeitem.setAttribute("container", "true"); treeitem.id = accountKey; // Load the 'open' state of the account from XULStore.json. treeitem.setAttribute("open", this._getAccountOpenState(accountKey)); // Let the XULStore.json automatically save the 'open' state of the // account when it is changed. treeitem.setAttribute("persist", "open"); } treeitem._account = account; } markDefaultServer(getDefaultAccount(), null); // Now add the outgoing server node. var treeitem = document.createElement("treeitem"); mainTree.appendChild(treeitem); var treerow = document.createElement("treerow"); treeitem.appendChild(treerow); var treecell = document.createElement("treecell"); treerow.appendChild(treecell); treecell.setAttribute("label", getString("prefPanel-smtp")); treeitem.setAttribute("PageTag", "am-smtp.xul"); treecell.setAttribute("properties", "folderNameCol isServer-true serverType-smtp"); } };