diff options
Diffstat (limited to 'mailnews/base/prefs')
60 files changed, 16462 insertions, 0 deletions
diff --git a/mailnews/base/prefs/content/AccountManager.js b/mailnews/base/prefs/content/AccountManager.js new file mode 100644 index 000000000..5de98ecd0 --- /dev/null +++ b/mailnews/base/prefs/content/AccountManager.js @@ -0,0 +1,1604 @@ +/* -*- 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 = MailServices.accounts.defaultAccount; + 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(MailServices.accounts.defaultAccount, 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) { + 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 != 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. + * 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; +} + +/** + * 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; + + 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(MailServices.accounts.defaultAccount, 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"); + } +}; diff --git a/mailnews/base/prefs/content/AccountManager.xul b/mailnews/base/prefs/content/AccountManager.xul new file mode 100644 index 000000000..a14156b32 --- /dev/null +++ b/mailnews/base/prefs/content/AccountManager.xul @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/AccountManager.dtd"> +<dialog id="accountManager" + windowtype="mailnews:accountmanager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&accountManagerTitle.label;" + style="&accountManager.size;" + persist="width height screenX screenY" + buttons="accept,cancel" + onload="onLoad(event);" + onunload="onUnload();" + ondialogaccept="return onAccept(true);"> +<stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> +<stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/> +<script type="application/javascript" src="chrome://messenger/content/accountUtils.js"/> +<script type="application/javascript" src="chrome://messenger/content/am-prefs.js"/> +<script type="application/javascript" src="chrome://messenger/content/AccountManager.js"/> +<script type="application/javascript" src="chrome://messenger/content/am-help.js"/> +<script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + + <hbox flex="1"> + <vbox style="&accountTree.width;"> + <tree flex="1" onselect="onAccountTreeSelect(null, null);" id="accounttree" + seltype="single" hidecolumnpicker="true"> + <treecols> + <treecol id="AccountCol" flex="1" primary="true" hideheader="true"/> + </treecols> + <treechildren id="account-tree-children"/> + </tree> + +#if defined(MOZ_THUNDERBIRD) && defined(HYPE_ICEDOVE) + <button id="accountActionsButton" type="menu" + label="&accountActionsButton.label;" + accesskey="&accountActionsButton.accesskey;"> + <menupopup id="accountActionsDropdown" + onpopupshowing="initAccountActionsButtons(this);"> + <menuitem id="accountActionsAddMailAccount" + label="&addMailAccountButton.label;" + accesskey="&addMailAccountButton.accesskey;" + prefstring="mail.disable_new_account_addition" + oncommand="AddMailAccount(event); event.stopPropagation();"/> + <menuitem id="accountActionsAddFeedAccount" + label="&addFeedAccountButton.label;" + accesskey="&addFeedAccountButton.accesskey;" + prefstring="mail.disable_new_account_addition" + oncommand="AddFeedAccount(event); event.stopPropagation();"/> + <menuitem id="accountActionsAddOtherAccount" + label="&addOtherAccountButton.label;" + accesskey="&addOtherAccountButton.accesskey;" + prefstring="mail.disable_new_account_addition" + oncommand="onAddAccount(event); event.stopPropagation();"/> + <menuseparator id="accountActionsDropdownSep1"/> + <menuitem id="accountActionsDropdownSetDefault" + label="&setDefaultButton.label;" + accesskey="&setDefaultButton.accesskey;" + prefstring="mail.disable_button.set_default_account" + oncommand="onSetDefault(event); event.stopPropagation();"/> + <menuitem id="accountActionsDropdownRemove" + label="&removeButton.label;" + accesskey="&removeButton.accesskey;" + prefstring="mail.disable_button.delete_account" + oncommand="onRemoveAccount(event); event.stopPropagation();"/> + </menupopup> + </button> +#else + <button label="&addAccountButton.label;" oncommand="onAddAccount(event);" id="addAccountButton" + prefstring="mail.disable_new_account_addition" + accesskey="&addAccountButton.accesskey;"/> + <button label="&setDefaultButton.label;" oncommand="onSetDefault(event);" disabled="true" id="setDefaultButton" + prefstring="mail.disable_button.set_default_account" + accesskey="&setDefaultButton.accesskey;"/> + <button disabled="true" label="&removeButton.label;" oncommand="onRemoveAccount(event);" id="removeButton" + prefstring="mail.disable_button.delete_account" + accesskey="&removeButton.accesskey;"/> +#endif + </vbox> + + <iframe id="contentFrame" name="contentFrame" flex="1"/> + </hbox> +</dialog> diff --git a/mailnews/base/prefs/content/AccountWizard.js b/mailnews/base/prefs/content/AccountWizard.js new file mode 100644 index 000000000..90d729418 --- /dev/null +++ b/mailnews/base/prefs/content/AccountWizard.js @@ -0,0 +1,988 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +/* the okCallback is used for sending a callback for the parent window */ +var okCallback = null; +/* The account wizard creates new accounts */ + +/* + data flow into the account wizard like this: + + For new accounts: + * pageData -> Array -> createAccount -> finishAccount + + For accounts coming from the ISP setup: + * RDF -> Array -> pageData -> Array -> createAccount -> finishAccount + + for "unfinished accounts" + * account -> Array -> pageData -> Array -> finishAccount + + Where: + pageData - the actual pages coming out of the Widget State Manager + RDF - the ISP datasource + Array - associative array of attributes, that very closely + resembles the nsIMsgAccount/nsIMsgIncomingServer/nsIMsgIdentity + structure + createAccount() - creates an account from the above Array + finishAccount() - fills an existing account with data from the above Array + +*/ + +/* + the account wizard path is something like: + + accounttype -> identity -> server -> login -> accname -> done + \-> newsserver ----/ + + where the accounttype determines which path to take + (server vs. newsserver) +*/ + +Components.utils.import("resource:///modules/mailServices.js"); + +var contentWindow; + +var gPageData; + +var nsIMsgIdentity = Components.interfaces.nsIMsgIdentity; +var nsIMsgIncomingServer = Components.interfaces.nsIMsgIncomingServer; +var gPrefsBundle, gMessengerBundle; + +// the current nsIMsgAccount +var gCurrentAccount; + +// The default account before we create a new account. +// We need to store this as just asking for the default account may switch +// it to the newly created one if there was none before. +var gDefaultAccount; + +// the current associative array that +// will eventually be dumped into the account +var gCurrentAccountData; + +// default picker mode for copies and folders +var gDefaultSpecialFolderPickerMode = "0"; + +// event handlers +function onAccountWizardLoad() { + gPrefsBundle = document.getElementById("bundle_prefs"); + gMessengerBundle = document.getElementById("bundle_messenger"); + + if ("testingIspServices" in this) { + if ("SetCustomizedWizardDimensions" in this && testingIspServices()) { + SetCustomizedWizardDimensions(); + } + } + + /* We are checking here for the callback argument */ + if (window.arguments && window.arguments[0]) { + if(window.arguments[0].okCallback ) + { + //dump("There is okCallback"); + top.okCallback = window.arguments[0].okCallback; + } + } + + checkForInvalidAccounts(); + + // It is fine if there is no default account, this is expected the first + // time you launch mail on a new profile. + gDefaultAccount = MailServices.accounts.defaultAccount; + + // Set default value for global inbox checkbox + var checkGlobalInbox = document.getElementById("deferStorage"); + try { + checkGlobalInbox.checked = Services.prefs.getBoolPref("mail.accountwizard.deferstorage"); + } catch(e) {} +} + +function onCancel() +{ + if ("ActivationOnCancel" in this && ActivationOnCancel()) + return false; + var firstInvalidAccount = getFirstInvalidAccount(); + var closeWizard = true; + + // if the user cancels the the wizard when it pops up because of + // an invalid account (example, a webmail account that activation started) + // we just force create it by setting some values and calling the FinishAccount() + // see bug #47521 for the full discussion + if (firstInvalidAccount) { + var pageData = GetPageData(); + // set the fullName if it doesn't exist + if (!pageData.identity.fullName || !pageData.identity.fullName.value) { + setPageData(pageData, "identity", "fullName", ""); + } + + // set the email if it doesn't exist + if (!pageData.identity.email || !pageData.identity.email.value) { + setPageData(pageData, "identity", "email", "user@domain.invalid"); + } + + // call FinishAccount() and not onFinish(), since the "finish" + // button may be disabled + FinishAccount(); + } + else { + // since this is not an invalid account + // really cancel if the user hits the "cancel" button + // if the length of the account list is less than 1, there are no accounts + if (MailServices.accounts.accounts.length < 1) { + let confirmMsg = gPrefsBundle.getString("cancelWizard"); + let confirmTitle = gPrefsBundle.getString("accountWizard"); + let result = Services.prompt.confirmEx(window, confirmTitle, confirmMsg, + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1), + gPrefsBundle.getString('WizardExit'), + gPrefsBundle.getString('WizardContinue'), + null, null, {value:0}); + + if (result == 1) + closeWizard = false; + } + + if(top.okCallback && closeWizard) { + var state = false; + top.okCallback(state); + } + } + return closeWizard; +} + +function FinishAccount() +{ + try { + var pageData = GetPageData(); + + var accountData= gCurrentAccountData; + + if (!accountData) + { + accountData = new Object; + // Time to set the smtpRequiresUsername attribute + if (!serverIsNntp(pageData)) + accountData.smtpRequiresUsername = true; + } + + // we may need local folders before account is "Finished" + // if it's a pop3 account which defers to Local Folders. + verifyLocalFoldersAccount(); + + PageDataToAccountData(pageData, accountData); + + FixupAccountDataForIsp(accountData); + + // we might be simply finishing another account + if (!gCurrentAccount) + gCurrentAccount = createAccount(accountData); + + // transfer all attributes from the accountdata + finishAccount(gCurrentAccount, accountData); + + setupCopiesAndFoldersServer(gCurrentAccount, getCurrentServerIsDeferred(pageData), accountData); + + if (gCurrentAccount.incomingServer.canBeDefaultServer) + EnableCheckMailAtStartUpIfNeeded(gCurrentAccount); + + if (!document.getElementById("downloadMsgs").hidden) { + // skip the default biff, we will load messages manually if needed + window.opener.gLoadStartFolder = false; + if (document.getElementById("downloadMsgs").checked) { + window.opener.gNewAccountToLoad = gCurrentAccount; // load messages for new POP account + } + } + + // in case we crash, force us a save of the prefs file NOW + try { + MailServices.accounts.saveAccountInfo(); + } + catch (ex) { + dump("Error saving account info: " + ex + "\n"); + } + window.close(); + if(top.okCallback) + { + var state = true; + //dump("finish callback"); + top.okCallback(state); + } + } + catch(ex) { + dump("FinishAccount failed, " + ex +"\n"); + } +} + +// prepopulate pageData with stuff from accountData +// use: to prepopulate the wizard with account information +function AccountDataToPageData(accountData, pageData) +{ + if (!accountData) { + dump("null account data! clearing..\n"); + // handle null accountData as if it were an empty object + // so that we clear-out any old pagedata from a + // previous accountdata. The trick is that + // with an empty object, accountData.identity.slot is undefined, + // so this will clear out the prefill data in setPageData + + accountData = new Object; + accountData.incomingServer = new Object; + accountData.identity = new Object; + accountData.smtp = new Object; + } + + var server = accountData.incomingServer; + + if (server.type == undefined) { + // clear out the old server data + //setPageData(pageData, "accounttype", "mailaccount", undefined); + // setPageData(pageData, "accounttype", "newsaccount", undefined); + setPageData(pageData, "server", "servertype", undefined); + setPageData(pageData, "server", "hostname", undefined); + + } + else { + if (server.type == "nntp") { + setPageData(pageData, "accounttype", "newsaccount", true); + setPageData(pageData, "accounttype", "mailaccount", false); + setPageData(pageData, "newsserver", "hostname", server.hostName); + } + else { + setPageData(pageData, "accounttype", "mailaccount", true); + setPageData(pageData, "accounttype", "newsaccount", false); + setPageData(pageData, "server", "servertype", server.type); + setPageData(pageData, "server", "hostname", server.hostName); + } + setPageData(pageData, "accounttype", "otheraccount", false); + } + + setPageData(pageData, "login", "username", server.username || ""); + setPageData(pageData, "login", "password", server.password || ""); + setPageData(pageData, "accname", "prettyName", server.prettyName || ""); + setPageData(pageData, "accname", "userset", false); + setPageData(pageData, "ispdata", "supplied", false); + + var identity; + + if (accountData.identity) { + dump("This is an accountdata\n"); + identity = accountData.identity; + } + else if (accountData.identities) { + identity = accountData.identities.queryElementAt(0, Components.interfaces.nsIMsgIdentity); + dump("this is an account, id= " + identity + "\n"); + } + + setPageData(pageData, "identity", "email", identity.email || ""); + setPageData(pageData, "identity", "fullName", identity.fullName || ""); + + var smtp; + + if (accountData.smtp) { + smtp = accountData.smtp; + setPageData(pageData, "server", "smtphostname", smtp.hostname); + setPageData(pageData, "login", "smtpusername", smtp.username); + } +} + +// take data from each page of pageData and dump it into accountData +// use: to put results of wizard into a account-oriented object +function PageDataToAccountData(pageData, accountData) +{ + if (!accountData.identity) + accountData.identity = new Object; + if (!accountData.incomingServer) + accountData.incomingServer = new Object; + if (!accountData.smtp) + accountData.smtp = new Object; + if (!accountData.pop3) + accountData.pop3 = new Object; + if (!accountData.imap) + accountData.imap = new Object; + + var identity = accountData.identity; + var server = accountData.incomingServer; + var smtp = accountData.smtp; + var pop3 = accountData.pop3; + var imap = accountData.imap; + + if (pageData.identity.email) + identity.email = pageData.identity.email.value; + if (pageData.identity.fullName) + identity.fullName = pageData.identity.fullName.value; + + server.type = getCurrentServerType(pageData); + server.hostName = getCurrentHostname(pageData); + if (getCurrentServerIsDeferred(pageData)) + { + try + { + let localFoldersServer = MailServices.accounts.localFoldersServer; + let localFoldersAccount = MailServices.accounts.FindAccountForServer(localFoldersServer); + pop3.deferredToAccount = localFoldersAccount.key; + pop3.deferGetNewMail = true; + server["ServerType-pop3"] = pop3; + } + catch (ex) {dump ("exception setting up deferred account" + ex);} + } + if (serverIsNntp(pageData)) { + // this stuff probably not relevant + dump("not setting username/password/etc\n"); + } + else { + if (pageData.login) { + if (pageData.login.username) + server.username = pageData.login.username.value; + if (pageData.login.password) + server.password = pageData.login.password.value; + if (pageData.login.smtpusername) + smtp.username = pageData.login.smtpusername.value; + } + + dump("pageData.server = " + pageData.server + "\n"); + if (pageData.server) { + dump("pageData.server.smtphostname.value = " + pageData.server.smtphostname + "\n"); + if (pageData.server.smtphostname && + pageData.server.smtphostname.value) + smtp.hostname = pageData.server.smtphostname.value; + } + if (pageData.identity && pageData.identity.smtpServerKey) + identity.smtpServerKey = pageData.identity.smtpServerKey.value; + + if (pageData.server.port && + pageData.server.port.value) + { + if (server.type == 'imap') + { + imap.port = pageData.server.port.value; + server["ServerType-imap"] = imap; + } + else if (server.type == 'pop3') + { + pop3.port = pageData.server.port.value; + server["ServerType-pop3"] = pop3; + } + } + + if (pageData.server.leaveMessagesOnServer && + pageData.server.leaveMessagesOnServer.value) + { + pop3.leaveMessagesOnServer = pageData.server.leaveMessagesOnServer.value; + server["ServerType-pop3"] = pop3; + } + } + + if (pageData.accname) { + if (pageData.accname.prettyName) + server.prettyName = pageData.accname.prettyName.value; + } + +} + +// given an accountData structure, create an account +// (but don't fill in any fields, that's for finishAccount() +function createAccount(accountData) +{ + // Retrieve the server (data) from the account data. + var server = accountData.incomingServer; + + // for news, username is always null + var username = (server.type == "nntp") ? null : server.username; + dump("MailServices.accounts.createIncomingServer(" + + username + ", " + server.hostName + ", " + server.type + ")\n"); + // Create a (actual) server. + server = MailServices.accounts.createIncomingServer(username, server.hostName, server.type); + + dump("MailServices.accounts.createAccount()\n"); + // Create an account. + let account = MailServices.accounts.createAccount(); + + // only create an identity for this account if we really have one + // (use the email address as a check) + if (accountData.identity && accountData.identity.email) + { + dump("MailServices.accounts.createIdentity()\n"); + // Create an identity. + let identity = MailServices.accounts.createIdentity(); + + // New nntp identities should use plain text by default; + // we want that GNKSA (The Good Net-Keeping Seal of Approval). + if (server.type == "nntp") + identity.composeHtml = false; + + account.addIdentity(identity); + } + + // we mark the server as invalid so that the account manager won't + // tell RDF about the new server - it's not quite finished getting + // set up yet, in particular, the deferred storage pref hasn't been set. + server.valid = false; + // Set the new account to use the new server. + account.incomingServer = server; + server.valid = true; + return account; +} + +// given an accountData structure, copy the data into the +// given account, incoming server, and so forth +function finishAccount(account, accountData) +{ + if (accountData.incomingServer) { + + var destServer = account.incomingServer; + var srcServer = accountData.incomingServer; + copyObjectToInterface(destServer, srcServer, true); + + // see if there are any protocol-specific attributes + // if so, we use the type to get the IID, QueryInterface + // as appropriate, then copy the data over + dump("srcServer.ServerType-" + srcServer.type + " = " + + srcServer["ServerType-" + srcServer.type] + "\n"); + if (srcServer["ServerType-" + srcServer.type]) { + // handle server-specific stuff + var IID; + try { + IID = destServer.protocolInfo.serverIID; + } catch (ex) { + Components.utils.reportError("Could not get IID for " + srcServer.type + ": " + ex); + } + + if (IID) { + destProtocolServer = destServer.QueryInterface(IID); + srcProtocolServer = srcServer["ServerType-" + srcServer.type]; + + dump("Copying over " + srcServer.type + "-specific data\n"); + copyObjectToInterface(destProtocolServer, srcProtocolServer, false); + } + } + + account.incomingServer.valid=true; + // hack to cause an account loaded notification now the server is valid + account.incomingServer = account.incomingServer; + } + + // copy identity info + var destIdentity = account.identities.length ? + account.identities.queryElementAt(0, nsIMsgIdentity) : + null; + + if (destIdentity) // does this account have an identity? + { + if (accountData.identity && accountData.identity.email) { + // fixup the email address if we have a default domain + var emailArray = accountData.identity.email.split('@'); + if (emailArray.length < 2 && accountData.domain) { + accountData.identity.email += '@' + accountData.domain; + } + + copyObjectToInterface(destIdentity, accountData.identity, true); + destIdentity.valid=true; + } + + /** + * If signature file need to be set, get the path to the signature file. + * Signature files, if exist, are placed under default location. Get + * default files location for messenger using directory service. Signature + * file name should be extracted from the account data to build the complete + * path for signature file. Once the path is built, set the identity's signature pref. + */ + if (destIdentity.attachSignature) + { + var sigFileName = accountData.signatureFileName; + let sigFile = MailServices.mailSession.getDataFilesDir("messenger"); + sigFile.append(sigFileName); + destIdentity.signature = sigFile; + } + + if (accountData.smtp.hostname && !destIdentity.smtpServerKey) + { + // hostname + no key => create a new SMTP server. + + let smtpServer = MailServices.smtp.createServer(); + var isDefaultSmtpServer; + if (!MailServices.smtp.defaultServer.hostname) { + MailServices.smtp.defaultServer = smtpServer; + isDefaultSmtpServer = true; + } + + copyObjectToInterface(smtpServer, accountData.smtp, false); + + // If it's the default server we created, make the identity use + // "Use Default" by default. + destIdentity.smtpServerKey = + (isDefaultSmtpServer) ? "" : smtpServer.key; + } + } // if the account has an identity... + + if (this.FinishAccountHook != undefined) { + FinishAccountHook(accountData.domain); + } +} + +// Helper method used by copyObjectToInterface which attempts to set dest[attribute] as a generic +// attribute on the xpconnect object, src. +// This routine skips any attribute that begins with ServerType- +function setGenericAttribute(dest, src, attribute) +{ + if (!(attribute.toLowerCase().startsWith("servertype-")) && src[attribute]) + { + switch (typeof src[attribute]) + { + case "string": + dest.setUnicharAttribute(attribute, src[attribute]); + break; + case "boolean": + dest.setBoolAttribute(attribute, src[attribute]); + break; + case "number": + dest.setIntAttribute(attribute, src[attribute]); + break; + default: + dump("Error: No Generic attribute " + attribute + " found for: " + dest + "\n"); + break; + } + } +} + +// copy over all attributes from dest into src that already exist in src +// the assumption is that src is an XPConnect interface full of attributes +// @param useGenericFallback if we can't set an attribute directly on src, then fall back +// and try setting it generically. This assumes that src supports setIntAttribute, setUnicharAttribute +// and setBoolAttribute. +function copyObjectToInterface(dest, src, useGenericFallback) +{ + if (!dest) return; + if (!src) return; + + var attribute; + for (attribute in src) + { + if (dest.__lookupSetter__(attribute)) + { + if (dest[attribute] != src[attribute]) + dest[attribute] = src[attribute]; + } + else if (useGenericFallback) // fall back to setting the attribute generically + setGenericAttribute(dest, src, attribute); + } // for each attribute in src we want to copy +} + +// check if there already is a "Local Folders" +// if not, create it. +function verifyLocalFoldersAccount() +{ + var localMailServer = null; + try { + localMailServer = MailServices.accounts.localFoldersServer; + } + catch (ex) { + // dump("exception in findserver: " + ex + "\n"); + localMailServer = null; + } + + try { + if (!localMailServer) + { + // dump("Creating local mail account\n"); + // creates a copy of the identity you pass in + MailServices.accounts.createLocalMailAccount(); + try { + localMailServer = MailServices.accounts.localFoldersServer; + } + catch (ex) { + dump("error! we should have found the local mail server after we created it.\n"); + localMailServer = null; + } + } + } + catch (ex) {dump("Error in verifyLocalFoldersAccount" + ex + "\n"); } + +} + +function setupCopiesAndFoldersServer(account, accountIsDeferred, accountData) +{ + try { + var server = account.incomingServer; + + // This function sets up the default send preferences. The send preferences + // go on identities, so there is no need to continue without any identities. + if (server.type == "rss" || account.identities.length == 0) + return false; + let identity = account.identities.queryElementAt(0, Components.interfaces.nsIMsgIdentity); + // For this server, do we default the folder prefs to this server, or to the "Local Folders" server + // If it's deferred, we use the local folders account. + var defaultCopiesAndFoldersPrefsToServer = !accountIsDeferred && server.defaultCopiesAndFoldersPrefsToServer; + + var copiesAndFoldersServer = null; + if (defaultCopiesAndFoldersPrefsToServer) + { + copiesAndFoldersServer = server; + } + else + { + if (!MailServices.accounts.localFoldersServer) + { + dump("error! we should have a local mail server at this point\n"); + return false; + } + copiesAndFoldersServer = MailServices.accounts.localFoldersServer; + } + + setDefaultCopiesAndFoldersPrefs(identity, copiesAndFoldersServer, accountData); + + } catch (ex) { + // return false (meaning we did not setupCopiesAndFoldersServer) + // on any error + dump("Error in setupCopiesAndFoldersServer: " + ex + "\n"); + return false; + } + return true; +} + +function setDefaultCopiesAndFoldersPrefs(identity, server, accountData) +{ + var rootFolder = server.rootFolder; + + // we need to do this or it is possible that the server's draft, + // stationery fcc folder will not be in rdf + // + // this can happen in a couple cases + // 1) the first account we create, creates the local mail. since + // local mail was just created, it obviously hasn't been opened, + // or in rdf.. + // 2) the account we created is of a type where + // defaultCopiesAndFoldersPrefsToServer is true + // this since we are creating the server, it obviously hasn't been + // opened, or in rdf. + // + // this makes the assumption that the server's draft, stationery fcc folder + // are at the top level (ie subfolders of the root folder.) this works + // because we happen to be doing things that way, and if the user changes + // that, it will work because to change the folder, it must be in rdf, + // coming from the folder cache, in the worst case. + var msgFolder = rootFolder.QueryInterface(Components.interfaces.nsIMsgFolder); + + /** + * When a new account is created, folders 'Sent', 'Drafts' + * and 'Templates' are not created then, but created on demand at runtime. + * But we do need to present them as possible choices in the Copies and Folders + * UI. To do that, folder URIs have to be created and stored in the prefs file. + * So, if there is a need to build special folders, append the special folder + * names and create right URIs. + */ + var folderDelim = "/"; + + /* we use internal names known to everyone like Sent, Templates and Drafts */ + /* if folder names were already given in isp rdf, we use them, + otherwise we use internal names known to everyone like Sent, Templates and Drafts */ + + // Note the capital F, D and S! + var draftFolder = (accountData.identity && accountData.identity.DraftFolder ? + accountData.identity.DraftFolder : "Drafts"); + var stationeryFolder = (accountData.identity && accountData.identity.StationeryFolder ? + accountData.identity.StationeryFolder : "Templates"); + var fccFolder = (accountData.identity && accountData.identity.FccFolder ? + accountData.identity.FccFolder : "Sent"); + + identity.draftFolder = msgFolder.server.serverURI+ folderDelim + draftFolder; + identity.stationeryFolder = msgFolder.server.serverURI+ folderDelim + stationeryFolder; + identity.fccFolder = msgFolder.server.serverURI+ folderDelim + fccFolder; + + // Note the capital F, D and S! + identity.fccFolderPickerMode = (accountData.identity && + accountData.identity.FccFolder ? 1 : gDefaultSpecialFolderPickerMode); + identity.draftsFolderPickerMode = (accountData.identity && + accountData.identity.DraftFolder ? 1 : gDefaultSpecialFolderPickerMode); + identity.tmplFolderPickerMode = (accountData.identity && + accountData.identity.StationeryFolder ? 1 : gDefaultSpecialFolderPickerMode); +} + +function AccountExists(userName, hostName, serverType) +{ + return MailServices.accounts.findRealServer(userName, hostName, serverType, 0); +} + +function getFirstInvalidAccount() +{ + let invalidAccounts = getInvalidAccounts(MailServices.accounts.accounts); + + if (invalidAccounts.length > 0) + return invalidAccounts[0]; + else + return null; +} + +function checkForInvalidAccounts() +{ + var firstInvalidAccount = getFirstInvalidAccount(); + + if (firstInvalidAccount) { + var pageData = GetPageData(); + dump("We have an invalid account, " + firstInvalidAccount + ", let's use that!\n"); + gCurrentAccount = firstInvalidAccount; + + // there's a possibility that the invalid account has ISP defaults + // as well.. so first pre-fill accountData with ISP info, then + // overwrite it with the account data + + + var identity = + firstInvalidAccount.identities.queryElementAt(0, nsIMsgIdentity); + + var accountData = null; + // If there is a email address already provided, try to get to other ISP defaults. + // If not, get pre-configured data, if any. + if (identity.email) { + dump("Invalid account: trying to get ISP data for " + identity.email + "\n"); + accountData = getIspDefaultsForEmail(identity.email); + dump("Invalid account: Got " + accountData + "\n"); + + // account -> accountData -> pageData + accountData = AccountToAccountData(firstInvalidAccount, accountData); + } + else { + accountData = getPreConfigDataForAccount(firstInvalidAccount); + } + + AccountDataToPageData(accountData, pageData); + + gCurrentAccountData = accountData; + + setupWizardPanels(); + // Set the page index to identity page. + document.documentElement.pageIndex = 1; + } +} + +// Transfer all invalid account information to AccountData. Also, get those special +// preferences (not associated with any interfaces but preconfigurable via prefs or rdf files) +// like whether not the smtp server associated with this account requires +// a user name (mail.identity.<id_key>.smtpRequiresUsername) and the choice of skipping +// panels (mail.identity.<id_key>.wizardSkipPanels). +function getPreConfigDataForAccount(account) +{ + var accountData = new Object; + accountData = new Object; + accountData.incomingServer = new Object; + accountData.identity = new Object; + accountData.smtp = new Object; + + accountData = AccountToAccountData(account, null); + + let identity = account.identities.queryElementAt(0, nsIMsgIdentity); + + try { + var skipPanelsPrefStr = "mail.identity." + identity.key + ".wizardSkipPanels"; + accountData.wizardSkipPanels = Services.prefs.getCharPref(skipPanelsPrefStr); + + if (identity.smtpServerKey) { + let smtpServer = MailServices.smtp.getServerByKey(identity.smtpServerKey); + accountData.smtp = smtpServer; + + var smtpRequiresUsername = false; + var smtpRequiresPrefStr = "mail.identity." + identity.key + ".smtpRequiresUsername"; + smtpRequiresUsername = Services.prefs.getBoolPref(smtpRequiresPrefStr); + accountData.smtpRequiresUsername = smtpRequiresUsername; + } + } + catch(ex) { + // reached here as special identity pre-configuration prefs + // (wizardSkipPanels, smtpRequiresUsername) are not defined. + } + + return accountData; +} + +function AccountToAccountData(account, defaultAccountData) +{ + dump("AccountToAccountData(" + account + ", " + defaultAccountData + ")\n"); + var accountData = defaultAccountData; + if (!accountData) + accountData = new Object; + + accountData.incomingServer = account.incomingServer; + accountData.identity = account.identities.queryElementAt(0, nsIMsgIdentity); + accountData.smtp = MailServices.smtp.defaultServer; + + return accountData; +} + +// sets the page data, automatically creating the arrays as necessary +function setPageData(pageData, tag, slot, value) { + if (!pageData[tag]) pageData[tag] = []; + + if (value == undefined) { + // clear out this slot + if (pageData[tag][slot]) delete pageData[tag][slot]; + } + else { + // pre-fill this slot + if (!pageData[tag][slot]) pageData[tag][slot] = []; + pageData[tag][slot].id = slot; + pageData[tag][slot].value = value; + } +} + +// value of checkbox on the first page +function serverIsNntp(pageData) { + if (pageData.accounttype.newsaccount) + return pageData.accounttype.newsaccount.value; + return false; +} + +function getUsernameFromEmail(aEmail, aEnsureDomain) +{ + var username = aEmail.substr(0, aEmail.indexOf("@")); + if (aEnsureDomain && gCurrentAccountData && gCurrentAccountData.domain) + username += '@' + gCurrentAccountData.domain; + return username; +} + +function getCurrentUserName(pageData) +{ + var userName = ""; + + if (pageData.login) { + if (pageData.login.username) { + userName = pageData.login.username.value; + } + } + if (userName == "") { + var email = pageData.identity.email.value; + userName = getUsernameFromEmail(email, false); + } + return userName; +} + +function getCurrentServerType(pageData) { + var servertype = "pop3"; // hopefully don't resort to default! + if (serverIsNntp(pageData)) + servertype = "nntp"; + else if (pageData.server && pageData.server.servertype) + servertype = pageData.server.servertype.value; + return servertype; +} + +function getCurrentServerIsDeferred(pageData) { + var serverDeferred = false; + if (getCurrentServerType(pageData) == "pop3" && pageData.server && pageData.server.deferStorage) + serverDeferred = pageData.server.deferStorage.value; + + return serverDeferred; +} + +function getCurrentHostname(pageData) { + if (serverIsNntp(pageData)) + return pageData.newsserver.hostname.value; + else + return pageData.server.hostname.value; +} + +function GetPageData() +{ + if (!gPageData) + gPageData = new Object; + + return gPageData; +} + +function PrefillAccountForIsp(ispName) +{ + dump("AccountWizard.prefillAccountForIsp(" + ispName + ")\n"); + + var ispData = getIspDefaultsForUri(ispName); + + var pageData = GetPageData(); + + if (!ispData) { + SetCurrentAccountData(null); + return; + } + + // prefill the rest of the wizard + dump("PrefillAccountForISP: filling with " + ispData + "\n"); + SetCurrentAccountData(ispData); + AccountDataToPageData(ispData, pageData); + + setPageData(pageData, "ispdata", "supplied", true); +} + +// does any cleanup work for the the account data +// - sets the username from the email address if it's not already set +// - anything else? +function FixupAccountDataForIsp(accountData) +{ + // no fixup for news + // setting the username does bad things + // see bugs #42105 and #154213 + if (accountData.incomingServer.type == "nntp") + return; + + var email = accountData.identity.email; + + // The identity might not have an email address, which is what the rest of + // this function is looking for. + if (!email) + return; + + // fix up the username + if (!accountData.incomingServer.username) + accountData.incomingServer.username = + getUsernameFromEmail(email, accountData.incomingServerUserNameRequiresDomain); + + if (!accountData.smtp.username && + accountData.smtpRequiresUsername) { + // fix for bug #107953 + // if incoming hostname is same as smtp hostname + // use the server username (instead of the email username) + if (accountData.smtp.hostname == accountData.incomingServer.hostName && + accountData.smtpUserNameRequiresDomain == accountData.incomingServerUserNameRequiresDomain) + accountData.smtp.username = accountData.incomingServer.username; + else + accountData.smtp.username = getUsernameFromEmail(email, accountData.smtpUserNameRequiresDomain); + } +} + +function SetCurrentAccountData(accountData) +{ + // dump("Setting current account data (" + gCurrentAccountData + ") to " + accountData + "\n"); + gCurrentAccountData = accountData; +} + +// flush the XUL cache - just for debugging purposes - not called +function onFlush() { + Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", true); + Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", false); +} + +/** If there are no default accounts.. + * this is will be the new default, so enable + * check for mail at startup + */ +function EnableCheckMailAtStartUpIfNeeded(newAccount) +{ + // Check if default account existed. + // If no such account, make this one the default account + // and turn on the new mail check at startup for the current account + if (!gDefaultAccount) { + MailServices.accounts.defaultAccount = newAccount; + newAccount.incomingServer.loginAtStartUp = true; + newAccount.incomingServer.downloadOnBiff = true; + } +} + +function SetSmtpRequiresUsernameAttribute(accountData) +{ + // If this is the default server, time to set the smtp user name + // Set the generic attribute for requiring user name for smtp to true. + // ISPs can override the pref via rdf files. + if (!gDefaultAccount) { + accountData.smtpRequiresUsername = true; + } +} + +function setNextPage(currentPageId, nextPageId) { + var currentPage = document.getElementById(currentPageId); + currentPage.next = nextPageId; +} diff --git a/mailnews/base/prefs/content/AccountWizard.xul b/mailnews/base/prefs/content/AccountWizard.xul new file mode 100644 index 000000000..2aee2e3a5 --- /dev/null +++ b/mailnews/base/prefs/content/AccountWizard.xul @@ -0,0 +1,371 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountWizard.css" type="text/css"?> + +<!DOCTYPE wizard SYSTEM "chrome://messenger/locale/AccountWizard.dtd"> + +<wizard id="AccountWizard" title="&windowTitle.label;" + onwizardcancel="return onCancel();" + onwizardfinish="return FinishAccount();" +#if defined(MOZ_THUNDERBIRD) && defined(HYPE_ICEDOVE) + onload="onAccountWizardLoad(); initAccountWizardTB(window.arguments);" +#else + onload="onAccountWizardLoad();" +#endif + style="&accountWizard.size;" + xmlns:nc="http://home.netscape.com/NC-rdf#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <script type="application/javascript" src="chrome://messenger/content/accountUtils.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + <script type="application/javascript" src="chrome://messenger/content/AccountWizard.js"/> + <script type="application/javascript" src="chrome://messenger/content/ispUtils.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-accounttype.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-identity.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-incoming.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-outgoing.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-accname.js"/> + <script type="application/javascript" src="chrome://messenger/content/aw-done.js"/> + + <!-- Account Type page : Displays choices of mail and news accounts that user can create --> + <wizardpage id="accounttype" pageid="accounttype" + label="&accountTypeTitle.label;" + onpageshow="document.documentElement.canAdvance = true;" + onpageadvanced="return acctTypePageUnload();"> + <vbox flex="1"> + <description>&accountSetupInfo2.label;</description> + <description>&accountTypeDesc2.label;</description> + <label control="acctyperadio">&accountTypeDirections.label;</label> + <separator/> + <radiogroup id="acctyperadio" > +#if !defined(MOZ_THUNDERBIRD) || !defined(HYPE_ICEDOVE) + <radio id="mailaccount" value="mailaccount" + label="&accountTypeMail.label;" accesskey="&accountTypeMail.accesskey;" + selected="true"/> +#endif + <vbox datasources="rdf:ispdefaults" + containment="http://home.netscape.com/NC-rdf#providers" + id="ispBox" + ref="NC:ispinfo"> + <template> + <rule nc:wizardShow="true"> + <radio uri="..." + value="rdf:http://home.netscape.com/NC-rdf#wizardShortName" + label="rdf:http://home.netscape.com/NC-rdf#wizardLongName" + accesskey="rdf:http://home.netscape.com/NC-rdf#wizardLongNameAccesskey"/> + </rule> + </template> + </vbox> + <radio id="newsaccount" value="newsaccount" + label="&accountTypeNews.label;" accesskey="&accountTypeNews.accesskey;"/> + </radiogroup> + </vbox> + </wizardpage> + + <!-- Identity page : Collects user's full name and email address --> + <wizardpage id="identitypage" pageid="identitypage" + label="&identityTitle.label;" + onpageshow="return identityPageInit();" + onpageadvanced="return identityPageUnload();"> + <vbox flex="1"> + <description>&identityDesc.label;</description> + <separator/> + <description>&fullnameDesc.label; &fullnameExample.label;</description> + <separator class="thin"/> + <hbox align="center"> + <label class="awIdentityLabel" value="&fullnameLabel.label;" + accesskey="&fullnameLabel.accesskey;" control="fullName"/> + <textbox mailtype="identity" wsm_persist="true" name="fullName" id="fullName" flex="1" oninput="identityPageValidate();"/> + </hbox> + <separator/> + <grid> + <columns> + <column flex="1"/> + </columns> + <rows> + <row> + <description id="emailDescText"/> + </row> + <separator class="thin"/> + <row> + <hbox align="center"> + <label class="awIdentityLabel" id="emailFieldLabel" value="&emailLabel.label;" + accesskey="&emailLabel.accesskey;" control="email"/> + <hbox class="uri-element" align="center" flex="1"> + <textbox wsm_persist="true" mailtype="identity" name="email" + oninput="identityPageValidate();" + id="email" flex="6" class="uri-element"/> + <label id="postEmailText"/> + </hbox> + </hbox> + </row> + </rows> + </grid> + </vbox> + </wizardpage> + + <!-- Incoming page : User can choose to create mail account of his choice, POP3 or IMAP --> + <!-- Collects incoming server name and login name. --> + <!-- Login name is prefilled with user id from the email address provided in identity page --> + <!-- User can enter a login name here if it is different from the user id of his/her email address --> + <wizardpage id="incomingpage" pageid="incomingpage" + label="&incomingTitle.label;" + onpageshow="return incomingPageInit();" + onpageadvanced="return incomingPageUnload();"> + <vbox flex="1"> + <vbox id="serverTypeBox"> + <label control="servertype">&incomingServerTypeDesc.label;</label> + <separator class="thin"/> + <hbox align="center" class="serverDataBox"> + <!-- The initial value for the servertype radiogroup is set in onInit() --> + <radiogroup id="servertype" wsm_persist="true" orient="horizontal"> + <radio group="servertype" value="pop3" id="pop3" label="&popType.label;" + wsm_persist="true" oncommand="setServerType();" accesskey="&popType.accesskey;"/> + <radio group="servertype" value="imap" id="imap" label="&imapType.label;" + wsm_persist="true" oncommand="setServerType();" accesskey="&imapType.accesskey;"/> + </radiogroup> + <label id="serverPortLabel" control="serverPort" + accesskey="&portNum.accesskey;" + value="&portNum.label;"/> + <textbox id="serverPort" type="number" size="3" max="65535"/> + <label id="defaultPortLabel" value="&defaultPortLabel.label;"/> + <label id="defaultPortValue" value="&defaultPortValue.label;"/> + </hbox> + <separator/> + </vbox> + + <vbox id="incomingServerbox"> + <description>&incomingServer.description;</description> + <hbox align="center" class="serverDataBox"> + <label class="label, serverLabel" + value="&incomingServer.label;" + accesskey="&incomingServer.accesskey;" + control="incomingServer"/> + <textbox wsm_persist="true" + id="incomingServer" + flex="1" + class="uri-element" + oninput="incomingPageValidate();"/> + </hbox> + <hbox id="leaveMsgsOnSrvrBox" class="indent"> + <checkbox id="leaveMessagesOnServer" + label="&leaveMsgsOnSrvr.label;" + accesskey="&leaveMsgsOnSrvr.accesskey;" + wsm_persist="true" + oncommand="setServerPrefs(this);" + checked="true"/> + </hbox> + <separator/> + </vbox> + <description>&incomingUsername.description;</description> + <separator class="thin"/> + <hbox align="center"> + <label class="label" + value="&incomingUsername.label;" + style="width: 8em;" + accesskey="&incomingUsername.accesskey;" + control="username"/> + <textbox id="username" + wsm_persist="true" + flex="1" + oninput="incomingPageValidate();"/> + </hbox> + <vbox id="deferStorageBox"> + <separator class="groove"/> + <description> &deferStorageDesc.label;</description> + <hbox> + <checkbox id="deferStorage" + label="&deferStorage.label;" + accesskey="&deferStorage.accesskey;" + checked="true" + wsm_persist="true" + oncommand="setServerPrefs(this);"/> + </hbox> + </vbox> + </vbox> + </wizardpage> + + <!-- Outgoing page : Collects outgoing server name and login name. --> + <!-- Outgoing server name is collected if there isn't one already --> + <!-- Login name is prefilled with user id from the email address provided in identity page --> + <!-- User can enter a login name here if it is different from the user id of his/her email address --> + <wizardpage id="outgoingpage" pageid="outgoingpage" + label="&outgoingTitle.label;" + onpageshow="return outgoingPageInit();" + onpageadvanced="return outgoingPageUnload();"> + <vbox flex="1"> + <vbox id="noSmtp"> + <description>&outgoingServer.description;</description> + <hbox align="center" class="serverDataBox"> + <label class="label, serverLabel" + value="&outgoingServer.label;" + accesskey="&outgoingServer.accesskey;" + control="smtphostname"/> + <textbox id="smtphostname" + wsm_persist="true" + flex="1" + class="uri-element" + oninput="outgoingPageValidate();"/> + </hbox> + </vbox> + + <vbox id="haveSmtp"> + <description id="smtpStaticText1" + style="width: 200px;" + prefix="&haveSmtp1.prefix;" + suffix="&haveSmtp1.suffix3; &modifyOutgoing.suffix;">*</description> + </vbox> + + <vbox id="loginSet1"> + <description>&outgoingUsername.description;</description> + <separator class="thin"/> + <hbox align="center"> + <label class="label" + value="&outgoingUsername.label;" + style="width: 8em;" + accesskey="&outgoingUsername.accesskey;" + control="smtpusername"/> + <textbox id="smtpusername" wsm_persist="true" flex="1"/> + </hbox> + </vbox> + + <vbox id="loginSet2" hidden="true"> + <description id="smtpStaticText2" style="width: 200px;" prefix="&haveSmtp2.prefix;" + suffix="&haveSmtp2.suffix3; &modifyOutgoing.suffix;">*</description> + </vbox> + + <vbox id="loginSet3" hidden="true"> + <description id="smtpStaticText3" style="width: 200px;" prefix="&haveSmtp3.prefix;" + suffix="&haveSmtp3.suffix3; &modifyOutgoing.suffix;">*</description> + </vbox> + + </vbox> + </wizardpage> + + <!-- News Server page : Collects the News groups server name --> + <wizardpage id="newsserver" pageid="newsserver" + label="&incomingTitle.label;" + onpageshow="return incomingPageInit();" + onpageadvanced="return incomingPageUnload();"> + <vbox flex="1"> + <description>&newsServerNameDesc.label;</description> + <separator class="thin"/> + <hbox align="center"> + <label control="newsServer" value="&newsServerLabel.label;" accesskey="&newsServerLabel.accesskey;" style="width: 8em;"/> + <textbox id="newsServer" + wsm_persist="true" + flex="1" + class="uri-element" + oninput="incomingPageValidate();"/> + </hbox> + </vbox> + </wizardpage> + + <!-- Account name page : User gets a choice to enter a pretty name for the account --> + <!-- Defaults : Mail account -> Email address, Newsgroup account -> Newsgroup server name --> + <wizardpage id="accnamepage" pageid="accnamepage" + label="&accnameTitle.label;" + onpageshow="return acctNamePageInit();" + onpageadvanced="return acctNamePageUnload();"> + <vbox flex="1"> + <description>&accnameDesc.label;</description> + <separator class="thin"/> + <hbox align="center"> + <label class="label" value="&accnameLabel.label;" style="width: 8em;" + accesskey="&accnameLabel.accesskey;" control="prettyName"/> + <textbox id="prettyName" size="40" wsm_persist="true" flex="1" oninput="acctNamePageValidate();"/> + </hbox> + </vbox> + </wizardpage> + + <!-- Done page : this page summarizes information collected to create a mail/news account --> + <wizardpage id="done" pageid="done" + label="&completionTitle.label;" + onpageshow="return donePageInit();"> + <vbox flex="1"> + <description>&completionText.label;</description> + <separator class="thin"/> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center" id="account.name"> + <label class="label" flex="1" id="account.name.label" value="&accnameLabel.label;"/> + <label class="label" id="account.name.text"/> + </row> + <row align="center" id="identity.email"> + <label class="label" flex="1" id="identity.email.label" value="&emailLabel.label;"/> + <label class="label" id="identity.email.text"/> + </row> + <row align="center" id="server.username"> + <label class="label" flex="1" id="server.username.label" value="&incomingUsername.label;"/> + <label class="label" id="server.username.text"/> + </row> + <row align="center" id="server.name"> + <label class="label" flex="1" id="server.name.label" value="&serverNamePrefix.label;"/> + <label class="label" id="server.name.text"/> + </row> + <row align="center" id="server.type"> + <label class="label" flex="1" id="server.type.label" value="&serverTypePrefix.label;"/> + <label class="label" id="server.type.text"/> + </row> + <row align="center" id="server.port"> + <label class="label" id="server.port.label" flex="1" value="&portNum.label;"/> + <label class="label" id="server.port.text"/> + </row> + <row align="center" id="newsServer.name"> + <label class="label" flex="1" id="newsServer.name.label" value="&newsServerNamePrefix.label;"/> + <label class="label" id="newsServer.name.text"/> + </row> + <row align="center" id="smtpServer.username"> + <label class="label" flex="1" id="smtpServer.username.label" value="&outgoingUsername.label;"/> + <label class="label" id="smtpServer.username.text"/> + </row> + <row align="center" id="smtpServer.name"> + <label class="label" flex="1" id="smtpServer.name.label" value="&smtpServerNamePrefix.label;"/> + <label class="label" id="smtpServer.name.text"/> + </row> + </rows> + </grid> + <separator/> + <hbox id="downloadMsgsBox"> + <checkbox id="downloadMsgs" + label="&downloadOnLogin.label;" + accesskey="&downloadOnLogin.accesskey;" + hidden="true" + checked="true"/> + </hbox> + <spacer flex="1"/> +#ifndef XP_MACOSX + <description>&clickFinish.label;</description> +#else + <description>&clickFinish.labelMac;</description> +#endif + </vbox> + </wizardpage> + + <wizardpage id="ispPage1"/> + <wizardpage id="ispPage2"/> + <wizardpage id="ispPage3"/> + <wizardpage id="ispPage4"/> + <wizardpage id="ispPage5"/> + <wizardpage id="ispPage6"/> + <wizardpage id="ispPage7"/> + <wizardpage id="ispPage8"/> + <wizardpage id="ispPage9"/> + <wizardpage id="ispPage10"/> + <wizardpage id="ispPage11"/> + <wizardpage id="ispPage12"/> + <wizardpage id="ispPage13"/> + <wizardpage id="ispPage14"/> + <wizardpage id="ispPage15"/> + <wizardpage id="ispPage16"/> + +</wizard> diff --git a/mailnews/base/prefs/content/SmtpServerEdit.js b/mailnews/base/prefs/content/SmtpServerEdit.js new file mode 100644 index 000000000..f84bbd00b --- /dev/null +++ b/mailnews/base/prefs/content/SmtpServerEdit.js @@ -0,0 +1,46 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/hostnameUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +var gSmtpServer; + +function onLoad(event) +{ + gSmtpServer = window.arguments[0].server; + initSmtpSettings(gSmtpServer); +} + +function onAccept() +{ + if (!isLegalHostNameOrIP(cleanUpHostName(gSmtpHostname.value))) { + let prefsBundle = document.getElementById("bundle_prefs"); + let brandBundle = document.getElementById("bundle_brand"); + let alertTitle = brandBundle.getString("brandShortName"); + let alertMsg = prefsBundle.getString("enterValidServerName"); + Services.prompt.alert(window, alertTitle, alertMsg); + + window.arguments[0].result = false; + return false; + } + + // If we didn't have an SMTP server to initialize with, + // we must be creating one. + try { + if (!gSmtpServer) { + gSmtpServer = MailServices.smtp.createServer(); + window.arguments[0].addSmtpServer = gSmtpServer.key; + } + + saveSmtpSettings(gSmtpServer); + } catch (ex) { + Components.utils.reportError("Error saving smtp server: " + ex); + } + + window.arguments[0].result = true; + return true; +} diff --git a/mailnews/base/prefs/content/SmtpServerEdit.xul b/mailnews/base/prefs/content/SmtpServerEdit.xul new file mode 100644 index 000000000..767020d4d --- /dev/null +++ b/mailnews/base/prefs/content/SmtpServerEdit.xul @@ -0,0 +1,28 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xul-overlay href="chrome://messenger/content/smtpEditOverlay.xul"?> + +<!-- 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/. --> + +<?xml-stylesheet href="chrome://messenger/skin/dialogs.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/smtpEditOverlay.dtd"> + +<dialog title="&smtpEditTitle.label;" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + ondialogaccept="return onAccept();"> + <stringbundle id="bundle_prefs" + src="chrome://messenger/locale/prefs.properties"/> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_messenger" + src="chrome://messenger/locale/messenger.properties"/> + <script type="application/javascript" + src="chrome://messenger/content/SmtpServerEdit.js"/> + <vbox id="smtpServerEditor"/> +</dialog> diff --git a/mailnews/base/prefs/content/accountUtils.js b/mailnews/base/prefs/content/accountUtils.js new file mode 100644 index 000000000..8b5020915 --- /dev/null +++ b/mailnews/base/prefs/content/accountUtils.js @@ -0,0 +1,462 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +var gAnyValidIdentity = false; //If there are no valid identities for any account +// returns the first account with an invalid server or identity + +var gNewAccountToLoad = null; // used to load new messages if we come from the mail3pane + +function getInvalidAccounts(accounts) +{ + let numAccounts = accounts.length; + let invalidAccounts = new Array; + let numIdentities = 0; + for (let i = 0; i < numAccounts; i++) { + let account = accounts.queryElementAt(i, Components.interfaces.nsIMsgAccount); + try { + if (!account.incomingServer.valid) { + invalidAccounts[invalidAccounts.length] = account; + // skip to the next account + continue; + } + } catch (ex) { + // this account is busted, just keep going + continue; + } + + var identities = account.identities; + numIdentities = identities.length; + + for (var j = 0; j < numIdentities; j++) { + let identity = identities.queryElementAt(j, Components.interfaces.nsIMsgIdentity); + if (identity.valid) { + gAnyValidIdentity = true; + } + else { + invalidAccounts[invalidAccounts.length] = account; + } + } + } + return invalidAccounts; +} + +function showMailIntegrationDialog() { + const nsIShellService = Components.interfaces.nsIShellService; + + try { + var shellService = Components.classes["@mozilla.org/suite/shell-service;1"] + .getService(nsIShellService); + var appTypesCheck = shellService.shouldBeDefaultClientFor & + (nsIShellService.MAIL | nsIShellService.NEWS); + + // show the default client dialog only if we have at least one account, + // if we should check for the default client, and we want to check if we are + // the default for mail/news and are not the default client for mail/news + if (appTypesCheck && shellService.shouldCheckDefaultClient && + !shellService.isDefaultClient(true, appTypesCheck)) + window.openDialog("chrome://communicator/content/defaultClientDialog.xul", + "DefaultClient", "modal,centerscreen,chrome,resizable=no"); + } catch (ex) {} +} + +/** + * Verify that there is at least one account. If not, open a new account wizard. + * + * @param wizardCallback if the wizard is run, callback when it is done. + * @param needsIdentity True only when verifyAccounts is called from the + * compose window. This last condition is so that we open + * the account wizard if the user does not have any + * identities defined and tries to compose mail. + * @param wizardOpen optional param that allows the caller to specify a + * different method to open a wizard. The wizardOpen method + * takes wizardCallback as an argument. The wizardCallback + * doesn't take any arguments. + */ +function verifyAccounts(wizardCallback, needsIdentity, wizardOpen) +{ + var openWizard = false; + var prefillAccount; + var state=true; + var ret = true; + + try { + // migrate quoting preferences from global to per account. This function returns + // true if it had to migrate, which we will use to mean this is a just migrated + // or new profile + var newProfile = migrateGlobalQuotingPrefs(MailServices.accounts.allIdentities); + + var accounts = MailServices.accounts.accounts; + + // as long as we have some accounts, we're fine. + var accountCount = accounts.length; + var invalidAccounts = getInvalidAccounts(accounts); + if (invalidAccounts.length > 0 && invalidAccounts.length == accountCount) { + prefillAccount = invalidAccounts[0]; + } + + // if there are no accounts, or all accounts are "invalid" + // then kick off the account migration. Or if this is a new (to Mozilla) profile. + // MCD can set up accounts without the profile being used yet + if (newProfile) { + // check if MCD is configured. If not, say this is not a new profile + // so that we don't accidentally remigrate non MCD profiles. + var adminUrl; + try { + adminUrl = Services.prefs.getCharPref("autoadmin.global_config_url"); + } + catch (ex) {} + if (!adminUrl) + newProfile = false; + } + if ((newProfile && !accountCount) || accountCount == invalidAccounts.length) + openWizard = true; + + // openWizard is true if messenger migration returns some kind of + // error (including those cases where there is nothing to migrate). + // prefillAccount is non-null if there is at least one invalid account. + // gAnyValidIdentity is true when you've got at least one *valid* + // identity. Since local and RSS folders are identity-less accounts, if you + // only have one of those, it will be false. + // needsIdentity is true only when verifyAccounts is called from the + // compose window. This last condition is so that we open the account + // wizard if the user does not have any identities defined and tries to + // compose mail. + + if (openWizard || prefillAccount || ((!gAnyValidIdentity) && needsIdentity)) + { + if (wizardOpen != undefined) + wizardOpen(wizardCallback) + else + MsgAccountWizard(wizardCallback); + ret = false; + } + else + { + var localFoldersExists; + try + { + localFoldersExists = MailServices.accounts.localFoldersServer; + } + catch (ex) + { + localFoldersExists = false; + } + + // we didn't create the MsgAccountWizard - we need to verify that local folders exists. + if (!localFoldersExists) + MailServices.accounts.createLocalMailAccount(); + } + + // This will do nothing on platforms without a shell service + const NS_SHELLSERVICE_CID = "@mozilla.org/suite/shell-service;1" + if (NS_SHELLSERVICE_CID in Components.classes) + { + // hack, set a time out to do this, so that the window can load first + setTimeout(showMailIntegrationDialog, 0); + } + return ret; + } + catch (ex) { + dump("error verifying accounts " + ex + "\n"); + return false; + } +} + +// we do this from a timer because if this is called from the onload= +// handler, then the parent window doesn't appear until after the wizard +// has closed, and this is confusing to the user +function MsgAccountWizard(wizardCallback) +{ + setTimeout(function() { msgOpenAccountWizard(wizardCallback); }, 0); +} + +/** + * Open the Old Mail Account Wizard, or focus it if it's already open. + * + * @param wizardCallback if the wizard is run, callback when it is done. + * @param type - optional account type token, for Tb. + * @see msgNewMailAccount below for the new implementation. + */ +function msgOpenAccountWizard(wizardCallback, type) +{ + gNewAccountToLoad = null; + + window.openDialog("chrome://messenger/content/AccountWizard.xul", "AccountWizard", + "chrome,modal,titlebar,centerscreen", + {okCallback: wizardCallback, acctType: type}); + + loadInboxForNewAccount(); + + // If we started with no servers at all and "smtp servers" list selected, + // refresh display somehow. Bug 58506. + // TODO Better fix: select newly created account (in all cases) + if (typeof(getCurrentAccount) == "function" && // in AccountManager, not menu + !getCurrentAccount()) + selectServer(null, null); +} + +function initAccountWizardTB(args) { + let type = args[0] && args[0].acctType; + let selType = type == "newsgroups" ? "newsaccount" : + type == "movemail" ? "Movemail" : null; + let accountwizard = document.getElementById("AccountWizard"); + let acctyperadio = document.getElementById("acctyperadio"); + let feedRadio = acctyperadio.querySelector("radio[value='Feeds']"); + if (feedRadio) + feedRadio.remove(); + if (selType) { + acctyperadio.selectedItem = acctyperadio.querySelector("radio[value='"+selType+"']"); + accountwizard.advance("identitypage"); + } + else + acctyperadio.selectedItem = acctyperadio.getItemAtIndex(0); +} + +function AddFeedAccount() { + window.openDialog("chrome://messenger-newsblog/content/feedAccountWizard.xul", + "", "chrome,modal,titlebar,centerscreen"); +} + +/** + * Opens the account settings window on the specified account + * and page of settings. If the window is already open it is only focused. + * + * @param selectPage The xul file name for the viewing page or + * null for the account main page. Other pages are + * 'am-server.xul', 'am-copies.xul', 'am-offline.xul', + * 'am-addressing.xul', 'am-smtp.xul' + * @param aServer The server of the account to select. Optional. + */ +function MsgAccountManager(selectPage, aServer) +{ + var existingAccountManager = Services.wm.getMostRecentWindow("mailnews:accountmanager"); + + if (existingAccountManager) + existingAccountManager.focus(); + else { + if (!aServer) { + if (typeof GetSelectedMsgFolders === "function") { + let folders = GetSelectedMsgFolders(); + if (folders.length > 0) + aServer = folders[0].server; + } + if (!aServer && (typeof GetDefaultAccountRootFolder === "function")) { + let folder = GetDefaultAccountRootFolder(); + if (folder instanceof Components.interfaces.nsIMsgFolder) + aServer = folder.server; + } + } + + window.openDialog("chrome://messenger/content/AccountManager.xul", + "AccountManager", + "chrome,centerscreen,modal,titlebar,resizable", + { server: aServer, selectPage: selectPage }); + } +} + +function loadInboxForNewAccount() +{ + // gNewAccountToLoad is set in the final screen of the Account Wizard if a POP account + // was created, the download messages box is checked, and the wizard was opened from the 3pane + if (gNewAccountToLoad) { + var rootMsgFolder = gNewAccountToLoad.incomingServer.rootMsgFolder; + const kInboxFlag = Components.interfaces.nsMsgFolderFlags.Inbox; + var inboxFolder = rootMsgFolder.getFolderWithFlags(kInboxFlag); + SelectFolder(inboxFolder.URI); + window.focus(); + setTimeout(MsgGetMessage, 0); + gNewAccountToLoad = null; + } +} + +// returns true if we migrated - it knows this because 4.x did not have the +// pref mailnews.quotingPrefs.version, so if it's not set, we're either +// migrating from 4.x, or a much older version of Mozilla. +function migrateGlobalQuotingPrefs(allIdentities) +{ + // if reply_on_top and auto_quote exist then, if non-default + // migrate and delete, if default just delete. + var reply_on_top = 0; + var auto_quote = true; + var quotingPrefs = 0; + var migrated = false; + try { + quotingPrefs = Services.prefs.getIntPref("mailnews.quotingPrefs.version"); + } catch (ex) {} + + // If the quotingPrefs version is 0 then we need to migrate our preferences + if (quotingPrefs == 0) { + migrated = true; + try { + reply_on_top = Services.prefs.getIntPref("mailnews.reply_on_top"); + auto_quote = Services.prefs.getBoolPref("mail.auto_quote"); + } catch (ex) {} + + if (!auto_quote || reply_on_top) { + let numIdentities = allIdentities.length; + var identity = null; + for (var j = 0; j < numIdentities; j++) { + identity = allIdentities.queryElementAt(j, Components.interfaces.nsIMsgIdentity); + if (identity.valid) { + identity.autoQuote = auto_quote; + identity.replyOnTop = reply_on_top; + } + } + } + Services.prefs.setIntPref("mailnews.quotingPrefs.version", 1); + } + return migrated; +} + +// we do this from a timer because if this is called from the onload= +// handler, then the parent window doesn't appear until after the wizard +// has closed, and this is confusing to the user +function NewMailAccount(msgWindow, okCallback, extraData) +{ + if (!msgWindow) + throw new Error("NewMailAccount must be given a msgWindow."); + + // Populate the extra data. + if (!extraData) + extraData = {}; + extraData.msgWindow = msgWindow; + + let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane"); + + if (!extraData.NewMailAccount) + extraData.NewMailAccount = NewMailAccount; + + if (!extraData.msgNewMailAccount) + extraData.msgNewMailAccount = msgNewMailAccount; + + if (!extraData.NewComposeMessage) + extraData.NewComposeMessage = mail3Pane.ComposeMessage; + + if (!extraData.openAddonsMgr) + extraData.openAddonsMgr = mail3Pane.openAddonsMgr; + + if (!extraData.okCallback) + extraData.okCallback = null; + + if (!extraData.success) + extraData.success = false; + + setTimeout(extraData.msgNewMailAccount, 0, msgWindow, okCallback, extraData); +} + +function NewMailAccountProvisioner(aMsgWindow, args) { + if (!args) + args = {}; + if (!aMsgWindow) + aMsgWindow = MailServices.mailSession.topmostMsgWindow; + + args.msgWindow = aMsgWindow; + + let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane"); + + // If we couldn't find a 3pane, bail out. + if (!mail3Pane) { + Components.utils.reportError("Could not find a 3pane to connect to."); + return; + } + + let tabmail = mail3Pane.document.getElementById("tabmail"); + + if (!tabmail) { + Components.utils.reportError("Could not find a tabmail in the 3pane!"); + return; + } + + // If there's already an accountProvisionerTab open, just focus it instead + // of opening a new dialog. + let apTab = tabmail.getTabInfoForCurrentOrFirstModeInstance( + tabmail.tabModes["accountProvisionerTab"]); + + if (apTab) { + tabmail.switchToTab(apTab); + return; + } + + // XXX make sure these are all defined in all contexts... to be on the safe + // side, just get a mail:3pane and borrow the functions from it? + if (!args.NewMailAccount) + args.NewMailAccount = NewMailAccount; + + if (!args.msgNewMailAccount) + args.msgNewMailAccount = msgNewMailAccount; + + if (!args.NewComposeMessage) + args.NewComposeMessage = mail3Pane.ComposeMessage; + + if (!args.openAddonsMgr) + args.openAddonsMgr = mail3Pane.openAddonsMgr; + + if (!args.okCallback) + args.okCallback = null; + + let windowParams = "chrome,titlebar,centerscreen,width=640,height=480"; + + if (!args.success) { + args.success = false; + // If we're not opening up the success dialog, then our window should be + // modal. + windowParams = "modal," + windowParams; + } + + // NOTE: If you're a developer, and you notice that the jQuery code in + // accountProvisioner.xhtml isn't throwing errors or warnings, that's due + // to bug 688273. Just make the window non-modal to get those errors and + // warnings back, and then clear this comment when bug 688273 is closed. + window.openDialog( + "chrome://messenger/content/newmailaccount/accountProvisioner.xhtml", + "AccountCreation", + windowParams, + args); +} + +/** + * Open the New Mail Account Wizard, or focus it if it's already open. + * + * @param msgWindow a msgWindow for us to use to verify the accounts. + * @param okCallback an optional callback for us to call back to if + * everything's okay. + * @param extraData an optional param that allows us to pass data in and + * out. Used in the upcoming AccountProvisioner add-on. + * @see msgOpenAccountWizard above for the previous implementation. + */ +function msgNewMailAccount(msgWindow, okCallback, extraData) +{ + if (!msgWindow) + throw new Error("msgNewMailAccount must be given a msgWindow."); + + let existingWindow = Services.wm.getMostRecentWindow("mail:autoconfig"); + if (existingWindow) { + existingWindow.focus(); + } else { + // disabling modal for the time being, see 688273 REMOVEME + window.openDialog("chrome://messenger/content/accountcreation/emailWizard.xul", + "AccountSetup", "chrome,titlebar,centerscreen", + {msgWindow:msgWindow, + okCallback:okCallback, + extraData:extraData}); + } + + /* + // TODO: Enable this block of code once the dialog above is made modal. + // If we started with no servers at all and "smtp servers" list selected, + // refresh display somehow. Bug 58506. + // TODO Better fix: select newly created account (in all cases) + let existingAccountManager = + Services.wm.getMostRecentWindow("mailnews:accountmanager"); + // in AccountManager, not menu + if (existingAccountManager && typeof(getCurrentAccount) == "function" && + !getCurrentAccount()) { + selectServer(null, null); + } + */ +} diff --git a/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js new file mode 100644 index 000000000..9f4c5034e --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; 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/. */ + +/** + * This class implements nsIBadCertListener. It's job is to prevent "bad cert" + * security dialogs from being shown to the user. We call back to the + * 'callback' object's method "processCertError" so that it can deal with it as + * needed (in the case of autoconfig, setting up temporary overrides). + */ +function BadCertHandler(callback) +{ + this._init(callback); +} + +BadCertHandler.prototype = +{ + _init: function(callback) { + this._callback = callback; + }, + + // Suppress any certificate errors + notifyCertProblem: function(socketInfo, status, targetSite) { + return this._callback.processCertError(socketInfo, status, targetSite); + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Components.interfaces.nsIBadCertListener2) && + !iid.equals(Components.interfaces.nsIInterfaceRequestor) && + !iid.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + } +}; diff --git a/mailnews/base/prefs/content/accountcreation/accountConfig.js b/mailnews/base/prefs/content/accountcreation/accountConfig.js new file mode 100644 index 000000000..752c72624 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/accountConfig.js @@ -0,0 +1,261 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * This file creates the class AccountConfig, which is a JS object that holds + * a configuration for a certain account. It is *not* created in the backend + * yet (use aw-createAccount.js for that), and it may be incomplete. + * + * Several AccountConfig objects may co-exist, e.g. for autoconfig. + * One AccountConfig object is used to prefill and read the widgets + * in the Wizard UI. + * When we autoconfigure, we autoconfig writes the values into a + * new object and returns that, and the caller can copy these + * values into the object used by the UI. + * + * See also + * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat> + * for values stored. + */ + +function AccountConfig() +{ + this.incoming = this.createNewIncoming(); + this.incomingAlternatives = []; + this.outgoing = this.createNewOutgoing(); + this.outgoingAlternatives = []; + this.identity = + { + // displayed real name of user + realname : "%REALNAME%", + // email address of user, as shown in From of outgoing mails + emailAddress : "%EMAILADDRESS%", + }; + this.inputFields = []; + this.domains = []; +}; +AccountConfig.prototype = +{ + // @see createNewIncoming() + incoming : null, + // @see createNewOutgoing() + outgoing : null, + /** + * Other servers which can be used instead of |incoming|, + * in order of decreasing preference. + * (|incoming| itself should not be included here.) + * { Array of incoming/createNewIncoming() } + */ + incomingAlternatives : null, + outgoingAlternatives : null, +#ifdef MOZ_MAILNEWS_OAUTH2 + // OAuth2 configuration, if needed. + oauthSettings : null, +#endif + // just an internal string to refer to this. Do not show to user. + id : null, + // who created the config. + // { one of kSource* } + source : 0, + displayName : null, + // { Array of { varname (value without %), displayName, exampleValue } } + inputFields : null, + // email address domains for which this config is applicable + // { Array of Strings } + domains : null, + + /** + * Factory function for incoming and incomingAlternatives + */ + createNewIncoming : function() + { + return { + // { String-enum: "pop3", "imap", "nntp" } + type : null, + hostname : null, + // { Integer } + port : null, + // May be a placeholder (starts and ends with %). { String } + username : null, + password : null, + // { enum: 1 = plain, 2 = SSL/TLS, 3 = STARTTLS always, 0 = not inited } + // ('TLS when available' is insecure and not supported here) + socketType : 0, + /** + * true when the cert is invalid (and thus SSL useless), because it's + * 1) not from an accepted CA (including self-signed certs) + * 2) for a different hostname or + * 3) expired. + * May go back to false when user explicitly accepted the cert. + */ + badCert : false, + /** + * How to log in to the server: plaintext or encrypted pw, GSSAPI etc. + * Defined by Ci.nsMsgAuthMethod + * Same as server pref "authMethod". + */ + auth : 0, + /** + * Other auth methods that we think the server supports. + * They are ordered by descreasing preference. + * (|auth| itself is not included in |authAlternatives|) + * {Array of Ci.nsMsgAuthMethod} (same as .auth) + */ + authAlternatives : null, + // in minutes { Integer } + checkInterval : 10, + loginAtStartup : true, + // POP3 only: + // Not yet implemented. { Boolean } + useGlobalInbox : false, + leaveMessagesOnServer : true, + daysToLeaveMessagesOnServer : 14, + deleteByAgeFromServer : true, + // When user hits delete, delete from local store and from server + deleteOnServerWhenLocalDelete : true, + downloadOnBiff : true, + }; + }, + /** + * Factory function for outgoing and outgoingAlternatives + */ + createNewOutgoing : function() + { + return { + type : "smtp", + hostname : null, + port : null, // see incoming + username : null, // see incoming. may be null, if auth is 0. + password : null, // see incoming. may be null, if auth is 0. + socketType : 0, // see incoming + badCert : false, // see incoming + auth : 0, // see incoming + authAlternatives : null, // see incoming + addThisServer : true, // if we already have an SMTP server, add this + // if we already have an SMTP server, use it. + useGlobalPreferredServer : false, + // we should reuse an already configured SMTP server. + // nsISmtpServer.key + existingServerKey : null, + // user display value for existingServerKey + existingServerLabel : null, + }; + }, + + /** + * Returns a deep copy of this object, + * i.e. modifying the copy will not affect the original object. + */ + copy : function() + { + // Workaround: deepCopy() fails to preserve base obj (instanceof) + var result = new AccountConfig(); + for (var prop in this) + result[prop] = deepCopy(this[prop]); + + return result; + }, + isComplete : function() + { + return (!!this.incoming.hostname && !!this.incoming.port && + !!this.incoming.socketType && !!this.incoming.auth && + !!this.incoming.username && + (!!this.outgoing.existingServerKey || + (!!this.outgoing.hostname && !!this.outgoing.port && + !!this.outgoing.socketType && !!this.outgoing.auth && + !!this.outgoing.username))); + }, +}; + + +// enum consts + +// .source +AccountConfig.kSourceUser = 1; // user manually entered the config +AccountConfig.kSourceXML = 2; // config from XML from ISP or Mozilla DB +AccountConfig.kSourceGuess = 3; // guessConfig() + + +/** + * Some fields on the account config accept placeholders (when coming from XML). + * + * These are the predefined ones + * * %EMAILADDRESS% (full email address of the user, usually entered by user) + * * %EMAILLOCALPART% (email address, part before @) + * * %EMAILDOMAIN% (email address, part after @) + * * %REALNAME% + * as well as those defined in account.inputFields.*.varname, with % added + * before and after. + * + * These must replaced with real values, supplied by the user or app, + * before the account is created. This is done here. You call this function once + * you have all the data - gathered the standard vars mentioned above as well as + * all listed in account.inputFields, and pass them in here. This function will + * insert them in the fields, returning a fully filled-out account ready to be + * created. + * + * @param account {AccountConfig} + * The account data to be modified. It may or may not contain placeholders. + * After this function, it should not contain placeholders anymore. + * This object will be modified in-place. + * + * @param emailfull {String} + * Full email address of this account, e.g. "joe@example.com". + * Empty of incomplete email addresses will/may be rejected. + * + * @param realname {String} + * Real name of user, as will appear in From of outgoing messages + * + * @param password {String} + * The password for the incoming server and (if necessary) the outgoing server + */ +function replaceVariables(account, realname, emailfull, password) +{ + sanitize.nonemptystring(emailfull); + let emailsplit = emailfull.split("@"); + assert(emailsplit.length == 2, + "email address not in expected format: must contain exactly one @"); + let emaillocal = sanitize.nonemptystring(emailsplit[0]); + let emaildomain = sanitize.hostname(emailsplit[1]); + sanitize.label(realname); + sanitize.nonemptystring(realname); + + let otherVariables = {}; + otherVariables.EMAILADDRESS = emailfull; + otherVariables.EMAILLOCALPART = emaillocal; + otherVariables.EMAILDOMAIN = emaildomain; + otherVariables.REALNAME = realname; + + if (password) { + account.incoming.password = password; + account.outgoing.password = password; // set member only if auth required? + } + account.incoming.username = _replaceVariable(account.incoming.username, + otherVariables); + account.outgoing.username = _replaceVariable(account.outgoing.username, + otherVariables); + account.incoming.hostname = + _replaceVariable(account.incoming.hostname, otherVariables); + if (account.outgoing.hostname) // will be null if user picked existing server. + account.outgoing.hostname = + _replaceVariable(account.outgoing.hostname, otherVariables); + account.identity.realname = + _replaceVariable(account.identity.realname, otherVariables); + account.identity.emailAddress = + _replaceVariable(account.identity.emailAddress, otherVariables); + account.displayName = _replaceVariable(account.displayName, otherVariables); +} + +function _replaceVariable(variable, values) +{ + let str = variable; + if (typeof(str) != "string") + return str; + + for (let varname in values) + str = str.replace("%" + varname + "%", values[varname]); + + return str; +} diff --git a/mailnews/base/prefs/content/accountcreation/createInBackend.js b/mailnews/base/prefs/content/accountcreation/createInBackend.js new file mode 100644 index 000000000..cd984fd3c --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/createInBackend.js @@ -0,0 +1,337 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * Takes an |AccountConfig| JS object and creates that account in the + * Thunderbird backend (which also writes it to prefs). + * + * @param config {AccountConfig} The account to create + * + * @return - the account created. + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function createAccountInBackend(config) +{ + // incoming server + let inServer = MailServices.accounts.createIncomingServer( + config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, ["pop3", "imap", "nntp"])); + inServer.port = config.incoming.port; + inServer.authMethod = config.incoming.auth; + inServer.password = config.incoming.password; + if (config.rememberPassword && config.incoming.password.length) + rememberPassword(inServer, config.incoming.password); + +#ifdef MOZ_MAILNEWS_OAUTH2 + if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + inServer.setCharValue("oauth2.scope", config.oauthSettings.scope); + inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer); + } +#endif + + // SSL + if (config.incoming.socketType == 1) // plain + inServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.incoming.socketType == 2) // SSL / TLS + inServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.incoming.socketType == 3) // STARTTLS + inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + //inServer.prettyName = config.displayName; + inServer.prettyName = config.identity.emailAddress; + + inServer.doBiff = true; + inServer.biffMinutes = config.incoming.checkInterval; + const loginAtStartupPrefTemplate = + "mail.server.%serverkey%.login_at_startup"; + var loginAtStartupPref = + loginAtStartupPrefTemplate.replace("%serverkey%", inServer.key); + Services.prefs.setBoolPref(loginAtStartupPref, + config.incoming.loginAtStartup); + if (config.incoming.type == "pop3") + { + const leaveOnServerPrefTemplate = + "mail.server.%serverkey%.leave_on_server"; + const daysToLeaveOnServerPrefTemplate = + "mail.server.%serverkey%.num_days_to_leave_on_server"; + const deleteFromServerPrefTemplate = + "mail.server.%serverkey%.delete_mail_left_on_server"; + const deleteByAgeFromServerPrefTemplate = + "mail.server.%serverkey%.delete_by_age_from_server"; + const downloadOnBiffPrefTemplate = + "mail.server.%serverkey%.download_on_biff"; + var leaveOnServerPref = + leaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); + var ageFromServerPref = + deleteByAgeFromServerPrefTemplate.replace("%serverkey%", inServer.key); + var daysToLeaveOnServerPref = + daysToLeaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); + var deleteFromServerPref = + deleteFromServerPrefTemplate.replace("%serverkey%", inServer.key); + let downloadOnBiffPref = + downloadOnBiffPrefTemplate.replace("%serverkey%", inServer.key); + Services.prefs.setBoolPref(leaveOnServerPref, + config.incoming.leaveMessagesOnServer); + Services.prefs.setIntPref(daysToLeaveOnServerPref, + config.incoming.daysToLeaveMessagesOnServer); + Services.prefs.setBoolPref(deleteFromServerPref, + config.incoming.deleteOnServerWhenLocalDelete); + Services.prefs.setBoolPref(ageFromServerPref, + config.incoming.deleteByAgeFromServer); + Services.prefs.setBoolPref(downloadOnBiffPref, + config.incoming.downloadOnBiff); + } + inServer.valid = true; + + let username = config.outgoing.auth > 1 ? config.outgoing.username : null; + let outServer = MailServices.smtp.findServer(username, config.outgoing.hostname); + assert(config.outgoing.addThisServer || + config.outgoing.useGlobalPreferredServer || + config.outgoing.existingServerKey, + "No SMTP server: inconsistent flags"); + + if (config.outgoing.addThisServer && !outServer) + { + outServer = MailServices.smtp.createServer(); + outServer.hostname = config.outgoing.hostname; + outServer.port = config.outgoing.port; + outServer.authMethod = config.outgoing.auth; + if (config.outgoing.auth > 1) + { + outServer.username = username; + outServer.password = config.incoming.password; + if (config.rememberPassword && config.incoming.password.length) + rememberPassword(outServer, config.incoming.password); + } + +#ifdef MOZ_MAILNEWS_OAUTH2 + if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + let pref = "mail.smtpserver." + outServer.key + "."; + Services.prefs.setCharPref(pref + "oauth2.scope", + config.oauthSettings.scope); + Services.prefs.setCharPref(pref + "oauth2.issuer", + config.oauthSettings.issuer); + } +#endif + + if (config.outgoing.socketType == 1) // no SSL + outServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.outgoing.socketType == 2) // SSL / TLS + outServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.outgoing.socketType == 3) // STARTTLS + outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + // API problem: <http://mxr.mozilla.org/seamonkey/source/mailnews/compose/public/nsISmtpServer.idl#93> + outServer.description = config.displayName; + if (config.password) + outServer.password = config.outgoing.password; + + // If this is the first SMTP server, set it as default + if (!MailServices.smtp.defaultServer || + !MailServices.smtp.defaultServer.hostname) + MailServices.smtp.defaultServer = outServer; + } + + // identity + // TODO accounts without identity? + let identity = MailServices.accounts.createIdentity(); + identity.fullName = config.identity.realname; + identity.email = config.identity.emailAddress; + + // for new accounts, default to replies being positioned above the quote + // if a default account is defined already, take its settings instead + if (config.incoming.type == "imap" || config.incoming.type == "pop3") + { + identity.replyOnTop = 1; + // identity.sigBottom = false; // don't set this until Bug 218346 is fixed + + if (MailServices.accounts.accounts.length && + MailServices.accounts.defaultAccount) + { + let defAccount = MailServices.accounts.defaultAccount; + let defIdentity = defAccount.defaultIdentity; + if (defAccount.incomingServer.canBeDefaultServer && + defIdentity && defIdentity.valid) + { + identity.replyOnTop = defIdentity.replyOnTop; + identity.sigBottom = defIdentity.sigBottom; + } + } + } + + // due to accepted conventions, news accounts should default to plain text + if (config.incoming.type == "nntp") + identity.composeHtml = false; + + identity.valid = true; + + if (config.outgoing.existingServerKey) + identity.smtpServerKey = config.outgoing.existingServerKey; + else if (!config.outgoing.useGlobalPreferredServer) + identity.smtpServerKey = outServer.key; + + // account and hook up + // Note: Setting incomingServer will cause the AccountManager to refresh + // itself, which could be a problem if we came from it and we haven't set + // the identity (see bug 521955), so make sure everything else on the + // account is set up before you set the incomingServer. + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + account.incomingServer = inServer; + if (inServer.canBeDefaultServer && (!MailServices.accounts.defaultAccount || + !MailServices.accounts.defaultAccount + .incomingServer.canBeDefaultServer)) + MailServices.accounts.defaultAccount = account; + + verifyLocalFoldersAccount(MailServices.accounts); + setFolders(identity, inServer); + + // save + MailServices.accounts.saveAccountInfo(); + try { + Services.prefs.savePrefFile(null); + } catch (ex) { + ddump("Could not write out prefs: " + ex); + } + return account; +} + +function setFolders(identity, server) +{ + // TODO: support for local folders for global inbox (or use smart search + // folder instead) + + var baseURI = server.serverURI + "/"; + + // Names will be localized in UI, not in folder names on server/disk + // TODO allow to override these names in the XML config file, + // in case e.g. Google or AOL use different names? + // Workaround: Let user fix it :) + var fccName = "Sent"; + var draftName = "Drafts"; + var templatesName = "Templates"; + + identity.draftFolder = baseURI + draftName; + identity.stationeryFolder = baseURI + templatesName; + identity.fccFolder = baseURI + fccName; + + identity.fccFolderPickerMode = 0; + identity.draftsFolderPickerMode = 0; + identity.tmplFolderPickerMode = 0; +} + +function rememberPassword(server, password) +{ + if (server instanceof Components.interfaces.nsIMsgIncomingServer) + var passwordURI = server.localStoreType + "://" + server.hostName; + else if (server instanceof Components.interfaces.nsISmtpServer) + var passwordURI = "smtp://" + server.hostname; + else + throw new NotReached("Server type not supported"); + + let login = Cc["@mozilla.org/login-manager/loginInfo;1"] + .createInstance(Ci.nsILoginInfo); + login.init(passwordURI, null, passwordURI, server.username, password, "", ""); + try { + Services.logins.addLogin(login); + } catch (e) { + if (e.message.includes("This login already exists")) { + // TODO modify + } else { + throw e; + } + } +} + +/** + * Check whether the user's setup already has an incoming server + * which matches (hostname, port, username) the primary one + * in the config. + * (We also check the email address as username.) + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsIMsgIncomingServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkIncomingServerAlreadyExists(config) +{ + assert(config instanceof AccountConfig); + let incoming = config.incoming; + let existing = MailServices.accounts.findRealServer(incoming.username, + incoming.hostname, + sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), + incoming.port); + + // if username does not have an '@', also check the e-mail + // address form of the name. + if (!existing && !incoming.username.includes("@")) + existing = MailServices.accounts.findRealServer(config.identity.emailAddress, + incoming.hostname, + sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), + incoming.port); + return existing; +}; + +/** + * Check whether the user's setup already has an outgoing server + * which matches (hostname, port, username) the primary one + * in the config. + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsISmtpServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkOutgoingServerAlreadyExists(config) +{ + assert(config instanceof AccountConfig); + let smtpServers = MailServices.smtp.servers; + while (smtpServers.hasMoreElements()) + { + let existingServer = smtpServers.getNext() + .QueryInterface(Ci.nsISmtpServer); + // TODO check username with full email address, too, like for incoming + if (existingServer.hostname == config.outgoing.hostname && + existingServer.port == config.outgoing.port && + existingServer.username == config.outgoing.username) + return existingServer; + } + return null; +}; + +/** + * Check if there already is a "Local Folders". If not, create it. + * Copied from AccountWizard.js with minor updates. + */ +function verifyLocalFoldersAccount(am) +{ + let localMailServer; + try { + localMailServer = am.localFoldersServer; + } + catch (ex) { + localMailServer = null; + } + + try { + if (!localMailServer) + { + // creates a copy of the identity you pass in + am.createLocalMailAccount(); + try { + localMailServer = am.localFoldersServer; + } + catch (ex) { + ddump("Error! we should have found the local mail server " + + "after we created it."); + } + } + } + catch (ex) { ddump("Error in verifyLocalFoldersAccount " + ex); } +} diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.js b/mailnews/base/prefs/content/accountcreation/emailWizard.js new file mode 100644 index 000000000..2e4a44e2a --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/emailWizard.js @@ -0,0 +1,1976 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/hostnameUtils.jsm"); +#ifdef MOZ_MAILNEWS_OAUTH2 +Components.utils.import("resource://gre/modules/OAuth2Providers.jsm"); +#endif + +/** + * This is the dialog opened by menu File | New account | Mail... . + * + * It gets the user's realname, email address and password, + * and tries to automatically configure the account from that, + * using various mechanisms. If all fails, the user can enter/edit + * the config, then we create the account. + * + * Steps: + * - User enters realname, email address and password + * - check for config files on disk + * (shipping with Thunderbird, for enterprise deployments) + * - (if fails) try to get the config file from the ISP via a + * fixed URL on the domain of the email address + * - (if fails) try to get the config file from our own database + * at MoMo servers, maintained by the community + * - (if fails) try to guess the config, by guessing hostnames, + * probing ports, checking config via server's CAPS line etc.. + * - verify the setup, by trying to login to the configured servers + * - let user verify and maybe edit the server names and ports + * - If user clicks OK, create the account + */ + + +// from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html +var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i; + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} + +var gStringsBundle; +var gMessengerBundle; +var gBrandShortName; + +/********************* +TODO for bug 549045 +- autodetect protocol +Polish +- reformat code style to match +<https://developer.mozilla.org/En/Mozilla_Coding_Style_Guide#Control_Structures> +- bold status +- remove status when user edited in manual edit +- add and adapt test from bug 534588 +Bugs +- SSL cert errors + - invalid cert (hostname mismatch) doesn't trigger warning dialog as it should + - accept self-signed cert (e.g. imap.mail.ru) doesn't work + (works without my patch), + verifyConfig.js line 124 has no inServer, for whatever reason, + although I didn't change verifyConfig.js at all + (the change you see in that file is irrelevant: that was an attempt to fix + the bug and clean up the code). +- Set radio IMAP vs. POP3, see TODO in code +Things to test (works for me): +- state transitions, buttons enable, status msgs + - stop button + - showes up again after stopping detection and restarting it + - when stopping [retest]: buttons proper? + - enter nonsense domain. guess fails, (so automatically) manual, + change domain to real one (not in DB), guess succeeds. + former bug: goes to manual first shortly, then to result +**********************/ + +// To debug, set mail.wizard.logging.dump (or .console)="All" + +function e(elementID) +{ + return document.getElementById(elementID); +}; + +function _hide(id) +{ + e(id).hidden = true; +} + +function _show(id) +{ + e(id).hidden = false; +} + +function _enable(id) +{ + e(id).disabled = false; +} + +function _disable(id) +{ + e(id).disabled = true; +} + +function setText(id, value) +{ + var element = e(id); + assert(element, "setText() on non-existant element ID"); + + if (element.localName == "textbox" || element.localName == "label") { + element.value = value; + } else if (element.localName == "description") { + element.textContent = value; + } else { + throw new NotReached("XUL element type not supported"); + } +} + +function setLabelFromStringBundle(elementID, stringName) +{ + e(elementID).label = gMessengerBundle.getString(stringName); +}; + +function EmailConfigWizard() +{ + this._init(); +} +EmailConfigWizard.prototype = +{ + _init : function EmailConfigWizard__init() + { + gEmailWizardLogger.info("Initializing setup wizard"); + this._abortable = null; + }, + + onLoad : function() + { + /** + * this._currentConfig is the config we got either from the XML file or + * from guessing or from the user. Unless it's from the user, it contains + * placeholders like %EMAILLOCALPART% in username and other fields. + * + * The config here must retain these placeholders, to be able to + * adapt when the user enters a different realname, or password or + * email local part. (A change of the domain name will trigger a new + * detection anyways.) + * That means, before you actually use the config (e.g. to create an + * account or to show it to the user), you need to run replaceVariables(). + */ + this._currentConfig = null; + + let userFullname; + try { + let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo); + userFullname = userInfo.fullname; + } catch(e) { + // nsIUserInfo may not be implemented on all platforms, and name might + // not be avaialble even if it is. + } + + this._domain = ""; + this._email = ""; + this._realname = (userFullname) ? userFullname : ""; + e("realname").value = this._realname; + this._password = ""; + this._okCallback = null; + + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].msgWindow) { + this._parentMsgWindow = window.arguments[0].msgWindow; + } + if (window.arguments[0].okCallback) { + this._okCallback = window.arguments[0].okCallback; + } + } + + gEmailWizardLogger.info("Email account setup dialog loaded."); + + gStringsBundle = e("strings"); + gMessengerBundle = e("bundle_messenger"); + gBrandShortName = e("bundle_brand").getString("brandShortName"); + + setLabelFromStringBundle("in-authMethod-password-cleartext", + "authPasswordCleartextViaSSL"); // will warn about insecure later + setLabelFromStringBundle("in-authMethod-password-encrypted", + "authPasswordEncrypted"); + setLabelFromStringBundle("in-authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("in-authMethod-ntlm", "authNTLM"); +#ifdef MOZ_MAILNEWS_OAUTH2 + setLabelFromStringBundle("in-authMethod-oauth2", "authOAuth2"); +#endif + setLabelFromStringBundle("out-authMethod-no", "authNo"); + setLabelFromStringBundle("out-authMethod-password-cleartext", + "authPasswordCleartextViaSSL"); // will warn about insecure later + setLabelFromStringBundle("out-authMethod-password-encrypted", + "authPasswordEncrypted"); + setLabelFromStringBundle("out-authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("out-authMethod-ntlm", "authNTLM"); +#ifdef MOZ_MAILNEWS_OAUTH2 + setLabelFromStringBundle("out-authMethod-oauth2", "authOAuth2"); +#endif + + e("incoming_port").value = gStringsBundle.getString("port_auto"); + this.fillPortDropdown("smtp"); + + // If the account provisioner is preffed off, don't display + // the account provisioner button. + if (!Services.prefs.getBoolPref("mail.provider.enabled")) + _hide("provisioner_button"); + + // Populate SMTP server dropdown with already configured SMTP servers from + // other accounts. + var menulist = e("outgoing_hostname"); + let smtpServers = MailServices.smtp.servers; + while (smtpServers.hasMoreElements()) { + let server = smtpServers.getNext().QueryInterface(Ci.nsISmtpServer); + let label = server.displayname; + let key = server.key; + if (MailServices.smtp.defaultServer && + MailServices.smtp.defaultServer.key == key) { + label += " " + gStringsBundle.getString("default_server_tag"); + } + let menuitem = menulist.appendItem(label, key, ""); // label,value,descr + menuitem.serverKey = key; + } + // Add the entry for the new host to the menulist + let menuitem = menulist.insertItemAt(0, "", "-new-"); // pos,label,value + menuitem.serverKey = null; + + // admin-locked prefs hurray + if (!Services.prefs.getBoolPref("signon.rememberSignons")) { + let rememberPasswordE = e("remember_password"); + rememberPasswordE.checked = false; + rememberPasswordE.disabled = true; + } + + // First, unhide the main window areas, and store the width, + // so that we don't resize wildly when we unhide areas. + // switchToMode() will then hide the unneeded parts again. + // We will add some leeway of 10px, in case some of the <description>s wrap, + // e.g. outgoing username != incoming username. + _show("status_area"); + _show("result_area"); + _hide("manual-edit_area"); + window.sizeToContent(); + e("mastervbox").setAttribute("style", + "min-width: " + document.width + "px; " + + "min-height: " + (document.height + 10) + "px;"); + + this.switchToMode("start"); + e("realname").select(); + }, + + /** + * Changes the window configuration to the different modes we have. + * Shows/hides various window parts and buttons. + * @param modename {String-enum} + * "start" : Just the realname, email address, password fields + * "find-config" : detection step, adds the progress message/spinner + * "result" : We found a config and display it to the user. + * The user may create the account. + * "manual-edit" : The user wants (or needs) to manually enter their + * the server hostname and other settings. We'll use them as provided. + * Additionally, there are the following sub-modes which can be entered after + * you entered the main mode: + * "manual-edit-have-hostname" : user entered a hostname for both servers + * that we can use + * "manual-edit-testing" : User pressed the [Re-test] button and + * we're currently detecting the "Auto" values + * "manual-edit-complete" : user entered (or we tested) all necessary + * values, and we're ready to create to account + * Currently, this doesn't cover the warning dialogs etc.. It may later. + */ + switchToMode : function(modename) + { + if (modename == this._currentModename) { + return; + } + this._currentModename = modename; + gEmailWizardLogger.info("switching to UI mode " + modename) + + //_show("initialSettings"); always visible + //_show("cancel_button"); always visible + if (modename == "start") { + _hide("status_area"); + _hide("result_area"); + _hide("manual-edit_area"); + + _show("next_button"); + _disable("next_button"); // will be enabled by code + _hide("half-manual-test_button"); + _hide("create_button"); + _hide("stop_button"); + _hide("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "find-config") { + _show("status_area"); + _hide("result_area"); + _hide("manual-edit_area"); + + _show("next_button"); + _disable("next_button"); + _hide("half-manual-test_button"); + _hide("create_button"); + _show("stop_button"); + this.onStop = this.onStopFindConfig; + _show("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "result") { + _show("status_area"); + _show("result_area"); + _hide("manual-edit_area"); + + _hide("next_button"); + _hide("half-manual-test_button"); + _show("create_button"); + _enable("create_button"); + _hide("stop_button"); + _show("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "manual-edit") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + + _hide("next_button"); + _show("half-manual-test_button"); + _disable("half-manual-test_button"); + _show("create_button"); + _disable("create_button"); + _hide("stop_button"); + _hide("manual-edit_button"); + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-have-hostname") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _enable("half-manual-test_button"); + _disable("create_button"); + _hide("stop_button"); + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-testing") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _disable("half-manual-test_button"); + _disable("create_button"); + _show("stop_button"); + this.onStop = this.onStopHalfManualTesting; + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-complete") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _enable("half-manual-test_button"); + _enable("create_button"); + _hide("stop_button"); + _show("advanced-setup_button"); + _enable("advanced-setup_button"); + } else { + throw new NotReached("unknown mode"); + } + // If we're offline, we're going to disable the create button, but enable + // the advanced config button if we have a current config. + if (Services.io.offline) { + if (this._currentConfig != null) { + _show("advanced-setup_button"); + _enable("advanced-setup_button"); + _hide("half-manual-test_button"); + _hide("create_button"); + _hide("manual-edit_button"); + } + } + window.sizeToContent(); + }, + + /** + * Start from beginning with possibly new email address. + */ + onStartOver : function() { + this._currentConfig = null; + if (this._abortable) { + this.onStop(); + } + this.switchToMode("start"); + }, + + getConcreteConfig : function() + { + var result = this._currentConfig.copy(); + replaceVariables(result, this._realname, this._email, this._password); + result.rememberPassword = e("remember_password").checked && + !!this._password; + return result; + }, + + /* + * This checks if the email address is at least possibly valid, meaning it + * has an '@' before the last char. + */ + validateEmailMinimally : function(emailAddr) + { + let atPos = emailAddr.lastIndexOf("@"); + return atPos > 0 && atPos + 1 < emailAddr.length; + }, + + /* + * This checks if the email address is syntactically valid, + * as far as we can determine. We try hard to make full checks. + * + * OTOH, we have a very small chance of false negatives, + * because the RFC822 address spec is insanely complicated, + * but rarely needed, so when this here fails, we show an error message, + * but don't stop the user from continuing. + * In contrast, if validateEmailMinimally() fails, we stop the user. + */ + validateEmail : function(emailAddr) + { + return emailRE.test(emailAddr); + }, + + /** + * onInputEmail and onInputRealname are called on input = keypresses, and + * enable/disable the next button based on whether there's a semi-proper + * e-mail address and non-blank realname to start with. + * + * A change to the email address also automatically restarts the + * whole process. + */ + onInputEmail : function() + { + this._email = e("email").value; + this.onStartOver(); + this.checkStartDone(); + }, + onInputRealname : function() + { + this._realname = e("realname").value; + this.checkStartDone(); + }, + + onInputPassword : function() + { + this._password = e("password").value; + }, + + /** + * This does very little other than to check that a name was entered at all + * Since this is such an insignificant test we should be using a very light + * or even jovial warning. + */ + onBlurRealname : function() + { + let realnameEl = e("realname"); + if (this._realname) { + this.clearError("nameerror"); + _show("nametext"); + realnameEl.removeAttribute("error"); + // bug 638790: don't show realname error until user enter an email address + } else if (this.validateEmailMinimally(this._email)) { + _hide("nametext"); + this.setError("nameerror", "please_enter_name"); + realnameEl.setAttribute("error", "true"); + } + }, + + /** + * This check is only done as an informative warning. + * We don't want to block the person, if they've entered an email address + * that doesn't conform to our regex. + */ + onBlurEmail : function() + { + if (!this._email) { + return; + } + var emailEl = e("email"); + if (this.validateEmail(this._email)) { + this.clearError("emailerror"); + emailEl.removeAttribute("error"); + this.onBlurRealname(); + } else { + this.setError("emailerror", "double_check_email"); + emailEl.setAttribute("error", "true"); + } + }, + + /** + * If the user just tabbed through the password input without entering + * anything, set the type back to text so we don't wind up showing the + * emptytext as bullet characters. + */ + onBlurPassword : function() + { + if (!this._password) { + e("password").type = "text"; + } + }, + + /** + * @see onBlurPassword() + */ + onFocusPassword : function() + { + e("password").type = "password"; + }, + + /** + * Check whether the user entered the minimum of information + * needed to leave the "start" mode (entering of name, email, pw) + * and is allowed to proceed to detection step. + */ + checkStartDone : function() + { + if (this.validateEmailMinimally(this._email) && + this._realname) { + this._domain = this._email.split("@")[1].toLowerCase(); + _enable("next_button"); + } else { + _disable("next_button"); + } + }, + + /** + * When the [Continue] button is clicked, we move from the initial account + * information stage to using that information to configure account details. + */ + onNext : function() + { + this.findConfig(this._domain, this._email); + }, + + + ///////////////////////////////////////////////////////////////// + // Detection step + + /** + * Try to find an account configuration for this email address. + * This is the function which runs the autoconfig. + */ + findConfig : function(domain, email) + { + gEmailWizardLogger.info("findConfig()"); + if (this._abortable) { + this.onStop(); + } + this.switchToMode("find-config"); + this.startSpinner("looking_up_settings_disk"); + var self = this; + this._abortable = fetchConfigFromDisk(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_disk"); + }, + function(e) // fetchConfigFromDisk failed + { + if (e instanceof CancelledException) { + return; + } + gEmailWizardLogger.info("fetchConfigFromDisk failed: " + e); + self.startSpinner("looking_up_settings_isp"); + self._abortable = fetchConfigFromISP(domain, email, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_isp"); + }, + function(e) // fetchConfigFromISP failed + { + if (e instanceof CancelledException) { + return; + } + gEmailWizardLogger.info("fetchConfigFromISP failed: " + e); + logException(e); + self.startSpinner("looking_up_settings_db"); + self._abortable = fetchConfigFromDB(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_db"); + }, + function(e) // fetchConfigFromDB failed + { + if (e instanceof CancelledException) { + return; + } + logException(e); + gEmailWizardLogger.info("fetchConfigFromDB failed: " + e); + self.startSpinner("looking_up_settings_db"); + self._abortable = fetchConfigForMX(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_db"); + }, + function(e) // fetchConfigForMX failed + { + if (e instanceof CancelledException) { + return; + } + logException(e); + gEmailWizardLogger.info("fetchConfigForMX failed: " + e); + var initialConfig = new AccountConfig(); + self._prefillConfig(initialConfig); + self._guessConfig(domain, initialConfig); + }); + }); + }); + }); + }, + + /** + * Just a continuation of findConfig() + */ + _guessConfig : function(domain, initialConfig) + { + this.startSpinner("looking_up_settings_guess") + var self = this; + self._abortable = guessConfig(domain, + function(type, hostname, port, ssl, done, config) // progress + { + var msg = hostname + ":" + port + " ssl=" + ssl + " " + + type + ": progress callback"; + gEmailWizardLogger.info(msg); + }, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner(Services.io.offline ? + "guessed_settings_offline" : "found_settings_guess"); + window.sizeToContent(); + }, + function(e, config) // guessconfig failed + { + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gEmailWizardLogger.info("guessConfig failed: " + e); + self.showErrorStatus("failed_to_find_settings"); + self.editConfigDetails(); + }, + initialConfig, "both"); + }, + + /** + * When findConfig() was successful, it calls this. + * This displays the config to the user. + */ + foundConfig : function(config) + { + gEmailWizardLogger.info("foundConfig()"); + assert(config instanceof AccountConfig, + "BUG: Arg 'config' needs to be an AccountConfig object"); + + this._haveValidConfigForDomain = this._email.split("@")[1]; + + if (!this._realname || !this._email) { + return; + } + this._foundConfig2(config); + }, + + // Continuation of foundConfig2() after custom fields. + _foundConfig2 : function(config) + { + this.displayConfigResult(config); + }, + + /** + * [Stop] button click handler. + * This allows the user to abort any longer operation, esp. network activity. + * We currently have 3 such cases here: + * 1. findConfig(), i.e. fetch config from DB, guessConfig etc. + * 2. onHalfManualTest(), i.e. the [Retest] button in manual config. + * 3. verifyConfig() - We can't stop this yet, so irrelevant here currently. + * Given that these need slightly different actions, this function will be set + * to a function (i.e. overwritten) by whoever enables the stop button. + * + * We also call this from the code when the user started a different action + * without explicitly clicking [Stop] for the old one first. + */ + onStop : function() + { + throw new NotReached("onStop should be overridden by now"); + }, + _onStopCommon : function() + { + if (!this._abortable) { + throw new NotReached("onStop called although there's nothing to stop"); + } + gEmailWizardLogger.info("onStop cancelled _abortable"); + this._abortable.cancel(new UserCancelledException()); + this._abortable = null; + this.stopSpinner(); + }, + onStopFindConfig : function() + { + this._onStopCommon(); + this.switchToMode("start"); + this.checkStartDone(); + }, + onStopHalfManualTesting : function() + { + this._onStopCommon(); + this.validateManualEditComplete(); + }, + + + + /////////////////////////////////////////////////////////////////// + // status area + + startSpinner : function(actionStrName) + { + e("status_area").setAttribute("status", "loading"); + gEmailWizardLogger.warn("spinner start " + actionStrName); + this._showStatusTitle(actionStrName); + }, + + stopSpinner : function(actionStrName) + { + e("status_area").setAttribute("status", "result"); + _hide("stop_button"); + this._showStatusTitle(actionStrName); + gEmailWizardLogger.warn("all spinner stop " + actionStrName); + }, + + showErrorStatus : function(actionStrName) + { + e("status_area").setAttribute("status", "error"); + gEmailWizardLogger.warn("status error " + actionStrName); + this._showStatusTitle(actionStrName); + }, + + _showStatusTitle : function(msgName) + { + let msg = " "; // assure height. Do via min-height in CSS, for 2 lines? + try { + if (msgName) { + msg = gStringsBundle.getFormattedString(msgName, [gBrandShortName]); + } + } catch(ex) { + gEmailWizardLogger.error("missing string for " + msgName); + msg = msgName + " (missing string in translation!)"; + } + + e("status_msg").textContent = msg; + gEmailWizardLogger.info("status msg: " + msg); + }, + + + + ///////////////////////////////////////////////////////////////// + // Result area + + /** + * Displays a (probed) config to the user, + * in the result config details area. + * + * @param config {AccountConfig} The config to present to user + */ + displayConfigResult : function(config) + { + assert(config instanceof AccountConfig); + this._currentConfig = config; + var configFilledIn = this.getConcreteConfig(); + + var unknownString = gStringsBundle.getString("resultUnknown"); + + function _makeHostDisplayString(server, stringName) + { + let type = gStringsBundle.getString(sanitize.translate(server.type, + { imap : "resultIMAP", pop3 : "resultPOP3", smtp : "resultSMTP" }), + unknownString); + let host = server.hostname + + (isStandardPort(server.port) ? "" : ":" + server.port); + let ssl = gStringsBundle.getString(sanitize.translate(server.socketType, + { 1 : "resultNoEncryption", 2 : "resultSSL", 3 : "resultSTARTTLS" }), + unknownString); + let certStatus = gStringsBundle.getString(server.badCert ? + "resultSSLCertWeak" : "resultSSLCertOK"); + // TODO: we should really also display authentication method here. + return gStringsBundle.getFormattedString(stringName, + [ type, host, ssl, certStatus ]); + }; + + var incomingResult = unknownString; + if (configFilledIn.incoming.hostname) { + incomingResult = _makeHostDisplayString(configFilledIn.incoming, + "resultIncoming"); + } + + var outgoingResult = unknownString; + if (!config.outgoing.existingServerKey) { + if (configFilledIn.outgoing.hostname) { + outgoingResult = _makeHostDisplayString(configFilledIn.outgoing, + "resultOutgoing"); + } + } else { + outgoingResult = gStringsBundle.getString("resultOutgoingExisting"); + } + + var usernameResult; + if (configFilledIn.incoming.username == configFilledIn.outgoing.username) { + usernameResult = gStringsBundle.getFormattedString("resultUsernameBoth", + [ configFilledIn.incoming.username || unknownString ]); + } else { + usernameResult = gStringsBundle.getFormattedString( + "resultUsernameDifferent", + [ configFilledIn.incoming.username || unknownString, + configFilledIn.outgoing.username || unknownString ]); + } + + setText("result-incoming", incomingResult); + setText("result-outgoing", outgoingResult); + setText("result-username", usernameResult); + + gEmailWizardLogger.info(debugObject(config, "config")); + // IMAP / POP dropdown + var lookForAltType = + config.incoming.type == "imap" ? "pop3" : "imap"; + var alternative = null; + for (let i = 0; i < config.incomingAlternatives.length; i++) { + let alt = config.incomingAlternatives[i]; + if (alt.type == lookForAltType) { + alternative = alt; + break; + } + } + if (alternative) { + _show("result_imappop"); + e("result_select_" + alternative.type).configIncoming = alternative; + e("result_select_" + config.incoming.type).configIncoming = + config.incoming; + e("result_imappop").value = + config.incoming.type == "imap" ? 1 : 2; + } else { + _hide("result_imappop"); + } + + this.switchToMode("result"); + }, + + /** + * Handle the user switching between IMAP and POP3 settings using the + * radio buttons. + * + * Note: This function must only be called by user action, not by setting + * the value or selectedItem or selectedIndex of the radiogroup! + * This is why we use the oncommand attribute of the radio elements + * instead of the onselect attribute of the radiogroup. + */ + onResultIMAPOrPOP3 : function() + { + var config = this._currentConfig; + var radiogroup = e("result_imappop"); + // add current server as best alternative to start of array + config.incomingAlternatives.unshift(config.incoming); + // use selected server (stored as special property on the <radio> node) + config.incoming = radiogroup.selectedItem.configIncoming; + // remove newly selected server from list of alternatives + config.incomingAlternatives = config.incomingAlternatives.filter( + function(e) { return e != config.incoming; }); + this.displayConfigResult(config); + }, + + + + ///////////////////////////////////////////////////////////////// + // Manual Edit area + + /** + * Gets the values from the user in the manual edit area. + * + * Realname and password are not part of that area and still + * placeholders, but hostname and username are concrete and + * no placeholders anymore. + */ + getUserConfig : function() + { + var config = this.getConcreteConfig(); + if (!config) { + config = new AccountConfig(); + } + config.source = AccountConfig.kSourceUser; + + // Incoming server + try { + var inHostnameField = e("incoming_hostname"); + config.incoming.hostname = sanitize.hostname(inHostnameField.value); + inHostnameField.value = config.incoming.hostname; + } catch (e) { gEmailWizardLogger.warn(e); } + try { + config.incoming.port = sanitize.integerRange(e("incoming_port").value, + kMinPort, kMaxPort); + } catch (e) { + config.incoming.port = undefined; // incl. default "Auto" + } + config.incoming.type = sanitize.translate(e("incoming_protocol").value, + { 1: "imap", 2 : "pop3", 0 : null }); + config.incoming.socketType = sanitize.integer(e("incoming_ssl").value); + config.incoming.auth = sanitize.integer(e("incoming_authMethod").value); + config.incoming.username = e("incoming_username").value; + + // Outgoing server + + // Did the user select one of the already configured SMTP servers from the + // drop-down list? If so, use it. + var outHostnameCombo = e("outgoing_hostname"); + var outMenuitem = outHostnameCombo.selectedItem; + if (outMenuitem && outMenuitem.serverKey) { + config.outgoing.existingServerKey = outMenuitem.serverKey; + config.outgoing.existingServerLabel = outMenuitem.label; + config.outgoing.addThisServer = false; + config.outgoing.useGlobalPreferredServer = false; + } else { + config.outgoing.existingServerKey = null; + config.outgoing.addThisServer = true; + config.outgoing.useGlobalPreferredServer = false; + + try { + config.outgoing.hostname = sanitize.hostname( + outHostnameCombo.inputField.value); + outHostnameCombo.inputField.value = config.outgoing.hostname; + } catch (e) { gEmailWizardLogger.warn(e); } + try { + config.outgoing.port = sanitize.integerRange(e("outgoing_port").value, + kMinPort, kMaxPort); + } catch (e) { + config.outgoing.port = undefined; // incl. default "Auto" + } + config.outgoing.socketType = sanitize.integer(e("outgoing_ssl").value); + config.outgoing.auth = sanitize.integer(e("outgoing_authMethod").value); + } + config.outgoing.username = e("outgoing_username").value; + + return config; + }, + + /** + * [Manual Config] button click handler. This turns the config details area + * into an editable form and makes the (Go) button appear. The edit button + * should only be available after the config probing is completely finished, + * replacing what was the (Stop) button. + */ + onManualEdit : function() + { + if (this._abortable) { + this.onStop(); + } + this.editConfigDetails(); + }, + + /** + * Setting the config details form so it can be edited. We also disable + * (and hide) the create button during this time because we don't know what + * might have changed. The function called from the button that restarts + * the config check should be enabling the config button as needed. + */ + editConfigDetails : function() + { + gEmailWizardLogger.info("manual edit"); + + if (!this._currentConfig) { + this._currentConfig = new AccountConfig(); + this._currentConfig.incoming.type = "imap"; + this._currentConfig.incoming.username = "%EMAILLOCALPART%"; + this._currentConfig.outgoing.username = "%EMAILLOCALPART%"; + this._currentConfig.incoming.hostname = ".%EMAILDOMAIN%"; + this._currentConfig.outgoing.hostname = ".%EMAILDOMAIN%"; + } + // Although we go manual, and we need to display the concrete username, + // however the realname and password is not part of manual config and + // must stay a placeholder in _currentConfig. @see getUserConfig() + + this._fillManualEditFields(this.getConcreteConfig()); + + // _fillManualEditFields() indirectly calls validateManualEditComplete(), + // but it's important to not forget it in case the code is rewritten, + // so calling it explicitly again. Doesn't do harm, speed is irrelevant. + this.validateManualEditComplete(); + }, + + /** + * Fills the manual edit textfields with the provided config. + * @param config {AccountConfig} The config to present to user + */ + _fillManualEditFields : function(config) + { + assert(config instanceof AccountConfig); + + // incoming server + e("incoming_protocol").value = sanitize.translate(config.incoming.type, + { "imap" : 1, "pop3" : 2 }, 1); + e("incoming_hostname").value = config.incoming.hostname; + e("incoming_ssl").value = sanitize.enum(config.incoming.socketType, + [ 0, 1, 2, 3 ], 0); + e("incoming_authMethod").value = sanitize.enum(config.incoming.auth, +#ifdef MOZ_MAILNEWS_OAUTH2 + [ 0, 3, 4, 5, 6, 10 ], 0); +#else + [ 0, 3, 4, 5, 6 ], 0); +#endif + e("incoming_username").value = config.incoming.username; + if (config.incoming.port) { + e("incoming_port").value = config.incoming.port; + } else { + this.adjustIncomingPortToSSLAndProtocol(config); + } + this.fillPortDropdown(config.incoming.type); + +#ifdef MOZ_MAILNEWS_OAUTH2 + // If the hostname supports OAuth2 and imap is enabled, enable OAuth2. + let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname); + gEmailWizardLogger.info("OAuth2 details for incoming hostname " + + config.incoming.hostname + " is " + iDetails); + e("in-authMethod-oauth2").hidden = !(iDetails && e("incoming_protocol").value == 1); + if (!e("in-authMethod-oauth2").hidden) { + config.oauthSettings = {}; + [config.oauthSettings.issuer, config.oauthSettings.scope] = iDetails; + // oauthsettings are not stored nor changable in the user interface, so just + // store them in the base configuration. + this._currentConfig.oauthSettings = config.oauthSettings; + } +#endif + + // outgoing server + e("outgoing_hostname").value = config.outgoing.hostname; + e("outgoing_username").value = config.outgoing.username; + // While sameInOutUsernames is true we synchronize values of incoming + // and outgoing username. + this.sameInOutUsernames = true; + e("outgoing_ssl").value = sanitize.enum(config.outgoing.socketType, + [ 0, 1, 2, 3 ], 0); + e("outgoing_authMethod").value = sanitize.enum(config.outgoing.auth, + [ 0, 1, 3, 4, 5, 6, 10 ], 0); + if (config.outgoing.port) { + e("outgoing_port").value = config.outgoing.port; + } else { + this.adjustOutgoingPortToSSLAndProtocol(config); + } + +#ifdef MOZ_MAILNEWS_OAUTH2 + // If the hostname supports OAuth2 and imap is enabled, enable OAuth2. + let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname); + gEmailWizardLogger.info("OAuth2 details for outgoing hostname " + + config.outgoing.hostname + " is " + oDetails); + e("out-authMethod-oauth2").hidden = !oDetails; + if (!e("out-authMethod-oauth2").hidden) { + config.oauthSettings = {}; + [config.oauthSettings.issuer, config.oauthSettings.scope] = oDetails; + // oauthsettings are not stored nor changable in the user interface, so just + // store them in the base configuration. + this._currentConfig.oauthSettings = config.oauthSettings; + } +#endif + + // populate fields even if existingServerKey, in case user changes back + if (config.outgoing.existingServerKey) { + let menulist = e("outgoing_hostname"); + // We can't use menulist.value = config.outgoing.existingServerKey + // because would overwrite the text field, so have to do it manually: + let menuitems = menulist.menupopup.childNodes; + for (let menuitem of menuitems) { + if (menuitem.serverKey == config.outgoing.existingServerKey) { + menulist.selectedItem = menuitem; + break; + } + } + } + this.onChangedOutgoingDropdown(); // show/hide outgoing port, SSL, ... + }, + + /** + * Automatically fill port field in manual edit, + * unless user entered a non-standard port. + * @param config {AccountConfig} + */ + adjustIncomingPortToSSLAndProtocol : function(config) + { + var autoPort = gStringsBundle.getString("port_auto"); + var incoming = config.incoming; + // we could use getHostEntry() here, but that API is bad, so don't bother + var newInPort = undefined; + if (!incoming.port || isStandardPort(incoming.port)) { + if (incoming.type == "imap") { + if (incoming.socketType == 1 || incoming.socketType == 3) { + newInPort = 143; + } else if (incoming.socketType == 2) { // Normal SSL + newInPort = 993; + } else { // auto + newInPort = autoPort; + } + } else if (incoming.type == "pop3") { + if (incoming.socketType == 1 || incoming.socketType == 3) { + newInPort = 110; + } else if (incoming.socketType == 2) { // Normal SSLs + newInPort = 995; + } else { // auto + newInPort = autoPort; + } + } + } + if (newInPort != undefined) { + e("incoming_port").value = newInPort; + e("incoming_authMethod").value = 0; // auto + } + }, + + /** + * @see adjustIncomingPortToSSLAndProtocol() + */ + adjustOutgoingPortToSSLAndProtocol : function(config) + { + var autoPort = gStringsBundle.getString("port_auto"); + var outgoing = config.outgoing; + var newOutPort = undefined; + if (!outgoing.port || isStandardPort(outgoing.port)) { + if (outgoing.socketType == 1 || outgoing.socketType == 3) { + // standard port is 587 *or* 25, so set to auto + // unless user or config already entered one of these two ports. + if (outgoing.port != 25 && outgoing.port != 587) { + newOutPort = autoPort; + } + } else if (outgoing.socketType == 2) { // Normal SSL + newOutPort = 465; + } else { // auto + newOutPort = autoPort; + } + } + if (newOutPort != undefined) { + e("outgoing_port").value = newOutPort; + e("outgoing_authMethod").value = 0; // auto + } + }, + + /** + * If the user changed the port manually, adjust the SSL value, + * (only) if the new port is impossible with the old SSL value. + * @param config {AccountConfig} + */ + adjustIncomingSSLToPort : function(config) + { + var incoming = config.incoming; + var newInSocketType = undefined; + if (!incoming.port || // auto + !isStandardPort(incoming.port)) { + return; + } + if (incoming.type == "imap") { + // normal SSL impossible + if (incoming.port == 143 && incoming.socketType == 2) { + newInSocketType = 0; // auto + // must be normal SSL + } else if (incoming.port == 993 && incoming.socketType != 2) { + newInSocketType = 2; + } + } else if (incoming.type == "pop3") { + // normal SSL impossible + if (incoming.port == 110 && incoming.socketType == 2) { + newInSocketType = 0; // auto + // must be normal SSL + } else if (incoming.port == 995 && incoming.socketType != 2) { + newInSocketType = 2; + } + } + if (newInSocketType != undefined) { + e("incoming_ssl").value = newInSocketType; + e("incoming_authMethod").value = 0; // auto + } + }, + + /** + * @see adjustIncomingSSLToPort() + */ + adjustOutgoingSSLToPort : function(config) + { + var outgoing = config.outgoing; + var newOutSocketType = undefined; + if (!outgoing.port || // auto + !isStandardPort(outgoing.port)) { + return; + } + // normal SSL impossible + if ((outgoing.port == 587 || outgoing.port == 25) && + outgoing.socketType == 2) { + newOutSocketType = 0; // auto + // must be normal SSL + } else if (outgoing.port == 465 && outgoing.socketType != 2) { + newOutSocketType = 2; + } + if (newOutSocketType != undefined) { + e("outgoing_ssl").value = newOutSocketType; + e("outgoing_authMethod").value = 0; // auto + } + }, + + /** + * Sets the prefilled values of the port fields. + * Filled statically with the standard ports for the given protocol, + * plus "Auto". + */ + fillPortDropdown : function(protocolType) + { + var menu = e(protocolType == "smtp" ? "outgoing_port" : "incoming_port"); + + // menulist.removeAllItems() is nice, but nicely clears the user value, too + var popup = menu.menupopup; + while (popup.hasChildNodes()) + popup.lastChild.remove(); + + // add standard ports + var autoPort = gStringsBundle.getString("port_auto"); + menu.appendItem(autoPort, autoPort, ""); // label,value,descr + for (let port of getStandardPorts(protocolType)) { + menu.appendItem(port, port, ""); // label,value,descr + } + }, + + onChangedProtocolIncoming : function() + { + var config = this.getUserConfig(); + this.adjustIncomingPortToSSLAndProtocol(config); + this.fillPortDropdown(config.incoming.type); + this.onChangedManualEdit(); + }, + onChangedPortIncoming : function() + { + gEmailWizardLogger.info("incoming port changed"); + this.adjustIncomingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedPortOutgoing : function() + { + gEmailWizardLogger.info("outgoing port changed"); + this.adjustOutgoingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedSSLIncoming : function() + { + this.adjustIncomingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedSSLOutgoing : function() + { + this.adjustOutgoingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedInAuth : function() + { + this.onChangedManualEdit(); + }, + onChangedOutAuth : function(aSelectedAuth) + { + if (aSelectedAuth) { + e("outgoing_label").hidden = e("outgoing_username").hidden = + (aSelectedAuth.id == "out-authMethod-no"); + } + this.onChangedManualEdit(); + }, + onInputInUsername : function() + { + if (this.sameInOutUsernames) + e("outgoing_username").value = e("incoming_username").value; + this.onChangedManualEdit(); + }, + onInputOutUsername : function() + { + this.sameInOutUsernames = false; + this.onChangedManualEdit(); + }, + onInputHostname : function() + { + this.onChangedManualEdit(); + }, + + /** + * Sets the label of the first entry of the dropdown which represents + * the new outgoing server. + */ + onOpenOutgoingDropdown : function() + { + var menulist = e("outgoing_hostname"); + // If the menulist is not editable, there is nothing to update + // and menulist.inputField does not even exist. + if (!menulist.editable) + return; + + var menuitem = menulist.getItemAtIndex(0); + assert(!menuitem.serverKey, "I wanted the special item for the new host"); + menuitem.label = menulist.inputField.value; + }, + + /** + * User selected an existing SMTP server (or deselected it). + * This changes only the UI. The values are read in getUserConfig(). + */ + onChangedOutgoingDropdown : function() + { + var menulist = e("outgoing_hostname"); + var menuitem = menulist.selectedItem; + if (menuitem && menuitem.serverKey) { + // an existing server has been selected from the dropdown + menulist.editable = false; + _hide("outgoing_port"); + _hide("outgoing_ssl"); + _hide("outgoing_authMethod"); + } else { + // new server, with hostname, port etc. + menulist.editable = true; + _show("outgoing_port"); + _show("outgoing_ssl"); + _show("outgoing_authMethod"); + } + + this.onChangedManualEdit(); + }, + + onChangedManualEdit : function() + { + if (this._abortable) { + this.onStop(); + } + this.validateManualEditComplete(); + }, + + /** + * This enables the buttons which allow the user to proceed + * once he has entered enough information. + * + * We can easily and fairly surely autodetect everything apart from the + * hostname (and username). So, once the user has entered + * proper hostnames, change to "manual-edit-have-hostname" mode + * which allows to press [Re-test], which starts the detection + * of the other values. + * Once the user has entered (or we detected) all values, he may + * do [Create Account] (tests login and if successful creates the account) + * or [Advanced Setup] (goes to Account Manager). Esp. in the latter case, + * we will not second-guess his setup and just to as told, so here we make + * sure that he at least entered all values. + */ + validateManualEditComplete : function() + { + // getUserConfig() is expensive, but still OK, not a problem + var manualConfig = this.getUserConfig(); + this._currentConfig = manualConfig; + if (manualConfig.isComplete()) { + this.switchToMode("manual-edit-complete"); + } else if (!!manualConfig.incoming.hostname && + !!manualConfig.outgoing.hostname) { + this.switchToMode("manual-edit-have-hostname"); + } else { + this.switchToMode("manual-edit"); + } + }, + + /** + * [Switch to provisioner] button click handler. Always active, allows + * one to switch to the account provisioner screen. + */ + onSwitchToProvisioner : function () + { + // We have to close this window first, otherwise msgNewMailAccount + // in accountUtils.js will think that this window still + // exists when it's called from the account provisioner window. + // This is because the account provisioner window is modal, + // and therefore blocks. Therefore, we override the _okCallback + // with a function that spawns the account provisioner, and then + // close the window. + this._okCallback = function() { + NewMailAccountProvisioner(window.arguments[0].msgWindow, window.arguments[0].extraData); + } + window.close(); + }, + + /** + * [Advanced Setup...] button click handler + * Only active in manual edit mode, and goes straight into + * Account Settings (pref UI) dialog. Requires a backend account, + * which requires proper hostname, port and protocol. + */ + onAdvancedSetup : function() + { + assert(this._currentConfig instanceof AccountConfig); + let configFilledIn = this.getConcreteConfig(); + + if (checkIncomingServerAlreadyExists(configFilledIn)) { + alertPrompt(gStringsBundle.getString("error_creating_account"), + gStringsBundle.getString("incoming_server_exists")); + return; + } + + gEmailWizardLogger.info("creating account in backend"); + let newAccount = createAccountInBackend(configFilledIn); + + let existingAccountManager = Services.wm + .getMostRecentWindow("mailnews:accountmanager"); + if (existingAccountManager) { + existingAccountManager.focus(); + } else { + window.openDialog("chrome://messenger/content/AccountManager.xul", + "AccountManager", "chrome,centerscreen,modal,titlebar", + { server: newAccount.incomingServer, + selectPage: "am-server.xul" }); + } + window.close(); + }, + + /** + * [Re-test] button click handler. + * Restarts the config guessing process after a person editing the server + * fields. + * It's called "half-manual", because we take the user-entered values + * as given and will not second-guess them, to respect the user wishes. + * (Yes, Sir! Will do as told!) + * The values that the user left empty or on "Auto" will be guessed/probed + * here. We will also check that the user-provided values work. + */ + onHalfManualTest : function() + { + var newConfig = this.getUserConfig(); + gEmailWizardLogger.info(debugObject(newConfig, "manualConfigToTest")); + this.startSpinner("looking_up_settings_halfmanual"); + this.switchToMode("manual-edit-testing"); + // if (this._userPickedOutgoingServer) TODO + var self = this; + this._abortable = guessConfig(this._domain, + function(type, hostname, port, ssl, done, config) // progress + { + gEmailWizardLogger.info("progress callback host " + hostname + + " port " + port + " type " + type); + }, + function(config) // success + { + self._abortable = null; + self._fillManualEditFields(config); + self.switchToMode("manual-edit-complete"); + self.stopSpinner("found_settings_halfmanual"); + }, + function(e, config) // guessconfig failed + { + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gEmailWizardLogger.info("guessConfig failed: " + e); + self.showErrorStatus("failed_to_find_settings"); + self.switchToMode("manual-edit-have-hostname"); + }, + newConfig, + newConfig.outgoing.existingServerKey ? "incoming" : "both"); + }, + + + + ///////////////////////////////////////////////////////////////// + // UI helper functions + + _prefillConfig : function(initialConfig) + { + var emailsplit = this._email.split("@"); + assert(emailsplit.length > 1); + var emaillocal = sanitize.nonemptystring(emailsplit[0]); + initialConfig.incoming.username = emaillocal; + initialConfig.outgoing.username = emaillocal; + return initialConfig; + }, + + clearError : function(which) + { + _hide(which); + _hide(which + "icon"); + e(which).textContent = ""; + }, + + setError : function(which, msg_name) + { + try { + _show(which); + _show(which + "icon"); + e(which).textContent = gStringsBundle.getString(msg_name); + window.sizeToContent(); + } catch (ex) { alertPrompt("missing error string", msg_name); } + }, + + + + ///////////////////////////////////////////////////////////////// + // Finish & dialog close functions + + onKeyDown : function(event) + { + let key = event.keyCode; + if (key == 27) { // Escape key + this.onCancel(); + return true; + } + if (key == 13) { // OK key + let buttons = [ + { id: "next_button", action: makeCallback(this, this.onNext) }, + { id: "create_button", action: makeCallback(this, this.onCreate) }, + { id: "half-manual-test_button", + action: makeCallback(this, this.onHalfManualTest) }, + ]; + for (let button of buttons) { + button.e = e(button.id); + if (button.e.hidden || button.e.disabled) { + continue; + } + button.action(); + return true; + } + } + return false; + }, + + onCancel : function() + { + window.close(); + // The window onclose handler will call onWizardShutdown for us. + }, + + onWizardShutdown : function() + { + if (this._abortable) { + this._abortable.cancel(new UserCancelledException()); + } + + if (this._okCallback) { + this._okCallback(); + } + gEmailWizardLogger.info("Shutting down email config dialog"); + }, + + + onCreate : function() + { + try { + gEmailWizardLogger.info("Create button clicked"); + + var configFilledIn = this.getConcreteConfig(); + var self = this; + // If the dialog is not needed, it will go straight to OK callback + gSecurityWarningDialog.open(this._currentConfig, configFilledIn, true, + function() // on OK + { + self.validateAndFinish(configFilledIn); + }, + function() {}); // on cancel, do nothing + } catch (ex) { + gEmailWizardLogger.error("Error creating account. ex=" + ex + + ", stack=" + ex.stack); + alertPrompt(gStringsBundle.getString("error_creating_account"), ex); + } + }, + + // called by onCreate() + validateAndFinish : function() + { + var configFilledIn = this.getConcreteConfig(); + + if (checkIncomingServerAlreadyExists(configFilledIn)) { + alertPrompt(gStringsBundle.getString("error_creating_account"), + gStringsBundle.getString("incoming_server_exists")); + return; + } + + if (configFilledIn.outgoing.addThisServer) { + let existingServer = checkOutgoingServerAlreadyExists(configFilledIn); + if (existingServer) { + configFilledIn.outgoing.addThisServer = false; + configFilledIn.outgoing.existingServerKey = existingServer.key; + } + } + + // TODO use a UI mode (switchToMode()) for verfication, too. + // But we need to go back to the previous mode, because we might be in + // "result" or "manual-edit-complete" mode. + _disable("create_button"); + _disable("half-manual-test_button"); + _disable("advanced-setup_button"); + // no stop button: backend has no ability to stop :-( + var self = this; + this.startSpinner("checking_password"); + // logic function defined in verifyConfig.js + verifyConfig( + configFilledIn, + // guess login config? + configFilledIn.source != AccountConfig.kSourceXML, + // TODO Instead, the following line would be correct, but I cannot use it, + // because some other code doesn't adhere to the expectations/specs. + // Find out what it was and fix it. + //concreteConfig.source == AccountConfig.kSourceGuess, + this._parentMsgWindow, + function(successfulConfig) // success + { + self.stopSpinner(successfulConfig.incoming.password ? + "password_ok" : null); + + // the auth might have changed, so we + // should back-port it to the current config. + self._currentConfig.incoming.auth = successfulConfig.incoming.auth; + self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth; + self._currentConfig.incoming.username = successfulConfig.incoming.username; + self._currentConfig.outgoing.username = successfulConfig.outgoing.username; + +#ifdef MOZ_MAILNEWS_OAUTH2 + // We loaded dynamic client registration, fill this data back in to the + // config set. + if (successfulConfig.oauthSettings) + self._currentConfig.oauthSettings = successfulConfig.oauthSettings; +#endif + + self.finish(); + }, + function(e) // failed + { + self.showErrorStatus("config_unverifiable"); + // TODO bug 555448: wrong error msg, there may be a 1000 other + // reasons why this failed, and this is misleading users. + self.setError("passworderror", "user_pass_invalid"); + // TODO use switchToMode(), see above + // give user something to proceed after fixing + _enable("create_button"); + // hidden in non-manual mode, so it's fine to enable + _enable("half-manual-test_button"); + _enable("advanced-setup_button"); + }); + }, + + finish : function() + { + gEmailWizardLogger.info("creating account in backend"); + createAccountInBackend(this.getConcreteConfig()); + window.close(); + }, +}; + +var gEmailConfigWizard = new EmailConfigWizard(); + +function serverMatches(a, b) +{ + return a.type == b.type && + a.hostname == b.hostname && + a.port == b.port && + a.socketType == b.socketType && + a.auth == b.auth; +} + +var _gStandardPorts = {}; +_gStandardPorts["imap"] = [ 143, 993 ]; +_gStandardPorts["pop3"] = [ 110, 995 ]; +_gStandardPorts["smtp"] = [ 587, 25, 465 ]; // order matters +var _gAllStandardPorts = _gStandardPorts["smtp"] + .concat(_gStandardPorts["imap"]).concat(_gStandardPorts["pop3"]); + +function isStandardPort(port) +{ + return _gAllStandardPorts.indexOf(port) != -1; +} + +function getStandardPorts(protocolType) +{ + return _gStandardPorts[protocolType]; +} + + +/** + * Warning dialog, warning user about lack of, or inappropriate, encryption. + * + * This is effectively a separate dialog, but implemented as part of + * this dialog. It works by hiding the main dialog part and unhiding + * the this part, and vice versa, and resizing the dialog. + */ +function SecurityWarningDialog() +{ + this._acknowledged = new Array(); +} +SecurityWarningDialog.prototype = +{ + /** + * {Array of {(incoming or outgoing) server part of {AccountConfig}} + * A list of the servers for which we already showed this dialog and the + * user approved the configs. For those, we won't show the warning again. + * (Make sure to store a copy in case the underlying object is changed.) + */ + _acknowledged : null, + + _inSecurityBad: 0x0001, + _inCertBad: 0x0010, + _outSecurityBad: 0x0100, + _outCertBad: 0x1000, + + /** + * Checks whether we need to warn about this config. + * + * We (currently) warn if + * - the mail travels unsecured (no SSL/STARTTLS) + * - the SSL certificate is not proper + * - (We don't warn about unencrypted passwords specifically, + * because they'd be encrypted with SSL and without SSL, we'd + * warn anyways.) + * + * We may not warn despite these conditions if we had shown the + * warning for that server before and the user acknowledged it. + * (Given that this dialog object is static/global and persistent, + * we can store that approval state here in this object.) + * + * @param configSchema @see open() + * @param configFilledIn @see open() + * @returns {Boolean} true when the dialog should be shown + * (call open()). if false, the dialog can and should be skipped. + */ + needed : function(configSchema, configFilledIn) + { + assert(configSchema instanceof AccountConfig); + assert(configFilledIn instanceof AccountConfig); + assert(configSchema.isComplete()); + assert(configFilledIn.isComplete()); + + var incomingBad = ((configFilledIn.incoming.socketType > 1) ? 0 : this._inSecurityBad) | + ((configFilledIn.incoming.badCert) ? this._inCertBad : 0); + var outgoingBad = 0; + if (!configFilledIn.outgoing.existingServerKey) { + outgoingBad = ((configFilledIn.outgoing.socketType > 1) ? 0 : this._outSecurityBad) | + ((configFilledIn.outgoing.badCert) ? this._outCertBad : 0); + } + + if (incomingBad > 0) { + if (this._acknowledged.some( + function(ackServer) { + return serverMatches(ackServer, configFilledIn.incoming); + })) + incomingBad = 0; + } + if (outgoingBad > 0) { + if (this._acknowledged.some( + function(ackServer) { + return serverMatches(ackServer, configFilledIn.outgoing); + })) + outgoingBad = 0; + } + + return incomingBad | outgoingBad; + }, + + /** + * Opens the dialog, fills it with values, and shows it to the user. + * + * The function is async: it returns immediately, and when the user clicks + * OK or Cancel, the callbacks are called. There the callers proceed as + * appropriate. + * + * @param configSchema The config, with placeholders not replaced yet. + * This object may be modified to store the user's confirmations, but + * currently that's not the case. + * @param configFilledIn The concrete config with placeholders replaced. + * @param onlyIfNeeded {Boolean} If there is nothing to warn about, + * call okCallback() immediately (and sync). + * @param okCallback {function(config {AccountConfig})} + * Called when the user clicked OK and approved the config including + * the warnings. |config| is without placeholders replaced. + * @param cancalCallback {function()} + * Called when the user decided to heed the warnings and not approve. + */ + open : function(configSchema, configFilledIn, onlyIfNeeded, + okCallback, cancelCallback) + { + assert(typeof(okCallback) == "function"); + assert(typeof(cancelCallback) == "function"); + // needed() also checks the parameters + var needed = this.needed(configSchema, configFilledIn); + if ((needed == 0) && onlyIfNeeded) { + okCallback(); + return; + } + + assert(needed > 0 , "security dialog opened needlessly"); + this._currentConfigFilledIn = configFilledIn; + this._okCallback = okCallback; + this._cancelCallback = cancelCallback; + var incoming = configFilledIn.incoming; + var outgoing = configFilledIn.outgoing; + + _hide("mastervbox"); + _show("warningbox"); + // reset dialog, in case we've shown it before + e("acknowledge_warning").checked = false; + _disable("iknow"); + e("incoming_technical").removeAttribute("expanded"); + e("incoming_details").setAttribute("collapsed", true); + e("outgoing_technical").removeAttribute("expanded"); + e("outgoing_details").setAttribute("collapsed", true); + + if (needed & this._inSecurityBad) { + setText("warning_incoming", gStringsBundle.getFormattedString( + "cleartext_warning", [incoming.hostname])); + setText("incoming_details", gStringsBundle.getString( + "cleartext_details")); + _show("incoming_box"); + } else if (needed & this._inCertBad) { + setText("warning_incoming", gStringsBundle.getFormattedString( + "selfsigned_warning", [incoming.hostname])); + setText("incoming_details", gStringsBundle.getString( + "selfsigned_details")); + _show("incoming_box"); + } else { + _hide("incoming_box"); + } + + if (needed & this._outSecurityBad) { + setText("warning_outgoing", gStringsBundle.getFormattedString( + "cleartext_warning", [outgoing.hostname])); + setText("outgoing_details", gStringsBundle.getString( + "cleartext_details")); + _show("outgoing_box"); + } else if (needed & this._outCertBad) { + setText("warning_outgoing", gStringsBundle.getFormattedString( + "selfsigned_warning", [outgoing.hostname])); + setText("outgoing_details", gStringsBundle.getString( + "selfsigned_details")); + _show("outgoing_box"); + } else { + _hide("outgoing_box"); + } + _show("acknowledge_warning"); + assert(!e("incoming_box").hidden || !e("outgoing_box").hidden, + "warning dialog shown for unknown reason"); + + window.sizeToContent(); + }, + + toggleDetails : function (id) + { + let details = e(id + "_details"); + let tech = e(id + "_technical"); + if (details.getAttribute("collapsed")) { + details.removeAttribute("collapsed"); + tech.setAttribute("expanded", true); + } else { + details.setAttribute("collapsed", true); + tech.removeAttribute("expanded"); + } + }, + + /** + * user checked checkbox that he understood it and wishes + * to ignore the warning. + */ + toggleAcknowledge : function() + { + if (e("acknowledge_warning").checked) { + _enable("iknow"); + } else { + _disable("iknow"); + } + }, + + /** + * [Cancel] button pressed. Get me out of here! + */ + onCancel : function() + { + _hide("warningbox"); + _show("mastervbox"); + window.sizeToContent(); + + this._cancelCallback(); + }, + + /** + * [OK] button pressed. + * Implies that the user toggled the acknowledge checkbox, + * i.e. approved the config and ignored the warnings, + * otherwise the button would have been disabled. + */ + onOK : function() + { + assert(e("acknowledge_warning").checked); + + var overrideOK = this.showCertOverrideDialog(this._currentConfigFilledIn); + if (!overrideOK) { + this.onCancel(); + return; + } + + // need filled in, in case hostname is placeholder + var storeConfig = this._currentConfigFilledIn.copy(); + this._acknowledged.push(storeConfig.incoming); + this._acknowledged.push(storeConfig.outgoing); + + _show("mastervbox"); + _hide("warningbox"); + window.sizeToContent(); + + this._okCallback(); + }, + + /** + * Shows a(nother) dialog which allows the user to see and override + * (manually accept) a bad certificate. It also optionally adds it + * permanently to the "good certs" store of NSS in the profile. + * Only shows the dialog, if there are bad certs. Otherwise, it's a no-op. + * + * The dialog is the standard PSM cert override dialog. + * + * @param config {AccountConfig} concrete + * @returns true, if all certs are fine or the user accepted them. + * false, if the user cancelled. + * + * static function + * sync function: blocks until the dialog is closed. + */ + showCertOverrideDialog : function(config) + { + if (config.incoming.socketType > 1 && // SSL or STARTTLS + config.incoming.badCert) { + var params = { + exceptionAdded : false, + prefetchCert : true, + location : config.incoming.targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + if (params.exceptionAdded) { // set by dialog + config.incoming.badCert = false; + } else { + return false; + } + } + if (!config.outgoing.existingServerKey) { + if (config.outgoing.socketType > 1 && // SSL or STARTTLS + config.outgoing.badCert) { + var params = { + exceptionAdded : false, + prefetchCert : true, + location : config.outgoing.targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + if (params.exceptionAdded) { // set by dialog + config.outgoing.badCert = false; + } else { + return false; + } + } + } + return true; + }, +} +var gSecurityWarningDialog = new SecurityWarningDialog(); diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.xul b/mailnews/base/prefs/content/accountcreation/emailWizard.xul new file mode 100644 index 000000000..261545671 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/emailWizard.xul @@ -0,0 +1,497 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountCreation.css" + type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % acDTD SYSTEM "chrome://messenger/locale/accountCreation.dtd"> + %acDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="autoconfigWizard" + windowtype="mail:autoconfig" + title="&autoconfigWizard.title;" + onload="gEmailConfigWizard.onLoad();" + onkeypress="gEmailConfigWizard.onKeyDown(event);" + onclose="gEmailConfigWizard.onWizardShutdown();" + onunload="gEmailConfigWizard.onWizardShutdown();" + > + + <stringbundleset> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + <stringbundle id="strings" + src="chrome://messenger/locale/accountCreation.properties"/> + <stringbundle id="utilstrings" + src="chrome://messenger/locale/accountCreationUtil.properties"/> + <stringbundle id="bundle_messenger" + src="chrome://messenger/locale/messenger.properties"/> + </stringbundleset> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/util.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/accountConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/emailWizard.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/sanitizeDatatypes.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/fetchhttp.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/readFromXML.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/guessConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/verifyConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/fetchConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/createInBackend.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/MyBadCertHandler.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountUtils.js" /> + + <keyset id="mailKeys"> + <key keycode="VK_ESCAPE" oncommand="window.close();"/> + </keyset> + + <panel id="insecureserver-cleartext-panel" class="popup-panel"> + <hbox> + <image class="insecureLarry"/> + <vbox flex="1"> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureUnencrypted.description;</description> + </vbox> + </hbox> + </panel> + <panel id="insecureserver-selfsigned-panel" class="popup-panel"> + <hbox> + <image class="insecureLarry"/> + <vbox flex="1"> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureSelfSigned.description;</description> + </vbox> + </hbox> + </panel> + <panel id="secureserver-panel" class="popup-panel"> + <hbox> + <image class="secureLarry"/> + <vbox flex="1"> + <description class="title">&secureServer.description;</description> + </vbox> + </hbox> + </panel> + + <tooltip id="insecureserver-cleartext"> + <hbox> + <image class="insecureLarry"/> + <vbox> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureServer.tooltip.details;</description> + </vbox> + </hbox> + </tooltip> + <tooltip id="insecureserver-selfsigned"> + <hbox> + <image class="insecureLarry"/> + <vbox> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureServer.tooltip.details;</description> + </vbox> + </hbox> + </tooltip> + <tooltip id="secureservertooltip"> + <hbox> + <image class="secureLarry"/> + <description class="title">&secureServer.description;</description> + </hbox> + </tooltip> + <tooltip id="optional-password"> + <description>&password.text;</description> + </tooltip> + + <spacer id="fullwidth"/> + + <vbox id="mastervbox" class="mastervbox" flex="1"> + <grid id="initialSettings"> + <columns> + <column/> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <label accesskey="&name.accesskey;" + class="autoconfigLabel" + value="&name.label;" + control="realname"/> + <textbox id="realname" + class="padded" + placeholder="&name.placeholder;" + oninput="gEmailConfigWizard.onInputRealname();" + onblur="gEmailConfigWizard.onBlurRealname();"/> + <hbox> + <description id="nametext" class="initialDesc">&name.text;</description> + <image id="nameerroricon" + hidden="true" + class="warningicon"/> + <description id="nameerror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center"> + <label accesskey="&email.accesskey;" + class="autoconfigLabel" + value="&email.label;" + control="email"/> + <textbox id="email" + class="padded uri-element" + placeholder="&email.placeholder;" + oninput="gEmailConfigWizard.onInputEmail();" + onblur="gEmailConfigWizard.onBlurEmail();"/> + <hbox> + <image id="emailerroricon" + hidden="true" + class="warningicon"/> + <description id="emailerror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center"> + <!-- this starts out as text so the emptytext shows, but then + changes to type=password once it's not empty --> + <label accesskey="&password.accesskey;" + class="autoconfigLabel" + value="&password.label;" + control="password" + tooltip="optional-password"/> + <textbox id="password" + class="padded" + placeholder="&password.placeholder;" + type="text" + oninput="gEmailConfigWizard.onInputPassword();" + onfocus="gEmailConfigWizard.onFocusPassword();" + onblur="gEmailConfigWizard.onBlurPassword();"/> + <hbox> + <image id="passworderroricon" + hidden="true" + class="warningicon"/> + <description id="passworderror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center" pack="start"> + <label class="autoconfigLabel"/> + <checkbox id="remember_password" + label="&rememberPassword.label;" + accesskey="&rememberPassword.accesskey;" + checked="true"/> + </row> + </rows> + </grid> + <spacer flex="1" /> + + <hbox id="status_area" flex="1"> + <vbox id="status_img_before" pack="start"/> + <description id="status_msg"> </description> + <!-- Include 160 = nbsp, to make the element occupy the + full height, for at least one line. With a normal space, + it does not have sufficient height. --> + <vbox id="status_img_after" pack="start"/> + </hbox> + + <groupbox id="result_area" hidden="true"> + <radiogroup id="result_imappop" orient="horizontal"> + <radio id="result_select_imap" label="&imapLong.label;" value="1" + oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/> + <radio id="result_select_pop3" label="&pop3Long.label;" value="2" + oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/> + </radiogroup> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label class="textbox-label" value="&incoming.label;" + control="result-incoming"/> + <textbox id="result-incoming" disabled="true" flex="1"/> + </row> + <row align="center"> + <label class="textbox-label" value="&outgoing.label;" + control="result-outgoing"/> + <textbox id="result-outgoing" disabled="true" flex="1"/> + </row> + <row align="center"> + <label class="textbox-label" value="&username.label;" + control="result-username"/> + <textbox id="result-username" disabled="true" flex="1"/> + </row> + </rows> + </grid> + </groupbox> + + <groupbox id="manual-edit_area" hidden="true"> + <grid> + <columns> + <column/><!-- row label, e.g. "incoming" --> + <column/><!-- protocol, e.g. "IMAP" --> + <column flex="1"/><!-- hostname / username --> + <column/><!-- port --> + <column/><!-- SSL --> + <column/><!-- auth method --> + </columns> + <rows> + <row id="labels_row" align="center"> + <spacer/> + <spacer/> + <label value="&hostname.label;" class="columnHeader"/> + <label value="&port.label;" class="columnHeader"/> + <label value="&ssl.label;" class="columnHeader"/> + <label value="&auth.label;" class="columnHeader"/> + </row> + <row id="incoming_server_area"> + <hbox align="center" pack="end"> + <label class="textbox-label" + value="&incoming.label;" + control="incoming_hostname"/> + </hbox> + <menulist id="incoming_protocol" + oncommand="gEmailConfigWizard.onChangedProtocolIncoming();" + sizetopopup="always"> + <menupopup> + <menuitem label="&imap.label;" value="1"/> + <menuitem label="&pop3.label;" value="2"/> + </menupopup> + </menulist> + <textbox id="incoming_hostname" + oninput="gEmailConfigWizard.onInputHostname();" + class="host uri-element"/> + <menulist id="incoming_port" + editable="true" + oninput="gEmailConfigWizard.onChangedPortIncoming();" + oncommand="gEmailConfigWizard.onChangedPortIncoming();" + class="port"> + <menupopup/> + </menulist> + <menulist id="incoming_ssl" + class="security" + oncommand="gEmailConfigWizard.onChangedSSLIncoming();" + sizetopopup="always"> + <menupopup> + <!-- values defined in nsMsgSocketType --> + <menuitem label="&autodetect.label;" value="0"/> + <menuitem label="&noEncryption.label;" value="1"/> + <menuitem label="&starttls.label;" value="3"/> + <menuitem label="&sslTls.label;" value="2"/> + </menupopup> + </menulist> + <menulist id="incoming_authMethod" + class="auth" + oncommand="gEmailConfigWizard.onChangedInAuth();" + sizetopopup="always"> + <menupopup> + <menuitem label="&autodetect.label;" value="0"/> + <!-- values defined in nsMsgAuthMethod --> + <!-- labels set from messenger.properties + to avoid duplication --> + <menuitem id="in-authMethod-password-cleartext" value="3"/> + <menuitem id="in-authMethod-password-encrypted" value="4"/> + <menuitem id="in-authMethod-kerberos" value="5"/> + <menuitem id="in-authMethod-ntlm" value="6"/> +#ifdef MOZ_MAILNEWS_OAUTH2 + <menuitem id="in-authMethod-oauth2" value="10" hidden="true"/> +#endif + </menupopup> + </menulist> + </row> + <row id="outgoing_server_area" align="center"> + <label class="textbox-label" + value="&outgoing.label;" + control="outgoing_hostname"/> + <label id="outgoing_protocol" + value="&smtp.label;"/> + <menulist id="outgoing_hostname" + editable="true" + sizetopopup="none" + oninput="gEmailConfigWizard.onInputHostname();" + oncommand="gEmailConfigWizard.onChangedOutgoingDropdown();" + onpopupshowing="gEmailConfigWizard.onOpenOutgoingDropdown();" + class="host uri-element"> + <menupopup id="outgoing_hostname_popup"/> + </menulist> + <menulist id="outgoing_port" + editable="true" + oninput="gEmailConfigWizard.onChangedPortOutgoing();" + oncommand="gEmailConfigWizard.onChangedPortOutgoing();" + class="port"> + <menupopup/> + </menulist> + <menulist id="outgoing_ssl" + class="security" + oncommand="gEmailConfigWizard.onChangedSSLOutgoing();" + sizetopopup="always"> + <menupopup> + <!-- @see incoming --> + <menuitem label="&autodetect.label;" value="0"/> + <menuitem label="&noEncryption.label;" value="1"/> + <menuitem label="&starttls.label;" value="3"/> + <menuitem label="&sslTls.label;" value="2"/> + </menupopup> + </menulist> + <menulist id="outgoing_authMethod" + class="auth" + oncommand="gEmailConfigWizard.onChangedOutAuth(this.selectedItem);" + sizetopopup="always"> + <menupopup> + <menuitem label="&autodetect.label;" value="0"/> + <!-- @see incoming --> + <menuitem id="out-authMethod-no" value="1"/> + <menuitem id="out-authMethod-password-cleartext" value="3"/> + <menuitem id="out-authMethod-password-encrypted" value="4"/> + <menuitem id="out-authMethod-kerberos" value="5"/> + <menuitem id="out-authMethod-ntlm" value="6"/> +#ifdef MOZ_MAILNEWS_OAUTH2 + <menuitem id="out-authMethod-oauth2" value="10" hidden="true"/> +#endif + </menupopup> + </menulist> + </row> + <row id="username_area" align="center"> + <label class="textbox-label" + value="&username.label;"/> + <label class="columnHeader" + value="&incoming.label;" + control="incoming_username"/> + <textbox id="incoming_username" + oninput="gEmailConfigWizard.onInputInUsername();" + class="username"/> + <spacer/> + <label class="columnHeader" + id="outgoing_label" + value="&outgoing.label;" + control="outgoing_username"/> + <textbox id="outgoing_username" + oninput="gEmailConfigWizard.onInputOutUsername();" + class="username"/> + </row> + </rows> + </grid> + </groupbox> + + <spacer flex="1" /> + <hbox id="buttons_area"> + <hbox id="left_buttons_area" align="center" pack="start"> + <button id="provisioner_button" + label="&switch-to-provisioner.label;" + accesskey="&switch-to-provisioner.accesskey;" + class="larger-button" + oncommand="gEmailConfigWizard.onSwitchToProvisioner();"/> + <button id="manual-edit_button" + label="&manual-edit.label;" + accesskey="&manual-edit.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onManualEdit();"/> + <button id="advanced-setup_button" + label="&advancedSetup.label;" + accesskey="&advancedSetup.accesskey;" + disabled="true" + hidden="true" + oncommand="gEmailConfigWizard.onAdvancedSetup();"/> + </hbox> + <spacer flex="1"/> + <hbox id="right_buttons_area" align="center" pack="end"> + <button id="stop_button" + label="&stop.label;" + accesskey="&stop.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onStop();"/> + <button id="cancel_button" + label="&cancel.label;" + accesskey="&cancel.accesskey;" + oncommand="gEmailConfigWizard.onCancel();"/> + <button id="half-manual-test_button" + label="&half-manual-test.label;" + accesskey="&half-manual-test.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onHalfManualTest();"/> + <button id="next_button" + label="&continue.label;" + accesskey="&continue.accesskey;" + hidden="false" + oncommand="gEmailConfigWizard.onNext();"/> + <button id="create_button" + label="&doneAccount.label;" + accesskey="&doneAccount.accesskey;" + class="important-button" + hidden="true" + oncommand="gEmailConfigWizard.onCreate();"/> + </hbox> + </hbox> + </vbox> + + + <vbox id="warningbox" hidden="true" flex="1"> + <hbox class="warning" flex="1"> + <vbox class="larrybox"> + <image id="insecure_larry" class="insecureLarry"/> + </vbox> + <vbox flex="1" class="warning_text"> + <label class="warning-heading">&warning.label;</label> + <vbox id="incoming_box"> + <hbox> + <label class="warning_settings" value="&incomingSettings.label;"/> + <description id="warning_incoming"/> + </hbox> + <label id="incoming_technical" + class="technical_details" + value="&technicaldetails.label;" + onclick="gSecurityWarningDialog.toggleDetails('incoming');"/> + <description id="incoming_details" collapsed="true"/> + </vbox> + <vbox id="outgoing_box"> + <hbox> + <label class="warning_settings" value="&outgoingSettings.label;"/> + <description id="warning_outgoing"/> + </hbox> + <label id="outgoing_technical" + class="technical_details" + value="&technicaldetails.label;" + onclick="gSecurityWarningDialog.toggleDetails('outgoing');"/> + <description id="outgoing_details" collapsed="true"/> + </vbox> + <spacer flex="10"/> + <description id="findoutmore"> + &contactYourProvider.description;</description> + <spacer flex="100"/> + <checkbox id="acknowledge_warning" + label="&confirmWarning.label;" + accesskey="&confirmWarning.accesskey;" + class="acknowledge_checkbox" + oncommand="gSecurityWarningDialog.toggleAcknowledge()"/> + <hbox> + <button id="getmeoutofhere" + label="&changeSettings.label;" + accesskey="&changeSettings.accesskey;" + oncommand="gSecurityWarningDialog.onCancel()"/> + <spacer flex="1"/> + <button id="iknow" + label="&doneAccount.label;" + accesskey="&doneAccount.accesskey;" + disabled="true" + oncommand="gSecurityWarningDialog.onOK()"/> + </hbox> + </vbox> + </hbox> + </vbox> +</window> diff --git a/mailnews/base/prefs/content/accountcreation/fetchConfig.js b/mailnews/base/prefs/content/accountcreation/fetchConfig.js new file mode 100644 index 000000000..2fe604e69 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/fetchConfig.js @@ -0,0 +1,237 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * Tries to find a configuration for this ISP on the local harddisk, in the + * application install directory's "isp" subdirectory. + * Params @see fetchConfigFromISP() + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/JXON.js"); + + +function fetchConfigFromDisk(domain, successCallback, errorCallback) +{ + return new TimeoutAbortable(runAsync(function() + { + try { + // <TB installdir>/isp/example.com.xml + var configLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile); + configLocation.append("isp"); + configLocation.append(sanitize.hostname(domain) + ".xml"); + + var contents = + readURLasUTF8(Services.io.newFileURI(configLocation)); + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Ci.nsIDOMParser); + successCallback(readFromXML(JXON.build( + domParser.parseFromString(contents, "text/xml")))); + } catch (e) { errorCallback(e); } + })); +} + +/** + * Tries to get a configuration from the ISP / mail provider directly. + * + * Disclaimers: + * - To support domain hosters, we cannot use SSL. That means we + * rely on insecure DNS and http, which means the results may be + * forged when under attack. The same is true for guessConfig(), though. + * + * @param domain {String} The domain part of the user's email address + * @param emailAddress {String} The user's email address + * @param successCallback {Function(config {AccountConfig}})} A callback that + * will be called when we could retrieve a configuration. + * The AccountConfig object will be passed in as first parameter. + * @param errorCallback {Function(ex)} A callback that + * will be called when we could not retrieve a configuration, + * for whatever reason. This is expected (e.g. when there's no config + * for this domain at this location), + * so do not unconditionally show this to the user. + * The first paramter will be an exception object or error string. + */ +function fetchConfigFromISP(domain, emailAddress, successCallback, + errorCallback) +{ + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromISP.enabled")) { + errorCallback("ISP fetch disabled per user preference"); + return; + } + + let conf1 = "autoconfig." + sanitize.hostname(domain) + + "/mail/config-v1.1.xml"; + // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04> + let conf2 = sanitize.hostname(domain) + + "/.well-known/autoconfig/mail/config-v1.1.xml"; + // This list is sorted by decreasing priority + var urls = ["https://" + conf1, "https://" + conf2]; + if (!Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.sslOnly")) { + urls.push("http://" + conf1, "http://" + conf2); + } + let sucAbortable = new SuccessiveAbortable(); + var urlArgs = { emailaddress: emailAddress }; + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromISP.sendEmailAddress")) { + delete urlArgs.emailaddress; + } + var time; + + let success = function(result) { + successCallback(readFromXML(result)); + }; + + let error = function(i, e) { + ddump("fetchisp " + i + " <" + urls[i] + "> took " + + (Date.now() - time) + "ms and failed with " + e); + + if (i == urls.length - 1 || // implies all fetches failed + e instanceof CancelledException) { + errorCallback(e); + return; + } + let fetch = new FetchHTTP(urls[i + 1], urlArgs, false, success, + function(e) { error(i + 1, e) }); + sucAbortable.current = fetch; + time = Date.now(); + fetch.start(); + }; + + let fetch = new FetchHTTP(urls[0], urlArgs, false, success, + function(e) { error(0, e) }); + sucAbortable.current = fetch; + time = Date.now(); + fetch.start(); + return sucAbortable; +} + +/** + * Tries to get a configuration for this ISP from a central database at + * Mozilla servers. + * Params @see fetchConfigFromISP() + */ + +function fetchConfigFromDB(domain, successCallback, errorCallback) +{ + let url = Services.prefs.getCharPref("mailnews.auto_config_url"); + domain = sanitize.hostname(domain); + + // If we don't specify a place to put the domain, put it at the end. + if (!url.includes("{{domain}}")) + url = url + domain; + else + url = url.replace("{{domain}}", domain); + url = url.replace("{{accounts}}", MailServices.accounts.accounts.length); + + if (!url.length) + return errorCallback("no fetch url set"); + let fetch = new FetchHTTP(url, null, false, + function(result) + { + successCallback(readFromXML(result)); + }, + errorCallback); + fetch.start(); + return fetch; +} + +/** + * Does a lookup of DNS MX, to get the server who is responsible for + * recieving mail for this domain. Then it takes the domain of that + * server, and does another lookup (in ISPDB and possible at ISP autoconfig + * server) and if such a config is found, returns that. + * + * Disclaimers: + * - DNS is unprotected, meaning the results could be forged. + * The same is true for fetchConfigFromISP() and guessConfig(), though. + * - DNS MX tells us the incoming server, not the mailbox (IMAP) server. + * They are different. This mechnism is only an approximation + * for hosted domains (yourname.com is served by mx.hoster.com and + * therefore imap.hoster.com - that "therefore" is exactly the + * conclusional jump we make here.) and alternative domains + * (e.g. yahoo.de -> yahoo.com). + * - We make a look up for the base domain. E.g. if MX is + * mx1.incoming.servers.hoster.com, we look up hoster.com. + * Thanks to Services.eTLD, we also get bbc.co.uk right. + * + * Params @see fetchConfigFromISP() + */ +function fetchConfigForMX(domain, successCallback, errorCallback) +{ + domain = sanitize.hostname(domain); + + var sucAbortable = new SuccessiveAbortable(); + var time = Date.now(); + sucAbortable.current = getMX(domain, + function(mxHostname) // success + { + ddump("getmx took " + (Date.now() - time) + "ms"); + let sld = Services.eTLD.getBaseDomainFromHost(mxHostname); + ddump("base domain " + sld + " for " + mxHostname); + if (sld == domain) + { + errorCallback("MX lookup would be no different from domain"); + return; + } + sucAbortable.current = fetchConfigFromDB(sld, successCallback, + errorCallback); + }, + errorCallback); + return sucAbortable; +} + +/** + * Queries the DNS MX for the domain + * + * The current implementation goes to a web service to do the + * DNS resolve for us, because Mozilla unfortunately has no implementation + * to do it. That's just a workaround. Once bug 545866 is fixed, we make + * the DNS query directly on the client. The API of this function should not + * change then. + * + * Returns (in successCallback) the hostname of the MX server. + * If there are several entires with different preference values, + * only the most preferred (i.e. those with the lowest value) + * is returned. If there are several most preferred servers (i.e. + * round robin), only one of them is returned. + * + * @param domain @see fetchConfigFromISP() + * @param successCallback {function(hostname {String}) + * Called when we found an MX for the domain. + * For |hostname|, see description above. + * @param errorCallback @see fetchConfigFromISP() + * @returns @see fetchConfigFromISP() + */ +function getMX(domain, successCallback, errorCallback) +{ + domain = sanitize.hostname(domain); + + let url = Services.prefs.getCharPref("mailnews.mx_service_url"); + if (!url) + errorCallback("no URL for MX service configured"); + url += domain; + + let fetch = new FetchHTTP(url, null, false, + function(result) + { + // result is plain text, with one line per server. + // So just take the first line + ddump("MX query result: \n" + result + "(end)"); + assert(typeof(result) == "string"); + let first = result.split("\n")[0]; + first.toLowerCase().replace(/[^a-z0-9\-_\.]*/g, ""); + if (first.length == 0) + { + errorCallback("no MX found"); + return; + } + successCallback(first); + }, + errorCallback); + fetch.start(); + return fetch; +} diff --git a/mailnews/base/prefs/content/accountcreation/fetchhttp.js b/mailnews/base/prefs/content/accountcreation/fetchhttp.js new file mode 100644 index 000000000..d2323f7b6 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/fetchhttp.js @@ -0,0 +1,264 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * This is a small wrapper around XMLHttpRequest, which solves various + * inadequacies of the API, e.g. error handling. It is entirely generic and + * can be used for purposes outside of even mail. + * + * It does not provide download progress, but assumes that the + * fetched resource is so small (<1 10 KB) that the roundtrip and + * response generation is far more significant than the + * download time of the response. In other words, it's fine for RPC, + * but not for bigger file downloads. + */ + +Components.utils.import("resource://gre/modules/JXON.js"); + +/** + * Set up a fetch. + * + * @param url {String} URL of the server function. + * ATTENTION: The caller needs to make sure that the URL is secure to call. + * @param urlArgs {Object, associative array} Parameters to add + * to the end of the URL as query string. E.g. + * { foo: "bla", bar: "blub blub" } will add "?foo=bla&bar=blub%20blub" + * to the URL + * (unless the URL already has a "?", then it adds "&foo..."). + * The values will be urlComponentEncoded, so pass them unencoded. + * @param post {Boolean} HTTP GET or POST + * Only influences the HTTP request method, + * i.e. first line of the HTTP request, not the body or parameters. + * Use POST when you modify server state, + * GET when you only request information. + * + * @param successCallback {Function(result {String})} + * Called when the server call worked (no errors). + * |result| will contain the body of the HTTP reponse, as string. + * @param errorCallback {Function(ex)} + * Called in case of error. ex contains the error + * with a user-displayable but not localized |.message| and maybe a + * |.code|, which can be either + * - an nsresult error code, + * - an HTTP result error code (0...1000) or + * - negative: 0...-100 : + * -2 = can't resolve server in DNS etc. + * -4 = response body (e.g. XML) malformed + */ +/* not yet supported: + * @param headers {Object, associative array} Like urlArgs, + * just that the params will be added as HTTP headers. + * { foo: "blub blub" } will add "Foo: Blub blub" + * The values will be urlComponentEncoded, apart from space, + * so pass them unencoded. + * @param headerArgs {Object, associative array} Like urlArgs, + * just that the params will be added as HTTP headers. + * { foo: "blub blub" } will add "X-Moz-Arg-Foo: Blub blub" + * The values will be urlComponentEncoded, apart from space, + * so pass them unencoded. + * @param bodyArgs {Object, associative array} Like urlArgs, + * just that the params will be sent x-url-encoded in the body, + * like a HTML form post. + * The values will be urlComponentEncoded, so pass them unencoded. + * This cannot be used together with |uploadBody|. + * @param uploadbody {Object} Arbitrary object, which to use as + * body of the HTTP request. Will also set the mimetype accordingly. + * Only supported object types, currently only E4X is supported + * (sending XML). + * Usually, you have nothing to upload, so just pass |null|. + */ +function FetchHTTP(url, urlArgs, post, successCallback, errorCallback) +{ + assert(typeof(successCallback) == "function", "BUG: successCallback"); + assert(typeof(errorCallback) == "function", "BUG: errorCallback"); + this._url = sanitize.string(url); + if (!urlArgs) + urlArgs = {}; + + this._urlArgs = urlArgs; + this._post = sanitize.boolean(post); + this._successCallback = successCallback; + this._errorCallback = errorCallback; +} +FetchHTTP.prototype = +{ + __proto__: Abortable.prototype, + _url : null, // URL as passed to ctor, without arguments + _urlArgs : null, + _post : null, + _successCallback : null, + _errorCallback : null, + _request : null, // the XMLHttpRequest object + result : null, + + start : function() + { + var url = this._url; + for (var name in this._urlArgs) + { + url += (!url.includes("?") ? "?" : "&") + + name + "=" + encodeURIComponent(this._urlArgs[name]); + } + this._request = new XMLHttpRequest(); + let request = this._request; + request.open(this._post ? "POST" : "GET", url); + request.channel.loadGroup = null; + // needs bug 407190 patch v4 (or higher) - uncomment if that lands. + // try { + // var channel = request.channel.QueryInterface(Ci.nsIHttpChannel2); + // channel.connectTimeout = 5; + // channel.requestTimeout = 5; + // } catch (e) { dump(e + "\n"); } + + var me = this; + request.onload = function() { me._response(true); } + request.onerror = function() { me._response(false); } + request.send(null); + }, + _response : function(success, exStored) + { + try + { + var errorCode = null; + var errorStr = null; + + if (success && this._request.status >= 200 && + this._request.status < 300) // HTTP level success + { + try + { + // response + var mimetype = this._request.getResponseHeader("Content-Type"); + if (!mimetype) + mimetype = ""; + mimetype = mimetype.split(";")[0]; + if (mimetype == "text/xml" || + mimetype == "application/xml" || + mimetype == "text/rdf") + { + this.result = JXON.build(this._request.responseXML); + } + else + { + //ddump("mimetype: " + mimetype + " only supported as text"); + this.result = this._request.responseText; + } + //ddump("result:\n" + this.result); + } + catch (e) + { + success = false; + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties") + .GetStringFromName("bad_response_content.error"); + errorCode = -4; + } + } + else + { + success = false; + errorCode = this._request.status; + if (errorCode == 0) { + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties") + .GetStringFromName("cannot_contact_server.error"); + ddump(errorStr); + } else { + errorStr = this._request.statusText; + } + } + + // Callbacks + if (success) + { + try { + this._successCallback(this.result); + } catch (e) { + logException(e); + this._error(e); + } + } + else if (exStored) + this._error(exStored); + else + this._error(new ServerException(errorStr, errorCode, this._url)); + + if (this._finishedCallback) + { + try { + this._finishedCallback(this); + } catch (e) { + logException(e); + this._error(e); + } + } + + } catch (e) { + // error in our fetchhttp._response() code + logException(e); + this._error(e); + } + }, + _error : function(e) + { + try { + this._errorCallback(e); + } catch (e) { + // error in errorCallback, too! + logException(e); + alertPrompt("Error in errorCallback for fetchhttp", e); + } + }, + /** + * Call this between start() and finishedCallback fired. + */ + cancel : function(ex) + { + assert(!this.result, "Call already returned"); + + this._request.abort(); + + // Need to manually call error handler + // <https://bugzilla.mozilla.org/show_bug.cgi?id=218236#c11> + this._response(false, ex ? ex : new UserCancelledException()); + }, + /** + * Allows caller or lib to be notified when the call is done. + * This is useful to enable and disable a Cancel button in the UI, + * which allows to cancel the network request. + */ + setFinishedCallback : function(finishedCallback) + { + this._finishedCallback = finishedCallback; + } +} + +function CancelledException(msg) +{ + Exception.call(this, msg); +} +CancelledException.prototype = Object.create(Exception.prototype); +CancelledException.prototype.constructor = CancelledException; + +function UserCancelledException(msg) +{ + // The user knows they cancelled so I don't see a need + // for a message to that effect. + if (!msg) + msg = "User cancelled"; + CancelledException.call(this, msg); +} +UserCancelledException.prototype = Object.create(CancelledException.prototype); +UserCancelledException.prototype.constructor = UserCancelledException; + +function ServerException(msg, code, uri) +{ + Exception.call(this, msg); + this.code = code; + this.uri = uri; +} +ServerException.prototype = Object.create(Exception.prototype); +ServerException.prototype.constructor = ServerException; + diff --git a/mailnews/base/prefs/content/accountcreation/guessConfig.js b/mailnews/base/prefs/content/accountcreation/guessConfig.js new file mode 100644 index 000000000..6ba9f1dfb --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/guessConfig.js @@ -0,0 +1,1192 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Cu.import("resource:///modules/gloda/log4moz.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +// This is a bit ugly - we set outgoingDone to false +// when emailWizard.js cancels the outgoing probe because the user picked +// an outoing server. It does this by poking the probeAbortable object, +// so we need outgoingDone to have global scope. +var outgoingDone = false; + +/** + * Try to guess the config, by: + * - guessing hostnames (pop3.<domain>, pop.<domain>, imap.<domain>, + * mail.<domain> etc.) + * - probing known ports (for IMAP, POP3 etc., with SSL, STARTTLS etc.) + * - opening a connection via the right protocol and checking the + * protocol-specific CAPABILITIES like that the server returns. + * + * Final verification is not done here, but in verifyConfig(). + * + * This function is async. + * @param domain {String} the domain part of the email address + * @param progressCallback {function(type, hostname, port, ssl, done)} + * Called when we try a new hostname/port. + * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp" + * hostname {String} + * port {Integer} + * socketType {Integer-enum} @see AccountConfig.incoming.socketType + * 1 = plain, 2 = SSL, 3 = STARTTLS + * done {Boolean} false, if we start probing this host/port, true if we're + * done and the host is good. (there is no notification when a host is + * bad, we'll just tell about the next host tried) + * @param successCallback {function(config {AccountConfig})} + * Called when we could guess the config. + * param accountConfig {AccountConfig} The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param errorCallback function(ex) + * Called when we could guess not the config, either + * because we have not found anything or + * because there was an error (e.g. no network connection). + * The ex.message will contain a user-presentable message. + * @param resultConfig {AccountConfig} (optional) + * A config which may be partially filled in. If so, it will be used as base + * for the guess. + * @param which {String-enum} (optional) "incoming", "outgoing", or "both". + * Default "both". Whether to guess only the incoming or outgoing server. + * @result {Abortable} Allows you to cancel the guess + */ +function guessConfig(domain, progressCallback, successCallback, errorCallback, + resultConfig, which) +{ + assert(typeof(progressCallback) == "function", "need progressCallback"); + assert(typeof(successCallback) == "function", "need successCallback"); + assert(typeof(errorCallback) == "function", "need errorCallback"); + +#ifdef MOZ_MAILNEWS_OAUTH2 + // Servers that we know enough that they support OAuth2 do not need guessing. + if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) { + successCallback(resultConfig); + return null; + } +#endif + + if (!resultConfig) + resultConfig = new AccountConfig(); + resultConfig.source = AccountConfig.kSourceGuess; + + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.guess.enabled")) { + errorCallback("Guessing config disabled per user preference"); + return; + } + + var incomingHostDetector = null; + var outgoingHostDetector = null; + var incomingEx = null; // if incoming had error, store ex here + var outgoingEx = null; // if incoming had error, store ex here + var incomingDone = (which == "outgoing"); + var outgoingDone = (which == "incoming"); + // If we're offline, we're going to pick the most common settings. + // (Not the "best" settings, but common). + if (Services.io.offline) + { + resultConfig.source = AccountConfig.kSourceUser; + resultConfig.incoming.hostname = "mail." + domain; + resultConfig.incoming.username = resultConfig.identity.emailAddress; + resultConfig.outgoing.username = resultConfig.identity.emailAddress; + resultConfig.incoming.type = "imap"; + resultConfig.incoming.port = 143; + resultConfig.incoming.socketType = 3; // starttls + resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.outgoing.hostname = "smtp." + domain; + resultConfig.outgoing.socketType = 1; + resultConfig.outgoing.port = 587; + resultConfig.outgoing.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.incomingAlternatives.push({ + hostname: "mail." + domain, + username: resultConfig.identity.emailAddress, + type: "pop3", + port: 110, + socketType: 3, + auth: Ci.nsMsgAuthMethod.passwordCleartext + }); + successCallback(resultConfig); + return null; + } + var progress = function(thisTry) + { + progressCallback(protocolToString(thisTry.protocol), thisTry.hostname, + thisTry.port, sslConvertToSocketType(thisTry.ssl), false, + resultConfig); + }; + + var updateConfig = function(config) + { + resultConfig = config; + }; + + var errorInCallback = function(e) + { + // The caller's errorCallback threw. + // hopefully shouldn't happen for users. + alertPrompt("Error in errorCallback for guessConfig()", e); + }; + + var checkDone = function() + { + if (incomingEx) + { + try { + errorCallback(incomingEx, resultConfig); + } catch (e) { errorInCallback(e); } + return; + } + if (outgoingEx) + { + try { + errorCallback(outgoingEx, resultConfig); + } catch (e) { errorInCallback(e); } + return; + } + if (incomingDone && outgoingDone) + { + try { + successCallback(resultConfig); + } catch (e) { + try { + errorCallback(e); + } catch (e) { errorInCallback(e); } + } + return; + } + }; + + var logger = Log4Moz.getConfiguredLogger("mail.wizard"); + var HostTryToAccountServer = function(thisTry, server) + { + server.type = protocolToString(thisTry.protocol); + server.hostname = thisTry.hostname; + server.port = thisTry.port; + server.socketType = sslConvertToSocketType(thisTry.ssl); + server.auth = chooseBestAuthMethod(thisTry.authMethods); + server.authAlternatives = thisTry.authMethods; + // TODO + // cert is also bad when targetSite is set. (Same below for incoming.) + // Fix SSLErrorHandler and security warning dialog in emailWizard.js. + server.badCert = thisTry.selfSignedCert; + server.targetSite = thisTry.targetSite; + logger.info("CHOOSING " + server.type + " "+ server.hostname + ":" + + server.port + ", auth method " + server.auth + " " + + server.authAlternatives.join(",") + ", SSL " + server.socketType + + (server.badCert ? " (bad cert!)" : "")); + }; + + var outgoingSuccess = function(thisTry, alternativeTries) + { + assert(thisTry.protocol == SMTP, "I only know SMTP for outgoing"); + // Ensure there are no previously saved outgoing errors, if we've got + // success here. + outgoingEx = null; + HostTryToAccountServer(thisTry, resultConfig.outgoing); + + for (let alternativeTry of alternativeTries) + { + // resultConfig.createNewOutgoing(); misses username etc., so copy + let altServer = deepCopy(resultConfig.outgoing); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.outgoingAlternatives); + resultConfig.outgoingAlternatives.push(altServer); + } + + progressCallback(resultConfig.outgoing.type, + resultConfig.outgoing.hostname, resultConfig.outgoing.port, + resultConfig.outgoing.socketType, true, resultConfig); + outgoingDone = true; + checkDone(); + }; + + var incomingSuccess = function(thisTry, alternativeTries) + { + // Ensure there are no previously saved incoming errors, if we've got + // success here. + incomingEx = null; + HostTryToAccountServer(thisTry, resultConfig.incoming); + + for (let alternativeTry of alternativeTries) + { + // resultConfig.createNewIncoming(); misses username etc., so copy + let altServer = deepCopy(resultConfig.incoming); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.incomingAlternatives); + resultConfig.incomingAlternatives.push(altServer); + } + + progressCallback(resultConfig.incoming.type, + resultConfig.incoming.hostname, resultConfig.incoming.port, + resultConfig.incoming.socketType, true, resultConfig); + incomingDone = true; + checkDone(); + }; + + var incomingError = function(ex) + { + incomingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + var outgoingError = function(ex) + { + outgoingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + incomingHostDetector = new IncomingHostDetector(progress, incomingSuccess, + incomingError); + outgoingHostDetector = new OutgoingHostDetector(progress, outgoingSuccess, + outgoingError); + if (which == "incoming" || which == "both") + { + incomingHostDetector.start(resultConfig.incoming.hostname ? + resultConfig.incoming.hostname : domain, + !!resultConfig.incoming.hostname, resultConfig.incoming.type, + resultConfig.incoming.port, resultConfig.incoming.socketType); + } + if (which == "outgoing" || which == "both") + { + outgoingHostDetector.start(resultConfig.outgoing.hostname ? + resultConfig.outgoing.hostname : domain, + !!resultConfig.outgoing.hostname, "smtp", + resultConfig.outgoing.port, resultConfig.outgoing.socketType); + } + + return new GuessAbortable(incomingHostDetector, outgoingHostDetector, + updateConfig); +} + +function GuessAbortable(incomingHostDetector, outgoingHostDetector, + updateConfig) +{ + Abortable.call(this); + this._incomingHostDetector = incomingHostDetector; + this._outgoingHostDetector = outgoingHostDetector; + this._updateConfig = updateConfig; +} +GuessAbortable.prototype = Object.create(Abortable.prototype); +GuessAbortable.prototype.constructor = GuessAbortable; +GuessAbortable.prototype.cancel = function(ex) +{ + this._incomingHostDetector.cancel(ex); + this._outgoingHostDetector.cancel(ex); +}; + +////////////////////////////////////////////////////////////////////////////// +// Implementation +// +// Objects, functions and constants that follow are not to be used outside +// this file. + +var kNotTried = 0; +var kOngoing = 1; +var kFailed = 2; +var kSuccess = 3; + +/** + * Internal object holding one server that we should try or did try. + * Used as |thisTry|. + * + * Note: The consts it uses for protocol and ssl are defined towards the end + * of this file and not the same as those used in AccountConfig (type, + * socketType). (fix this) + */ +function HostTry() +{ +} +HostTry.prototype = +{ + // IMAP, POP or SMTP + protocol : UNKNOWN, + // {String} + hostname : undefined, + // {Integer} + port : undefined, + // NONE, SSL or TLS + ssl : UNKNOWN, + // {String} what to send to server + commands : null, + // {Integer-enum} kNotTried, kOngoing, kFailed or kSuccess + status : kNotTried, + // {Abortable} allows to cancel the socket comm + abortable : null, + + // {Array of {Integer-enum}} @see _advertisesAuthMethods() result + // Info about the server, from the protocol and SSL chat + authMethods : null, + // {String} Whether the SSL cert is not from a proper CA + selfSignedCert : false, + // {String} Which host the SSL cert is made for, if not hostname. + // If set, this is an SSL error. + targetSite : null, +}; + +/** + * When the success or errorCallbacks are called to abort the other requests + * which happened in parallel, this ex is used as param for cancel(), so that + * the cancel doesn't trigger another callback. + */ +function CancelOthersException() +{ + CancelledException.call(this, "we're done, cancelling the other probes"); +} +CancelOthersException.prototype = Object.create(CancelledException.prototype); +CancelOthersException.prototype.constructor = CancelOthersException; + +/** + * @param successCallback {function(result {HostTry}, alts {Array of HostTry})} + * Called when the config is OK + * |result| is the most preferred server. + * |alts| currently exists only for |IncomingHostDetector| and contains + * some servers of the other type (POP3 instead of IMAP), if available. + * @param errorCallback {function(ex)} Called when we could not find a config + * @param progressCallback { function(server {HostTry}) } Called when we tried + * (will try?) a new hostname and port + */ +function HostDetector(progressCallback, successCallback, errorCallback) +{ + this.mSuccessCallback = successCallback; + this.mProgressCallback = progressCallback; + this.mErrorCallback = errorCallback; + this._cancel = false; + // {Array of {HostTry}}, ordered by decreasing preference + this._hostsToTry = new Array(); + + // init logging + this._log = Log4Moz.getConfiguredLogger("mail.wizard"); + this._log.info("created host detector"); +} + +HostDetector.prototype = +{ + cancel : function(ex) + { + this._cancel = true; + // We have to actively stop the network calls, as they may result in + // callbacks e.g. to the cert handler. If the dialog is gone by the time + // this happens, the javascript stack is horked. + for (let i = 0; i < this._hostsToTry.length; i++) + { + let thisTry = this._hostsToTry[i]; // {HostTry} + if (thisTry.abortable) + thisTry.abortable.cancel(ex); + thisTry.status = kFailed; // or don't set? Maybe we want to continue. + } + if (ex instanceof CancelOthersException) + return; + if (!ex) + ex = new CancelledException(); + this.mErrorCallback(ex); + }, + + /** + * Start the detection + * + * @param domain {String} to be used as base for guessing. + * Should be a domain (e.g. yahoo.co.uk). + * If hostIsPrecise == true, it should be a full hostname + * @param hostIsPrecise {Boolean} (default false) use only this hostname, + * do not guess hostnames. + * @param type {String-enum}@see AccountConfig type + * (Optional. default, 0, undefined, null = guess it) + * @param port {Integer} (Optional. default, 0, undefined, null = guess it) + * @param socketType {Integer-enum}@see AccountConfig socketType + * (Optional. default, 0, undefined, null = guess it) + */ + start : function(domain, hostIsPrecise, type, port, socketType) + { + domain = domain.replace(/\s*/g, ""); // Remove whitespace + if (!hostIsPrecise) + hostIsPrecise = false; + var protocol = sanitize.translate(type, + { "imap" : IMAP, "pop3" : POP, "smtp" : SMTP }, UNKNOWN); + if (!port) + port = UNKNOWN; + var ssl_only = Services.prefs.getBoolPref("mailnews.auto_config.guess.sslOnly"); + var ssl = ConvertSocketTypeToSSL(socketType); + this._cancel = false; + this._log.info("doing auto detect for protocol " + protocol + + ", domain " + domain + ", (exactly: " + hostIsPrecise + + "), port " + port + ", ssl " + ssl); + + // fill this._hostsToTry + this._hostsToTry = []; + var hostnamesToTry = []; + // if hostIsPrecise is true, it's because that's what the user input + // explicitly, and we'll just try it, nothing else. + if (hostIsPrecise) + hostnamesToTry.push(domain); + else + hostnamesToTry = this._hostnamesToTry(protocol, domain); + + for (let i = 0; i < hostnamesToTry.length; i++) + { + let hostname = hostnamesToTry[i]; + let hostEntries = this._portsToTry(hostname, protocol, ssl, port); + for (let j = 0; j < hostEntries.length; j++) + { + let hostTry = hostEntries[j]; // from getHostEntry() + if (ssl_only && hostTry.ssl == NONE) + continue; + hostTry.hostname = hostname; + hostTry.status = kNotTried; + hostTry.desc = hostTry.hostname + ":" + hostTry.port + + " ssl=" + hostTry.ssl + " " + + protocolToString(hostTry.protocol); + this._hostsToTry.push(hostTry); + } + } + + this._hostsToTry = sortTriesByPreference(this._hostsToTry); + this._tryAll(); + }, + + // We make all host/port combinations run in parallel, store their + // results in an array, and as soon as one finishes successfully and all + // higher-priority ones have failed, we abort all lower-priority ones. + + _tryAll : function() + { + if (this._cancel) + return; + var me = this; + var timeout = Services.prefs.getIntPref("mailnews.auto_config.guess.timeout"); + // We assume we'll resolve the same proxy for all tries, and + // proceed to use the first resolved proxy for all tries. This + // assumption is generally sound, but not always: mechanisms like + // the pref network.proxy.no_proxies_on can make imap.domain and + // pop.domain resolve differently. + doProxy(this._hostsToTry[0].hostname, function(proxy) { + for (let i = 0; i < me._hostsToTry.length; i++) { + let thisTry = me._hostsToTry[i]; // {HostTry} + if (thisTry.status != kNotTried) + continue; + me._log.info(thisTry.desc + ": initializing probe..."); + if (i == 0) // showing 50 servers at once is pointless + me.mProgressCallback(thisTry); + + thisTry.abortable = SocketUtil( + thisTry.hostname, thisTry.port, thisTry.ssl, + thisTry.commands, timeout, proxy, + new SSLErrorHandler(thisTry, me._log), + function(wiredata) // result callback + { + if (me._cancel) + return; // don't use response anymore + me.mProgressCallback(thisTry); + me._processResult(thisTry, wiredata); + me._checkFinished(); + }, + function(e) // error callback + { + if (me._cancel) + return; // who set cancel to true already called mErrorCallback() + me._log.warn(thisTry.desc + ": " + e); + thisTry.status = kFailed; + me._checkFinished(); + } + ); + thisTry.status = kOngoing; + } + }); + }, + + /** + * @param thisTry {HostTry} + * @param wiredata {Array of {String}} what the server returned + * in response to our protocol chat + */ + _processResult : function(thisTry, wiredata) + { + if (thisTry._gotCertError) + { + if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_MISMATCH) + { + thisTry._gotCertError = 0; + thisTry.status = kFailed; + return; + } + + if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_UNTRUSTED || + thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_TIME) + { + this._log.info(thisTry.desc + ": TRYING AGAIN, hopefully with exception recorded"); + thisTry._gotCertError = 0; + thisTry.selfSignedCert = true; // _next_ run gets this exception + thisTry.status = kNotTried; // try again (with exception) + this._tryAll(); + return; + } + } + + if (wiredata == null || wiredata === undefined) + { + this._log.info(thisTry.desc + ": no data"); + thisTry.status = kFailed; + return; + } + this._log.info(thisTry.desc + ": wiredata: " + wiredata.join("")); + thisTry.authMethods = + this._advertisesAuthMethods(thisTry.protocol, wiredata); + if (thisTry.ssl == TLS && !this._hasTLS(thisTry, wiredata)) + { + this._log.info(thisTry.desc + ": STARTTLS wanted, but not offered"); + thisTry.status = kFailed; + return; + } + this._log.info(thisTry.desc + ": success" + + (thisTry.selfSignedCert ? " (selfSignedCert)" : "")); + thisTry.status = kSuccess; + + if (thisTry.selfSignedCert) { // eh, ERROR_UNTRUSTED or ERROR_TIME + // We clear the temporary override now after success. If we clear it + // earlier we get into an infinite loop, probably because the cert + // remembering is temporary and the next try gets a new connection which + // isn't covered by that temporariness. + this._log.info(thisTry.desc + ": clearing validity override for " + + thisTry.hostname); + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .clearValidityOverride(thisTry.hostname, thisTry.port); + } + }, + + _checkFinished : function() + { + var successfulTry = null; + var successfulTryAlternative = null; // POP3 + var unfinishedBusiness = false; + // this._hostsToTry is ordered by decreasing preference + for (let i = 0; i < this._hostsToTry.length; i++) + { + let thisTry = this._hostsToTry[i]; + if (thisTry.status == kNotTried || thisTry.status == kOngoing) + unfinishedBusiness = true; + // thisTry is good, and all higher preference tries failed, so use this + else if (thisTry.status == kSuccess && !unfinishedBusiness) + { + if (!successfulTry) + { + successfulTry = thisTry; + if (successfulTry.protocol == SMTP) + break; + } + else if (successfulTry.protocol != thisTry.protocol) + { + successfulTryAlternative = thisTry; + break; + } + } + } + if (successfulTry && (successfulTryAlternative || !unfinishedBusiness)) + { + this.mSuccessCallback(successfulTry, + successfulTryAlternative ? [ successfulTryAlternative ] : []); + this.cancel(new CancelOthersException()); + } + else if (!unfinishedBusiness) // all failed + { + this._log.info("ran out of options"); + var errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_find_server.error"); + this.mErrorCallback(new Exception(errorMsg)); + // no need to cancel, all failed + } + // else let ongoing calls continue + }, + + + /** + * Which auth mechanism the server claims to support. + * (That doesn't necessarily reflect reality, it is more an upper bound.) + * + * @param protocol {Integer-enum} IMAP, POP or SMTP + * @param capaResponse {Array of {String}} on the wire data + * that the server returned. May be the full exchange or just capa. + * @returns {Array of {Integer-enum} values for AccountConfig.incoming.auth + * (or outgoing), in decreasing order of preference. + * E.g. [ 5, 4 ] for a server that supports only Kerberos and + * encrypted passwords. + */ + _advertisesAuthMethods : function(protocol, capaResponse) + { + // for imap, capabilities include e.g.: + // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN" + // for pop3, the auth mechanisms are returned in capa as the following: + // "CRAM-MD5", "NTLM", "MSN", "GSSAPI" + // For smtp, EHLO will return AUTH and then a list of the + // mechanism(s) supported, e.g., + // AUTH LOGIN NTLM MSN CRAM-MD5 GSSAPI + var result = new Array(); + var line = capaResponse.join("\n").toUpperCase(); + var prefix = ""; + if (protocol == POP) + prefix = ""; + else if (protocol == IMAP) + prefix = "AUTH="; + else if (protocol == SMTP) + prefix = "AUTH.*"; + else + throw NotReached("must pass protocol"); + // add in decreasing order of preference + if (new RegExp(prefix + "GSSAPI").test(line)) + result.push(Ci.nsMsgAuthMethod.GSSAPI); + if (new RegExp(prefix + "CRAM-MD5").test(line)) + result.push(Ci.nsMsgAuthMethod.passwordEncrypted); + if (new RegExp(prefix + "(NTLM|MSN)").test(line)) + result.push(Ci.nsMsgAuthMethod.NTLM); + if (protocol != IMAP || !line.includes("LOGINDISABLED")) + result.push(Ci.nsMsgAuthMethod.passwordCleartext); + return result; + }, + + _hasTLS : function(thisTry, wiredata) + { + var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS"; + return thisTry.ssl == TLS && + wiredata.join("").toUpperCase().includes(capa); + }, +} + +/** + * @param authMethods @see return value of _advertisesAuthMethods() + * Note: the returned auth method will be removed from the array. + * @return one of them, the preferred one + * Note: this might be Kerberos, which might not actually work, + * so you might need to try the others, too. + */ +function chooseBestAuthMethod(authMethods) +{ + if (!authMethods || !authMethods.length) + return Ci.nsMsgAuthMethod.passwordCleartext; + return authMethods.shift(); // take first (= most preferred) +} + + +function IncomingHostDetector( + progressCallback, successCallback, errorCallback) +{ + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +IncomingHostDetector.prototype = +{ + __proto__: HostDetector.prototype, + _hostnamesToTry : function(protocol, domain) + { + var hostnamesToTry = []; + if (protocol != POP) + hostnamesToTry.push("imap." + domain); + if (protocol != IMAP) + { + hostnamesToTry.push("pop3." + domain); + hostnamesToTry.push("pop." + domain); + } + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry : getIncomingTryOrder, +} + +function OutgoingHostDetector( + progressCallback, successCallback, errorCallback) +{ + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +OutgoingHostDetector.prototype = +{ + __proto__: HostDetector.prototype, + _hostnamesToTry : function(protocol, domain) + { + var hostnamesToTry = []; + hostnamesToTry.push("smtp." + domain); + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry : getOutgoingTryOrder, +} + +////////////////////////////////////////////////////////////////////////// +// Encode protocol ports and order of preference + +// Protocol Types +var UNKNOWN = -1; +var IMAP = 0; +var POP = 1; +var SMTP = 2; +// Security Types +var NONE = 0; // no encryption +//1 would be "TLS if available" +var TLS = 2; // STARTTLS +var SSL = 3; // SSL / TLS + +var IMAP_PORTS = {} +IMAP_PORTS[NONE] = 143; +IMAP_PORTS[TLS] = 143; +IMAP_PORTS[SSL] = 993; + +var POP_PORTS = {} +POP_PORTS[NONE] = 110; +POP_PORTS[TLS] = 110; +POP_PORTS[SSL] = 995; + +var SMTP_PORTS = {} +SMTP_PORTS[NONE] = 587; +SMTP_PORTS[TLS] = 587; +SMTP_PORTS[SSL] = 465; + +var CMDS = {} +CMDS[IMAP] = ["1 CAPABILITY\r\n", "2 LOGOUT\r\n"]; +CMDS[POP] = ["CAPA\r\n", "QUIT\r\n"]; +CMDS[SMTP] = ["EHLO we-guess.mozilla.org\r\n", "QUIT\r\n"]; + +/** + * Sort by preference of SSL, IMAP etc. + * @param tries {Array of {HostTry}} + * @returns {Array of {HostTry}} + */ +function sortTriesByPreference(tries) +{ + return tries.sort(function __sortByPreference(a, b) + { + // -1 = a is better; 1 = b is better; 0 = equal + // Prefer SSL/TLS above all else + if (a.ssl != NONE && b.ssl == NONE) + return -1; + if (b.ssl != NONE && a.ssl == NONE) + return 1; + // Prefer IMAP over POP + if (a.protocol == IMAP && b.protocol == POP) + return -1; + if (b.protocol == IMAP && a.protocol == POP) + return 1; + // For hostnames, leave existing sorting, as in _hostnamesToTry() + // For ports, leave existing sorting, as in getOutgoingTryOrder() + return 0; + }); +}; + +// TODO prefer SSL over STARTTLS, +// either in sortTriesByPreference or in getIncomingTryOrder() (and outgoing) + +/** + * @returns {Array of {HostTry}} + */ +function getIncomingTryOrder(host, protocol, ssl, port) +{ + var lowerCaseHost = host.toLowerCase(); + + if (protocol == UNKNOWN && + (lowerCaseHost.startsWith("pop.") || lowerCaseHost.startsWith("pop3."))) + protocol = POP; + else if (protocol == UNKNOWN && lowerCaseHost.startsWith("imap.")) + protocol = IMAP; + + if (protocol != UNKNOWN) { + if (ssl == UNKNOWN) + return [getHostEntry(protocol, TLS, port), + //getHostEntry(protocol, SSL, port), + getHostEntry(protocol, NONE, port)]; + return [getHostEntry(protocol, ssl, port)]; + } + if (ssl == UNKNOWN) + return [getHostEntry(IMAP, TLS, port), + //getHostEntry(IMAP, SSL, port), + getHostEntry(POP, TLS, port), + //getHostEntry(POP, SSL, port), + getHostEntry(IMAP, NONE, port), + getHostEntry(POP, NONE, port)]; + return [getHostEntry(IMAP, ssl, port), + getHostEntry(POP, ssl, port)]; +}; + +/** + * @returns {Array of {HostTry}} + */ +function getOutgoingTryOrder(host, protocol, ssl, port) +{ + assert(protocol == SMTP, "need SMTP as protocol for outgoing"); + if (ssl == UNKNOWN) + { + if (port == UNKNOWN) + // neither SSL nor port known + return [getHostEntry(SMTP, TLS, UNKNOWN), + getHostEntry(SMTP, TLS, 25), + //getHostEntry(SMTP, SSL, UNKNOWN), + getHostEntry(SMTP, NONE, UNKNOWN), + getHostEntry(SMTP, NONE, 25)]; + // port known, SSL not + return [getHostEntry(SMTP, TLS, port), + //getHostEntry(SMTP, SSL, port), + getHostEntry(SMTP, NONE, port)]; + } + // SSL known, port not + if (port == UNKNOWN) + { + if (ssl == SSL) + return [getHostEntry(SMTP, SSL, UNKNOWN)]; + else // TLS or NONE + return [getHostEntry(SMTP, ssl, UNKNOWN), + getHostEntry(SMTP, ssl, 25)]; + } + // SSL and port known + return [getHostEntry(SMTP, ssl, port)]; +}; + +/** + * @returns {HostTry} with proper default port and commands, + * but without hostname. + */ +function getHostEntry(protocol, ssl, port) +{ + if (!port || port == UNKNOWN) { + switch (protocol) { + case POP: + port = POP_PORTS[ssl]; + break; + case IMAP: + port = IMAP_PORTS[ssl]; + break; + case SMTP: + port = SMTP_PORTS[ssl]; + break; + default: + throw new NotReached("unsupported protocol " + protocol); + } + } + + var r = new HostTry(); + r.protocol = protocol; + r.ssl = ssl; + r.port = port; + r.commands = CMDS[protocol]; + return r; +}; + + +// Convert consts from those used here to those from AccountConfig +// TODO adjust consts to match AccountConfig + +// here -> AccountConfig +function sslConvertToSocketType(ssl) +{ + if (ssl == NONE) + return 1; + if (ssl == SSL) + return 2; + if (ssl == TLS) + return 3; + throw new NotReached("unexpected SSL type"); +} + +// AccountConfig -> here +function ConvertSocketTypeToSSL(socketType) +{ + if (socketType == 1) + return NONE; + if (socketType == 2) + return SSL; + if (socketType == 3) + return TLS; + return UNKNOWN; +} + +// here -> AccountConfig +function protocolToString(type) +{ + if (type == IMAP) + return "imap"; + if (type == POP) + return "pop3"; + if (type == SMTP) + return "smtp"; + throw new NotReached("unexpected protocol"); +} + + + +///////////////////////////////////////////////////////// +// SSL cert error handler + +/** + * Called by MyBadCertHandler.js, which called by PSM + * to tell us about SSL certificate errors. + * @param thisTry {HostTry} + * @param logger {Log4Moz logger} + */ +function SSLErrorHandler(thisTry, logger) +{ + this._try = thisTry; + this._log = logger; + // _ gotCertError will be set to an error code (one of those defined in + // nsICertOverrideService) + this._gotCertError = 0; +} +SSLErrorHandler.prototype = +{ + processCertError : function(socketInfo, status, targetSite) + { + this._log.error("Got Cert error for "+ targetSite); + + if (!status) + return true; + + let cert = status.QueryInterface(Ci.nsISSLStatus).serverCert; + let flags = 0; + + let parts = targetSite.split(":"); + let host = parts[0]; + let port = parts[1]; + + /* The following 2 cert problems are unfortunately common: + * 1) hostname mismatch: + * user is custeromer at a domain hoster, he owns yourname.org, + * and the IMAP server is imap.hoster.com (but also reachable as + * imap.yourname.org), and has a cert for imap.hoster.com. + * 2) self-signed: + * a company has an internal IMAP server, and it's only for + * 30 employees, and they didn't want to buy a cert, so + * they use a self-signed cert. + * + * We would like the above to pass, somehow, with user confirmation. + * The following case should *not* pass: + * + * 1) MITM + * User has @gmail.com, and an attacker is between the user and + * the Internet and runs a man-in-the-middle (MITM) attack. + * Attacker controls DNS and sends imap.gmail.com to his own + * imap.attacker.com. He has either a valid, CA-issued + * cert for imap.attacker.com, or a self-signed cert. + * Of course, attacker.com could also be legit-sounding gmailservers.com. + * + * What makes it dangerous is that we (!) propose the server to the user, + * and he cannot judge whether imap.gmailservers.com is correct or not, + * and he will likely approve it. + */ + + if (status.isDomainMismatch) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH; + flags |= Ci.nsICertOverrideService.ERROR_MISMATCH; + } + else if (status.isUntrusted) { // e.g. self-signed + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED; + flags |= Ci.nsICertOverrideService.ERROR_UNTRUSTED; + } + else if (status.isNotValidAtThisTime) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_TIME; + flags |= Ci.nsICertOverrideService.ERROR_TIME; + } + else { + this._try._gotCertError = -1; // other + } + + /* We will add a temporary cert exception here, so that + * we can continue and connect and try. + * But we will remove it again as soon as we close the + * connection, in _processResult(). + * _gotCertError will serve as the marker that we + * have to clear the override later. + * + * In verifyConfig(), before we send the password, we *must* + * get another cert exception, this time with dialog to the user + * so that he gets informed about this and can make a choice. + */ + + this._try.targetSite = targetSite; + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .rememberValidityOverride(host, port, cert, flags, + true); // temporary override + this._log.warn("!! Overrode bad cert temporarily " + host + " " + port + + " flags=" + flags + "\n"); + return true; + }, +} + + + +////////////////////////////////////////////////////////////////// +// Socket Util + + +/** + * @param hostname {String} The DNS hostname to connect to. + * @param port {Integer} The numberic port to connect to on the host. + * @param ssl {Integer} SSL, TLS or NONE + * @param commands {Array of String}: protocol commands + * to send to the server. + * @param timeout {Integer} seconds to wait for a server response, then cancel. + * @param proxy {nsIProxyInfo} The proxy to use (or null to not use any). + * @param sslErrorHandler {SSLErrorHandler} + * @param resultCallback {function(wiredata)} This function will + * be called with the result string array from the server + * or null if no communication occurred. + * @param errorCallback {function(e)} + */ +function SocketUtil(hostname, port, ssl, commands, timeout, proxy, + sslErrorHandler, resultCallback, errorCallback) +{ + assert(commands && commands.length, "need commands"); + + var index = 0; // commands[index] is next to send to server + var initialized = false; + var aborted = false; + + function _error(e) + { + if (aborted) + return; + aborted = true; + errorCallback(e); + } + + function timeoutFunc() + { + if (!initialized) + _error("timeout"); + } + + // In case DNS takes too long or does not resolve or another blocking + // issue occurs before the timeout can be set on the socket, this + // ensures that the listener callback will be fired in a timely manner. + // XXX There might to be some clean up needed after the timeout is fired + // for socket and io resources. + + // The timeout value plus 2 seconds + setTimeout(timeoutFunc, (timeout * 1000) + 2000); + + var transportService = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + // @see NS_NETWORK_SOCKET_CONTRACTID_PREFIX + var socketTypeName = ssl == SSL ? "ssl" : (ssl == TLS ? "starttls" : null); + var transport = transportService.createTransport([socketTypeName], + ssl == NONE ? 0 : 1, + hostname, port, proxy); + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout); + try { + transport.securityCallbacks = new BadCertHandler(sslErrorHandler); + } catch (e) { + _error(e); + } + var outstream = transport.openOutputStream(0, 0, 0); + var stream = transport.openInputStream(0, 0, 0); + var instream = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + instream.init(stream); + + var dataListener = + { + data : new Array(), + onStartRequest: function(request, context) + { + try { + initialized = true; + if (!aborted) + { + // Send the first request + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } catch (e) { _error(e); } + }, + onStopRequest: function(request, context, status) + { + try { + instream.close(); + outstream.close(); + resultCallback(this.data.length ? this.data : null); + } catch (e) { _error(e); } + }, + onDataAvailable: function(request, context, inputStream, offset, count) + { + try { + if (!aborted) + { + let inputData = instream.read(count); + this.data.push(inputData); + if (index < commands.length) + { + // Send the next request to the server. + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } + } catch (e) { _error(e); } + } + }; + + try { + var pump = Cc["@mozilla.org/network/input-stream-pump;1"] + .createInstance(Ci.nsIInputStreamPump); + + pump.init(stream, -1, -1, 0, 0, false); + pump.asyncRead(dataListener, null); + return new SocketAbortable(transport); + } catch (e) { _error(e); } + return null; +} + +function SocketAbortable(transport) +{ + Abortable.call(this); + assert(transport instanceof Ci.nsITransport, "need transport"); + this._transport = transport; +} +SocketAbortable.prototype = Object.create(Abortable.prototype); +SocketAbortable.prototype.constructor = UserCancelledException; +SocketAbortable.prototype.cancel = function(ex) +{ + try { + this._transport.close(Components.results.NS_ERROR_ABORT); + } catch (e) { + ddump("canceling socket failed: " + e); + } +} + +/** + * Resolve a proxy for some domain and expose it via a callback. + * + * @param hostname {String} The hostname which a proxy will be resolved for + * @param resultCallback {function(proxyInfo)} + * Called after the proxy has been resolved for hostname. + * proxy {nsIProxyInfo} The resolved proxy, or null if none were found + * for hostname + */ +function doProxy(hostname, resultCallback) { + // This implements the nsIProtocolProxyCallback interface: + function ProxyResolveCallback() { } + ProxyResolveCallback.prototype = { + onProxyAvailable(req, uri, proxy, status) { + // Anything but a SOCKS proxy will be unusable for email. + if (proxy != null && proxy.type != "socks" && + proxy.type != "socks4") { + proxy = null; + } + resultCallback(proxy); + }, + }; + var proxyService = Cc["@mozilla.org/network/protocol-proxy-service;1"] + .getService(Ci.nsIProtocolProxyService); + // Use some arbitrary scheme just because it is required... + var uri = Services.io.newURI("http://" + hostname); + // ... we'll ignore it any way. We prefer SOCKS since that's the + // only thing we can use for email protocols. + var proxyFlags = Ci.nsIProtocolProxyService.RESOLVE_IGNORE_URI_SCHEME | + Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY; + if (Services.prefs.getBoolPref("network.proxy.socks_remote_dns")) { + proxyFlags |= Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + } + proxyService.asyncResolve(uri, proxyFlags, new ProxyResolveCallback()); +} diff --git a/mailnews/base/prefs/content/accountcreation/readFromXML.js b/mailnews/base/prefs/content/accountcreation/readFromXML.js new file mode 100644 index 000000000..4ef13ec1d --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/readFromXML.js @@ -0,0 +1,243 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * Takes an XML snipplet (as JXON) and reads the values into + * a new AccountConfig object. + * It does so securely (or tries to), by trying to avoid remote execution + * and similar holes which can appear when reading too naively. + * Of course it cannot tell whether the actual values are correct, + * e.g. it can't tell whether the host name is a good server. + * + * The XML format is documented at + * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat> + * + * @param clientConfigXML {JXON} The <clientConfig> node. + * @return AccountConfig object filled with the data from XML + */ +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +function readFromXML(clientConfigXML) +{ + function array_or_undef(value) { + return value === undefined ? [] : value; + } + var exception; + if (typeof(clientConfigXML) != "object" || + !("clientConfig" in clientConfigXML) || + !("emailProvider" in clientConfigXML.clientConfig)) + { + dump("client config xml = " + JSON.stringify(clientConfigXML) + "\n"); + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties"); + throw stringBundle.GetStringFromName("no_emailProvider.error"); + } + var xml = clientConfigXML.clientConfig.emailProvider; + + var d = new AccountConfig(); + d.source = AccountConfig.kSourceXML; + + d.id = sanitize.hostname(xml["@id"]); + d.displayName = d.id; + try { + d.displayName = sanitize.label(xml.displayName); + } catch (e) { logException(e); } + for (var domain of xml.$domain) + { + try { + d.domains.push(sanitize.hostname(domain)); + } catch (e) { logException(e); exception = e; } + } + if (d.domains.length == 0) + throw exception ? exception : "need proper <domain> in XML"; + exception = null; + + // incoming server + for (let iX of array_or_undef(xml.$incomingServer)) // input (XML) + { + let iO = d.createNewIncoming(); // output (object) + try { + // throws if not supported + iO.type = sanitize.enum(iX["@type"], ["pop3", "imap", "nntp"]); + iO.hostname = sanitize.hostname(iX.hostname); + iO.port = sanitize.integerRange(iX.port, kMinPort, kMaxPort); + // We need a username even for Kerberos, need it even internally. + iO.username = sanitize.string(iX.username); // may be a %VARIABLE% + + if ("password" in iX) { + d.rememberPassword = true; + iO.password = sanitize.string(iX.password); + } + + for (let iXsocketType of array_or_undef(iX.$socketType)) + { + try { + iO.socketType = sanitize.translate(iXsocketType, + { plain : 1, SSL: 2, STARTTLS: 3 }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!iO.socketType) + throw exception ? exception : "need proper <socketType> in XML"; + exception = null; + + for (let iXauth of array_or_undef(iX.$authentication)) + { + try { + iO.auth = sanitize.translate(iXauth, + { "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + "plain" : Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + "secure" : Ci.nsMsgAuthMethod.passwordEncrypted, + "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI, + "NTLM" : Ci.nsMsgAuthMethod.NTLM, +#ifdef MOZ_MAILNEWS_OAUTH2 + "OAuth2" : Ci.nsMsgAuthMethod.OAuth2 +#endif + }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!iO.auth) + throw exception ? exception : "need proper <authentication> in XML"; + exception = null; + + // defaults are in accountConfig.js + if (iO.type == "pop3" && "pop3" in iX) + { + try { + if ("leaveMessagesOnServer" in iX.pop3) + iO.leaveMessagesOnServer = + sanitize.boolean(iX.pop3.leaveMessagesOnServer); + if ("daysToLeaveMessagesOnServer" in iX.pop3) + iO.daysToLeaveMessagesOnServer = + sanitize.integer(iX.pop3.daysToLeaveMessagesOnServer); + } catch (e) { logException(e); } + try { + if ("downloadOnBiff" in iX.pop3) + iO.downloadOnBiff = sanitize.boolean(iX.pop3.downloadOnBiff); + } catch (e) { logException(e); } + } + + // processed successfully, now add to result object + if (!d.incoming.hostname) // first valid + d.incoming = iO; + else + d.incomingAlternatives.push(iO); + } catch (e) { exception = e; } + } + if (!d.incoming.hostname) + // throw exception for last server + throw exception ? exception : "Need proper <incomingServer> in XML file"; + exception = null; + + // outgoing server + for (let oX of array_or_undef(xml.$outgoingServer)) // input (XML) + { + let oO = d.createNewOutgoing(); // output (object) + try { + if (oX["@type"] != "smtp") + { + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties"); + throw stringBundle.GetStringFromName("outgoing_not_smtp.error"); + } + oO.hostname = sanitize.hostname(oX.hostname); + oO.port = sanitize.integerRange(oX.port, kMinPort, kMaxPort); + + for (let oXsocketType of array_or_undef(oX.$socketType)) + { + try { + oO.socketType = sanitize.translate(oXsocketType, + { plain : 1, SSL: 2, STARTTLS: 3 }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!oO.socketType) + throw exception ? exception : "need proper <socketType> in XML"; + exception = null; + + for (let oXauth of array_or_undef(oX.$authentication)) + { + try { + oO.auth = sanitize.translate(oXauth, + { // open relay + "none" : Ci.nsMsgAuthMethod.none, + // inside ISP or corp network + "client-IP-address" : Ci.nsMsgAuthMethod.none, + // hope for the best + "smtp-after-pop" : Ci.nsMsgAuthMethod.none, + "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + "plain" : Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + "secure" : Ci.nsMsgAuthMethod.passwordEncrypted, + "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI, + "NTLM" : Ci.nsMsgAuthMethod.NTLM, +#ifdef MOZ_MAILNEWS_OAUTH2 + "OAuth2" : Ci.nsMsgAuthMethod.OAuth2, +#endif + }); + + break; // take first that we support + } catch (e) { exception = e; } + } + if (!oO.auth) + throw exception ? exception : "need proper <authentication> in XML"; + exception = null; + + if ("username" in oX || + // if password-based auth, we need a username, + // so go there anyways and throw. + oO.auth == Ci.nsMsgAuthMethod.passwordCleartext || + oO.auth == Ci.nsMsgAuthMethod.passwordEncrypted) + oO.username = sanitize.string(oX.username); + + if ("password" in oX) { + d.rememberPassword = true; + oO.password = sanitize.string(oX.password); + } + + try { + // defaults are in accountConfig.js + if ("addThisServer" in oX) + oO.addThisServer = sanitize.boolean(oX.addThisServer); + if ("useGlobalPreferredServer" in oX) + oO.useGlobalPreferredServer = + sanitize.boolean(oX.useGlobalPreferredServer); + } catch (e) { logException(e); } + + // processed successfully, now add to result object + if (!d.outgoing.hostname) // first valid + d.outgoing = oO; + else + d.outgoingAlternatives.push(oO); + } catch (e) { logException(e); exception = e; } + } + if (!d.outgoing.hostname) + // throw exception for last server + throw exception ? exception : "Need proper <outgoingServer> in XML file"; + exception = null; + + d.inputFields = new Array(); + for (let inputField of array_or_undef(xml.$inputField)) + { + try { + var fieldset = + { + varname : sanitize.alphanumdash(inputField["@key"]).toUpperCase(), + displayName : sanitize.label(inputField["@label"]), + exampleValue : sanitize.label(inputField.value) + }; + d.inputFields.push(fieldset); + } catch (e) { logException(e); } // for now, don't throw, + // because we don't support custom fields yet anyways. + } + + return d; +} diff --git a/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js new file mode 100644 index 000000000..3c927a1af --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js @@ -0,0 +1,205 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * This is a generic input validation lib. Use it when you process + * data from the network. + * + * Just a few functions which verify, for security purposes, that the + * input variables (strings, if nothing else is noted) are of the expected + * type and syntax. + * + * The functions take a string (unless noted otherwise) and return + * the expected datatype in JS types. If the value is not as expected, + * they throw exceptions. + */ + +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +var sanitize = +{ + integer : function(unchecked) + { + if (typeof(unchecked) == "number" && !isNaN(unchecked)) + return unchecked; + + var r = parseInt(unchecked); + if (isNaN(r)) + throw new MalformedException("no_number.error", unchecked); + + return r; + }, + + integerRange : function(unchecked, min, max) + { + var int = this.integer(unchecked); + if (int < min) + throw new MalformedException("number_too_small.error", unchecked); + + if (int > max) + throw new MalformedException("number_too_large.error", unchecked); + + return int; + }, + + boolean : function(unchecked) + { + if (typeof(unchecked) == "boolean") + return unchecked; + + if (unchecked == "true") + return true; + + if (unchecked == "false") + return false; + + throw new MalformedException("boolean.error", unchecked); + }, + + string : function(unchecked) + { + return String(unchecked); + }, + + nonemptystring : function(unchecked) + { + if (!unchecked) + throw new MalformedException("string_empty.error", unchecked); + + return this.string(unchecked); + }, + + /** + * Allow only letters, numbers, "-" and "_". + * + * Empty strings not allowed (good idea?). + */ + alphanumdash : function(unchecked) + { + var str = this.nonemptystring(unchecked); + if (!/^[a-zA-Z0-9\-\_]*$/.test(str)) + throw new MalformedException("alphanumdash.error", unchecked); + + return str; + }, + + /** + * DNS hostnames like foo.bar.example.com + * Allow only letters, numbers, "-" and "." + * Empty strings not allowed. + * Currently does not support IDN (international domain names). + */ + hostname : function(unchecked) + { + let str = cleanUpHostName(this.nonemptystring(unchecked)); + + // Allow placeholders. TODO move to a new hostnameOrPlaceholder() + // The regex is "anything, followed by one or more (placeholders than + // anything)". This doesn't catch the non-placeholder case, but that's + // handled down below. + if (/^[a-zA-Z0-9\-\.]*(%[A-Z0-9]+%[a-zA-Z0-9\-\.]*)+$/.test(str)) + return str; + + if (!isLegalHostNameOrIP(str)) + throw new MalformedException("hostname_syntax.error", unchecked); + + return str.toLowerCase(); + }, + /** + * A non-chrome URL that's safe to request. + */ + url : function (unchecked) + { + var str = this.string(unchecked); + if (!str.startsWith("http") && !str.startsWith("https")) + throw new MalformedException("url_scheme.error", unchecked); + + var uri; + try { + uri = Services.io.newURI(str, null, null); + uri = uri.QueryInterface(Ci.nsIURL); + } catch (e) { + throw new MalformedException("url_parsing.error", unchecked); + } + + if (uri.scheme != "http" && uri.scheme != "https") + throw new MalformedException("url_scheme.error", unchecked); + + return uri.spec; + }, + + /** + * A value which should be shown to the user in the UI as label + */ + label : function(unchecked) + { + return this.string(unchecked); + }, + + /** + * Allows only certain values as input, otherwise throw. + * + * @param unchecked {Any} The value to check + * @param allowedValues {Array} List of values that |unchecked| may have. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + enum : function(unchecked, allowedValues, defaultValue) + { + for (let allowedValue of allowedValues) + { + if (allowedValue == unchecked) + return allowedValue; + } + // value is bad + if (typeof(defaultValue) == "undefined") + throw new MalformedException("allowed_value.error", unchecked); + return defaultValue; + }, + + /** + * Like enum, allows only certain (string) values as input, but allows the + * caller to specify another value to return instead of the input value. E.g., + * if unchecked == "foo", return 1, if unchecked == "bar", return 2, + * otherwise throw. This allows to translate string enums into integer enums. + * + * @param unchecked {Any} The value to check + * @param mapping {Object} Associative array. property name is the input + * value, property value is the output value. E.g. the example above + * would be: { foo: 1, bar : 2 }. + * Use quotes when you need freaky characters: "baz-" : 3. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + translate : function(unchecked, mapping, defaultValue) + { + for (var inputValue in mapping) + { + if (inputValue == unchecked) + return mapping[inputValue]; + } + // value is bad + if (typeof(defaultValue) == "undefined") + throw new MalformedException("allowed_value.error", unchecked); + return defaultValue; + } +}; + +function MalformedException(msgID, uncheckedBadValue) +{ + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties"); + var msg = stringBundle.GetStringFromName(msgID); + Exception.call(this, msg); +} +MalformedException.prototype = Object.create(Exception.prototype); +MalformedException.prototype.constructor = MalformedException; + diff --git a/mailnews/base/prefs/content/accountcreation/util.js b/mailnews/base/prefs/content/accountcreation/util.js new file mode 100644 index 000000000..44c22ac45 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/util.js @@ -0,0 +1,304 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ +/** + * Some common, generic functions + */ + +try { + var Cc = Components.classes; + var Ci = Components.interfaces; +} catch (e) { ddump(e); } // if already declared, as in xpcshell-tests +try { + var Cu = Components.utils; +} catch (e) { ddump(e); } + +Cu.import("resource:///modules/errUtils.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +function assert(test, errorMsg) +{ + if (!test) + throw new NotReached(errorMsg ? errorMsg : + "Programming bug. Assertion failed, see log."); +} + +function makeCallback(obj, func) +{ + return function() + { + return func.apply(obj, arguments); + } +} + + +/** + * Runs the given function sometime later + * + * Currently implemented using setTimeout(), but + * can later be replaced with an nsITimer impl, + * when code wants to use it in a module. + */ +function runAsync(func) +{ + setTimeout(func, 0); +} + + +/** + * @param uriStr {String} + * @result {nsIURI} + */ +function makeNSIURI(uriStr) +{ + return Services.io.newURI(uriStr, null, null); +} + + +/** + * Reads UTF8 data from a URL. + * + * @param uri {nsIURI} what you want to read + * @return {Array of String} the contents of the file, one string per line + */ +function readURLasUTF8(uri) +{ + assert(uri instanceof Ci.nsIURI, "uri must be an nsIURI"); + try { + let chan = Services.io.newChannelFromURI2(uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + let is = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + is.init(chan.open(), "UTF-8", 1024, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + let content = ""; + let strOut = new Object(); + try { + while (is.readString(1024, strOut) != 0) + content += strOut.value; + // catch in outer try/catch + } finally { + is.close(); + } + + return content; + } catch (e) { + // TODO this has a numeric error message. We need to ship translations + // into human language. + throw e; + } +} + +/** + * Takes a string (which is typically the content of a file, + * e.g. the result returned from readURLUTF8() ), and splits + * it into lines, and returns an array with one string per line + * + * Linebreaks are not contained in the result,, + * and all of \r\n, (Windows) \r (Mac) and \n (Unix) counts as linebreak. + * + * @param content {String} one long string with the whole file + * @return {Array of String} one string per line (no linebreaks) + */ +function splitLines(content) +{ + content = content.replace("\r\n", "\n"); + content = content.replace("\r", "\n"); + return content.split("\n"); +} + +/** + * @param bundleURI {String} chrome URL to properties file + * @return nsIStringBundle + */ +function getStringBundle(bundleURI) +{ + try { + return Services.strings.createBundle(bundleURI); + } catch (e) { + throw new Exception("Failed to get stringbundle URI <" + bundleURI + + ">. Error: " + e); + } +} + + +function Exception(msg) +{ + this._message = msg; + + // get stack + try { + not.found.here += 1; // force a native exception ... + } catch (e) { + this.stack = e.stack; // ... to get the current stack + } +} +Exception.prototype = +{ + get message() + { + return this._message; + }, + toString : function() + { + return this._message; + } +} + +function NotReached(msg) +{ + Exception.call(this, msg); // call super constructor + logException(this); +} +// Make NotReached extend Exception. +NotReached.prototype = Object.create(Exception.prototype); +NotReached.prototype.constructor = NotReached; + +/** + * A handle for an async function which you can cancel. + * The async function will return an object of this type (a subtype) + * and you can call cancel() when you feel like killing the function. + */ +function Abortable() +{ +} +Abortable.prototype = +{ + cancel : function() + { + } +} + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * @param setTimeoutID {Integer} Return value of setTimeout() + */ +function TimeoutAbortable(setTimeoutID) +{ + Abortable.call(this, setTimeoutID); // call super constructor + this._id = setTimeoutID; +} +TimeoutAbortable.prototype = Object.create(Abortable.prototype); +TimeoutAbortable.prototype.constructor = TimeoutAbortable; +TimeoutAbortable.prototype.cancel = function() { clearTimeout(this._id); } + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * @param setIntervalID {Integer} Return value of setInterval() + */ +function IntervalAbortable(setIntervalID) +{ + Abortable.call(this, setIntervalID); // call super constructor + this._id = setIntervalID; +} +IntervalAbortable.prototype = Object.create(Abortable.prototype); +IntervalAbortable.prototype.constructor = IntervalAbortable; +IntervalAbortable.prototype.cancel = function() { clearInterval(this._id); } + +// Allows you to make several network calls, but return +// only one Abortable object. +function SuccessiveAbortable() +{ + Abortable.call(this); // call super constructor + this._current = null; +} +SuccessiveAbortable.prototype = { + __proto__: Abortable.prototype, + get current() { return this._current; }, + set current(abortable) + { + assert(abortable instanceof Abortable || abortable == null, + "need an Abortable object (or null)"); + this._current = abortable; + }, + cancel: function() + { + if (this._current) + this._current.cancel(); + } +} + +function deepCopy(org) +{ + if (typeof(org) == "undefined") + return undefined; + if (org == null) + return null; + if (typeof(org) == "string") + return org; + if (typeof(org) == "number") + return org; + if (typeof(org) == "boolean") + return org == true; + if (typeof(org) == "function") + return org; + if (typeof(org) != "object") + throw "can't copy objects of type " + typeof(org) + " yet"; + + //TODO still instanceof org != instanceof copy + //var result = new org.constructor(); + var result = new Object(); + if (typeof(org.length) != "undefined") + var result = new Array(); + for (var prop in org) + result[prop] = deepCopy(org[prop]); + return result; +} + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} +function ddump(text) +{ + gEmailWizardLogger.info(text); +} + +function debugObject(obj, name, maxDepth, curDepth) +{ + if (curDepth == undefined) + curDepth = 0; + if (maxDepth != undefined && curDepth > maxDepth) + return ""; + + var result = ""; + var i = 0; + for (let prop in obj) + { + i++; + try { + if (typeof(obj[prop]) == "object") + { + if (obj[prop] && obj[prop].length != undefined) + result += name + "." + prop + "=[probably array, length " + + obj[prop].length + "]\n"; + else + result += name + "." + prop + "=[" + typeof(obj[prop]) + "]\n"; + result += debugObject(obj[prop], name + "." + prop, + maxDepth, curDepth + 1); + } + else if (typeof(obj[prop]) == "function") + result += name + "." + prop + "=[function]\n"; + else + result += name + "." + prop + "=" + obj[prop] + "\n"; + } catch (e) { + result += name + "." + prop + "-> Exception(" + e + ")\n"; + } + } + if (!i) + result += name + " is empty\n"; + return result; +} + +function alertPrompt(alertTitle, alertMsg) +{ + Services.prompt.alert(window, alertTitle, alertMsg); +} diff --git a/mailnews/base/prefs/content/accountcreation/verifyConfig.js b/mailnews/base/prefs/content/accountcreation/verifyConfig.js new file mode 100644 index 000000000..073487ba9 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/verifyConfig.js @@ -0,0 +1,353 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * This checks a given config, by trying a real connection and login, + * with username and password. + * + * TODO + * - give specific errors, bug 555448 + * - return a working |Abortable| to allow cancel + * + * @param accountConfig {AccountConfig} The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param alter {boolean} + * Try other usernames and login schemes, until login works. + * Warning: Modifies |accountConfig|. + * + * This function is async. + * @param successCallback function(accountConfig) + * Called when we could guess the config. + * For accountConfig, see below. + * @param errorCallback function(ex) + * Called when we could guess not the config, either + * because we have not found anything or + * because there was an error (e.g. no network connection). + * The ex.message will contain a user-presentable message. + */ + +Components.utils.import("resource:///modules/mailServices.js"); +#ifdef MOZ_MAILNEWS_OAUTH2 +Components.utils.import("resource://gre/modules/OAuth2Providers.jsm"); +#endif + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} + +function verifyConfig(config, alter, msgWindow, successCallback, errorCallback) +{ + ddump(debugObject(config, "config", 3)); + assert(config instanceof AccountConfig, + "BUG: Arg 'config' needs to be an AccountConfig object"); + assert(typeof(alter) == "boolean"); + assert(typeof(successCallback) == "function"); + assert(typeof(errorCallback) == "function"); + + if (MailServices.accounts.findRealServer(config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, + ["pop3", "imap", "nntp"]), + config.incoming.port)) { + errorCallback("Incoming server exists"); + return; + } + + // incoming server + let inServer = + MailServices.accounts.createIncomingServer(config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, + ["pop3", "imap", "nntp"])); + inServer.port = config.incoming.port; + inServer.password = config.incoming.password; + if (config.incoming.socketType == 1) // plain + inServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.incoming.socketType == 2) // SSL + inServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.incoming.socketType == 3) // STARTTLS + inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + gEmailWizardLogger.info("Setting incoming server authMethod to " + + config.incoming.auth); + inServer.authMethod = config.incoming.auth; + + try { +#ifdef MOZ_MAILNEWS_OAUTH2 + // Lookup issuer if needed. + if (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 || + config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2) { + if (!config.oauthSettings) + config.oauthSettings = {}; + if (!config.oauthSettings.issuer || !config.oauthSettings.scope) { + // lookup issuer or scope from hostname + let hostname = (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) ? + config.incoming.hostname : config.outgoing.hostname; + let hostDetails = OAuth2Providers.getHostnameDetails(hostname); + if (hostDetails) + [config.oauthSettings.issuer, config.oauthSettings.scope] = hostDetails; + if (!config.oauthSettings.issuer || !config.oauthSettings.scope) + throw "Could not get issuer for oauth2 authentication"; + } + gEmailWizardLogger.info("Saving oauth parameters for issuer " + + config.oauthSettings.issuer); + inServer.setCharValue("oauth2.scope", config.oauthSettings.scope); + inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer); + gEmailWizardLogger.info("OAuth2 issuer, scope is " + + config.oauthSettings.issuer + ", " + config.oauthSettings.scope); + } + + if (inServer.password || + inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) +#else + if (inServer.password) +#endif + verifyLogon(config, inServer, alter, msgWindow, + successCallback, errorCallback); + else { + // Avoid pref pollution, clear out server prefs. + MailServices.accounts.removeIncomingServer(inServer, true); + successCallback(config); + } + return; + } + catch (e) { + gEmailWizardLogger.error("ERROR: verify logon shouldn't have failed"); + } + // Avoid pref pollution, clear out server prefs. + MailServices.accounts.removeIncomingServer(inServer, true); + errorCallback(e); +} + +function verifyLogon(config, inServer, alter, msgWindow, successCallback, + errorCallback) +{ + gEmailWizardLogger.info("verifyLogon for server at " + inServer.hostName); + // hack - save away the old callbacks. + let saveCallbacks = msgWindow.notificationCallbacks; + // set our own callbacks - this works because verifyLogon will + // synchronously create the transport and use the notification callbacks. + let listener = new urlListener(config, inServer, alter, msgWindow, + successCallback, errorCallback); + // our listener listens both for the url and cert errors. + msgWindow.notificationCallbacks = listener; + // try to work around bug where backend is clearing password. + try { + inServer.password = config.incoming.password; + let uri = inServer.verifyLogon(listener, msgWindow); + // clear msgWindow so url won't prompt for passwords. + uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null; + } + catch (e) { gEmailWizardLogger.error("verifyLogon failed: " + e); throw e;} + finally { + // restore them + msgWindow.notificationCallbacks = saveCallbacks; + } +} + +/** + * The url listener also implements nsIBadCertListener2. Its job is to prevent + * "bad cert" security dialogs from being shown to the user. Currently it puts + * up the cert override dialog, though we'd like to give the user more detailed + * information in the future. + */ + +function urlListener(config, server, alter, msgWindow, successCallback, + errorCallback) +{ + this.mConfig = config; + this.mServer = server; + this.mAlter = alter; + this.mSuccessCallback = successCallback; + this.mErrorCallback = errorCallback; + this.mMsgWindow = msgWindow; + this.mCertError = false; + this._log = Log4Moz.getConfiguredLogger("mail.wizard"); +} +urlListener.prototype = +{ + OnStartRunningUrl: function(aUrl) + { + this._log.info("Starting to test username"); + this._log.info(" username=" + (this.mConfig.incoming.username != + this.mConfig.identity.emailAddress) + + ", have savedUsername=" + + (this.mConfig.usernameSaved ? "true" : "false")); + this._log.info(" authMethod=" + this.mServer.authMethod); + }, + + OnStopRunningUrl: function(aUrl, aExitCode) + { + this._log.info("Finished verifyConfig resulted in " + aExitCode); + if (Components.isSuccessCode(aExitCode)) + { + this._cleanup(); + this.mSuccessCallback(this.mConfig); + } + // Logon failed, and we aren't supposed to try other variations. + else if (!this.mAlter) + { + this._cleanup(); + var errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + } + // Try other variations, unless there's a cert error, in which + // case we'll see what the user chooses. + else if (!this.mCertError) + { + this.tryNextLogon() + } + }, + + tryNextLogon: function() + { + this._log.info("tryNextLogon()"); + this._log.info(" username=" + (this.mConfig.incoming.username != + this.mConfig.identity.emailAddress) + + ", have savedUsername=" + + (this.mConfig.usernameSaved ? "true" : "false")); + this._log.info(" authMethod=" + this.mServer.authMethod); + // check if we tried full email address as username + if (this.mConfig.incoming.username != this.mConfig.identity.emailAddress) + { + this._log.info(" Changing username to email address."); + this.mConfig.usernameSaved = this.mConfig.incoming.username; + this.mConfig.incoming.username = this.mConfig.identity.emailAddress; + this.mConfig.outgoing.username = this.mConfig.identity.emailAddress; + this.mServer.username = this.mConfig.incoming.username; + this.mServer.password = this.mConfig.incoming.password; + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + return; + } + + if (this.mConfig.usernameSaved) + { + this._log.info(" Re-setting username."); + // If we tried the full email address as the username, then let's go + // back to trying just the username before trying the other cases. + this.mConfig.incoming.username = this.mConfig.usernameSaved; + this.mConfig.outgoing.username = this.mConfig.usernameSaved; + this.mConfig.usernameSaved = null; + this.mServer.username = this.mConfig.incoming.username; + this.mServer.password = this.mConfig.incoming.password; + } + + // sec auth seems to have failed, and we've tried both + // varieties of user name, sadly. + // So fall back to non-secure auth, and + // again try the user name and email address as username + assert(this.mConfig.incoming.auth == this.mServer.authMethod); + this._log.info(" Using SSL: " + + (this.mServer.socketType == Ci.nsMsgSocketType.SSL || + this.mServer.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS)); + if (this.mConfig.incoming.authAlternatives && + this.mConfig.incoming.authAlternatives.length) + // We may be dropping back to insecure auth methods here, + // which is not good. But then again, we already warned the user, + // if it is a config without SSL. + { + this._log.info(" auth alternatives = " + + this.mConfig.incoming.authAlternatives.join(",")); + this._log.info(" Decreasing auth."); + this._log.info(" Have password: " + + (this.mServer.password ? "true" : "false")); + let brokenAuth = this.mConfig.incoming.auth; + // take the next best method (compare chooseBestAuthMethod() in guess) + this.mConfig.incoming.auth = + this.mConfig.incoming.authAlternatives.shift(); + this.mServer.authMethod = this.mConfig.incoming.auth; + // Assume that SMTP server has same methods working as incoming. + // Broken assumption, but we currently have no SMTP verification. + // TODO implement real SMTP verification + if (this.mConfig.outgoing.auth == brokenAuth && + this.mConfig.outgoing.authAlternatives.indexOf( + this.mConfig.incoming.auth) != -1) + this.mConfig.outgoing.auth = this.mConfig.incoming.auth; + this._log.info(" outgoing auth: " + this.mConfig.outgoing.auth); + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + return; + } + + // Tried all variations we can. Give up. + this._log.info("Giving up."); + this._cleanup(); + let errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + return; + }, + + _cleanup : function() + { + try { + // Avoid pref pollution, clear out server prefs. + if (this.mServer) { + MailServices.accounts.removeIncomingServer(this.mServer, true); + this.mServer = null; + } + } catch (e) { this._log.error(e); } + }, + + // Suppress any certificate errors + notifyCertProblem: function(socketInfo, status, targetSite) { + this.mCertError = true; + this._log.error("cert error"); + let self = this; + setTimeout(function () { + try { + self.informUserOfCertError(socketInfo, status, targetSite); + } catch (e) { logException(e); } + }, 0); + return true; + }, + + informUserOfCertError : function(socketInfo, status, targetSite) { + var params = { + exceptionAdded : false, + sslStatus : status, + prefetchCert : true, + location : targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + this._log.info("cert exception dialog closed"); + this._log.info("cert exceptionAdded = " + params.exceptionAdded); + if (!params.exceptionAdded) { + this._cleanup(); + let errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + } + else { + // Retry the logon now that we've added the cert exception. + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + } + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Components.interfaces.nsIBadCertListener2) && + !iid.equals(Components.interfaces.nsIInterfaceRequestor) && + !iid.equals(Components.interfaces.nsIUrlListener) && + !iid.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + return this; + } +} diff --git a/mailnews/base/prefs/content/am-addressing.js b/mailnews/base/prefs/content/am-addressing.js new file mode 100644 index 000000000..9f2584767 --- /dev/null +++ b/mailnews/base/prefs/content/am-addressing.js @@ -0,0 +1,79 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +function onLoad() +{ + parent.onPanelLoaded('am-addressing.xul'); +} + +function onInit(aPageId, aServerId) +{ + onInitCompositionAndAddressing(); +} + +function onInitCompositionAndAddressing() +{ + LDAPenabling(); + quoteEnabling(); +} + +function onEditDirectories() +{ + window.openDialog("chrome://messenger/content/addressbook/pref-editdirectories.xul", + "editDirectories", "chrome,modal=yes,resizable=no", null); +} + +function onPreInit(account, accountValues) +{ +} + +function LDAPenabling() +{ + onCheckItem("identity.directoryServer", ["directories"]); + onCheckItem("editButton", ["directories"]); +} + +function quoteEnabling() +{ + var quotebox = document.getElementById("thenBox"); + var placebox = document.getElementById("placeBox"); + var quotecheck = document.getElementById("identity.autoQuote"); + + if (quotecheck.checked && !quotecheck.disabled && + (document.getElementById("identity.replyOnTop").value == 1)) { + placebox.firstChild.removeAttribute("disabled"); + placebox.lastChild.removeAttribute("disabled"); + } + else { + placebox.firstChild.setAttribute("disabled", "true"); + placebox.lastChild.setAttribute("disabled", "true"); + } + if (quotecheck.checked && !quotecheck.disabled) { + quotebox.firstChild.removeAttribute("disabled"); + quotebox.lastChild.removeAttribute("disabled"); + } + else { + quotebox.firstChild.setAttribute("disabled", "true"); + quotebox.lastChild.setAttribute("disabled", "true"); + } +} + +/** + * Open the Preferences dialog on the tab with Addressing options. + */ +function showGlobalAddressingPrefs() +{ + openPrefsFromAccountManager("paneCompose", "addressingTab", null, "addressing_pane"); +} + +/** + * Open the Preferences dialog on the tab with Composing options. + */ +function showGlobalComposingPrefs() +{ + openPrefsFromAccountManager("paneCompose", "generalTab", null, "composing_messages_pane"); +} diff --git a/mailnews/base/prefs/content/am-addressing.xul b/mailnews/base/prefs/content/am-addressing.xul new file mode 100644 index 000000000..91eae4aac --- /dev/null +++ b/mailnews/base/prefs/content/am-addressing.xul @@ -0,0 +1,18 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xul-overlay href="chrome://messenger/content/am-addressingOverlay.xul"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-addressing.dtd"> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&addressing.label;" + onload="onLoad();"> + <vbox flex="1" style="overflow: auto;"> + <dialogheader title="&addressing.label;"/> + <vbox id="compositionAndAddressing"/> + </vbox> +</page> diff --git a/mailnews/base/prefs/content/am-addressingOverlay.xul b/mailnews/base/prefs/content/am-addressingOverlay.xul new file mode 100644 index 000000000..a5f3c1582 --- /dev/null +++ b/mailnews/base/prefs/content/am-addressingOverlay.xul @@ -0,0 +1,135 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/content/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/am-addressing.dtd"> + +<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://messenger/content/am-addressing.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-prefs.js"/> + + <vbox flex="1" id="compositionAndAddressing"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + + <groupbox> + <caption label="&compositionGroupTitle.label;"/> + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.composeHtml" label="&useHtml.label;" + accesskey="&useHtml.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.compose_html"/> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.autoQuote" label="&autoQuote.label;" + oncommand="quoteEnabling();" accesskey="&autoQuote.accesskey;" + pref="true" preftype="bool" prefattribute="value" + prefstring="mail.identity.%identitykey%.auto_quote"/> + </hbox> + <hbox class="indent" align="center" id="thenBox"> + <label value="&then.label;" accesskey="&then.accesskey;" control="identity.replyOnTop"/> + <menulist wsm_persist="true" id="identity.replyOnTop" oncommand="quoteEnabling();" + pref="true" preftype="int" prefattribute="value" + prefstring="mail.identity.%identitykey%.reply_on_top"> + <menupopup> + <menuitem value="1" label="&aboveQuote.label;"/> + <menuitem value="0" label="&belowQuote.label;"/> + <menuitem value="2" label="&selectAndQuote.label;"/> + </menupopup> + </menulist> + </hbox> + <hbox class="indent" align="center" id="placeBox"> + <label value="&place.label;" accesskey="&place.accesskey;" control="identity.sig_bottom"/> + <menulist wsm_persist="true" id="identity.sig_bottom" genericattr="true" + pref="true" preftype="bool" prefattribute="value" + prefstring="mail.identity.%identitykey%.sig_bottom"> + <menupopup> + <menuitem value="true" label="&belowText.label;"/> + <menuitem value="false" label="&aboveText.label;"/> + </menupopup> + </menulist> + </hbox> + + <checkbox id="identity.sig_on_reply" wsm_persist="true" + label="&includeSigOnReply.label;" + accesskey="&includeSigOnReply.accesskey;" + preftype="bool" genericattr="true" + prefstring="mail.identity.%identitykey%.sig_on_reply"/> + + <checkbox id="identity.sig_on_fwd" wsm_persist="true" + label="&includeSigOnForward.label;" + accesskey="&includeSigOnForward.accesskey;" + preftype="bool" genericattr="true" + prefstring="mail.identity.%identitykey%.sig_on_fwd"/> + + <separator class="thin"/> + + <hbox pack="start"> + <button id="globalComposingPrefsLink" + label="&globalComposingPrefs.label;" + accesskey="&globalComposingPrefs.accesskey;" + oncommand="showGlobalComposingPrefs();"/> + </hbox> + </groupbox> + + <separator class="thin"/> + + <groupbox> + <caption label="&addressingGroupTitle.label;"/> +#ifndef MOZ_THUNDERBIRD + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.autocompleteToMyDomain" + label="&autocompleteToMyDomain.label;" + accesskey="&autocompleteToMyDomain.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.autocompleteToMyDomain"/> + </hbox> + + <separator class="thin"/> +#endif + + <label control="identity.overrideGlobal_Pref">&addressingText.label;</label> + <radiogroup id="identity.overrideGlobal_Pref" class="indent" + oncommand="LDAPenabling();" wsm_persist="true" + genericattr="true" preftype="bool" + prefstring="mail.identity.%identitykey%.overrideGlobal_Pref"> + <radio value="false" label="&useGlobal.label;" + accesskey="&useGlobal.accesskey;"/> + <radio value="true" id="directories" label="&directories.label;" + accesskey="&directories.accesskey;"/> + <hbox class="indent"> + <menulist id="identity.directoryServer" + wsm_persist="true" preftype="string" + prefstring="mail.identity.%identitykey%.directoryServer" + style="min-width: 16em;" aria-labelledby="directories"> + <menupopup class="addrbooksPopup" + none="&directoriesNone.label;" + remoteonly="true" value="dirPrefId"/> + </menulist> + <button id="editButton" label="&editDirectories.label;" + accesskey="&editDirectories.accesskey;" + oncommand="onEditDirectories();"/> + </hbox> + </radiogroup> + + <separator class="thin"/> + + <hbox pack="start"> + <button id="globalAddressingPrefsLink" + label="&globalAddressingPrefs.label;" + accesskey="&globalAddressingPrefs.accesskey;" + oncommand="showGlobalAddressingPrefs();"/> + </hbox> + </groupbox> + </vbox> +</overlay> diff --git a/mailnews/base/prefs/content/am-archiveoptions.js b/mailnews/base/prefs/content/am-archiveoptions.js new file mode 100644 index 000000000..ae09ae156 --- /dev/null +++ b/mailnews/base/prefs/content/am-archiveoptions.js @@ -0,0 +1,69 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +var gIdentity = null; + +/** + * Load the archive options dialog, set the radio/checkbox items to the + * appropriate values, and update the archive hierarchy example. + */ +function onLoadArchiveOptions() { + // extract the account + gIdentity = window.arguments[0]; + + let granularity = document.getElementById("archiveGranularity"); + granularity.selectedIndex = gIdentity.archiveGranularity; + granularity.addEventListener("command", updateArchiveExample, false); + + let kfs = document.getElementById("archiveKeepFolderStructure"); + kfs.checked = gIdentity.archiveKeepFolderStructure; + kfs.addEventListener("command", updateArchiveExample, false); + + updateArchiveExample(); +} + +/** + * Save the archive settings to the current identity. + */ +function onAcceptArchiveOptions() { + gIdentity.archiveGranularity = + document.getElementById("archiveGranularity").selectedIndex; + gIdentity.archiveKeepFolderStructure = + document.getElementById("archiveKeepFolderStructure").checked; +} + +/** + * Update the example tree to show what the current options would look like. + */ +function updateArchiveExample() { + let granularity = document.getElementById("archiveGranularity").selectedIndex; + let kfs = document.getElementById("archiveKeepFolderStructure").checked; + let hierarchy = [ document.getElementsByClassName("root"), + document.getElementsByClassName("year"), + document.getElementsByClassName("month") ]; + + // First, show/hide the appropriate levels in the hierarchy and turn the + // necessary items into containers. + for (let i = 0; i < hierarchy.length; i++) { + for (let j = 0; j < hierarchy[i].length; j++) { + hierarchy[i][j].setAttribute("container", granularity > i); + hierarchy[i][j].setAttribute("open", granularity > i); + hierarchy[i][j].hidden = granularity < i; + } + } + + // Next, handle the "keep folder structures" case by moving a tree item around + // and making sure its parent is a container. + let folders = document.getElementById("folders"); + folders.hidden = !kfs; + if (kfs) { + let parent = hierarchy[granularity][0]; + parent.setAttribute("container", true); + parent.setAttribute("open", true); + + let treechildren = parent.children[1]; + treechildren.appendChild(folders); + } +} diff --git a/mailnews/base/prefs/content/am-archiveoptions.xul b/mailnews/base/prefs/content/am-archiveoptions.xul new file mode 100644 index 000000000..9d7ecb57c --- /dev/null +++ b/mailnews/base/prefs/content/am-archiveoptions.xul @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/am-archiveoptions.dtd" > + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="archive-options" + title="&dialogTitle.label;" + width="350" height="350" + persist="width height" + onload="onLoadArchiveOptions();" + ondialogaccept="onAcceptArchiveOptions();"> + + <script type="application/javascript" src="chrome://messenger/content/am-archiveoptions.js"/> + + <vbox flex="1"> + <label>&archiveGranularityPrefix.label;</label> + <radiogroup id="archiveGranularity"> + <radio label="&archiveFlat.label;" accesskey="&archiveFlat.accesskey;" + class="indent"/> + <radio label="&archiveYearly.label;" + accesskey="&archiveYearly.accesskey;" class="indent"/> + <radio label="&archiveMonthly.label;" + accesskey="&archiveMonthly.accesskey;" class="indent"/> + </radiogroup> + <checkbox id="archiveKeepFolderStructure" + label="&keepFolderStructure.label;" + accesskey="&keepFolderStructure.accesskey;"/> + + <groupbox flex="1"> + <caption label="&archiveExample.label;"/> + <tree id="archiveTree" hidecolumnpicker="true" disabled="true" flex="1"> + <treecols> + <treecol primary="true" hideheader="true" flex="1" + id="folderNameCol"/> + </treecols> + <treechildren> + <treeitem class="root"> + <treerow> + <treecell properties="specialFolder-Archive" + label="&archiveFolderName.label;"/> + </treerow> + <treechildren> + <treeitem id="folders"> + <treerow> + <treecell label="&inboxFolderName.label;"/> + </treerow> + </treeitem> + <treeitem class="year"> + <treerow> + <treecell label="2010"/> + </treerow> + <treechildren> + <treeitem class="month"> + <treerow> + <treecell label="2010-11"/> + </treerow> + <treechildren/> + </treeitem> + <treeitem class="month"> + <treerow> + <treecell label="2010-12"/> + </treerow> + <treechildren/> + </treeitem> + </treechildren> + </treeitem> + <treeitem class="year"> + <treerow> + <treecell label="2011"/> + </treerow> + <treechildren> + <treeitem class="month"> + <treerow> + <treecell label="2011-01"/> + </treerow> + <treechildren/> + </treeitem> + <treeitem class="month"> + <treerow> + <treecell label="2011-02"/> + </treerow> + <treechildren/> + </treeitem> + </treechildren> + </treeitem> + </treechildren> + </treeitem> + </treechildren> + </tree> + </groupbox> + </vbox> +</dialog> diff --git a/mailnews/base/prefs/content/am-copies.js b/mailnews/base/prefs/content/am-copies.js new file mode 100644 index 000000000..5fc3836a6 --- /dev/null +++ b/mailnews/base/prefs/content/am-copies.js @@ -0,0 +1,471 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/MailUtils.js"); + +var gFccRadioElemChoice, gDraftsRadioElemChoice, gArchivesRadioElemChoice, gTmplRadioElemChoice; +var gFccRadioElemChoiceLocked, gDraftsRadioElemChoiceLocked, gArchivesRadioElemChoiceLocked, gTmplRadioElemChoiceLocked; +var gDefaultPickerMode = "1"; + +var gFccFolderWithDelim, gDraftsFolderWithDelim, gArchivesFolderWithDelim, gTemplatesFolderWithDelim; +var gAccount; +var gCurrentServerId; + +function onPreInit(account, accountValues) +{ + gAccount = account; + var type = parent.getAccountValue(account, accountValues, "server", "type", null, false); + hideShowControls(type); +} + +/* + * Set the global radio element choices and initialize folder/account pickers. + * Also, initialize other UI elements (cc, bcc, fcc picker controller checkboxes). + */ +function onInit(aPageId, aServerId) +{ + gCurrentServerId = aServerId; + onInitCopiesAndFolders(); +} + +function onInitCopiesAndFolders() +{ + SetGlobalRadioElemChoices(); + + SetFolderDisplay(gFccRadioElemChoice, gFccRadioElemChoiceLocked, + "fcc", + "msgFccAccountPicker", + "identity.fccFolder", + "msgFccFolderPicker"); + + SetFolderDisplay(gArchivesRadioElemChoice, gArchivesRadioElemChoiceLocked, + "archive", + "msgArchivesAccountPicker", + "identity.archiveFolder", + "msgArchivesFolderPicker"); + + SetFolderDisplay(gDraftsRadioElemChoice, gDraftsRadioElemChoiceLocked, + "draft", + "msgDraftsAccountPicker", + "identity.draftFolder", + "msgDraftsFolderPicker"); + + SetFolderDisplay(gTmplRadioElemChoice, gTmplRadioElemChoiceLocked, + "tmpl", + "msgStationeryAccountPicker", + "identity.stationeryFolder", + "msgStationeryFolderPicker"); + + setupCcTextbox(true); + setupBccTextbox(true); + setupFccItems(); + setupArchiveItems(); + + SetSpecialFolderNamesWithDelims(); +} + +// Initialize the picker mode choices (account/folder picker) into global vars +function SetGlobalRadioElemChoices() +{ + var pickerModeElement = document.getElementById("identity.fccFolderPickerMode"); + gFccRadioElemChoice = pickerModeElement.getAttribute("value"); + gFccRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled"); + if (!gFccRadioElemChoice) gFccRadioElemChoice = gDefaultPickerMode; + + pickerModeElement = document.getElementById("identity.archivesFolderPickerMode"); + gArchivesRadioElemChoice = pickerModeElement.getAttribute("value"); + gArchivesRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled"); + if (!gArchivesRadioElemChoice) gArchivesRadioElemChoice = gDefaultPickerMode; + + pickerModeElement = document.getElementById("identity.draftsFolderPickerMode"); + gDraftsRadioElemChoice = pickerModeElement.getAttribute("value"); + gDraftsRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled"); + if (!gDraftsRadioElemChoice) gDraftsRadioElemChoice = gDefaultPickerMode; + + pickerModeElement = document.getElementById("identity.tmplFolderPickerMode"); + gTmplRadioElemChoice = pickerModeElement.getAttribute("value"); + gTmplRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled"); + if (!gTmplRadioElemChoice) gTmplRadioElemChoice = gDefaultPickerMode; +} + +/* + * Set Account and Folder elements based on the values read from + * preferences file. Default picker mode, if none specified at this stage, is + * set to 1 i.e., Other picker displaying the folder value read from the + * preferences file. + */ +function SetFolderDisplay(pickerMode, disableMode, + radioElemPrefix, + accountPickerId, + folderPickedField, + folderPickerId) +{ + if (!pickerMode) + pickerMode = gDefaultPickerMode; + + var selectAccountRadioId = radioElemPrefix + "_selectAccount"; + var selectAccountRadioElem = document.getElementById(selectAccountRadioId); + var selectFolderRadioId = radioElemPrefix + "_selectFolder"; + var selectFolderRadioElem = document.getElementById(selectFolderRadioId); + var accountPicker = document.getElementById(accountPickerId); + var folderPicker = document.getElementById(folderPickerId); + var rg = selectAccountRadioElem.radioGroup; + var folderPickedElement = document.getElementById(folderPickedField); + var uri = folderPickedElement.getAttribute("value"); + // Get message folder from the given uri. Second argument (false) signifies + // that there is no need to check for the existence of special folders as + // these folders are created on demand at runtime in case of imap accounts. + // For POP3 accounts, special folders are created at the account creation time. + var msgFolder = MailUtils.getFolderForURI(uri, false); + InitFolderDisplay(msgFolder.server.rootFolder, accountPicker); + InitFolderDisplay(msgFolder, folderPicker); + + switch (pickerMode) + { + case "0" : + rg.selectedItem = selectAccountRadioElem; + SetPickerEnabling(accountPickerId, folderPickerId); + break; + + case "1" : + rg.selectedItem = selectFolderRadioElem; + SetPickerEnabling(folderPickerId, accountPickerId); + break; + + default : + dump("Error in setting initial folder display on pickers\n"); + break; + } + + // Check to see if we need to lock page elements. Disable radio buttons + // and account/folder pickers when locked. + if (disableMode) { + selectAccountRadioElem.setAttribute("disabled","true"); + selectFolderRadioElem.setAttribute("disabled","true"); + accountPicker.setAttribute("disabled","true"); + folderPicker.setAttribute("disabled","true"); + } +} + +// Initialize the folder display based on prefs values +function InitFolderDisplay(folder, folderPicker) { + folderPicker.menupopup.selectFolder(folder); + folderPicker.folder = folder; +} + +/** + * Capture any menulist changes and update the folder property. + * + * @param aGroup the prefix for the menulist we're handling (e.g. "drafts") + * @param aType "Account" for the account picker or "Folder" for the folder + * picker + * @param aEvent the event that we're responding to + */ +function noteSelectionChange(aGroup, aType, aEvent) +{ + var checkedElem = document.getElementById(aGroup+"_select"+aType); + var folder = aEvent.target._folder; + var modeValue = checkedElem.value; + var radioGroup = checkedElem.radioGroup.getAttribute("id"); + var picker; + + switch (radioGroup) { + case "doFcc" : + gFccRadioElemChoice = modeValue; + picker = document.getElementById("msgFcc"+aType+"Picker"); + break; + + case "messageArchives" : + gArchivesRadioElemChoice = modeValue; + picker = document.getElementById("msgArchives"+aType+"Picker"); + updateArchiveHierarchyButton(folder); + break; + + case "messageDrafts" : + gDraftsRadioElemChoice = modeValue; + picker = document.getElementById("msgDrafts"+aType+"Picker"); + break; + + case "messageTemplates" : + gTmplRadioElemChoice = modeValue; + picker = document.getElementById("msgStationery"+aType+"Picker"); + break; + } + + picker.folder = folder; + picker.menupopup.selectFolder(folder); +} + +// Need to append special folders when account picker is selected. +// Create a set of global special folder vars to be suffixed to the +// server URI of the selected account. +function SetSpecialFolderNamesWithDelims() +{ + var folderDelim = "/"; + /* we use internal names known to everyone like "Sent", "Templates" and "Drafts" */ + + gFccFolderWithDelim = folderDelim + "Sent"; + gArchivesFolderWithDelim = folderDelim + "Archives"; + gDraftsFolderWithDelim = folderDelim + "Drafts"; + gTemplatesFolderWithDelim = folderDelim + "Templates"; +} + +// Save all changes on this page +function onSave() +{ + onSaveCopiesAndFolders(); +} + +function onSaveCopiesAndFolders() +{ + SaveFolderSettings( gFccRadioElemChoice, + "doFcc", + gFccFolderWithDelim, + "msgFccAccountPicker", + "msgFccFolderPicker", + "identity.fccFolder", + "identity.fccFolderPickerMode" ); + + SaveFolderSettings( gArchivesRadioElemChoice, + "messageArchives", + gArchivesFolderWithDelim, + "msgArchivesAccountPicker", + "msgArchivesFolderPicker", + "identity.archiveFolder", + "identity.archivesFolderPickerMode" ); + + SaveFolderSettings( gDraftsRadioElemChoice, + "messageDrafts", + gDraftsFolderWithDelim, + "msgDraftsAccountPicker", + "msgDraftsFolderPicker", + "identity.draftFolder", + "identity.draftsFolderPickerMode" ); + + SaveFolderSettings( gTmplRadioElemChoice, + "messageTemplates", + gTemplatesFolderWithDelim, + "msgStationeryAccountPicker", + "msgStationeryFolderPicker", + "identity.stationeryFolder", + "identity.tmplFolderPickerMode" ); +} + +// Save folder settings and radio element choices +function SaveFolderSettings(radioElemChoice, + radioGroupId, + folderSuffix, + accountPickerId, + folderPickerId, + folderElementId, + folderPickerModeId) +{ + var formElement = document.getElementById(folderElementId); + var uri; + + if (radioElemChoice == "0" || + !document.getElementById(folderPickerId).value) { + // Default or revert to default if no folder chosen. + radioElemChoice = "0"; + uri = document.getElementById(accountPickerId).folder.URI; + if (uri) { + // Create Folder URI. + uri = uri + folderSuffix; + } + } + else if (radioElemChoice == "1") { + uri = document.getElementById(folderPickerId).folder.URI; + } + else { + dump ("Error saving folder preferences.\n"); + return; + } + + formElement.setAttribute("value", uri); + + formElement = document.getElementById(folderPickerModeId); + formElement.setAttribute("value", radioElemChoice); +} + +// Check the Fcc Self item and setup associated picker state +function setupFccItems() +{ + var broadcaster = document.getElementById("broadcaster_doFcc"); + + var checked = document.getElementById("identity.doFcc").checked; + if (checked) { + broadcaster.removeAttribute("disabled"); + switch (gFccRadioElemChoice) { + case "0" : + if (!gFccRadioElemChoiceLocked) + SetPickerEnabling("msgFccAccountPicker", "msgFccFolderPicker"); + SetRadioButtons("fcc_selectAccount", "fcc_selectFolder"); + break; + + case "1" : + if (!gFccRadioElemChoiceLocked) + SetPickerEnabling("msgFccFolderPicker", "msgFccAccountPicker"); + SetRadioButtons("fcc_selectFolder", "fcc_selectAccount"); + break; + + default : + dump("Error in setting Fcc elements.\n"); + break; + } + } + else + broadcaster.setAttribute("disabled", "true"); +} + +// Disable CC textbox if CC checkbox is not checked +function setupCcTextbox(init) +{ + var ccChecked = document.getElementById("identity.doCc").checked; + var ccTextbox = document.getElementById("identity.doCcList"); + + ccTextbox.disabled = !ccChecked; + + if (ccChecked) { + if (ccTextbox.value == "") { + ccTextbox.value = document.getElementById("identity.email").value; + if (!init) + ccTextbox.select(); + } + } else if ((ccTextbox.value == document.getElementById("identity.email").value) || + (init && ccTextbox.getAttribute("value") == "")) + ccTextbox.value = ""; +} + +// Disable BCC textbox if BCC checkbox is not checked +function setupBccTextbox(init) +{ + var bccChecked = document.getElementById("identity.doBcc").checked; + var bccTextbox = document.getElementById("identity.doBccList"); + + bccTextbox.disabled = !bccChecked; + + if (bccChecked) { + if (bccTextbox.value == "") { + bccTextbox.value = document.getElementById("identity.email").value; + if (!init) + bccTextbox.select(); + } + } else if ((bccTextbox.value == document.getElementById("identity.email").value) || + (init && bccTextbox.getAttribute("value") == "")) + bccTextbox.value = ""; +} + +// Enable and disable pickers based on the radio element clicked +function SetPickerEnabling(enablePickerId, disablePickerId) +{ + var activePicker = document.getElementById(enablePickerId); + activePicker.removeAttribute("disabled"); + + var inactivePicker = document.getElementById(disablePickerId); + inactivePicker.setAttribute("disabled", "true"); +} + +// Set radio element choices and picker states +function setPickersState(enablePickerId, disablePickerId, event) +{ + SetPickerEnabling(enablePickerId, disablePickerId); + + var radioElemValue = event.target.value; + + switch (event.target.id) { + case "fcc_selectAccount": + case "fcc_selectFolder": + gFccRadioElemChoice = radioElemValue; + break; + case "archive_selectAccount": + case "archive_selectFolder": + gArchivesRadioElemChoice = radioElemValue; + updateArchiveHierarchyButton(document.getElementById(enablePickerId) + .folder); + break; + case "draft_selectAccount": + case "draft_selectFolder": + gDraftsRadioElemChoice = radioElemValue; + break; + case "tmpl_selectAccount": + case "tmpl_selectFolder": + gTmplRadioElemChoice = radioElemValue; + break; + default: + dump("Error in setting picker state.\n"); + return; + } +} + +// This routine is to restore the correct radio element +// state when the fcc self checkbox broadcasts the change +function SetRadioButtons(selectPickerId, unselectPickerId) +{ + var activeRadioElem = document.getElementById(selectPickerId); + activeRadioElem.radioGroup.selectedItem = activeRadioElem; +} + +/** + * Enable/disable the archive hierarchy button depending on what folder is + * currently selected (Gmail IMAP folders should have the button disabled, since + * changing the archive hierarchy does nothing there. + * + * @param archiveFolder the currently-selected folder to store archives in + */ +function updateArchiveHierarchyButton(archiveFolder) { + let isGmailImap = (archiveFolder.server.type == "imap" && + archiveFolder.server.QueryInterface( + Components.interfaces.nsIImapIncomingServer) + .isGMailServer); + document.getElementById("archiveHierarchyButton").disabled = isGmailImap; +} + +/** + * Enable or disable (as appropriate) the controls for setting archive options + */ +function setupArchiveItems() { + let broadcaster = document.getElementById("broadcaster_archiveEnabled"); + let checked = document.getElementById("identity.archiveEnabled").checked; + let archiveFolder; + + if (checked) { + broadcaster.removeAttribute("disabled"); + switch (gArchivesRadioElemChoice) { + case "0": + if (!gArchivesRadioElemChoiceLocked) + SetPickerEnabling("msgArchivesAccountPicker", "msgArchivesFolderPicker"); + SetRadioButtons("archive_selectAccount", "archive_selectFolder"); + updateArchiveHierarchyButton(document.getElementById( + "msgArchivesAccountPicker").folder); + break; + + case "1": + if (!gArchivesRadioElemChoiceLocked) + SetPickerEnabling("msgArchivesFolderPicker", "msgArchivesAccountPicker"); + SetRadioButtons("archive_selectFolder", "archive_selectAccount"); + updateArchiveHierarchyButton(document.getElementById( + "msgArchivesFolderPicker").folder); + break; + + default: + dump("Error in setting Archive elements.\n"); + return; + } + } + else + broadcaster.setAttribute("disabled", "true"); +} + +/** + * Open a dialog to edit the folder hierarchy used when archiving messages. + */ +function ChangeArchiveHierarchy() { + let identity = parent.gIdentity || parent.getCurrentAccount().defaultIdentity; + + top.window.openDialog("chrome://messenger/content/am-archiveoptions.xul", + "", "centerscreen,chrome,modal,titlebar,resizable=yes", + identity); + return true; +} diff --git a/mailnews/base/prefs/content/am-copies.xul b/mailnews/base/prefs/content/am-copies.xul new file mode 100644 index 000000000..dce174a22 --- /dev/null +++ b/mailnews/base/prefs/content/am-copies.xul @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xul-overlay href="chrome://messenger/content/am-copiesOverlay.xul"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-copies.dtd"> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="©AndFolderTitle.label;" + onload="parent.onPanelLoaded('am-copies.xul');"> + + <vbox flex="1" style="overflow: auto;"> + <script type="application/javascript" src="chrome://messenger/content/AccountManager.js"/> + + <dialogheader title="©AndFolderTitle.label;"/> + <vbox id="copiesAndFolders"/> + </vbox> +</page> diff --git a/mailnews/base/prefs/content/am-copiesOverlay.xul b/mailnews/base/prefs/content/am-copiesOverlay.xul new file mode 100644 index 000000000..4b4cdbcfb --- /dev/null +++ b/mailnews/base/prefs/content/am-copiesOverlay.xul @@ -0,0 +1,311 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> + +<!DOCTYPE overlay [ +<!ENTITY % copiesDTD SYSTEM "chrome://messenger/locale/am-copies.dtd"> +%copiesDTD; +]> + +<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="chrome://messenger/content/am-copies.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + + <vbox flex="1" id="copiesAndFolders"> + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + </stringbundleset> + + <broadcaster id="broadcaster_doFcc"/> + <broadcaster id="broadcaster_archiveEnabled"/> + + <label hidden="true" wsm_persist="true" id="identity.fccFolder" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.fcc_folder"/> + <label hidden="true" wsm_persist="true" id="identity.draftFolder" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.draft_folder"/> + <label hidden="true" wsm_persist="true" id="identity.archiveFolder" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.archive_folder"/> + <label hidden="true" wsm_persist="true" id="identity.stationeryFolder" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.stationary_folder"/> + <label hidden="true" wsm_persist="true" id="identity.email"/> + <label hidden="true" wsm_persist="true" id="identity.fccFolderPickerMode" + pref="true" preftype="int" prefattribute="value" + prefstring="mail.identity.%identitykey%.fcc_folder_picker_mode"/> + <label hidden="true" wsm_persist="true" id="identity.draftsFolderPickerMode" + pref="true" preftype="int" prefattribute="value" + prefstring="mail.identity.%identitykey%.drafts_folder_picker_mode"/> + <label hidden="true" wsm_persist="true" id="identity.archivesFolderPickerMode" + pref="true" preftype="int" prefattribute="value" + prefstring="mail.identity.%identitykey%.archives_folder_picker_mode"/> + <label hidden="true" wsm_persist="true" id="identity.tmplFolderPickerMode" + pref="true" preftype="int" prefattribute="value" + prefstring="mail.identity.%identitykey%.tmpl_folder_picker_mode"/> + <groupbox id="copiesGroup"> + <caption label="&sendingPrefix.label;"/> + + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.doFcc" label="&fccMailFolder.label;" + accesskey="&fccMailFolder.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.fcc" + oncommand="setupFccItems();"/> + </hbox> + <radiogroup id="doFcc" aria-labelledby="identity.doFcc"> + <grid class="specialFolderPickerGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <radio id="fcc_selectAccount" + value="0" label="&sentFolderOn.label;" + accesskey="&sentFolderOn.accesskey;" + oncommand="setPickersState('msgFccAccountPicker', 'msgFccFolderPicker', event)" + observes="broadcaster_doFcc"/> + <menulist id="msgFccAccountPicker" + class="folderMenuItem" + aria-labelledby="fcc_selectAccount" + observes="broadcaster_doFcc"> + <menupopup id="msgFccAccountPopup" type="folder" mode="filing" + expandFolders="false" + oncommand="noteSelectionChange('fcc', 'Account', event)"/> + </menulist> + </row> + <row align="center"> + <radio id="fcc_selectFolder" + value="1" label="&sentInOtherFolder.label;" + accesskey="&sentInOtherFolder.accesskey;" + oncommand="setPickersState('msgFccFolderPicker', 'msgFccAccountPicker', event)" + observes="broadcaster_doFcc"/> + <menulist id="msgFccFolderPicker" + class="folderMenuItem" + aria-labelledby="fcc_selectFolder" + displayformat="verbose" + observes="broadcaster_doFcc"> + <menupopup id="msgFccFolderPopup" type="folder" mode="filing" + class="menulist-menupopup" + showFileHereLabel="true" + oncommand="noteSelectionChange('fcc', 'Folder', event)"/> + </menulist> + </row> + </rows> + </grid> + </radiogroup> + + <hbox align="center" class="fccReplyFollowsParent" hidable="true" hidefor="nntp,rss"> + <checkbox wsm_persist="true" id="identity.fccReplyFollowsParent" + label="&fccReplyFollowsParent.label;" + accesskey="&fccReplyFollowsParent.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.fcc_reply_follows_parent" + observes="broadcaster_doFcc"/> + </hbox> + + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + + <rows> + <row align="center"> + <checkbox wsm_persist="true" id="identity.doCc" label="&ccAddress.label;" + accesskey="&ccAddress.accesskey;" + control="identity.doCcList" + oncommand="setupCcTextbox();" + prefattribute="value" + prefstring="mail.identity.%identitykey%.doCc"/> + <textbox wsm_persist="true" id="identity.doCcList" flex="1" + aria-labelledby="identity.doCc" + prefstring="mail.identity.%identitykey%.doCcList" class="uri-element" + placeholder="&ccAddressList.placeholder;"/> + </row> + <row align="center"> + <checkbox wsm_persist="true" id="identity.doBcc" label="&bccAddress.label;" + accesskey="&bccAddress.accesskey;" + control="identity.doBccList" + oncommand="setupBccTextbox();" + prefattribute="value" + prefstring="mail.identity.%identitykey%.doBcc"/> + <textbox wsm_persist="true" id="identity.doBccList" flex="1" + aria-labelledby="identity.doBcc" + prefstring="mail.identity.%identitykey%.doBccList" class="uri-element" + placeholder="&bccAddressList.placeholder;"/> + </row> + </rows> + </grid> + + </groupbox> + + <groupbox id="archivesGroup"> + <caption label="&archivesTitle.label;"/> + + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.archiveEnabled" + label="&keepArchives.label;" + accesskey="&keepArchives.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.archive_enabled" + oncommand="setupArchiveItems();"/> + <spacer flex="1"/> + <button id="archiveHierarchyButton" + label="&archiveHierarchyButton.label;" + accesskey="&archiveHierarchyButton.accesskey;" + oncommand="ChangeArchiveHierarchy();" + observes="broadcaster_archiveEnabled"/> + </hbox> + + <radiogroup id="messageArchives"> + <grid class="specialFolderPickerGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <radio id="archive_selectAccount" + value="0" label="&archivesFolderOn.label;" + accesskey="&archivesFolderOn.accesskey;" + oncommand="setPickersState('msgArchivesAccountPicker', 'msgArchivesFolderPicker', event)" + observes="broadcaster_archiveEnabled"/> + <menulist id="msgArchivesAccountPicker" + class="folderMenuItem" + aria-labelledby="archive_selectAccount" + observes="broadcaster_archiveEnabled"> + <menupopup id="msgArchivesAccountPopup" type="folder" mode="filing" + expandFolders="false" + oncommand="noteSelectionChange('archive', 'Account', event)"/> + </menulist> + </row> + <row align="center"> + <radio id="archive_selectFolder" + value="1" label="&archiveInOtherFolder.label;" + accesskey="&archiveInOtherFolder.accesskey;" + oncommand="setPickersState('msgArchivesFolderPicker', 'msgArchivesAccountPicker', event)" + observes="broadcaster_archiveEnabled"/> + <menulist id="msgArchivesFolderPicker" + class="folderMenuItem" + aria-labelledby="archive_selectFolder" + displayformat="verbose" + observes="broadcaster_archiveEnabled"> + <menupopup id="msgArchivesFolderPopup" type="folder" mode="filing" + class="menulist-menupopup" + showFileHereLabel="true" + oncommand="noteSelectionChange('archive', 'Folder', event)"/> + </menulist> + </row> + </rows> + </grid> + </radiogroup> + </groupbox> + + <groupbox id="foldersGroup"> + <caption label="&specialFolders.label;"/> + + <hbox align="center"> + <label value="&keepDrafts2.label;" control="messageDrafts"/> + </hbox> + + <radiogroup id="messageDrafts"> + <grid class="specialFolderPickerGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <radio id="draft_selectAccount" + oncommand="setPickersState('msgDraftsAccountPicker', 'msgDraftsFolderPicker', event)" + value="0" label="&draftsFolderOn.label;" + accesskey="&draftsFolderOn.accesskey;"/> + <menulist id="msgDraftsAccountPicker" + class="folderMenuItem" + aria-labelledby="draft_selectAccount"> + <menupopup id="msgDraftAccountPopup" type="folder" mode="filing" + expandFolders="false" + oncommand="noteSelectionChange('draft', 'Account', event)"/> + </menulist> + </row> + <row align="center"> + <radio id="draft_selectFolder" + oncommand="setPickersState('msgDraftsFolderPicker', 'msgDraftsAccountPicker', event)" + value="1" label="&draftInOtherFolder.label;" + accesskey="&draftInOtherFolder.accesskey;"/> + <menulist id="msgDraftsFolderPicker" + class="folderMenuItem" + aria-labelledby="draft_selectFolder" + displayformat="verbose"> + <menupopup id="msgDraftFolderPopup" type="folder" mode="filing" + class="menulist-menupopup" + showFileHereLabel="true" + oncommand="noteSelectionChange('draft', 'Folder', event)"/> + </menulist> + </row> + </rows> + </grid> + </radiogroup> + + <hbox align="center"> + <label value="&keepTemplates.label;" control="messageTemplates"/> + </hbox> + + <radiogroup id="messageTemplates"> + <grid class="specialFolderPickerGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <radio id="tmpl_selectAccount" + oncommand="setPickersState('msgStationeryAccountPicker', 'msgStationeryFolderPicker', event)" + value="0" label="&templatesFolderOn.label;" + accesskey="&templatesFolderOn.accesskey;"/> + <menulist id="msgStationeryAccountPicker" + class="folderMenuItem" + aria-labelledby="tmpl_selectAccount"> + <menupopup id="msgFccAccountPopup" type="folder" mode="filing" + expandFolders="false" + oncommand="noteSelectionChange('tmpl', 'Account', event)"/> + </menulist> + </row> + <row align="center"> + <radio id="tmpl_selectFolder" + oncommand="setPickersState('msgStationeryFolderPicker', 'msgStationeryAccountPicker', event)" + value="1" label="&templateInOtherFolder.label;" + accesskey="&templateInOtherFolder.accesskey;"/> + <menulist id="msgStationeryFolderPicker" + class="folderMenuItem" + aria-labelledby="tmpl_selectFolder" + displayformat="verbose"> + <menupopup id="msgTemplFolderPopup" type="folder" mode="filing" + class="menulist-menupopup" + showFileHereLabel="true" + oncommand="noteSelectionChange('tmpl', 'Folder', event)"/> + </menulist> + </row> + </rows> + </grid> + </radiogroup> + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.showSaveMsgDlg" label="&saveMessageDlg.label;" + accesskey="&saveMessageDlg.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.showSaveMsgDlg"/> + </hbox> + </groupbox> + </vbox> +</overlay> diff --git a/mailnews/base/prefs/content/am-help.js b/mailnews/base/prefs/content/am-help.js new file mode 100644 index 000000000..da9d0b076 --- /dev/null +++ b/mailnews/base/prefs/content/am-help.js @@ -0,0 +1,76 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +/** + * Key value pairs to derive the tag based on the page loaded. + * Each key is the page loaded when user clicks on one of the items on + * the accounttree of the AccountManager window. + * Value is a tag that is preset which will be used to display + * context sensitive help. + */ +var pageTagPairs = { + "chrome://messenger/content/am-main.xul": "mail_account_identity", + "chrome://messenger/content/am-server.xul": "mail", + "chrome://messenger/content/am-copies.xul": "mail_copies", + "chrome://messenger/content/am-addressing.xul": "mail_addressing_settings", + "chrome://messenger/content/am-junk.xul": "mail-account-junk", + "chrome://messenger/content/am-offline.xul": "mail-offline-accounts", + "chrome://messenger/content/am-smtp.xul": "mail_smtp", + "chrome://messenger/content/am-smime.xul": "mail_security_settings", + "chrome://messenger/content/am-serverwithnoidentities.xul": "mail_local_folders_settings", + "chrome://messenger/content/am-mdn.xul": "mail-account-receipts", +} + +function doHelpButton() +{ + // Get the URI of the page loaded in the AccountManager's content frame. + var pageSourceURI = contentFrame.location.href; + // Get the help tag corresponding to the page loaded. + var helpTag = pageTagPairs[pageSourceURI]; + + // If the help tag is generic or offline, check if there is a need to set tags per server type + if ((helpTag == "mail") || (helpTag == "mail-offline-accounts")) { + // Get server type, as we may need to set help tags per server type for some pages + var serverType = GetServerType(); + + /** + * Check the page to be loaded. Following pages needed to be presented with the + * help content that is based on server type. For any pages with such requirement + * do add comments here about the page and a new case statement for pageSourceURI + * switch. + * - server settings ("chrome://messenger/content/am-server.xul") + * - offline/diskspace settings ("chrome://messenger/content/am-offline.xul") + */ + switch (pageSourceURI) { + case "chrome://messenger/content/am-server.xul": + helpTag = "mail_server_" + serverType; + break; + + case "chrome://messenger/content/am-offline.xul": + helpTag = "mail_offline_" + serverType; + break; + + default : + break; + } + } + + if ( helpTag ) + openHelp(helpTag); + else + openHelp('mail'); +} + +/** + * Get server type of the seleted item + */ +function GetServerType() +{ + var serverType = null; + var currentAccount = parent.getCurrentAccount(); + if (currentAccount) + serverType = currentAccount.incomingServer.type; + return serverType; +} diff --git a/mailnews/base/prefs/content/am-identities-list.js b/mailnews/base/prefs/content/am-identities-list.js new file mode 100644 index 000000000..2cabb400d --- /dev/null +++ b/mailnews/base/prefs/content/am-identities-list.js @@ -0,0 +1,180 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +Components.utils.import("resource:///modules/iteratorUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gIdentityListBox; // the root <listbox> node +var gAddButton; +var gEditButton; +var gSetDefaultButton; +var gDeleteButton; + +var gAccount = null; // the account we are showing the identities for + +function onLoad() +{ + gIdentityListBox = document.getElementById("identitiesList"); + gAddButton = document.getElementById("cmd_add"); + gEditButton = document.getElementById("cmd_edit"); + gSetDefaultButton = document.getElementById("cmd_default"); + gDeleteButton = document.getElementById("cmd_delete"); + + // extract the account + gAccount = window.arguments[0].account; + + var accountName = window.arguments[0].accountName; + document.title = document.getElementById("bundle_prefs") + .getFormattedString("identity-list-title", [accountName]); + + refreshIdentityList(0); +} + +/** + * Rebuilds the listbox holding the list of identities. + * + * @param aSelectIndex Attempt to select the identity with this index. + */ +function refreshIdentityList(aSelectIndex) +{ + // Remove all children. + while (gIdentityListBox.hasChildNodes()) + gIdentityListBox.lastChild.remove(); + + // Build the list from the identities array. + let identities = gAccount.identities; + for (let identity in fixIterator(identities, + Components.interfaces.nsIMsgIdentity)) + { + if (identity.valid) + { + let listitem = document.createElement("listitem"); + listitem.setAttribute("label", identity.identityName); + listitem.setAttribute("key", identity.key); + gIdentityListBox.appendChild(listitem); + } + } + + // Ensure one identity is always selected. + if (!aSelectIndex || aSelectIndex < 0) + aSelectIndex = 0; + else if (aSelectIndex >= gIdentityListBox.itemCount) + aSelectIndex = gIdentityListBox.itemCount - 1; + + // This also fires the onselect event, which in turn calls updateButtons(). + gIdentityListBox.selectedIndex = aSelectIndex; +} + +/** + * Opens the identity editor dialog. + * + * @param identity the identity (if any) to load in the dialog + */ +function openIdentityEditor(identity) +{ + let args = { identity: identity, account: gAccount, result: false }; + + let indexToSelect = identity ? gIdentityListBox.selectedIndex : + gIdentityListBox.itemCount; + + window.openDialog("am-identity-edit.xul", "", + "chrome,modal,resizable,centerscreen", args); + + if (args.result) + refreshIdentityList(indexToSelect); +} + +function getSelectedIdentity() +{ + if (gIdentityListBox.selectedItems.length != 1) + return null; + + var identityKey = gIdentityListBox.selectedItems[0].getAttribute("key"); + let identities = gAccount.identities; + for (let identity in fixIterator(identities, + Components.interfaces.nsIMsgIdentity)) + { + if (identity.valid && identity.key == identityKey) + return identity; + } + + return null; // no identity found +} + +function onEdit(event) +{ + var id = (event.target.localName == 'listbox') ? null : getSelectedIdentity(); + openIdentityEditor(id); +} + +/** + * Enable/disable buttons depending on number of identities and current selection. + */ +function updateButtons() +{ + // In this listbox there should always be one item selected. + if (gIdentityListBox.selectedItems.length != 1 || gIdentityListBox.itemCount == 0) { + // But in case this is not met (e.g. there is no identity for some reason, + // or the list is being rebuilt), disable all buttons. + gEditButton.setAttribute("disabled", "true"); + gDeleteButton.setAttribute("disabled", "true"); + gSetDefaultButton.setAttribute("disabled", "true"); + return; + } + + gEditButton.setAttribute("disabled", "false"); + gDeleteButton.setAttribute("disabled", (gIdentityListBox.itemCount <= 1) ? "true" : "false"); + gSetDefaultButton.setAttribute("disabled", (gIdentityListBox.selectedIndex == 0) ? "true" : "false"); + // The Add command is always enabled. +} + +function onSetDefault(event) +{ + let identity = getSelectedIdentity(); + if (!identity) + return; + + // If the first identity is selected, there is nothing to do. + if (gIdentityListBox.selectedIndex == 0) + return; + + gAccount.defaultIdentity = identity; + // Rebuilt the identity list and select the moved identity again. + refreshIdentityList(0); +} + +function onDelete(event) +{ + if (gIdentityListBox.itemCount <= 1) // don't support deleting the last identity + return; + + // get delete confirmation + let selectedIdentity = getSelectedIdentity(); + + let prefsBundle = document.getElementById("bundle_prefs"); + let confirmTitle = prefsBundle.getFormattedString("identity-delete-confirm-title", + [window.arguments[0].accountName]); + let confirmText = prefsBundle.getFormattedString("identity-delete-confirm", + [selectedIdentity.identityName]); + let confirmButton = prefsBundle.getString("identity-delete-confirm-button"); + + if (Services.prompt.confirmEx(window, confirmTitle, confirmText, + (Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING) + + (Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL), + confirmButton, null, null, null, {})) + return; + + let selectedItemIndex = gIdentityListBox.selectedIndex; + + gAccount.removeIdentity(selectedIdentity); + + refreshIdentityList(selectedItemIndex); +} + +function onOk() +{ + window.arguments[0].result = true; + return true; +} diff --git a/mailnews/base/prefs/content/am-identities-list.xul b/mailnews/base/prefs/content/am-identities-list.xul new file mode 100644 index 000000000..2acbc45d4 --- /dev/null +++ b/mailnews/base/prefs/content/am-identities-list.xul @@ -0,0 +1,67 @@ +<?xml version="1.0"?> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!-- 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/. --> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/am-identities-list.dtd"> + +<dialog + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoad();" + buttons="accept" + buttonlabelaccept="&identitiesListClose.label;" + buttonaccesskeyaccept="&identitiesListClose.accesskey;" + ondialogaccept="return onOk();" + ondialogcancel="return onOk();" + style="width: 40em;"> + +<stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/> +<script type="application/javascript" + src="chrome://messenger/content/am-identities-list.js"/> + +<commandset> + <command id="cmd_add" oncommand="openIdentityEditor(null);"/> + <command id="cmd_edit" oncommand="onEdit(event);" disabled="true"/> + <command id="cmd_default" oncommand="onSetDefault(event);" disabled="true"/> + <command id="cmd_delete" oncommand="onDelete(event);" disabled="true"/> +</commandset> + +<keyset> + <key keycode="VK_INSERT" command="cmd_add"/> + <key keycode="VK_DELETE" command="cmd_delete"/> +</keyset> + +<label control="identitiesList">&identitiesListManageDesc.label;</label> + +<separator class="thin"/> + +<hbox flex="1"> + <listbox id="identitiesList" + ondblclick="onEdit(event);" + onselect="updateButtons();" + seltype="single" + flex="1" + style="height: 0px;"/> + + <vbox> + <button id="addButton" + command="cmd_add" label="&identitiesListAdd.label;" + accesskey="&identitiesListAdd.accesskey;"/> + <button id="editButton" disabled="true" + command="cmd_edit" label="&identitiesListEdit.label;" + accesskey="&identitiesListEdit.accesskey;"/> + <button id="setDefaultButton" disabled="true" + command="cmd_default" label="&identitiesListDefault.label;" + accesskey="&identitiesListDefault.accesskey;"/> + <button id="deleteButton" disabled="true" + command="cmd_delete" label="&identitiesListDelete.label;" + accesskey="&identitiesListDelete.accesskey;"/> + </vbox> +</hbox> + +</dialog> diff --git a/mailnews/base/prefs/content/am-identity-edit.js b/mailnews/base/prefs/content/am-identity-edit.js new file mode 100644 index 000000000..cd6750779 --- /dev/null +++ b/mailnews/base/prefs/content/am-identity-edit.js @@ -0,0 +1,405 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gIdentity = null; // the identity we are editing (may be null for a new identity) +var gAccount = null; // the account the identity is (or will be) associated with + +function onLoadIdentityProperties() +{ + // extract the account + gIdentity = window.arguments[0].identity; + gAccount = window.arguments[0].account; + let prefBundle = document.getElementById("bundle_prefs"); + + // Make the dialog the same height and 90% of the width of the main Account + // manager page when the Account manager is not maximized. + let accountDialog = Services.wm.getMostRecentWindow("mailnews:accountmanager") + .document; + if (accountDialog.documentElement.getAttribute("sizemode") != "maximized") { + document.getElementById("identityDialog").style.width = + accountDialog.getElementById("accountManager").clientWidth * 0.9 + "px"; + document.getElementById("identityDialog").style.height = + accountDialog.getElementById("accountManager").clientHeight + "px"; + } + + if (gIdentity) { + let listName = gIdentity.identityName; + document.title = prefBundle + .getFormattedString("identityDialogTitleEdit", [listName]); + } else { + document.title = prefBundle.getString("identityDialogTitleAdd"); + } + + loadSMTPServerList(); + + initIdentityValues(gIdentity); + initCopiesAndFolder(gIdentity); + initCompositionAndAddressing(gIdentity); +} + +// based on the values of gIdentity, initialize the identity fields we expose to the user +function initIdentityValues(identity) +{ + function initSmtpServer(aServerKey) { + // Select a server in the SMTP server menulist by its key. + // The value of the identity.smtpServerKey is null when the + // "use default server" option is used so, if we get that passed in, select + // the useDefaultItem representing this option by using the value of "". + document.getElementById("identity.smtpServerKey").value = aServerKey || ""; + } + + if (identity) + { + document.getElementById('identity.fullName').value = identity.fullName; + document.getElementById('identity.email').value = identity.email; + document.getElementById('identity.replyTo').value = identity.replyTo; + document.getElementById('identity.organization').value = identity.organization; + document.getElementById('identity.attachSignature').checked = identity.attachSignature; + document.getElementById('identity.htmlSigText').value = identity.htmlSigText; + document.getElementById('identity.htmlSigFormat').checked = identity.htmlSigFormat; + + if (identity.signature) + document.getElementById('identity.signature').value = identity.signature.path; + + document.getElementById('identity.attachVCard').checked = identity.attachVCard; + document.getElementById('identity.escapedVCard').value = identity.escapedVCard; + initSmtpServer(identity.smtpServerKey); + + // This field does not exist for the default identity shown in the am-main.xul pane. + let idLabel = document.getElementById("identity.label"); + if (idLabel) + idLabel.value = identity.label; + } + else + { + // We're adding an identity, use the best default we have. + initSmtpServer(gAccount.defaultIdentity.smtpServerKey); + } + + setupSignatureItems(); +} + +function initCopiesAndFolder(identity) +{ + // if we are editing an existing identity, use it...otherwise copy our values from the default identity + var copiesAndFoldersIdentity = identity ? identity : gAccount.defaultIdentity; + + document.getElementById('identity.fccFolder').value = copiesAndFoldersIdentity.fccFolder; + document.getElementById('identity.draftFolder').value = copiesAndFoldersIdentity.draftFolder; + document.getElementById('identity.archiveFolder').value = copiesAndFoldersIdentity.archiveFolder; + document.getElementById('identity.stationeryFolder').value = copiesAndFoldersIdentity.stationeryFolder; + + document.getElementById('identity.fccFolderPickerMode').value = copiesAndFoldersIdentity.fccFolderPickerMode ? copiesAndFoldersIdentity.fccFolderPickerMode : 0; + document.getElementById('identity.draftsFolderPickerMode').value = copiesAndFoldersIdentity.draftsFolderPickerMode ? copiesAndFoldersIdentity.draftsFolderPickerMode : 0; + document.getElementById('identity.archivesFolderPickerMode').value = copiesAndFoldersIdentity.archivesFolderPickerMode ? copiesAndFoldersIdentity.archivesFolderPickerMode : 0; + document.getElementById('identity.tmplFolderPickerMode').value = copiesAndFoldersIdentity.tmplFolderPickerMode ? copiesAndFoldersIdentity.tmplFolderPickerMode : 0; + + document.getElementById('identity.doCc').checked = copiesAndFoldersIdentity.doCc; + document.getElementById('identity.doCcList').value = copiesAndFoldersIdentity.doCcList; + document.getElementById('identity.doBcc').checked = copiesAndFoldersIdentity.doBcc; + document.getElementById('identity.doBccList').value = copiesAndFoldersIdentity.doBccList; + document.getElementById('identity.doFcc').checked = copiesAndFoldersIdentity.doFcc; + document.getElementById('identity.fccReplyFollowsParent').checked = copiesAndFoldersIdentity.fccReplyFollowsParent; + document.getElementById('identity.showSaveMsgDlg').checked = copiesAndFoldersIdentity.showSaveMsgDlg; + document.getElementById('identity.archiveEnabled').checked = copiesAndFoldersIdentity.archiveEnabled; + + onInitCopiesAndFolders(); // am-copies.js method +} + +function initCompositionAndAddressing(identity) +{ + // if we are editing an existing identity, use it...otherwise copy our values from the default identity + var addressingIdentity = identity ? identity : gAccount.defaultIdentity; + + document.getElementById('identity.directoryServer').value = addressingIdentity.directoryServer; + document.getElementById('identity.overrideGlobal_Pref').value = addressingIdentity.overrideGlobalPref; + let autoCompleteElement = document.getElementById('identity.autocompleteToMyDomain'); + if (autoCompleteElement) // Thunderbird does not have this element. + autoCompleteElement.checked = addressingIdentity.autocompleteToMyDomain; + + document.getElementById('identity.composeHtml').checked = addressingIdentity.composeHtml; + document.getElementById('identity.autoQuote').checked = addressingIdentity.autoQuote; + document.getElementById('identity.replyOnTop').value = addressingIdentity.replyOnTop; + document.getElementById('identity.sig_bottom').value = addressingIdentity.sigBottom; + document.getElementById('identity.sig_on_reply').checked = addressingIdentity.sigOnReply; + document.getElementById('identity.sig_on_fwd').checked = addressingIdentity.sigOnForward; + + onInitCompositionAndAddressing(); // am-addressing.js method +} + +function onOk() +{ + if (!validEmailAddress()) + return false; + + // if we are adding a new identity, create an identity, set the fields and add it to the + // account. + if (!gIdentity) + { + // ask the account manager to create a new identity for us + gIdentity = MailServices.accounts.createIdentity(); + + // copy in the default identity settings so we inherit lots of stuff like the defaul drafts folder, etc. + gIdentity.copy(gAccount.defaultIdentity); + + // assume the identity is valid by default? + gIdentity.valid = true; + + // add the identity to the account + gAccount.addIdentity(gIdentity); + + // now fall through to saveFields which will save our new values + } + + // if we are modifying an existing identity, save the fields + saveIdentitySettings(gIdentity); + saveCopiesAndFolderSettings(gIdentity); + saveAddressingAndCompositionSettings(gIdentity); + + window.arguments[0].result = true; + + return true; +} + +// returns false and prompts the user if +// the identity does not have an email address +function validEmailAddress() +{ + var emailAddress = document.getElementById('identity.email').value; + + // quickly test for an @ sign to test for an email address. We don't have + // to be anymore precise than that. + if (!emailAddress.includes("@")) + { + // alert user about an invalid email address + + var prefBundle = document.getElementById("bundle_prefs"); + + Services.prompt.alert(window, prefBundle.getString("identity-edit-req-title"), + prefBundle.getString("identity-edit-req")); + return false; + } + + return true; +} + +function saveIdentitySettings(identity) +{ + if (identity) + { + let idLabel = document.getElementById('identity.label'); + if (idLabel) + identity.label = idLabel.value; + identity.fullName = document.getElementById('identity.fullName').value; + identity.email = document.getElementById('identity.email').value; + identity.replyTo = document.getElementById('identity.replyTo').value; + identity.organization = document.getElementById('identity.organization').value; + identity.attachSignature = document.getElementById('identity.attachSignature').checked; + identity.htmlSigText = document.getElementById('identity.htmlSigText').value; + identity.htmlSigFormat = document.getElementById('identity.htmlSigFormat').checked; + + identity.attachVCard = document.getElementById('identity.attachVCard').checked; + identity.escapedVCard = document.getElementById('identity.escapedVCard').value; + identity.smtpServerKey = document.getElementById('identity.smtpServerKey').value; + + var attachSignaturePath = document.getElementById('identity.signature').value; + identity.signature = null; // this is important so we don't accidentally inherit the default + + if (attachSignaturePath) + { + // convert signature path back into a nsIFile + var sfile = Components.classes["@mozilla.org/file/local;1"] + .createInstance(Components.interfaces.nsIFile); + sfile.initWithPath(attachSignaturePath); + if (sfile.exists()) + identity.signature = sfile; + } + } +} + +function saveCopiesAndFolderSettings(identity) +{ + onSaveCopiesAndFolders(); // am-copies.js routine + + identity.fccFolder = document.getElementById('identity.fccFolder').value; + identity.draftFolder = document.getElementById('identity.draftFolder').value; + identity.archiveFolder = document.getElementById('identity.archiveFolder').value; + identity.stationeryFolder = document.getElementById('identity.stationeryFolder').value; + identity.fccFolderPickerMode = document.getElementById('identity.fccFolderPickerMode').value; + identity.draftsFolderPickerMode = document.getElementById('identity.draftsFolderPickerMode').value; + identity.archivesFolderPickerMode = document.getElementById('identity.archivesFolderPickerMode').value; + identity.tmplFolderPickerMode = document.getElementById('identity.tmplFolderPickerMode').value; + identity.doCc = document.getElementById('identity.doCc').checked; + identity.doCcList = document.getElementById('identity.doCcList').value; + identity.doBcc = document.getElementById('identity.doBcc').checked; + identity.doBccList = document.getElementById('identity.doBccList').value; + identity.doFcc = document.getElementById('identity.doFcc').checked; + identity.fccReplyFollowsParent = document.getElementById('identity.fccReplyFollowsParent').checked; + identity.showSaveMsgDlg = document.getElementById('identity.showSaveMsgDlg').checked; + identity.archiveEnabled = document.getElementById('identity.archiveEnabled').checked; +} + +function saveAddressingAndCompositionSettings(identity) +{ + identity.directoryServer = document.getElementById('identity.directoryServer').value; + identity.overrideGlobalPref = document.getElementById('identity.overrideGlobal_Pref').value == "true"; + let autoCompleteElement = document.getElementById('identity.autocompleteToMyDomain'); + if (autoCompleteElement) // Thunderbird does not have this element. + identity.autocompleteToMyDomain = autoCompleteElement.checked; + identity.composeHtml = document.getElementById('identity.composeHtml').checked; + identity.autoQuote = document.getElementById('identity.autoQuote').checked; + identity.replyOnTop = document.getElementById('identity.replyOnTop').value; + identity.sigBottom = document.getElementById('identity.sig_bottom').value == 'true'; + identity.sigOnReply = document.getElementById('identity.sig_on_reply').checked; + identity.sigOnForward = document.getElementById('identity.sig_on_fwd').checked; +} + +function selectFile() +{ + const nsIFilePicker = Components.interfaces.nsIFilePicker; + + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + + var prefBundle = document.getElementById("bundle_prefs"); + var title = prefBundle.getString("choosefile"); + fp.init(window, title, nsIFilePicker.modeOpen); + fp.appendFilters(nsIFilePicker.filterAll); + + // Get current signature folder, if there is one. + // We can set that to be the initial folder so that users + // can maintain their signatures better. + var sigFolder = GetSigFolder(); + if (sigFolder) + fp.displayDirectory = sigFolder; + + var ret = fp.show(); + if (ret == nsIFilePicker.returnOK) { + var folderField = document.getElementById("identity.signature"); + folderField.value = fp.file.path; + } +} + +function GetSigFolder() +{ + var sigFolder = null; + try + { + var account = parent.getCurrentAccount(); + var identity = account.defaultIdentity; + var signatureFile = identity.signature; + + if (signatureFile) + { + signatureFile = signatureFile.QueryInterface(Components.interfaces.nsIFile); + sigFolder = signatureFile.parent; + + if (!sigFolder.exists()) + sigFolder = null; + } + } + catch (ex) { + dump("failed to get signature folder..\n"); + } + return sigFolder; +} + +// Signature textbox is active unless option to select from file is checked. +// If a signature is need to be attached, the associated items which +// displays the absolute path to the signature (in a textbox) and the way +// to select a new signature file (a button) are enabled. Otherwise, they +// are disabled. Check to see if the attachSignature is locked to block +// broadcasting events. +function setupSignatureItems() +{ + var signature = document.getElementById("identity.signature"); + var browse = document.getElementById("identity.sigbrowsebutton"); + var htmlSigText = document.getElementById("identity.htmlSigText"); + var htmlSigFormat = document.getElementById("identity.htmlSigFormat"); + var attachSignature = document.getElementById("identity.attachSignature"); + var checked = attachSignature.checked; + + if (checked) + { + htmlSigText.setAttribute("disabled", "true"); + htmlSigFormat.setAttribute("disabled", "true"); + } + else + { + htmlSigText.removeAttribute("disabled"); + htmlSigFormat.removeAttribute("disabled"); + } + + if (checked && !getAccountValueIsLocked(signature)) + signature.removeAttribute("disabled"); + else + signature.setAttribute("disabled", "true"); + + if (checked && !getAccountValueIsLocked(browse)) + browse.removeAttribute("disabled"); + else + browse.setAttribute("disabled", "true"); +} + +function editVCardCallback(escapedVCardStr) +{ + var escapedVCard = document.getElementById("identity.escapedVCard"); + escapedVCard.value = escapedVCardStr; +} + +function editVCard() +{ + var escapedVCard = document.getElementById("identity.escapedVCard"); + + // read vCard hidden value from UI + window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul", + "", + "chrome,modal,resizable=no,centerscreen", + {escapedVCardStr:escapedVCard.value, okCallback:editVCardCallback, + titleProperty:"editVCardTitle", hideABPicker:true}); +} + +function getAccountForFolderPickerState() +{ + return gAccount; +} + +/** + * Build the SMTP server list for display. + */ +function loadSMTPServerList() +{ + var smtpServerList = document.getElementById("identity.smtpServerKey"); + let servers = MailServices.smtp.servers; + let defaultServer = MailServices.smtp.defaultServer; + + var smtpPopup = smtpServerList.menupopup; + while (smtpPopup.lastChild.nodeName != "menuseparator") + smtpPopup.lastChild.remove(); + + while (servers.hasMoreElements()) + { + var server = servers.getNext(); + + if (server instanceof Components.interfaces.nsISmtpServer) + { + var serverName = ""; + if (server.description) + serverName = server.description + ' - '; + else if (server.username) + serverName = server.username + ' - '; + serverName += server.hostname; + + if (defaultServer.key == server.key) + serverName += " " + document.getElementById("bundle_messenger") + .getString("defaultServerTag"); + + smtpServerList.appendItem(serverName, server.key); + } + } +} diff --git a/mailnews/base/prefs/content/am-identity-edit.xul b/mailnews/base/prefs/content/am-identity-edit.xul new file mode 100644 index 000000000..448892382 --- /dev/null +++ b/mailnews/base/prefs/content/am-identity-edit.xul @@ -0,0 +1,154 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/am-copiesOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/am-addressingOverlay.xul"?> + +<!DOCTYPE dialog [ +<!ENTITY % identityEditDTD SYSTEM "chrome://messenger/locale/am-identity-edit.dtd" > +%identityEditDTD; +<!ENTITY % identityDTD SYSTEM "chrome://messenger/locale/am-main.dtd" > +%identityDTD; +]> + +<dialog id="identityDialog" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="onLoadIdentityProperties();" + ondialogaccept="return onOk();" + style="&identityDialog.style;"> + + <stringbundle id="bundle_prefs" + src="chrome://messenger/locale/prefs.properties"/> + <stringbundle id="bundle_messenger" + src="chrome://messenger/locale/messenger.properties"/> + + <script type="application/javascript" + src="chrome://messenger/content/am-prefs.js"/> + <script type="application/javascript" + src="chrome://messenger/content/am-identity-edit.js"/> + + <broadcaster id="broadcaster_attachSignature"/> + + <description flex="1">&identityListDesc.label;</description> + <separator class="thin"/> + + <tabbox flex="1" style="overflow: auto;"> + <tabs id="identitySettings"> + <tab label="&settingsTab.label;"/> + <tab label="&copiesFoldersTab.label;"/> + <tab label="&addressingTab.label;"/> + </tabs> + + <tabpanels id="identityTabsPanels" flex="1"> + <!-- Identity Settings Tab --> + <vbox flex="1" name="settings"> + <groupbox> + <caption label="&publicData.label;"/> + + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&name.label;" control="identity.fullName" accesskey="&name.accesskey;"/> + <textbox id="identity.fullName" size="30"/> + </row> + <row align="center"> + <label value="&email.label;" control="identity.email" accesskey="&email.accesskey;"/> + <textbox id="identity.email" class="uri-element"/> + </row> + <row align="center"> + <label value="&replyTo.label;" control="identity.replyTo" accesskey="&replyTo.accesskey;"/> + <textbox id="identity.replyTo" class="uri-element" placeholder="&replyTo.placeholder;"/> + </row> + <row align="center"> + <label value="&organization.label;" control="identity.organization" accesskey="&organization.accesskey;"/> + <textbox id="identity.organization"/> + </row> + <separator class="thin"/> + <row align="center"> + <label value="&signatureText.label;" control="identity.htmlSigText" accesskey="&signatureText.accesskey;"/> + <checkbox id="identity.htmlSigFormat" label="&signatureHtml.label;" accesskey="&signatureHtml.accesskey;"/> + </row> + </rows> + </grid> + + <hbox class="indent" flex="1"> + <textbox id="identity.htmlSigText" flex="1" multiline="true" wrap="off" rows="4" class="signatureBox"/> + </hbox> + + <hbox align="center"> + <checkbox id="identity.attachSignature" label="&signatureFile.label;" flex="1" + accesskey="&signatureFile.accesskey;" + oncommand="setupSignatureItems();"/> + </hbox> + + <hbox align="center" class="indent"> + <textbox id="identity.signature" datatype="nsIFile" flex="1" name="identity.signature" + aria-labelledby="identity.attachSignature" + observes="broadcaster_attachSignature" class="uri-element"/> + <button class="push" name="browse" label="&choose.label;" + accesskey="&choose.accesskey;" + oncommand="selectFile()" + observes="broadcaster_attachSignature" + id="identity.sigbrowsebutton"/> + </hbox> + + <hbox align="center"> + <checkbox id="identity.attachVCard" label="&attachVCard.label;" flex="1" + accesskey="&attachVCard.accesskey;"/> + <button class="push" name="editVCard" label="&editVCard.label;" + accesskey="&editVCard.accesskey;" + oncommand="editVCard()"/> + <label hidden="true" id="identity.escapedVCard"/> + </hbox> + </groupbox> + + <groupbox> + <caption label="&privateData.label;"/> + + <hbox align="center"> + <label value="&smtpName.label;" + control="identity.smtpServerKey" + accesskey="&smtpName.accesskey;"/> + <menulist id="identity.smtpServerKey" flex="1"> + <menupopup id="smtpPopup"> + <menuitem value="" + label="&smtpDefaultServer.label;" + id="useDefaultItem"/> + <menuseparator/> + <!-- list will be inserted here --> + </menupopup> + </menulist> + </hbox> + + <separator class="thin"/> + + <hbox align="center"> + <label value="&identityAlias.label;" + accesskey="&identityAlias.accesskey;" + control="identity.label"/> + <textbox id="identity.label" flex="1"/> + </hbox> + </groupbox> + + <spacer flex="1"/> + </vbox> + + <!-- Copies & Folders Tab --> + <vbox flex="1" name="copiesAndFolders" id="copiesAndFolders"/> + + <!-- Composition & Addressing Tab --> + <vbox flex="1" name="composeAddressing" id="compositionAndAddressing"/> + + </tabpanels> + </tabbox> +</dialog> diff --git a/mailnews/base/prefs/content/am-junk.js b/mailnews/base/prefs/content/am-junk.js new file mode 100644 index 000000000..77daac2ca --- /dev/null +++ b/mailnews/base/prefs/content/am-junk.js @@ -0,0 +1,296 @@ +/* -*- Mode: Java; tab-width: 2; 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/. + */ +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource:///modules/iteratorUtils.jsm"); +Components.utils.import("resource:///modules/MailUtils.js"); + +var gDeferredToAccount = ""; + +function onInit(aPageId, aServerId) +{ + // manually adjust several pref UI elements + document.getElementById('server.spamLevel.visible').setAttribute("checked", + document.getElementById('server.spamLevel').value > 0); + + let deferredToURI = null; + if (gDeferredToAccount) + deferredToURI = MailServices.accounts + .getAccount(gDeferredToAccount) + .incomingServer.serverURI; + + let spamActionTargetAccountElement = + document.getElementById("server.spamActionTargetAccount"); + let spamActionTargetFolderElement = + document.getElementById("server.spamActionTargetFolder"); + + let spamActionTargetAccount = spamActionTargetAccountElement.value; + let spamActionTargetFolder = spamActionTargetFolderElement.value; + + let moveOnSpamCheckbox = document.getElementById("server.moveOnSpam"); + let moveOnSpamValue = moveOnSpamCheckbox.checked; + + // Check if there are any invalid junk targets and fix them. + [ spamActionTargetAccount, spamActionTargetFolder, moveOnSpamValue ] = + sanitizeJunkTargets(spamActionTargetAccount, + spamActionTargetFolder, + deferredToURI || aServerId, + document.getElementById("server.moveTargetMode").value, + MailUtils.getFolderForURI(aServerId, false).server.spamSettings, + moveOnSpamValue); + + spamActionTargetAccountElement.value = spamActionTargetAccount; + spamActionTargetFolderElement.value = spamActionTargetFolder; + moveOnSpamCheckbox.checked = moveOnSpamValue; + + let server = MailUtils.getFolderForURI(spamActionTargetAccount, false); + document.getElementById("actionAccountPopup").selectFolder(server); + + let folder = MailUtils.getFolderForURI(spamActionTargetFolder, true); + document.getElementById("actionFolderPopup").selectFolder(folder); + + var currentArray = []; + if (document.getElementById("server.useWhiteList").checked) + currentArray = document.getElementById("server.whiteListAbURI").value.split(" "); + + // set up the whitelist UI + var wList = document.getElementById("whiteListAbURI"); + // Ensure the whitelist is empty + for (let i = wList.itemCount - 1; i >= 0; i--) { + wList.removeItemAt(i); + } + + // Populate the listbox with address books + let abItems = []; + for (let ab in fixIterator(MailServices.ab.directories, + Components.interfaces.nsIAbDirectory)) { + // We skip mailing lists and remote address books. + if (ab.isMailList || ab.isRemote) + continue; + + abItems.push({ label: ab.dirName, URI: ab.URI }); + } + + // Sort the list + function sortFunc(a, b) { + return a.label.localeCompare(b.label); + } + abItems.sort(sortFunc); + + // And then append each item to the listbox + for (let abItem of abItems) { + let item = wList.appendItem(abItem.label, abItem.URI); + item.setAttribute("type", "checkbox"); + item.setAttribute("class", "listitem-iconic"); + + // Due to bug 448582, we have to use setAttribute to set the + // checked value of the listitem. + item.setAttribute("checked", currentArray.includes(abItem.URI)); + } + + // enable or disable the whitelist + onAdaptiveJunkToggle(); + + // set up trusted IP headers + var serverFilterList = document.getElementById("useServerFilterList"); + serverFilterList.value = + document.getElementById("server.serverFilterName").value; + if (!serverFilterList.selectedItem) + serverFilterList.selectedIndex = 0; + + // enable or disable the useServerFilter checkbox + onCheckItem("useServerFilterList", ["server.useServerFilter"]); + + updateJunkTargetsAndRetention(); +} + +function onPreInit(account, accountValues) +{ + if (getAccountValue(account, accountValues, "server", "type", null, false) == "pop3") + gDeferredToAccount = getAccountValue(account, accountValues, + "pop3", "deferredToAccount", + null, false); + + buildServerFilterMenuList(); +} + +/** + * Called when someone checks or unchecks the adaptive junk mail checkbox. + * set the value of the hidden element accordingly + * + * @param aValue the boolean value of the checkbox + */ +function updateSpamLevel(aValue) +{ + document.getElementById('server.spamLevel').value = aValue ? 100 : 0; + onAdaptiveJunkToggle(); +} + +/** + * Propagate changes to the server filter menu list back to + * our hidden wsm element. + */ +function onServerFilterListChange() +{ + document.getElementById('server.serverFilterName').value = + document.getElementById("useServerFilterList").value; +} + +/** + * Called when someone checks or unchecks the adaptive junk mail checkbox. + * We need to enable or disable the whitelist accordingly. + */ +function onAdaptiveJunkToggle() +{ + onCheckItem("whiteListAbURI", ["server.spamLevel.visible"]); + onCheckItem("whiteListLabel", ["server.spamLevel.visible"]); + + // Enable/disable individual listbox rows. + // Setting enable/disable on the parent listbox does not seem to work. + let wList = document.getElementById("whiteListAbURI"); + let wListDisabled = wList.disabled; + + for (let i = 0; i < wList.getRowCount(); i++) { + wList.getItemAtIndex(i).setAttribute("disabled", wListDisabled); + } +} + +/** + * Called when someone checks or unchecks the "move new junk messages to" + * Enable/disable the radio group accordingly. + */ +function updateJunkTargetsAndRetention() { + onCheckItem("server.moveTargetMode", ["server.moveOnSpam"]); + updateJunkTargets(); + onCheckItem("server.purgeSpam", ["server.moveOnSpam"]); + document.getElementById("purgeLabel").disabled = + document.getElementById("server.purgeSpam").disabled; + updateJunkRetention(); +} + +/** + * Enable/disable the folder pickers depending on which radio item is selected. + */ +function updateJunkTargets() { + onCheckItem("actionTargetAccount", ["server.moveOnSpam", "moveTargetMode0"]); + onCheckItem("actionTargetFolder", ["server.moveOnSpam", "moveTargetMode1"]); +} + +/** + * Enable/disable the junk deletion interval depending on the state + * of the controlling checkbox. + */ +function updateJunkRetention() { + onCheckItem("server.purgeSpamInterval", ["server.purgeSpam", "server.moveOnSpam"]); +} + +function onSave() +{ + onSaveWhiteList(); +} + +/** + * Propagate changes to the whitelist menu list back to + * our hidden wsm element. + */ +function onSaveWhiteList() +{ + var wList = document.getElementById("whiteListAbURI"); + var wlArray = []; + + for (var i = 0; i < wList.getRowCount(); i++) + { + // Due to bug 448582, do not trust any properties of the listitems + // as they may not return the right value or may even not exist. + // Always get the attributes only. + var wlNode = wList.getItemAtIndex(i); + if (wlNode.getAttribute("checked") == "true") { + let abURI = wlNode.getAttribute("value"); + wlArray.push(abURI); + } + } + var wlValue = wlArray.join(" "); + document.getElementById("server.whiteListAbURI").setAttribute("value", wlValue); + document.getElementById("server.useWhiteList").checked = (wlValue != ""); +} + +/** + * Called when a new value is chosen in one of the junk target folder pickers. + * Sets the menu label according to the folder name. + */ +function onActionTargetChange(aEvent, aWSMElementId) +{ + let folder = aEvent.target._folder; + document.getElementById(aWSMElementId).value = folder.URI; + document.getElementById("actionFolderPopup").selectFolder(folder); +} + +/** + * Enumerates over the "ISPDL" directories, calling buildServerFilterListFromDir + * for each one. + */ +function buildServerFilterMenuList() +{ + const KEY_ISP_DIRECTORY_LIST = "ISPDL"; + let ispHeaderList = document.getElementById("useServerFilterList"); + + // Ensure the menulist is empty. + ispHeaderList.removeAllItems(); + + // Now walk through the isp directories looking for sfd files. + let ispDirectories = Services.dirsvc.get(KEY_ISP_DIRECTORY_LIST, + Components.interfaces.nsISimpleEnumerator); + + let menuEntries = []; + while (ispDirectories.hasMoreElements()) + { + let ispDirectory = ispDirectories.getNext() + .QueryInterface(Components.interfaces.nsIFile); + if (ispDirectory) + menuEntries.push.apply(menuEntries, buildServerFilterListFromDir(ispDirectory, menuEntries)); + } + + menuEntries.sort((a, b) => a.localeCompare(b)); + for (let entry of menuEntries) { + ispHeaderList.appendItem(entry, entry); + } +} + +/** + * Helper function called by buildServerFilterMenuList. Enumerates over the + * passed in directory looking for .sfd files. For each entry found, it gets + * appended to the menu list. + * + * @param aDir directory to look for .sfd files + * @param aExistingEntries Filter names already found. + */ +function buildServerFilterListFromDir(aDir, aExistingEntries) +{ + let newEntries = []; + // Now iterate over each file in the directory looking for .sfd files. + const kSuffix = ".sfd"; + let entries = aDir.directoryEntries + .QueryInterface(Components.interfaces.nsIDirectoryEnumerator); + + while (entries.hasMoreElements()) { + let entry = entries.nextFile; + // we only care about files that end in .sfd + if (entry.isFile() && entry.leafName.endsWith(kSuffix)) { + let fileName = entry.leafName.slice(0, -kSuffix.length); + // If we've already added an item with this name, then don't add it again. + if (aExistingEntries.indexOf(fileName) == -1) + newEntries.push(fileName); + } + } + return newEntries; +} + +/** + * Open the Preferences dialog on the Junk settings tab. + */ +function showGlobalJunkPrefs() +{ + openPrefsFromAccountManager("paneSecurity", "junkTab", null, "junk_pane"); +} diff --git a/mailnews/base/prefs/content/am-junk.xul b/mailnews/base/prefs/content/am-junk.xul new file mode 100644 index 000000000..96ee9eaf6 --- /dev/null +++ b/mailnews/base/prefs/content/am-junk.xul @@ -0,0 +1,232 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. +--> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> + +<!DOCTYPE window [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % junkMailDTD SYSTEM "chrome://messenger/locale/am-junk.dtd"> +%junkMailDTD; +]> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:nc="http://home.netscape.com/NC-rdf#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + title="&junkSettings.label;" + onload="parent.onPanelLoaded('am-junk.xul');"> + + <vbox flex="1" style="overflow: auto;"> + <script type="application/javascript" src="chrome://messenger/content/AccountManager.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-junk.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-prefs.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + + <dialogheader title="&junkSettings.label;"/> + + <groupbox> + <caption label="&junkClassification.label;"/> + + <label hidden="true" + id="server.spamLevel" + wsm_persist="true" + pref="true" + preftype="int" + prefattribute="value" + genericattr="true" + prefstring="mail.server.%serverkey%.spamLevel"/> + <label hidden="true" + id="server.spamActionTargetAccount" + wsm_persist="true" + pref="true" + preftype="string" + prefattribute="value" + genericattr="true" + prefstring="mail.server.%serverkey%.spamActionTargetAccount"/> + <label hidden="true" + id="server.spamActionTargetFolder" + wsm_persist="true" + pref="true" + preftype="string" + prefattribute="value" + genericattr="true" + prefstring="mail.server.%serverkey%.spamActionTargetFolder"/> + <label hidden="true" + id="server.whiteListAbURI" + wsm_persist="true" + pref="true" + preftype="string" + prefattribute="value" + genericattr="true" + prefstring="mail.server.%serverkey%.whiteListAbURI"/> + <label hidden="true" + id="server.serverFilterName" + wsm_persist="true" + pref="true" + preftype="string" + prefattribute="value" + genericattr="true" + prefstring="mail.server.%serverkey%.serverFilterName"/> + + <checkbox id="server.spamLevel.visible" + oncommand="updateSpamLevel(this.checked);" + accesskey="&level.accesskey;" + prefstring="mail.server.%serverkey%.spamLevel" + label="&level.label;"/> + + <separator class="thin"/> + + <description width="1">&trainingDescription.label;</description> + + <separator class="thin"/> + <spacer height="3"/> + + <vbox class="indent"> + <checkbox hidden="true" + id="server.useWhiteList" + genericattr="true" + pref="true" + wsm_persist="true" + preftype="bool" + prefstring="mail.server.%serverkey%.useWhiteList"/> + <label id="whiteListLabel" + accesskey="&whitelistHeader.accesskey;" + control="whiteListAbURI">&whitelistHeader.label;</label> + <listbox id="whiteListAbURI" rows="5"/> + </vbox> + + <separator/> + + <vbox> + <hbox> + <checkbox id="server.useServerFilter" + label="&ispHeaders.label;" + accesskey="&ispHeaders.accesskey;" + genericattr="true" + pref="true" + wsm_persist="true" + preftype="bool" + oncommand="onCheckItem('useServerFilterList', [this.id]);" + prefstring="mail.server.%serverkey%.useServerFilter"/> + <menulist id="useServerFilterList" + oncommand="onServerFilterListChange();" + aria-labelledby="server.useServerFilter"/> + </hbox> + </vbox> + + <separator class="thin"/> + + <description width="1">&ispHeadersWarning.label;</description> + + </groupbox> + + <groupbox> + <caption label="&junkActions.label;"/> + + <checkbox id="server.moveOnSpam" + label="&move.label;" + accesskey="&move.accesskey;" + oncommand="updateJunkTargetsAndRetention();" + wsm_persist="true" + pref="true" + preftype="bool" + genericattr="true" + prefstring="mail.server.%serverkey%.moveOnSpam"/> + + <radiogroup id="server.moveTargetMode" + aria-labelledby="server.moveOnSpam" + prefstring="mail.server.%serverkey%.moveTargetMode" + wsm_persist="true" + pref="true" + preftype="int" + genericattr="true" + oncommand="updateJunkTargets();" + prefvalue="value"> + + <grid class="specialFolderPickerGrid indent"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row> + <radio id="moveTargetMode0" + value="0" + label="&junkFolderOn.label;" + accesskey="&junkFolderOn.accesskey;"/> + <menulist id="actionTargetAccount" + class="folderMenuItem" + aria-labelledby="moveTargetMode0"> + <menupopup id="actionAccountPopup" + type="folder" + class="menulist-menupopup" + expandFolders="false" + mode="filing" + oncommand="onActionTargetChange(event, 'server.spamActionTargetAccount');"/> + </menulist> + </row> + <row> + <radio id="moveTargetMode1" + value="1" + label="&otherFolder.label;" + accesskey="&otherFolder.accesskey;"/> + <menulist id="actionTargetFolder" + class="folderMenuItem" + aria-labelledby="moveTargetMode1" + displayformat="verbose"> + <menupopup id="actionFolderPopup" + type="folder" + mode="junk" + showFileHereLabel="true" + oncommand="onActionTargetChange(event, 'server.spamActionTargetFolder');"/> + </menulist> + </row> + </rows> + </grid> + </radiogroup> + + <hbox align="center" class="indent"> + <checkbox id="server.purgeSpam" + genericattr="true" + pref="true" + wsm_persist="true" + preftype="bool" + prefstring="mail.server.%serverkey%.purgeSpam" + accesskey="&purge1.accesskey;" + oncommand="updateJunkRetention();" + label="&purge1.label;"/> + <textbox size="3" + type="number" + min="1" + id="server.purgeSpamInterval" + genericattr="true" + pref="true" + wsm_persist="true" + preftype="int" + aria-labelledby="server.purgeSpam server.purgeSpamInterval purgeLabel" + prefstring="mail.server.%serverkey%.purgeSpamInterval"/> + <label id="purgeLabel" + value="&purge2.label;" + control="server.purgeSpamInterval"/> + </hbox> + + </groupbox> + + <separator class="thin"/> + + <hbox pack="start"> + <button id="globalJunkPrefsLink" + label="&globalJunkPrefs.label;" + accesskey="&globalJunkPrefs.accesskey;" + oncommand="showGlobalJunkPrefs();"/> + </hbox> + + </vbox> +</page> diff --git a/mailnews/base/prefs/content/am-main.js b/mailnews/base/prefs/content/am-main.js new file mode 100644 index 000000000..6725239e7 --- /dev/null +++ b/mailnews/base/prefs/content/am-main.js @@ -0,0 +1,55 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +function onInit(aPageId, aServerId) +{ + var accountName = document.getElementById("server.prettyName"); + var title = document.getElementById("am-main-title"); + var defaultTitle = title.getAttribute("defaultTitle"); + var titleValue; + + if(accountName.value) + titleValue = defaultTitle+" - <"+accountName.value+">"; + else + titleValue = defaultTitle; + + title.setAttribute("title",titleValue); + document.title = titleValue; + + setupSignatureItems(); +} + +function onPreInit(account, accountValues) +{ + loadSMTPServerList(); +} + +function manageIdentities() +{ + // We want to save the current identity information before bringing up the multiple identities + // UI. This ensures that the changes are reflected in the identity list dialog + // onSave(); + + var account = parent.getCurrentAccount(); + if (!account) + return; + + var accountName = document.getElementById("server.prettyName").value; + + var args = { account: account, accountName: accountName, result: false }; + + // save the current identity settings so they show up correctly + // if the user just changed them in the manage identities dialog + var identity = account.defaultIdentity; + saveIdentitySettings(identity); + + window.openDialog("am-identities-list.xul", "", "chrome,modal,resizable=no,centerscreen", args); + + if (args.result) { + // now re-initialize the default identity settings in case they changed + identity = account.defaultIdentity; // refetch the default identity in case it changed + initIdentityValues(identity); + } +} diff --git a/mailnews/base/prefs/content/am-main.xul b/mailnews/base/prefs/content/am-main.xul new file mode 100644 index 000000000..53e87bec1 --- /dev/null +++ b/mailnews/base/prefs/content/am-main.xul @@ -0,0 +1,147 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-main.dtd" > + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="parent.onPanelLoaded('am-main.xul');"> + + <vbox flex="1" style="overflow: auto;"> + <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + + <script type="application/javascript" src="chrome://messenger/content/am-identity-edit.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-main.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-prefs.js"/> + + <broadcaster id="broadcaster_attachSignature"/> + + <dialogheader id="am-main-title" defaultTitle="&accountTitle.label;"/> + + <hbox align="center"> + <label value="&accountName.label;" control="server.prettyName" + accesskey="&accountName.accesskey;"/> + <textbox wsm_persist="true" size="30" id="server.prettyName" + prefstring="mail.server.%serverkey%.name"/> + </hbox> + + <separator class="thin"/> + + <groupbox style="width: 20em !important;" flex="1"> + <caption label="&identityTitle.label;"/> + <description>&identityDesc.label;</description> + <separator class="thin"/> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&name.label;" control="identity.fullName" + accesskey="&name.accesskey;"/> + <textbox wsm_persist="true" id="identity.fullName" size="30" + prefstring="mail.identity.%identitykey%.fullName"/> + </row> + <row align="center"> + <label value="&email.label;" control="identity.email" + accesskey="&email.accesskey;"/> + <textbox wsm_persist="true" id="identity.email" + prefstring="mail.identity.%identitykey%.useremail" class="uri-element"/> + </row> + <row align="center"> + <label value="&replyTo.label;" control="identity.replyTo" + accesskey="&replyTo.accesskey;"/> + <textbox wsm_persist="true" id="identity.replyTo" + prefstring="mail.identity.%identitykey%.reply_to" class="uri-element" + placeholder="&replyTo.placeholder;"/> + </row> + <row align="center"> + <label value="&organization.label;" control="identity.organization" + accesskey="&organization.accesskey;"/> + <textbox wsm_persist="true" id="identity.organization" + prefstring="mail.identity.%identitykey%.organization"/> + </row> + <separator class="thin"/> + <row align="center"> + <label value="&signatureText.label;" control="identity.htmlSigText" accesskey="&signatureText.accesskey;"/> + <checkbox wsm_persist="true" id="identity.htmlSigFormat" label="&signatureHtml.label;" + prefattribute="value" accesskey="&signatureHtml.accesskey;" + prefstring="mail.identity.%identitykey%.htmlSigFormat"/> + </row> + </rows> + </grid> + + <hbox class="indent" flex="1" style="min-height: 50px;"> + <textbox wsm_persist="true" id="identity.htmlSigText" multiline="true" wrap="off" rows="4" flex="1" + prefstring="mail.identity.%identitykey%.htmlSigText" class="signatureBox"/> + </hbox> + + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.attachSignature" label="&signatureFile.label;" flex="1" + accesskey="&signatureFile.accesskey;" + oncommand="setupSignatureItems();" + prefattribute="value" + prefstring="mail.identity.%identitykey%.attach_signature"/> + </hbox> + + <hbox align="center" class="indent"> + <textbox wsm_persist="true" id="identity.signature" datatype="nsIFile" flex="1" name="identity.signature" + aria-labelledby="identity.attachSignature" + observes="broadcaster_attachSignature" + prefstring="mail.identity.%identitykey%.sig_file" class="uri-element"/> + <button class="push" name="browse" label="&choose.label;" + accesskey="&choose.accesskey;" + oncommand="selectFile()" + observes="broadcaster_attachSignature" + wsm_persist="true" id="identity.sigbrowsebutton" + prefstring="mail.identity.%identitykey%.sigbrowse.disable"/> + </hbox> + + <hbox align="center"> + <checkbox wsm_persist="true" id="identity.attachVCard" label="&attachVCard.label;" flex="1" + accesskey="&attachVCard.accesskey;" + prefattribute="value" + prefstring="mail.identity.%identitykey%.attach_vcard"/> + <button class="push" name="editVCard" label="&editVCard.label;" + accesskey="&editVCard.accesskey;" + oncommand="editVCard()"/> + <label hidden="true" wsm_persist="true" id="identity.escapedVCard" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.escapedVCard"/> + </hbox> + + <separator class="thin"/> + <hbox align="center"> + <label value="&smtpName.label;" control="identity.smtpServerKey" + accesskey="&smtpName.accesskey;"/> + <menulist wsm_persist="true" id="identity.smtpServerKey" flex="1" + pref="true" preftype="string" prefattribute="value" + prefstring="mail.identity.%identitykey%.smtpServer"> + <menupopup id="smtpPopup"> + <menuitem value="" label="&smtpDefaultServer.label;" id="useDefaultItem"/> + <menuseparator/> + <!-- list will be inserted here --> + </menupopup> + </menulist> + </hbox> + </groupbox> + + <separator class="thin"/> + + <hbox align="center"> + <spacer flex="1"/> + <button label="&manageIdentities.label;" oncommand="manageIdentities(event);" + accesskey="&manageIdentities.accesskey;" + wsm_persist="true" id="identity.manageIdentitiesbutton"/> + </hbox> + + <spacer flex="1"/> + </vbox> + +</page> diff --git a/mailnews/base/prefs/content/am-offline.js b/mailnews/base/prefs/content/am-offline.js new file mode 100644 index 000000000..3f45db967 --- /dev/null +++ b/mailnews/base/prefs/content/am-offline.js @@ -0,0 +1,351 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +Components.utils.import("resource:///modules/iteratorUtils.jsm"); + +var gIncomingServer; +var gServerType; +var gImapIncomingServer; +var gPref = null; +var gLockedPref = null; +var gOfflineMap = null; // map of folder URLs to offline flags + +function onInit(aPageId, aServerId) +{ + onLockPreference(); + + // init values here + initServerSettings(); + initRetentionSettings(); + initDownloadSettings(); + initOfflineSettings(); + + onCheckItem("offline.notDownloadMin", "offline.notDownload"); + onCheckItem("nntp.downloadMsgMin", "nntp.downloadMsg"); + onCheckItem("nntp.removeBodyMin", "nntp.removeBody"); + onCheckKeepMsg(); +} + +function initOfflineSettings() +{ + gOfflineMap = collectOfflineFolders(); +} + +function initServerSettings() +{ + document.getElementById("offline.notDownload").checked = gIncomingServer.limitOfflineMessageSize; + document.getElementById("autosync.notDownload").checked = gIncomingServer.limitOfflineMessageSize; + if(gIncomingServer.maxMessageSize > 0) + document.getElementById("offline.notDownloadMin").value = gIncomingServer.maxMessageSize; + else + document.getElementById("offline.notDownloadMin").value = "50"; + + if(gServerType == "imap") { + gImapIncomingServer = gIncomingServer.QueryInterface(Components.interfaces.nsIImapIncomingServer); + document.getElementById("offline.folders").checked = gImapIncomingServer.offlineDownload; + } +} + +function initRetentionSettings() +{ + let retentionSettings = gIncomingServer.retentionSettings; + initCommonRetentionSettings(retentionSettings); + + document.getElementById("nntp.removeBody").checked = retentionSettings.cleanupBodiesByDays; + document.getElementById("nntp.removeBodyMin").value = + (retentionSettings.daysToKeepBodies > 0) ? retentionSettings.daysToKeepBodies : 30; +} + +function initDownloadSettings() +{ + let downloadSettings = gIncomingServer.downloadSettings; + document.getElementById("nntp.downloadMsg").checked = downloadSettings.downloadByDate; + document.getElementById("nntp.notDownloadRead").checked = downloadSettings.downloadUnreadOnly; + document.getElementById("nntp.downloadMsgMin").value = + (downloadSettings.ageLimitOfMsgsToDownload > 0) ? + downloadSettings.ageLimitOfMsgsToDownload : 30; + + // Figure out what the most natural division of the autosync pref into + // a value and an interval is. + let autosyncSelect = document.getElementById("autosyncSelect"); + let autosyncInterval = document.getElementById("autosyncInterval"); + let autosyncValue = document.getElementById("autosyncValue"); + let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays"); + let autosyncPrefValue = (autosyncPref.value == "") ? -1 : + parseInt(autosyncPref.value, 10); + + // Clear the preference until we're done initializing. + autosyncPref.value = ""; + + if (autosyncPrefValue <= 0) { + // Special-case values <= 0 to have an interval of "All" and disabled + // controls for value and interval. + autosyncSelect.value = 0; + autosyncInterval.value = 1; + autosyncInterval.disabled = true; + autosyncValue.value = 30; + autosyncValue.disabled = true; + } + else { + // Otherwise, get the list of possible intervals, in order from + // largest to smallest. + let valuesToTest = []; + for (let i = autosyncInterval.itemCount - 1; i >= 0; i--) + valuesToTest.push(autosyncInterval.getItemAtIndex(i).value); + + // and find the first one that divides the preference evenly. + for (let i in valuesToTest) { + if (!(autosyncPrefValue % valuesToTest[i])) { + autosyncSelect.value = 1; + autosyncInterval.value = valuesToTest[i]; + autosyncValue.value = autosyncPrefValue / autosyncInterval.value; + break; + } + } + autosyncInterval.disabled = false; + autosyncValue.disabled = false; + } + autosyncPref.value = autosyncPrefValue; +} + + +function onPreInit(account, accountValues) +{ + gServerType = getAccountValue(account, accountValues, "server", "type", null, false); + hideShowControls(gServerType); + gIncomingServer = account.incomingServer; + gIncomingServer.type = gServerType; + + // 10 is OFFLINE_SUPPORT_LEVEL_REGULAR, see nsIMsgIncomingServer.idl + // currently, there is no offline without diskspace + var titleStringID = (gIncomingServer.offlineSupportLevel >= 10) ? + "prefPanel-synchronization" : "prefPanel-diskspace"; + + var prefBundle = document.getElementById("bundle_prefs"); + var headertitle = document.getElementById("headertitle"); + headertitle.setAttribute('title',prefBundle.getString(titleStringID)); + document.title = prefBundle.getString(titleStringID); + + if (gServerType == "pop3") { + var pop3Server = gIncomingServer.QueryInterface(Components.interfaces.nsIPop3IncomingServer); + // hide retention settings for deferred accounts + if (pop3Server.deferredToAccount.length) { + var retentionRadio = document.getElementById("retention.keepMsg"); + retentionRadio.setAttribute("hidden", "true"); + var retentionLabel = document.getElementById("retentionDescriptionPop"); + retentionLabel.setAttribute("hidden", "true"); + var applyToFlaggedCheckbox = document.getElementById("retention.applyToFlagged"); + applyToFlaggedCheckbox.setAttribute("hidden", "true"); + } + } +} + +function onClickSelect() +{ + + top.window.openDialog("chrome://messenger/content/msgSelectOffline.xul", "", "centerscreen,chrome,modal,titlebar,resizable=yes"); + return true; + +} + +/** + * Handle updates to the Autosync + */ +function onAutosyncChange() +{ + let autosyncSelect = document.getElementById("autosyncSelect"); + let autosyncInterval = document.getElementById("autosyncInterval"); + let autosyncValue = document.getElementById("autosyncValue"); + let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays"); + + // If we're not done initializing, don't do anything. + // (See initDownloadSettings() for more details.) + if (autosyncPref.value == "") + return; + + // If the user selected the All option, disable the autosync and the + // textbox. + if (autosyncSelect.value == 0) { + autosyncPref.value = -1; + autosyncInterval.disabled = true; + autosyncValue.disabled = true; + return; + } + + let max = 0x7FFFFFFF / (60 * 60 * 24 * autosyncInterval.value); + autosyncValue.setAttribute("max", max); + if (autosyncValue.value > max) + autosyncValue.value = Math.floor(max); + + autosyncInterval.disabled = false; + autosyncValue.disabled = false; + autosyncPref.value = autosyncValue.value * autosyncInterval.value; +} + +function onAutosyncNotDownload() +{ + // This function is called when the autosync version of offline.notDownload + // is changed it simply copies the new checkbox value over to the element + // driving the preference. + document.getElementById("offline.notDownload").checked = + document.getElementById("autosync.notDownload").checked; + onCheckItem("offline.notDownloadMin", "offline.notDownload"); +} + +function onCancel() +{ + // restore the offline flags for all folders + restoreOfflineFolders(gOfflineMap); + return true; +} + +function onSave() +{ + var downloadSettings = + Components.classes["@mozilla.org/msgDatabase/downloadSettings;1"] + .createInstance(Components.interfaces.nsIMsgDownloadSettings); + + gIncomingServer.limitOfflineMessageSize = document.getElementById("offline.notDownload").checked; + gIncomingServer.maxMessageSize = document.getElementById("offline.notDownloadMin").value; + + var retentionSettings = saveCommonRetentionSettings(gIncomingServer.retentionSettings); + + retentionSettings.daysToKeepBodies = document.getElementById("nntp.removeBodyMin").value; + retentionSettings.cleanupBodiesByDays = document.getElementById("nntp.removeBody").checked; + + downloadSettings.downloadByDate = document.getElementById("nntp.downloadMsg").checked; + downloadSettings.downloadUnreadOnly = document.getElementById("nntp.notDownloadRead").checked; + downloadSettings.ageLimitOfMsgsToDownload = document.getElementById("nntp.downloadMsgMin").value; + + gIncomingServer.retentionSettings = retentionSettings; + gIncomingServer.downloadSettings = downloadSettings; + + if (gImapIncomingServer) { + // Set the pref on the incomingserver, and set the flag on all folders. + gImapIncomingServer.offlineDownload = document.getElementById("offline.folders").checked; + } +} + +// Does the work of disabling an element given the array which contains xul id/prefstring pairs. +// Also saves the id/locked state in an array so that other areas of the code can avoid +// stomping on the disabled state indiscriminately. +function disableIfLocked( prefstrArray ) +{ + if (!gLockedPref) + gLockedPref = new Array; + + for (var i=0; i<prefstrArray.length; i++) { + var id = prefstrArray[i].id; + var element = document.getElementById(id); + if (gPref.prefIsLocked(prefstrArray[i].prefstring)) { + element.disabled = true; + gLockedPref[id] = true; + } else { + element.removeAttribute("disabled"); + gLockedPref[id] = false; + } + } +} + +// Disables xul elements that have associated preferences locked. +function onLockPreference() +{ + var isDownloadLocked = false; + var isGetNewLocked = false; + var initPrefString = "mail.server"; + var finalPrefString; + + // This panel does not use the code in AccountManager.js to handle + // the load/unload/disable. keep in mind new prefstrings and changes + // to code in AccountManager, and update these as well. + var allPrefElements = [ + { prefstring:"limit_offline_message_size", id:"offline.notDownload"}, + { prefstring:"limit_offline_message_size", id:"autosync.notDownload"}, + { prefstring:"max_size", id:"offline.notDownloadMin"}, + { prefstring:"downloadUnreadOnly", id:"nntp.notDownloadRead"}, + { prefstring:"downloadByDate", id:"nntp.downloadMsg"}, + { prefstring:"ageLimit", id:"nntp.downloadMsgMin"}, + { prefstring:"retainBy", id:"retention.keepMsg"}, + { prefstring:"daysToKeepHdrs", id:"retention.keepOldMsgMin"}, + { prefstring:"numHdrsToKeep", id:"retention.keepNewMsgMin"}, + { prefstring:"daysToKeepBodies", id:"nntp.removeBodyMin"}, + { prefstring:"cleanupBodies", id:"nntp.removeBody" }, + { prefstring:"applyToFlagged", id:"retention.applyToFlagged"}, + { prefstring:"disable_button.selectFolder", id:"selectNewsgroupsButton"}, + { prefstring:"disable_button.selectFolder", id:"selectImapFoldersButton"} + ]; + + finalPrefString = initPrefString + "." + gIncomingServer.key + "."; + gPref = Services.prefs.getBranch(finalPrefString); + + disableIfLocked( allPrefElements ); +} + +function onCheckItem(changeElementId, checkElementId) +{ + var element = document.getElementById(changeElementId); + var checked = document.getElementById(checkElementId).checked; + if(checked && !gLockedPref[checkElementId] ) { + element.removeAttribute("disabled"); + } + else { + element.setAttribute("disabled", "true"); + } +} + +function toggleOffline() +{ + let offline = document.getElementById("offline.folders").checked; + let allFolders = gIncomingServer.rootFolder.descendants; + for (let folder in fixIterator(allFolders, Components.interfaces.nsIMsgFolder)) { + if (offline) + folder.setFlag(Components.interfaces.nsMsgFolderFlags.Offline); + else + folder.clearFlag(Components.interfaces.nsMsgFolderFlags.Offline); + } +} + +function collectOfflineFolders() +{ + let offlineFolderMap = {}; + let allFolders = gIncomingServer.rootFolder.descendants; + for (let folder in fixIterator(allFolders, Components.interfaces.nsIMsgFolder)) + offlineFolderMap[folder.folderURL] = folder.getFlag(Components.interfaces.nsMsgFolderFlags.Offline); + + return offlineFolderMap; +} + +function restoreOfflineFolders(offlineFolderMap) +{ + let allFolders = gIncomingServer.rootFolder.descendants; + for (let folder in fixIterator(allFolders, Components.interfaces.nsIMsgFolder)) { + if (offlineFolderMap[folder.folderURL]) + folder.setFlag(Components.interfaces.nsMsgFolderFlags.Offline); + else + folder.clearFlag(Components.interfaces.nsMsgFolderFlags.Offline); + } +} + +/** + * Checks if the user selected a permanent removal of messages from a server + * listed in the confirmfor attribute and warns about it. + * + * @param aRadio The radiogroup element containing the retention options. + */ +function warnServerRemove(aRadio) +{ + let confirmFor = aRadio.getAttribute("confirmfor"); + + if (confirmFor && confirmFor.split(',').includes(gServerType) && aRadio.value != 1) { + let prefBundle = document.getElementById("bundle_prefs"); + let title = prefBundle.getString("removeFromServerTitle"); + let question = prefBundle.getString("removeFromServer"); + if (!Services.prompt.confirm(window, title, question)) { + // If the user doesn't agree, fall back to not deleting anything. + aRadio.value = 1; + onCheckKeepMsg(); + } + } +} diff --git a/mailnews/base/prefs/content/am-offline.xul b/mailnews/base/prefs/content/am-offline.xul new file mode 100644 index 000000000..0c312dd74 --- /dev/null +++ b/mailnews/base/prefs/content/am-offline.xul @@ -0,0 +1,158 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-offline.dtd"> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="parent.onPanelLoaded('am-offline.xul');"> + + <vbox flex="1" style="overflow: auto;"> + <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/> + + <script type="application/javascript" src="chrome://messenger/content/AccountManager.js"/> + <script type="application/javascript" src="chrome://messenger/content/retention.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-offline.js"/> + + <label hidden="true" wsm_persist="true" id="server.type"/> + <label id="imap.autoSyncMaxAgeDays" hidden="true" + wsm_persist="true" preftype="int" + prefstring="mail.server.%serverkey%.autosync_max_age_days"/> + + <dialogheader id="headertitle"/> + + <groupbox id="offline.titlebox" hidefor="movemail,pop3,none,rss"> + <caption label="&syncGroupTitle.label;"/> + + <checkbox hidefor="movemail,pop3,nntp,none" + id="offline.folders" label="&allFoldersOffline.label;" + oncommand="toggleOffline()" + accesskey="&allFoldersOffline.accesskey;"/> + + <separator class="thin" hidefor="movemail,pop3,nntp,none"/> + + <hbox hidefor="movemail,pop3,nntp,none" align="right"> + <button label="&offlineImapAdvancedOffline.label;" accesskey="&offlineImapAdvancedOffline.accesskey;" + oncommand="onClickSelect()" id="selectImapFoldersButton" class="selectForOfflineUseButton"/> + </hbox> + + <hbox hidefor="movemail,pop3,imap,none" align="right"> + <button label="&offlineSelectNntp.label;" accesskey="&offlineSelectNntp.accesskey;" + oncommand="onClickSelect()" id="selectNewsgroupsButton" class="selectForOfflineUseButton"/> + </hbox> + + </groupbox> + + <groupbox id="diskspace.titlebox"> + <caption label="&diskspaceGroupTitle.label;" hidefor="movemail,pop3,none,rss"/> + + <description hidefor="pop3,nntp,movemail,none,rss">&doNotDownloadImap.label;</description> + <description hidefor="pop3,imap,movemail,none,rss">&doNotDownloadNntp.label;</description> + <description hidefor="imap,nntp,none,rss">&doNotDownloadPop3Movemail.label;</description> + + <!-- IMAP Autosync Preference --> + <radiogroup hidefor="pop3,nntp,movemail,none,rss" id="autosyncSelect" class="indent"> + <radio id="useAutosync.AllMsg" value="0" accesskey="&allAutosync.accesskey;" + label="&allAutosync.label;" oncommand="onAutosyncChange();"/> + <hbox flex="1" align="center"> + <radio id="useAutosync.ByAge" accesskey="&ageAutosync.accesskey;" + value="1" label="&ageAutosyncBefore.label;" oncommand="onAutosyncChange();"/> + <textbox id="autosyncValue" type="number" size="4" min="1" + class="autosync" onchange="onAutosyncChange();" + aria-labelledby="ageAutosyncBefore autosyncValue ageAutosyncMiddle autosyncInterval ageAutosyncAfter"/> + <label id="ageAutosyncMiddle" control="autosyncValue" value="&ageAutosyncMiddle.label;"/> + <menulist id="autosyncInterval" onselect="onAutosyncChange();"> + <menupopup> + <menuitem label="&dayAgeInterval.label;" value="1"/> + <menuitem label="&weekAgeInterval.label;" value="7"/> + <menuitem label="&monthAgeInterval.label;" value="31"/> + <menuitem label="&yearAgeInterval.label;" value="365"/> + </menupopup> + </menulist> + <label id="ageAutosyncAfter" control="autosyncInterval" value="&ageAutosyncAfter.label;"/> + </hbox> + </radiogroup> + + <hbox align="center" class="indent" hidefor="rss"> + <checkbox hidefor="movemail,pop3,imap,none" + id="nntp.notDownloadRead" wsm_persist="true" + label="&nntpNotDownloadRead.label;" + accesskey="&nntpNotDownloadRead.accesskey;"/> + </hbox> + + <hbox align="center" class="indent" hidefor="none,rss"> + <checkbox wsm_persist="true" id="offline.notDownload" hidefor="imap" + label="&offlineNotDownload.label;" + accesskey="&offlineNotDownload.accesskey;" + oncommand="onCheckItem('offline.notDownloadMin', 'offline.notDownload');"/> + <checkbox wsm_persist="true" id="autosync.notDownload" hidefor="pop3,nntp,movemail" + label="&autosyncNotDownload.label;" + accesskey="&autosyncNotDownload.accesskey;" + oncommand="onAutosyncNotDownload();"/> + <textbox wsm_persist="true" id="offline.notDownloadMin" + type="number" min="1" increment="10" size="4" value="50" + aria-labelledby="offline.notDownload offline.notDownloadMin kbLabel"/> + <label value="&kb.label;" control="offline.notDownloadMin" id="kbLabel"/> + </hbox> + + <hbox align="center" class="indent" hidefor="movemail,pop3,imap,none,rss"> + <checkbox wsm_persist="true" id="nntp.downloadMsg" + label="&nntpDownloadMsg.label;" + accesskey="&nntpDownloadMsg.accesskey;" + oncommand="onCheckItem('nntp.downloadMsgMin', 'nntp.downloadMsg');"/> + <textbox wsm_persist="true" id="nntp.downloadMsgMin" + type="number" min="1" size="2" value="30" + aria-labelledby="nntp.downloadMsg nntp.downloadMsgMin daysOldLabel"/> + <label value="&daysOld.label;" control="nntp.downloadMsgMin" + id="daysOldLabel"/> + </hbox> + + <vbox align="start"> + <separator hidefor="none,rss"/> + <label id="retentionDescription" hidefor="imap,pop3" class="desc" control="retention.keepMsg">&retentionCleanup.label;</label> + <label id="retentionDescriptionImap" hidefor="movemail,pop3,nntp,none,rss" class="desc" control="retention.keepMsg">&retentionCleanupImap.label;</label> + <label id="retentionDescriptionPop" hidefor="movemail,imap,nntp,none,rss" class="desc" control="retention.keepMsg">&retentionCleanupPop.label;</label> + + <radiogroup hidefor="" confirmfor="imap,pop3" id="retention.keepMsg" class="indent" + oncommand="warnServerRemove(this);"> + <radio id="retention.keepAllMsg" value="1" accesskey="&retentionKeepAll.accesskey;" + label="&retentionKeepAll.label;" oncommand="onCheckKeepMsg();"/> + <hbox flex="1" align="center"> + <radio id="retention.keepNewMsg" accesskey="&retentionKeepRecent.accesskey;" + value="3" label="&retentionKeepRecent.label;" oncommand="onCheckKeepMsg();"/> + <textbox id="retention.keepNewMsgMin" + type="number" min="1" increment="10" size="4" value="2000" + aria-labelledby="retention.keepNewMsg retention.keepNewMsgMin newMsgLabel"/> + <label value="&message.label;" control="retention.keepNewMsgMin" id="newMsgLabel"/> + </hbox> + <hbox flex="1" align="center"> + <radio id="retention.keepOldMsg" accesskey="&retentionKeepMsg.accesskey;" + value="2" label="&retentionKeepMsg.label;" oncommand="onCheckKeepMsg();"/> + <textbox id="retention.keepOldMsgMin" + type="number" min="1" size="4" value="30" + aria-labelledby="retention.keepOldMsg retention.keepOldMsgMin oldMsgLabel"/> + <label value="&daysOld.label;" control="retention.keepOldMsgMin" id="oldMsgLabel"/> + </hbox> + </radiogroup> + + <hbox align="center" class="indent"> + <checkbox id="retention.applyToFlagged" + label="&retentionApplyToFlagged.label;" hidefor="" + accesskey="&retentionApplyToFlagged.accesskey;" + checked="true"/> + </hbox> + <hbox align="center" class="indent" hidefor="movemail,pop3,imap,none,rss"> + <checkbox id="nntp.removeBody" accesskey="&nntpRemoveMsgBody.accesskey;" + label="&nntpRemoveMsgBody.label;" oncommand="onCheckItem('nntp.removeBodyMin','nntp.removeBody');"/> + <textbox id="nntp.removeBodyMin" size="2" value="30" + type="number" min="1" + aria-labelledby="nntp.removeBody nntp.removeBodyMin daysOldMsg"/> + <label value="&daysOld.label;" control="nntp.removeBodyMin" id="daysOldMsg"/> + </hbox> + </vbox> + </groupbox> + </vbox> +</page> diff --git a/mailnews/base/prefs/content/am-prefs.js b/mailnews/base/prefs/content/am-prefs.js new file mode 100644 index 000000000..6f88af016 --- /dev/null +++ b/mailnews/base/prefs/content/am-prefs.js @@ -0,0 +1,114 @@ +/* -*- 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/. */ + +/* functions for disabling front end elements when the appropriate + back-end preference is locked. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); + +/** + * Prefs in MailNews require dynamic portions to indicate + * which of multiple servers or identities. This function + * takes a string and a xul element. + * + * @param aStr The string is a prefstring with a token %tokenname%. + * @param aElement The xul element has an attribute of name |tokenname| + * whose value is substituted into the string and returned + * by the function. + * + * Any tokens which do not have associated attribute value + * are not substituted, and left in the string as-is. + */ +function substPrefTokens(aStr, aElement) +{ + let tokenpat = /%(\w+)%/; + let token; + let newprefstr = ""; + + let prefPartsArray = aStr.split("."); + /* here's a little loop that goes through + each part of the string separated by a dot, and + if any parts are of the form %string%, it will replace + them with the value of the attribute of that name from + the xul object */ + for (let i = 0; i < prefPartsArray.length; i++) { + token = prefPartsArray[i].match(tokenpat); + if (token) { /* we've got a %% match */ + if (token[1]) { + if (aElement[token[1]]) { + newprefstr += aElement[token[1]] + "."; // here's where we get the info + } else { /* all we got was this stinkin % */ + newprefstr += prefPartsArray[i] + "."; + } + } + } else /* if (token) */ { + newprefstr += prefPartsArray[i] + "."; + } + } + newprefstr = newprefstr.slice(0, -1); // remove the last char, a dot + if (newprefstr.length <= 0) + newprefstr = null; + + return newprefstr; +} + +/** + * A simple function to check if a pref in an element is locked. + * + * @param aElement a xul element with the pref related attributes + * (pref, preftype, prefstring) + * @return whether the prefstring specified in that element is + * locked (true/false). + * If it does not have a valid prefstring, a false is returned. + */ +function getAccountValueIsLocked(aElement) +{ + let prefstring = aElement.getAttribute("prefstring"); + if (prefstring) { + let prefstr = substPrefTokens(prefstring, aElement); + // see if the prefstring is locked + if (prefstr) + return Services.prefs.prefIsLocked(prefstr); + } + return false; +} + +/** + * Enables/disables element (slave) according to the checked state + * of another elements (masters). + * + * @param aChangeElementId Slave element which should be enabled + * if all the checkElementIDs are checked. + * Otherwise it gets disabled. + * @param aCheckElementIds An array of IDs of the master elements. + * + * See bug 728681 for the pattern on how this is used. + */ +function onCheckItem(aChangeElementId, aCheckElementIds) +{ + let elementToControl = document.getElementById(aChangeElementId); + let disabled = false; + + for (let notifyId of aCheckElementIds) { + let notifyElement = document.getElementById(notifyId); + let notifyElementState = null; + if ("checked" in notifyElement) + notifyElementState = notifyElement.checked; + else if ("selected" in notifyElement) + notifyElementState = notifyElement.selected; + else + Components.utils.reportError("Unknown type of control element: " + notifyElement.id); + + if (!notifyElementState) { + disabled = true; + break; + } + } + + if (!disabled && getAccountValueIsLocked(elementToControl)) + disabled = true; + + elementToControl.disabled = disabled; +} diff --git a/mailnews/base/prefs/content/am-server-advanced.js b/mailnews/base/prefs/content/am-server-advanced.js new file mode 100644 index 000000000..9232f525a --- /dev/null +++ b/mailnews/base/prefs/content/am-server-advanced.js @@ -0,0 +1,157 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +// pull stuff out of window.arguments +var gServerSettings = window.arguments[0]; + +var gFirstDeferredAccount; +// initialize the controls with the "gServerSettings" argument + +var gControls; +function getControls() +{ + if (!gControls) + gControls = document.getElementsByAttribute("amsa_persist", "true"); + return gControls; +} + +function getLocalFoldersAccount() +{ + return MailServices.accounts + .FindAccountForServer(MailServices.accounts.localFoldersServer); +} + +function onLoad() +{ + var prettyName = gServerSettings.serverPrettyName; + + if (prettyName) + document.getElementById("serverPrettyName").value = + document.getElementById("bundle_prefs") + .getFormattedString("forAccount", [prettyName]); + + if (gServerSettings.serverType == "imap") + { + document.getElementById("pop3Panel").hidden = true; + } + else if (gServerSettings.serverType == "pop3") + { + document.getElementById("imapPanel").hidden = true; + let radioGroup = document.getElementById("folderStorage"); + + gFirstDeferredAccount = gServerSettings.deferredToAccount; + let folderPopup = document.getElementById("deferredServerPopup"); + + // The current account should not be shown in the folder picker + // of the "other account" option. + folderPopup._teardown(); + folderPopup.setAttribute("excludeServers", + gServerSettings.account.incomingServer.key); + folderPopup._ensureInitialized(); + + if (gFirstDeferredAccount.length) + { + // The current account is deferred. + let account = MailServices.accounts.getAccount(gFirstDeferredAccount); + radioGroup.value = "otherAccount"; + folderPopup.selectFolder(account.incomingServer.rootFolder); + } + else + { + // Current account is not deferred. + radioGroup.value = "currentAccount"; + // If there are no suitable accounts to defer to, then the menulist is + // disabled by the picker with an appropriate message. + folderPopup.selectFolder(); + if (gServerSettings.account.incomingServer.isDeferredTo) { + // Some other account already defers to this account + // therefore this one can't be deferred further. + radioGroup.disabled = true; + } + } + + let picker = document.getElementById("deferredServerFolderPicker"); + picker.disabled = radioGroup.selectedIndex != 1; + } + + var controls = getControls(); + + for (var i = 0; i < controls.length; i++) + { + var slot = controls[i].id; + if (slot in gServerSettings) + { + if (controls[i].localName == "checkbox") + controls[i].checked = gServerSettings[slot]; + else + controls[i].value = gServerSettings[slot]; + } + } +} + +function onOk() +{ + // Handle account deferral settings for POP3 accounts. + if (gServerSettings.serverType == "pop3") + { + var radioGroup = document.getElementById("folderStorage"); + var gPrefsBundle = document.getElementById("bundle_prefs"); + let picker = document.getElementById("deferredServerFolderPicker"); + + // This account wasn't previously deferred, but is now deferred. + if (radioGroup.value != "currentAccount" && !gFirstDeferredAccount.length) + { + // If the user hasn't selected a folder, keep the default. + if (!picker.selectedItem) + return true; + + var confirmDeferAccount = + gPrefsBundle.getString("confirmDeferAccountWarning"); + + var confirmTitle = gPrefsBundle.getString("confirmDeferAccountTitle"); + + if (!Services.prompt.confirm(window, confirmTitle, confirmDeferAccount)) + return false; + } + switch (radioGroup.value) + { + case "currentAccount": + gServerSettings['deferredToAccount'] = ""; + break; + case "otherAccount": + let server = picker.selectedItem._folder.server; + let account = MailServices.accounts.FindAccountForServer(server); + gServerSettings['deferredToAccount'] = account.key; + break; + } + } + + // Save the controls back to the "gServerSettings" array. + var controls = getControls(); + for (var i = 0; i < controls.length; i++) + { + var slot = controls[i].id; + if (slot in gServerSettings) + { + if (controls[i].localName == "checkbox") + gServerSettings[slot] = controls[i].checked; + else + gServerSettings[slot] = controls[i].value; + } + } + + return true; +} + + +// Set radio element choices and picker states +function updateInboxAccount(enablePicker) +{ + document.getElementById("deferredServerFolderPicker").disabled = !enablePicker; + document.getElementById("deferGetNewMail").disabled = !enablePicker; +} diff --git a/mailnews/base/prefs/content/am-server-advanced.xul b/mailnews/base/prefs/content/am-server-advanced.xul new file mode 100644 index 000000000..82b85b975 --- /dev/null +++ b/mailnews/base/prefs/content/am-server-advanced.xul @@ -0,0 +1,146 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/am-server-advanced.dtd"> + +<dialog + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&serverAdvanced.label;" + buttons="accept,cancel" + onload="onLoad();" + ondialogaccept="return onOk();"> + + <script type="application/javascript" + src="chrome://messenger/content/am-server-advanced.js"/> + <stringbundle id="bundle_prefs" + src="chrome://messenger/locale/prefs.properties"/> + + <label id="serverPrettyName"/> + + <separator class="thin"/> + + <!-- IMAP Panel --> + <vbox id="imapPanel"> + <hbox align="center"> + <label value="&serverDirectory.label;" + accesskey="&serverDirectory.accesskey;" + control="serverDirectory"/> + <textbox amsa_persist="true" id="serverDirectory"/> + </hbox> + + <checkbox amsa_persist="true" id="usingSubscription" + label="&usingSubscription.label;" + accesskey="&usingSubscription.accesskey;"/> + + <checkbox amsa_persist="true" id="dualUseFolders" + label="&dualUseFolders.label;" + accesskey="&dualUseFolders.accesskey;"/> + + <separator class="groove"/> + <row> + <hbox align="center"> + <label control="maximumConnectionsNumber" + value="&maximumConnectionsNumber.label;" + accesskey="&maximumConnectionsNumber.accesskey;"/> + <textbox amsa_persist="true" size="3" type="number" + min="1" max="1000" id="maximumConnectionsNumber"/> + </hbox> + </row> + + <separator class="groove"/> + <description>&namespaceDesc.label;</description> + <grid> + <columns> + <column/> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <separator class="indent"/> + <label control="personalNamespace" value="&personalNamespace.label;" + accesskey="&personalNamespace.accesskey;"/> + <textbox amsa_persist="true" id="personalNamespace" /> + </row> + <row align="center"> + <separator class="indent"/> + <label control="publicNamespace" value="&publicNamespace.label;" + accesskey="&publicNamespace.accesskey;"/> + <textbox amsa_persist="true" id="publicNamespace"/> + </row> + <row align="center"> + <separator class="indent"/> + <label control="otherUsersNamespace" value="&otherUsersNamespace.label;" + accesskey="&otherUsersNamespace.accesskey;"/> + <textbox amsa_persist="true" id="otherUsersNamespace"/> + </row> + </rows> + </grid> + <grid> + <columns> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <separator class="indent"/> + <checkbox amsa_persist="true" id="overrideNamespaces" + label="&overrideNamespaces.label;" + accesskey="&overrideNamespaces.accesskey;"/> + </row> + </rows> + </grid> + </vbox> + + + <!-- POP3 Panel --> + <vbox id="pop3Panel"> + <label flex="1" control="folderStorage">&pop3DeferringDesc.label;</label> + <hbox align="center"> + <radiogroup id="folderStorage" + orient="horizontal" + amsa_persist="true" + onselect="updateInboxAccount(this.selectedIndex == 1);"> + <rows> + <row> + <radio id="deferToCurrentAccount" + value="currentAccount" + label="&accountInbox.label;" + accesskey="&accountInbox.accesskey;"/> + </row> + <row> + <vbox> + <hbox> + <radio id="deferToOtherAccount" + value="otherAccount" + label="&deferToServer.label;" + accesskey="&deferToServer.accesskey;"> + </radio> + <menulist id="deferredServerFolderPicker" + class="folderMenuItem" + aria-labelledby="deferToServer"> + <menupopup id="deferredServerPopup" + type="folder" + expandFolders="false" + mode="deferred" + oncommand="this.selectFolder(event.target._folder);"/> + </menulist> + </hbox> + <checkbox amsa_persist="true" id="deferGetNewMail" + label="&deferGetNewMail.label;" class="indent" + accesskey="&deferGetNewMail.accesskey;"/> + </vbox> + </row> + </rows> + </radiogroup> + </hbox> + + </vbox> +</dialog> diff --git a/mailnews/base/prefs/content/am-server-top.xul b/mailnews/base/prefs/content/am-server-top.xul new file mode 100644 index 000000000..890fa7a79 --- /dev/null +++ b/mailnews/base/prefs/content/am-server-top.xul @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-server-top.dtd" > + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +</page> diff --git a/mailnews/base/prefs/content/am-server.js b/mailnews/base/prefs/content/am-server.js new file mode 100644 index 000000000..01b22e4ae --- /dev/null +++ b/mailnews/base/prefs/content/am-server.js @@ -0,0 +1,404 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/iteratorUtils.jsm"); +Components.utils.import("resource:///modules/MailUtils.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gServer; + +function onSave() +{ + let storeContractID = document.getElementById("server.storeTypeMenulist") + .selectedItem + .value; + document.getElementById("server.storeContractID") + .setAttribute("value", storeContractID); +} + +function onInit(aPageId, aServerId) +{ + initServerType(); + + onCheckItem("server.biffMinutes", ["server.doBiff"]); + onCheckItem("nntp.maxArticles", ["nntp.notifyOn"]); + setupMailOnServerUI(); + setupFixedUI(); + let serverType = document.getElementById("server.type").getAttribute("value"); + if (serverType == "imap") + setupImapDeleteUI(aServerId); + + // TLS Cert (External) and OAuth2 are only supported on IMAP. +#ifdef MOZ_MAILNEWS_OAUTH2 + document.getElementById("authMethod-oauth2").hidden = (serverType != "imap"); +#endif + document.getElementById("authMethod-external").hidden = (serverType != "imap"); + + // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't + // allow users to choose it anymore. Hide the option unless the user already + // has it set. + hideUnlessSelected(document.getElementById("connectionSecurityType-1")); + + // UI for account store type. + let storeTypeElement = document.getElementById("server.storeTypeMenulist"); + // set the menuitem to match the account + let currentStoreID = document.getElementById("server.storeContractID") + .getAttribute("value"); + let targetItem = storeTypeElement.getElementsByAttribute("value", currentStoreID); + storeTypeElement.selectedItem = targetItem[0]; + // disable store type change if store has already been used + storeTypeElement.setAttribute("disabled", + gServer.getBoolValue("canChangeStoreType") ? "false" : "true"); +} + +function onPreInit(account, accountValues) +{ + var type = parent.getAccountValue(account, accountValues, "server", "type", null, false); + hideShowControls(type); + + Services.obs.notifyObservers(null, "charsetmenu-selected", "other"); + + gServer = account.incomingServer; + + if(!account.incomingServer.canEmptyTrashOnExit) + { + document.getElementById("server.emptyTrashOnExit").setAttribute("hidden", "true"); + document.getElementById("imap.deleteModel.box").setAttribute("hidden", "true"); + } +} + +function initServerType() +{ + var serverType = document.getElementById("server.type").getAttribute("value"); + var propertyName = "serverType-" + serverType; + + var messengerBundle = document.getElementById("bundle_messenger"); + var verboseName = messengerBundle.getString(propertyName); + setDivText("servertype.verbose", verboseName); + + secureSelect(true); + + setLabelFromStringBundle("authMethod-no", "authNo"); + setLabelFromStringBundle("authMethod-old", "authOld"); + setLabelFromStringBundle("authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("authMethod-external", "authExternal"); + setLabelFromStringBundle("authMethod-ntlm", "authNTLM"); +#ifdef MOZ_MAILNEWS_OAUTH2 + setLabelFromStringBundle("authMethod-oauth2", "authOAuth2"); +#endif + setLabelFromStringBundle("authMethod-anysecure", "authAnySecure"); + setLabelFromStringBundle("authMethod-any", "authAny"); + setLabelFromStringBundle("authMethod-password-encrypted", + "authPasswordEncrypted"); + //authMethod-password-cleartext already set in selectSelect() + + // Hide deprecated/hidden auth options, unless selected + hideUnlessSelected(document.getElementById("authMethod-no")); + hideUnlessSelected(document.getElementById("authMethod-old")); + hideUnlessSelected(document.getElementById("authMethod-anysecure")); + hideUnlessSelected(document.getElementById("authMethod-any")); +} + +function hideUnlessSelected(element) +{ + element.hidden = !element.selected; +} + +function setLabelFromStringBundle(elementID, stringName) +{ + document.getElementById(elementID).label = + document.getElementById("bundle_messenger").getString(stringName); +} + +function setDivText(divname, value) +{ + var div = document.getElementById(divname); + if (!div) + return; + div.setAttribute("value", value); +} + + +function onAdvanced() +{ + // Store the server type and, if an IMAP or POP3 server, + // the settings needed for the IMAP/POP3 tab into the array + var serverSettings = {}; + var serverType = document.getElementById("server.type").getAttribute("value"); + serverSettings.serverType = serverType; + + serverSettings.serverPrettyName = gServer.prettyName; + serverSettings.account = top.getCurrentAccount(); + + if (serverType == "imap") + { + serverSettings.dualUseFolders = document.getElementById("imap.dualUseFolders").checked; + serverSettings.usingSubscription = document.getElementById("imap.usingSubscription").checked; + serverSettings.maximumConnectionsNumber = document.getElementById("imap.maximumConnectionsNumber").getAttribute("value"); + // string prefs + serverSettings.personalNamespace = document.getElementById("imap.personalNamespace").getAttribute("value"); + serverSettings.publicNamespace = document.getElementById("imap.publicNamespace").getAttribute("value"); + serverSettings.serverDirectory = document.getElementById("imap.serverDirectory").getAttribute("value"); + serverSettings.otherUsersNamespace = document.getElementById("imap.otherUsersNamespace").getAttribute("value"); + serverSettings.overrideNamespaces = document.getElementById("imap.overrideNamespaces").checked; + } + else if (serverType == "pop3") + { + serverSettings.deferGetNewMail = document.getElementById("pop3.deferGetNewMail").checked; + serverSettings.deferredToAccount = document.getElementById("pop3.deferredToAccount").getAttribute("value"); + } + + window.openDialog("chrome://messenger/content/am-server-advanced.xul", + "_blank", "chrome,modal,titlebar", serverSettings); + + if (serverType == "imap") + { + document.getElementById("imap.dualUseFolders").checked = serverSettings.dualUseFolders; + document.getElementById("imap.usingSubscription").checked = serverSettings.usingSubscription; + document.getElementById("imap.maximumConnectionsNumber").setAttribute("value", serverSettings.maximumConnectionsNumber); + // string prefs + document.getElementById("imap.personalNamespace").setAttribute("value", serverSettings.personalNamespace); + document.getElementById("imap.publicNamespace").setAttribute("value", serverSettings.publicNamespace); + document.getElementById("imap.serverDirectory").setAttribute("value", serverSettings.serverDirectory); + document.getElementById("imap.otherUsersNamespace").setAttribute("value", serverSettings.otherUsersNamespace); + document.getElementById("imap.overrideNamespaces").checked = serverSettings.overrideNamespaces; + } + else if (serverType == "pop3") + { + document.getElementById("pop3.deferGetNewMail").checked = serverSettings.deferGetNewMail; + document.getElementById("pop3.deferredToAccount").setAttribute("value", serverSettings.deferredToAccount); + let pop3Server = gServer.QueryInterface(Components.interfaces.nsIPop3IncomingServer); + // we're explicitly setting this so we'll go through the SetDeferredToAccount method + pop3Server.deferredToAccount = serverSettings.deferredToAccount; + // Setting the server to be deferred causes a rebuild of the account tree, + // losing the current selection. Reselect the current server again as it + // didn't really disappear. + parent.selectServer(parent.getCurrentAccount().incomingServer, parent.currentPageId); + + // Iterate over all accounts to see if any of their junk targets are now + // invalid (pointed to the account that is now deferred). + // If any such target is found it is reset to a new safe folder + // (the deferred to account or Local Folders). If junk was really moved + // to that folder (moveOnSpam = true) then moving junk is disabled + // (so that the user notices it and checks the settings). + // This is the same sanitization as in am-junk.js, just applied to all POP accounts. + let deferredURI = serverSettings.deferredToAccount && + MailServices.accounts.getAccount(serverSettings.deferredToAccount) + .incomingServer.serverURI; + + for (let account in fixIterator(MailServices.accounts.accounts, + Components.interfaces.nsIMsgAccount)) { + let accountValues = parent.getValueArrayFor(account); + let type = parent.getAccountValue(account, accountValues, "server", "type", + null, false); + // Try to keep this list of account types not having Junk settings + // synchronized with the list in AccountManager.js. + if (type != "nntp" && type != "rss" && type != "im") { + let spamActionTargetAccount = parent.getAccountValue(account, accountValues, + "server", "spamActionTargetAccount", "string", true); + let spamActionTargetFolder = parent.getAccountValue(account, accountValues, + "server", "spamActionTargetFolder", "string", true); + let moveOnSpam = parent.getAccountValue(account, accountValues, + "server", "moveOnSpam", "bool", true); + + // Check if there are any invalid junk targets and fix them. + [ spamActionTargetAccount, spamActionTargetFolder, moveOnSpam ] = + sanitizeJunkTargets(spamActionTargetAccount, + spamActionTargetFolder, + deferredURI || account.incomingServer.serverURI, + parent.getAccountValue(account, accountValues, "server", "moveTargetMode", "int", true), + account.incomingServer.spamSettings, + moveOnSpam); + + parent.setAccountValue(accountValues, "server", "moveOnSpam", moveOnSpam); + parent.setAccountValue(accountValues, "server", "spamActionTargetAccount", + spamActionTargetAccount); + parent.setAccountValue(accountValues, "server", "spamActionTargetFolder", + spamActionTargetFolder); + } + } + } +} + +function secureSelect(aLoading) +{ + var socketType = document.getElementById("server.socketType").value; + var serverType = document.getElementById("server.type").value; + var defaultPort = gServer.protocolInfo.getDefaultServerPort(false); + var defaultPortSecure = gServer.protocolInfo.getDefaultServerPort(true); + var port = document.getElementById("server.port"); + var portDefault = document.getElementById("defaultPort"); + var prevDefaultPort = portDefault.value; + + const Ci = Components.interfaces; + if (socketType == Ci.nsMsgSocketType.SSL) { + portDefault.value = defaultPortSecure; + if (port.value == "" || (!aLoading && port.value == defaultPort && prevDefaultPort != portDefault.value)) + port.value = defaultPortSecure; + } else { + portDefault.value = defaultPort; + if (port.value == "" || (!aLoading && port.value == defaultPortSecure && prevDefaultPort != portDefault.value)) + port.value = defaultPort; + } + + // switch "insecure password" label + setLabelFromStringBundle("authMethod-password-cleartext", + socketType == Ci.nsMsgSocketType.SSL || + socketType == Ci.nsMsgSocketType.alwaysSTARTTLS ? + "authPasswordCleartextViaSSL" : "authPasswordCleartextInsecurely"); +} + +function setupMailOnServerUI() +{ + onCheckItem("pop3.deleteMailLeftOnServer", ["pop3.leaveMessagesOnServer"]); + setupAgeMsgOnServerUI(); +} + +function setupAgeMsgOnServerUI() +{ + const kLeaveMsgsId = "pop3.leaveMessagesOnServer"; + const kDeleteByAgeId = "pop3.deleteByAgeFromServer"; + onCheckItem(kDeleteByAgeId, [kLeaveMsgsId]); + onCheckItem("daysEnd", [kLeaveMsgsId]); + onCheckItem("pop3.numDaysToLeaveOnServer", [kLeaveMsgsId, kDeleteByAgeId]); +} + +function setupFixedUI() +{ + var controls = [document.getElementById("fixedServerName"), + document.getElementById("fixedUserName"), + document.getElementById("fixedServerPort")]; + + var len = controls.length; + for (var i=0; i<len; i++) { + var fixedElement = controls[i]; + var otherElement = document.getElementById(fixedElement.getAttribute("use")); + + fixedElement.setAttribute("collapsed", "true"); + otherElement.removeAttribute("collapsed"); + } +} + +function BrowseForNewsrc() +{ + const nsIFilePicker = Components.interfaces.nsIFilePicker; + const nsIFile = Components.interfaces.nsIFile; + + var newsrcTextBox = document.getElementById("nntp.newsrcFilePath"); + var fp = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker); + fp.init(window, + document.getElementById("browseForNewsrc").getAttribute("filepickertitle"), + nsIFilePicker.modeSave); + + var currentNewsrcFile; + try { + currentNewsrcFile = Components.classes["@mozilla.org/file/local;1"] + .createInstance(nsIFile); + currentNewsrcFile.initWithPath(newsrcTextBox.value); + } catch (e) { + dump("Failed to create nsIFile instance for the current newsrc file.\n"); + } + + if (currentNewsrcFile) { + fp.displayDirectory = currentNewsrcFile.parent; + fp.defaultString = currentNewsrcFile.leafName; + } + + fp.appendFilters(nsIFilePicker.filterAll); + + if (fp.show() != nsIFilePicker.returnCancel) + newsrcTextBox.value = fp.file.path; +} + +function setupImapDeleteUI(aServerId) +{ + // read delete_model preference + var deleteModel = document.getElementById("imap.deleteModel").getAttribute("value"); + selectImapDeleteModel(deleteModel); + + // read trash_folder_name preference + var trashFolderName = getTrashFolderName(); + + // set folderPicker menulist + var trashPopup = document.getElementById("msgTrashFolderPopup"); + trashPopup._teardown(); + trashPopup._parentFolder = MailUtils.getFolderForURI(aServerId); + trashPopup._ensureInitialized(); + + // TODO: There is something wrong here, selectFolder() fails even if the + // folder does exist. Try to fix in bug 802609. + let trashFolder = MailUtils.getFolderForURI(aServerId + "/" + trashFolderName, false); + try { + trashPopup.selectFolder(trashFolder); + } catch(ex) { + trashPopup.parentNode.setAttribute("label", trashFolder.prettyName); + } + trashPopup.parentNode.folder = trashFolder; +} + +function selectImapDeleteModel(choice) +{ + // set deleteModel to selected mode + document.getElementById("imap.deleteModel").setAttribute("value", choice); + + switch (choice) + { + case "0" : // markDeleted + // disable folderPicker + document.getElementById("msgTrashFolderPicker").setAttribute("disabled", "true"); + break; + case "1" : // moveToTrashFolder + // enable folderPicker + document.getElementById("msgTrashFolderPicker").removeAttribute("disabled"); + break; + case "2" : // deleteImmediately + // disable folderPicker + document.getElementById("msgTrashFolderPicker").setAttribute("disabled", "true"); + break; + default : + dump("Error in enabling/disabling server.TrashFolderPicker\n"); + break; + } +} + +// Capture any menulist changes from folderPicker +function folderPickerChange(aEvent) +{ + var folder = aEvent.target._folder; + var folderPath = getFolderPathFromRoot(folder); + + // Set the value to be persisted. + document.getElementById("imap.trashFolderName") + .setAttribute("value", folderPath); + + // Update the widget to show/do correct things even for subfolders. + var trashFolderPicker = document.getElementById("msgTrashFolderPicker"); + trashFolderPicker.menupopup.selectFolder(folder); +} + +/** Generate the relative folder path from the root. */ +function getFolderPathFromRoot(folder) +{ + var path = folder.name; + var parentFolder = folder.parent; + while (parentFolder && parentFolder != folder.rootFolder) { + path = parentFolder.name + "/" + path; + parentFolder = parentFolder.parent; + } + // IMAP Inbox URI's start with INBOX, not Inbox. + return path.replace(/^Inbox/, "INBOX"); +} + +// Get trash_folder_name from prefs +function getTrashFolderName() +{ + var trashFolderName = document.getElementById("imap.trashFolderName").getAttribute("value"); + // if the preference hasn't been set, set it to a sane default + if (!trashFolderName) { + trashFolderName = "Trash"; + document.getElementById("imap.trashFolderName").setAttribute("value",trashFolderName); + } + return trashFolderName; +} diff --git a/mailnews/base/prefs/content/am-server.xul b/mailnews/base/prefs/content/am-server.xul new file mode 100644 index 000000000..7828e4cf4 --- /dev/null +++ b/mailnews/base/prefs/content/am-server.xul @@ -0,0 +1,470 @@ +<?xml version="1.0"?> + +<!-- + + This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/content/charsetList.css" type="text/css"?> + +<!DOCTYPE page [ +<!ENTITY % trashDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd"> +%trashDTD; +]> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&serverSettings.label;" + onload="parent.onPanelLoaded('am-server.xul');"> + + <vbox flex="1" style="overflow: auto;"> + <script type="application/javascript" src="chrome://messenger/content/AccountManager.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-server.js"/> + <script type="application/javascript" src="chrome://messenger/content/am-prefs.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + + <label hidden="true" wsm_persist="true" id="server.type"/> + <label hidden="true" + wsm_persist="true" + preftype="string" + prefattribute="value" + prefstring="mail.server.%serverkey%.storeContractID" + genericattr="true" + id="server.storeContractID"/> + + <dialogheader title="&serverSettings.label;"/> + + <grid> + <columns> + <column/> + <column flex="1"/> + <column/> + </columns> + <rows> + <row align="center"> + <label value="&serverType.label;"/> + <label id="servertype.verbose"/> + </row> + <row align="center"> + <hbox> + <label value="&serverName.label;" accesskey="&serverName.accesskey;" + control="server.realHostName"/> + </hbox> + <hbox align="center"> + <label id="fixedServerName" collapsed="true" use="server.realHostName"/> + <textbox wsm_persist="true" + size="20" + flex="1" + id="server.realHostName" + prefstring="mail.server.%serverkey%.realhostname" + class="uri-element"/> + </hbox> + <hbox align="center"> + <label hidefor="movemail" value="&port.label;" + accesskey="&port.accesskey;" control="server.port"/> + <label id="fixedServerPort" hidefor="movemail" + collapsed="true" use="server.port"/> + <textbox wsm_persist="true" size="3" id="server.port" + preftype="int" hidefor="movemail" + prefstring="mail.server.%serverkey%.port" + type="number" min="1" max="65535" increment="1"/> + <label value="&serverPortDefault.label;" hidefor="movemail"/> + <label id="defaultPort" hidefor="movemail"/> + </hbox> + </row> + <row align="center"> + <hbox align="center" hidefor="nntp"> + <label value="&userName.label;" + accesskey="&userName.accesskey;" + control="server.realUsername"/> + </hbox> + <hbox align="center" hidefor="nntp"> + <label id="fixedUserName" collapsed="true" use="server.realUsername"/> + <textbox wsm_persist="true" + size="20" + flex="1" + id="server.realUsername" + prefstring="mail.server.%serverkey%.realusername"/> + </hbox> + </row> + </rows> + </grid> + + <separator class="thin"/> + + <groupbox hidefor="movemail"> + <caption label="&securitySettings.label;"/> + + <grid flex="1"> + <columns> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <label value="&connectionSecurity.label;" + accesskey="&connectionSecurity.accesskey;" + control="server.socketType"/> + <menulist wsm_persist="true" id="server.socketType" + oncommand="secureSelect();"> + <menupopup id="server.socketTypePopup"> + <menuitem value="0" label="&connectionSecurityType-0.label;"/> + <menuitem id="connectionSecurityType-1" + value="1" label="&connectionSecurityType-1.label;" + disabled="true"/> + <menuitem value="2" label="&connectionSecurityType-2.label;" + hidefor="nntp"/> + <menuitem value="3" label="&connectionSecurityType-3.label;"/> + </menupopup> + </menulist> + </row> + <row align="center" hidefor="nntp,movemail"> + <label value="&authMethod.label;" + accesskey="&authMethod.accesskey;" + control="server.authMethod"/> + <!-- same in smtpEditOverlay.xul/js --> + <menulist id="server.authMethod" + wsm_persist="true" + preftype="int" type="number" + prefstring="mail.server.%serverkey%.authMethod"> + <menupopup id="server.authMethodPopup"> + <menuitem id="authMethod-no" value="1"/> + <menuitem id="authMethod-old" value="2"/> + <menuitem id="authMethod-password-cleartext" value="3"/> + <menuitem id="authMethod-password-encrypted" value="4"/> + <menuitem id="authMethod-kerberos" value="5"/> + <menuitem id="authMethod-ntlm" value="6"/> + <menuitem id="authMethod-external" value="7"/> +#ifdef MOZ_MAILNEWS_OAUTH2 + <menuitem id="authMethod-oauth2" value="10"/> +#endif + <menuitem id="authMethod-anysecure" value="8"/> + <menuitem id="authMethod-any" value="9"/> + </menupopup> + </menulist> + </row> + </rows> + </grid> + </groupbox> + + <groupbox> + <caption label="&serverSettings.label;"/> + <vbox align="start"> + <checkbox wsm_persist="true" + id="server.loginAtStartUp" + label="&loginAtStartup.label;" + accesskey="&loginAtStartup.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.login_at_startup"/> + </vbox> + <hbox align="center"> + <checkbox wsm_persist="true" + id="server.doBiff" + label="&biffStart.label;" + accesskey="&biffStart.accesskey;" + oncommand="onCheckItem('server.biffMinutes', [this.id]);" + prefattribute="value" + prefstring="mail.server.%serverkey%.check_new_mail"/> + <textbox wsm_persist="true" + id="server.biffMinutes" + type="number" + size="3" + min="1" + increment="1" + aria-labelledby="server.doBiff server.biffMinutes biffEnd" + preftype="int" + prefstring="mail.server.%serverkey%.check_time"/> + <label id="biffEnd" + control="server.biffMinutes" + value="&biffEnd.label;"/> + </hbox> + <vbox align="start" + hidefor="pop3,nntp,movemail"> + <checkbox wsm_persist="true" + id="imap.useIdle" + label="&useIdleNotifications.label;" + accesskey="&useIdleNotifications.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.use_idle"/> + </vbox> + + <!-- Necessary for POP3 and Movemail (Bug 480945) --> + <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=480945 --> + <vbox align="start" hidefor="imap,nntp"> + <checkbox wsm_persist="true" id="server.downloadOnBiff" + label="&downloadOnBiff.label;" prefattribute="value" + accesskey="&downloadOnBiff.accesskey;" + prefstring="mail.server.%serverkey%.download_on_biff"/> + </vbox> + <!-- POP3 --> + <vbox align="start" hidefor="imap,nntp,movemail"> + <checkbox wsm_persist="true" id="pop3.headersOnly" + label="&headersOnly.label;" + accesskey="&headersOnly.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.headers_only"/> + + <checkbox wsm_persist="true" id="pop3.leaveMessagesOnServer" + label="&leaveOnServer.label;" oncommand="setupMailOnServerUI();" + accesskey="&leaveOnServer.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.leave_on_server"/> + + <hbox align="center"> + <checkbox wsm_persist="true" id="pop3.deleteByAgeFromServer" class="indent" + label="&deleteByAgeFromServer.label;" oncommand="setupAgeMsgOnServerUI();" + accesskey="&deleteByAgeFromServer.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.delete_by_age_from_server"/> + <textbox wsm_persist="true" id="pop3.numDaysToLeaveOnServer" size="3" + aria-labelledby="pop3.deleteByAgeFromServer pop3.numDaysToLeaveOnServer daysEnd" + preftype="int" type="number" min="1" increment="1" + prefstring="mail.server.%serverkey%.num_days_to_leave_on_server"/> + <label id="daysEnd" control="pop3.numDaysToLeaveOnServer" value="&daysEnd.label;"/> + </hbox> + + <checkbox wsm_persist="true" id="pop3.deleteMailLeftOnServer" class="indent" + label="&deleteOnServer2.label;" + accesskey="&deleteOnServer2.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.delete_mail_left_on_server"/> + + <!-- hidden elements for data transfer to and from advanced... dialog --> + <hbox flex="1" hidefor="imap,nntp,movemail" hidden="true"> + <checkbox hidden="true" wsm_persist="true" id="pop3.deferGetNewMail" + prefattribute="value" + prefstring="mail.server.%serverkey%.deferGetNewMail"/> + <label hidden="true" wsm_persist="true" id="pop3.deferredToAccount" + prefattribute="value" + prefstring="mail.server.%serverkey%.deferredToAccount"/> + + </hbox> + </vbox> + <!-- IMAP --> + <label hidden="true" wsm_persist="true" id="imap.trashFolderName" + prefattribute="value" + prefstring="mail.server.%serverkey%.trash_folder_name"/> + + <hbox align="center" hidefor="pop3,nntp,movemail"> + <label value="&deleteMessagePrefix.label;" align="left" + control="imap.deleteModel"/> + </hbox> + <vbox> + <hbox align="center" + id="imap.deleteModel.box" + hidefor="pop3,nntp,movemail" + flex="1"> + <radiogroup id="imap.deleteModel" + wsm_persist="true" + oncommand="selectImapDeleteModel(this.value);" + prefstring="mail.server.%serverkey%.delete_model"> + <grid class="specialFolderPickerGrid"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <radio id="modelMoveToTrash" + value="1" + label="&modelMoveToTrash.label;" + accesskey="&modelMoveToTrash.accesskey;"/> + <menulist id="msgTrashFolderPicker" flex="1" + class="folderMenuItem" + maxwidth="300" crop="center" + aria-labelledby="modelMoveToTrash" + displayformat="verbose"> + <menupopup id="msgTrashFolderPopup" + type="folder" + mode="filing" + showFileHereLabel="true" + oncommand="folderPickerChange(event);"/> + </menulist> + </row> + <row align="center"> + <radio id="modelMarkDeleted" + value="0" + label="&modelMarkDeleted.label;" + accesskey="&modelMarkDeleted.accesskey;"/> + </row> + <row align="center"> + <radio id="modelDeleteImmediately" + value="2" + label="&modelDeleteImmediately.label;" + accesskey="&modelDeleteImmediately.accesskey;"/> + </row> + </rows> + </grid> + </radiogroup> + </hbox> + <hbox pack="end"> + <!-- This button should have identical attributes to the + server.popAdvancedButton except the hidefor attribute. --> + <button label="&advancedButton.label;" + accesskey="&advancedButton.accesskey;" + oncommand="onAdvanced();" + wsm_persist="true" + id="server.imapAdvancedButton" + prefstring="mail.server.%serverkey%.advanced.disable" + hidefor="pop3,nntp,movemail"/> + </hbox> + </vbox> + + <!-- Advanced IMAP settings --> + <hbox flex="1" hidefor="pop3,nntp,movemail" hidden="true"> + <checkbox hidden="true" wsm_persist="true" id="imap.dualUseFolders" + prefattribute="value" + prefstring="mail.server.%serverkey%.dual_use_folders"/> + <checkbox hidden="true" wsm_persist="true" id="imap.usingSubscription" + prefattribute="value" + prefstring="mail.server.%serverkey%.using_subscription"/> + <label hidden="true" wsm_persist="true" id="imap.maximumConnectionsNumber"/> + <label hidden="true" wsm_persist="true" id="imap.personalNamespace"/> + <label hidden="true" wsm_persist="true" id="imap.publicNamespace"/> + <label hidden="true" wsm_persist="true" id="imap.otherUsersNamespace"/> + <label hidden="true" wsm_persist="true" id="imap.serverDirectory"/> + <checkbox hidden="true" wsm_persist="true" id="imap.overrideNamespaces" + prefattribute="value" + prefstring="mail.server.%serverkey%.override_namespaces"/> + </hbox> + + <!-- NNTP --> + <hbox hidefor="pop3,imap,movemail" align="center"> + <checkbox wsm_persist="true" id="nntp.notifyOn" + label="&maxMessagesStart.label;" + accesskey="&maxMessagesStart.accesskey;" + oncommand="onCheckItem('nntp.maxArticles', [this.id]);" + prefattribute="value" + prefstring="mail.server.%serverkey%.notify.on"/> + <textbox wsm_persist="true" id="nntp.maxArticles" + type="number" size="4" min="1" increment="10" + aria-labelledby="nntp.notifyOn nntp.maxArticles maxMessagesEnd" + preftype="int" + prefstring="mail.server.%serverkey%.max_articles"/> + <label control="nntp.maxArticles" value="&maxMessagesEnd.label;" id="maxMessagesEnd"/> + </hbox> + <checkbox hidefor="pop3,imap,movemail" wsm_persist="true" id="nntp.pushAuth" + label="&alwaysAuthenticate.label;" + accesskey="&alwaysAuthenticate.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.always_authenticate"/> + + <!-- take out for now - bug 45079 + <hbox flex="1" hidefor="pop3,imap,movemail"> + <groupbox> + <caption class="header" label="&abbreviate.label;"/> + + <radiogroup wsm_persist="true" id="nntp.abbreviate"> + <radio value="true" + label="&abbreviateOn.label;"/> + <radio value="false" + label="&abbreviateOff.label;"/> + </radiogroup> + </groupbox> + </hbox> + --> + + </groupbox> + + <groupbox> + <caption label="&messageStorage.label;"/> + + <hbox align="end"> + <vbox align="start" flex="1" id="exitHandlingBox="> + <checkbox hidefor="pop3,nntp,movemail" + wsm_persist="true" + id="imap.cleanupInboxOnExit" + label="&expungeOnExit.label;" + accesskey="&expungeOnExit.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.cleanup_inbox_on_exit"/> + <checkbox hidefor="nntp" + wsm_persist="true" + id="server.emptyTrashOnExit" + label="&emptyTrashOnExit.label;" + accesskey="&emptyTrashOnExit.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.empty_trash_on_exit"/> + </vbox> + <button label="&advancedButton.label;" + accesskey="&advancedButton.accesskey;" + oncommand="onAdvanced();" + wsm_persist="true" + id="server.popAdvancedButton" + prefstring="mail.server.%serverkey%.advanced.disable" + hidefor="imap,nntp,movemail"/> + </hbox> + <hbox align="center"> + <label value="&storeType.label;" + accesskey="&storeType.accesskey;" + control="server.storeTypeMenulist"/> + <menulist id="server.storeTypeMenulist"> + <menupopup id="server.storeTypeMenupopup"> + <menuitem id="server.mboxStore" + value="@mozilla.org/msgstore/berkeleystore;1" + label="&mboxStore2.label;"/> + <menuitem id="server.maildirStore" + value="@mozilla.org/msgstore/maildirstore;1" + label="&maildirStore.label;"/> + </menupopup> + </menulist> + </hbox> + + <vbox hidefor="imap,pop3,movemail"> + <label value="&newsrcFilePath.label;" control="nntp.newsrcFilePath"/> + <hbox align="center"> + <textbox readonly="true" + wsm_persist="true" + flex="1" + id="nntp.newsrcFilePath" + datatype="nsIFile" + prefstring="mail.server.%serverkey%.newsrc.file" + class="uri-element"/> + <button id="browseForNewsrc" + label="&browseNewsrc.label;" + filepickertitle="&newsrcPicker.label;" + accesskey="&browseNewsrc.accesskey;" + oncommand="BrowseForNewsrc();"/> + </hbox> + </vbox> + + <separator class="thin"/> + + <vbox> + <label value="&localPath.label;" control="server.localPath"/> + <hbox align="center"> + <textbox readonly="true" + wsm_persist="true" + flex="1" + id="server.localPath" + datatype="nsIFile" + prefstring="mail.server.%serverkey%.directory" + class="uri-element"/> + <button id="browseForLocalFolder" + label="&browseFolder.label;" + filepickertitle="&localFolderPicker.label;" + accesskey="&browseFolder.accesskey;" + oncommand="BrowseForLocalFolders();"/> + </hbox> + </vbox> + + </groupbox> + + <separator class="thin"/> + + <hbox hidefor="imap,pop3,movemail" align="center" valign="middle" iscontrolcontainer="true"> + <label value="&serverDefaultCharset2.label;" control="nntp.charset"/> + <menulist hidable="true" + type="charset" + hidefor="imap,pop3,movemail" + wsm_persist="true" + id="nntp.charset" + preftype="string" + prefstring="mail.server.%serverkey%.charset"/> + </hbox> + </vbox> +</page> diff --git a/mailnews/base/prefs/content/am-serverwithnoidentities.js b/mailnews/base/prefs/content/am-serverwithnoidentities.js new file mode 100644 index 000000000..696dc71b0 --- /dev/null +++ b/mailnews/base/prefs/content/am-serverwithnoidentities.js @@ -0,0 +1,34 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gServer; + +function onInit(aPageId, aServerId) { + + // UI for account store type + let storeTypeElement = document.getElementById("server.storeTypeMenulist"); + // set the menuitem to match the account + let currentStoreID = document.getElementById("server.storeContractID") + .getAttribute("value"); + let targetItem = storeTypeElement.getElementsByAttribute("value", currentStoreID); + storeTypeElement.selectedItem = targetItem[0]; + // disable store type change if store has already been used + storeTypeElement.setAttribute("disabled", + gServer.getBoolValue("canChangeStoreType") ? "false" : "true"); +} + +function onPreInit(account, accountValues) { + gServer = account.incomingServer; +} + +function onSave() +{ + let storeContractID = document.getElementById("server.storeTypeMenulist") + .selectedItem + .value; + document.getElementById("server.storeContractID") + .setAttribute("value", storeContractID); +} + diff --git a/mailnews/base/prefs/content/am-serverwithnoidentities.xul b/mailnews/base/prefs/content/am-serverwithnoidentities.xul new file mode 100644 index 000000000..e33216234 --- /dev/null +++ b/mailnews/base/prefs/content/am-serverwithnoidentities.xul @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE page [ +<!ENTITY % accountNoIdentDTD SYSTEM "chrome://messenger/locale/am-serverwithnoidentities.dtd" >%accountNoIdentDTD; +<!ENTITY % accountServerTopDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">%accountServerTopDTD; +]> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&accountTitle.label;" + onload="parent.onPanelLoaded('am-serverwithnoidentities.xul');"> + + <script type="application/javascript" src="chrome://messenger/content/am-serverwithnoidentities.js"/> + <script type="application/javascript" src="chrome://messenger/content/amUtils.js"/> + + <dialogheader title="&accountTitle.label;"/> + + <label hidden="true" + wsm_persist="true" + preftype="string" + prefattribute="value" + prefstring="mail.server.%serverkey%.storeContractID" + genericattr="true" + id="server.storeContractID"/> + + <description class="secDesc">&accountSettingsDesc.label;</description> + <hbox align="center"> + <label value="&accountName.label;" control="server.prettyName" + accesskey="&accountName.accesskey;"/> + <textbox wsm_persist="true" size="30" id="server.prettyName" + prefstring="mail.server.%serverkey%.name"/> + </hbox> + + <separator class="thin"/> + + <groupbox> + <caption label="&messageStorage.label;"/> + + <vbox align="start"> + <checkbox wsm_persist="true" id="server.emptyTrashOnExit" + label="&emptyTrashOnExit.label;" + accesskey="&emptyTrashOnExit.accesskey;" + prefattribute="value" + prefstring="mail.server.%serverkey%.empty_trash_on_exit"/> + <hbox align="center"> + <label value="&storeType.label;" + accesskey="&storeType.accesskey;" + control="server.storeTypeMenulist"/> + <menulist id="server.storeTypeMenulist"> + <menupopup id="server.storeTypeMenupopup"> + <menuitem id="server.mboxStore" + value="@mozilla.org/msgstore/berkeleystore;1" + label="&mboxStore2.label;"/> + <menuitem id="server.maildirStore" + value="@mozilla.org/msgstore/maildirstore;1" + label="&maildirStore.label;"/> + </menupopup> + </menulist> + </hbox> + </vbox> + + <separator class="thin"/> + + <label value="&localPath.label;" control="server.localPath"/> + <hbox align="center"> + <textbox readonly="true" wsm_persist="true" flex="1" id="server.localPath" datatype="nsIFile" + prefstring="mail.server.%serverkey%.directory" class="uri-element"/> + <button id="browseForLocalFolder" label="&browseFolder.label;" filepickertitle="&localFolderPicker.label;" + accesskey="&browseFolder.accesskey;" oncommand="BrowseForLocalFolders()"/> + </hbox> + </groupbox> +</page> diff --git a/mailnews/base/prefs/content/am-smtp.js b/mailnews/base/prefs/content/am-smtp.js new file mode 100644 index 000000000..44580bc20 --- /dev/null +++ b/mailnews/base/prefs/content/am-smtp.js @@ -0,0 +1,263 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +var gSmtpServerListWindow = +{ + mBundle: null, + mServerList: null, + mAddButton: null, + mEditButton: null, + mDeleteButton: null, + mSetDefaultServerButton: null, + + onLoad: function() + { + parent.onPanelLoaded('am-smtp.xul'); + + this.mBundle = document.getElementById("bundle_messenger"); + this.mServerList = document.getElementById("smtpList"); + this.mAddButton = document.getElementById("addButton"); + this.mEditButton = document.getElementById("editButton"); + this.mDeleteButton = document.getElementById("deleteButton"); + this.mSetDefaultServerButton = document.getElementById("setDefaultButton"); + + this.refreshServerList("", false); + + this.updateButtons(this.getSelectedServer()); + }, + + onSelectionChanged: function(aEvent) + { + if (this.mServerList.selectedItems.length <= 0) + return; + + var server = this.getSelectedServer(); + this.updateButtons(server); + this.updateServerInfoBox(server); + }, + + onDeleteServer: function (aEvent) + { + var server = this.getSelectedServer(); + if (server) + { + // confirm deletion + let cancel = Services.prompt.confirmEx(window, + this.mBundle.getString('smtpServers-confirmServerDeletionTitle'), + this.mBundle.getFormattedString('smtpServers-confirmServerDeletion', [server.hostname], 1), + Services.prompt.STD_YES_NO_BUTTONS, null, null, null, null, { }); + + if (!cancel) + { + // Remove password information first. + try { + server.forgetPassword(); + } catch (e) { /* It is OK if this fails. */ } + // Remove the server. + MailServices.smtp.deleteServer(server); + parent.replaceWithDefaultSmtpServer(server.key); + this.refreshServerList("", true); + } + } + }, + + onAddServer: function (aEvent) + { + this.openServerEditor(null); + }, + + onEditServer: function (aEvent) + { + if (this.mServerList.selectedItems.length <= 0) + return; + this.openServerEditor(this.getSelectedServer()); + }, + + onSetDefaultServer: function(aEvent) + { + if (this.mServerList.selectedItems.length <= 0) + return; + + MailServices.smtp.defaultServer = this.getSelectedServer(); + this.refreshServerList(MailServices.smtp.defaultServer.key, true); + }, + + updateButtons: function(aServer) + { + // can't delete default server + if (MailServices.smtp.defaultServer == aServer) + { + this.mSetDefaultServerButton.setAttribute("disabled", "true"); + this.mDeleteButton.setAttribute("disabled", "true"); + } + else + { + this.mSetDefaultServerButton.removeAttribute("disabled"); + this.mDeleteButton.removeAttribute("disabled"); + } + + if (this.mServerList.selectedItems.length == 0) + this.mEditButton.setAttribute("disabled", "true"); + else + this.mEditButton.removeAttribute("disabled"); + }, + + updateServerInfoBox: function(aServer) + { + var noneSelected = this.mBundle.getString("smtpServerList-NotSpecified"); + + document.getElementById('nameValue').value = aServer.hostname; + document.getElementById('descriptionValue').value = aServer.description || noneSelected; + document.getElementById('portValue').value = aServer.port; + document.getElementById('userNameValue').value = aServer.username || noneSelected; + document.getElementById('useSecureConnectionValue').value = + this.mBundle.getString("smtpServer-ConnectionSecurityType-" + + aServer.socketType); + + const AuthMethod = Components.interfaces.nsMsgAuthMethod; + const SocketType = Components.interfaces.nsMsgSocketType; + var authStr = ""; + switch (aServer.authMethod) + { + case AuthMethod.none: + authStr = "authNo"; + break; + case AuthMethod.passwordEncrypted: + authStr = "authPasswordEncrypted"; + break; + case AuthMethod.GSSAPI: + authStr = "authKerberos"; + break; + case AuthMethod.NTLM: + authStr = "authNTLM"; + break; + case AuthMethod.secure: + authStr = "authAnySecure"; + break; + case AuthMethod.passwordCleartext: + authStr = (aServer.socketType == SocketType.SSL || + aServer.socketType == SocketType.alwaysSTARTTLS) + ? "authPasswordCleartextViaSSL" + : "authPasswordCleartextInsecurely"; + break; +#ifdef MOZ_MAILNEWS_OAUTH2 + case AuthMethod.OAuth2: + authStr = "authOAuth2"; + break; +#endif + default: + // leave empty + Components.utils.reportError("Warning: unknown value for smtpserver... authMethod: " + + aServer.authMethod); + } + if (authStr) + document.getElementById("authMethodValue").value = + this.mBundle.getString(authStr); + }, + + refreshServerList: function(aServerKeyToSelect, aFocusList) + { + // remove all children + while (this.mServerList.hasChildNodes()) + this.mServerList.lastChild.remove(); + + this.fillSmtpServers(this.mServerList, + MailServices.smtp.servers, + MailServices.smtp.defaultServer); + + if (aServerKeyToSelect) + this.setSelectedServer(this.mServerList.querySelector('[key="' + aServerKeyToSelect + '"]')); + else // select the default server + this.setSelectedServer(this.mServerList.querySelector('[default="true"]')); + + if (aFocusList) + this.mServerList.focus(); + }, + + fillSmtpServers: function(aListBox, aServers, aDefaultServer) + { + if (!aListBox || !aServers) + return; + + while (aServers.hasMoreElements()) + { + var server = aServers.getNext(); + + if (server instanceof Components.interfaces.nsISmtpServer) + { + var isDefault = (aDefaultServer.key == server.key); + + var listitem = this.createSmtpListItem(server, isDefault); + aListBox.appendChild(listitem); + } + } + }, + + createSmtpListItem: function(aServer, aIsDefault) + { + var listitem = document.createElement("listitem"); + var serverName = ""; + + if (aServer.description) + serverName = aServer.description + ' - '; + else if (aServer.username) + serverName = aServer.username + ' - '; + + serverName += aServer.hostname; + + if (aIsDefault) + { + serverName += " " + this.mBundle.getString("defaultServerTag"); + listitem.setAttribute("default", "true"); + } + + listitem.setAttribute("label", serverName); + listitem.setAttribute("key", aServer.key); + listitem.setAttribute("class", "smtpServerListItem"); + + // give it some unique id + listitem.id = "smtpServer." + aServer.key; + return listitem; + }, + + openServerEditor: function(aServer) + { + var args = {server: aServer, + result: false, + addSmtpServer: ""}; + + window.openDialog("chrome://messenger/content/SmtpServerEdit.xul", + "smtpEdit", "chrome,titlebar,modal,centerscreen", args); + + // now re-select the server which was just added + if (args.result) + this.refreshServerList(aServer ? aServer.key : args.addSmtpServer, true); + + return args.result; + }, + + setSelectedServer: function(aServer) + { + if (!aServer) + return; + + setTimeout(function(aServerList) { + aServerList.ensureElementIsVisible(aServer); + aServerList.selectItem(aServer); + }, 0, this.mServerList); + }, + + getSelectedServer: function() + { + if (this.mServerList.selectedItems.length == 0) + return null; + + let serverKey = this.mServerList.selectedItems[0].getAttribute("key"); + return MailServices.smtp.getServerByKey(serverKey); + } +}; diff --git a/mailnews/base/prefs/content/am-smtp.xul b/mailnews/base/prefs/content/am-smtp.xul new file mode 100644 index 000000000..3c18bfe11 --- /dev/null +++ b/mailnews/base/prefs/content/am-smtp.xul @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/am-advanced.dtd"> + +<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&smtpServer.label;" + onload="gSmtpServerListWindow.onLoad();"> + <script type="application/javascript" + src="chrome://messenger/content/amUtils.js"/> + <script type="application/javascript" + src="chrome://messenger/content/am-smtp.js"/> + + <stringbundle id="bundle_messenger" + src="chrome://messenger/locale/messenger.properties"/> + + <vbox flex="1" style="overflow: auto;"> + <dialogheader title="&smtpServer.label;"/> + + <label control="smtpList">&smtpDescription.label;</label> + <separator class="thin"/> + + <hbox flex="1"> + <listbox id="smtpList" + onselect="gSmtpServerListWindow.onSelectionChanged(event);" + ondblclick="gSmtpServerListWindow.onEditServer(event);" + flex="1"/> + + <vbox> + <button id="addButton" + oncommand="gSmtpServerListWindow.onAddServer(event);" + label="&smtpListAdd.label;" + accesskey="&smtpListAdd.accesskey;"/> + <button id="editButton" + oncommand="gSmtpServerListWindow.onEditServer(event);" + label="&smtpListEdit.label;" + accesskey="&smtpListEdit.accesskey;"/> + <separator/> + <button id="deleteButton" disabled="true" + oncommand="gSmtpServerListWindow.onDeleteServer(event);" + label="&smtpListDelete.label;" + accesskey="&smtpListDelete.accesskey;"/> + <button id="setDefaultButton" disabled="true" + oncommand="gSmtpServerListWindow.onSetDefaultServer(event);" + label="&smtpListSetDefault.label;" + accesskey="&smtpListSetDefault.accesskey;"/> + </vbox> + </hbox> + + <separator/> + + <label class="header">&serverDetails.label;</label> + <hbox id="smtpServerInfoBox"> + <stack flex="1" class="inset"> + <spacer id="backgroundBox"/> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <hbox pack="end"> + <label id="descriptionLabel" + value="&serverDescription.label;" + control="descriptionValue"/> + </hbox> + <textbox id="descriptionValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="nameLabel" value="&serverName.label;" control="nameValue"/> + </hbox> + <textbox id="nameValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="portLabel" value="&serverPort.label;" control="portValue"/> + </hbox> + <textbox id="portValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="userNameLabel" value="&userName.label;" control="userNameValue"/> + </hbox> + <textbox id="userNameValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="authMethodLabel" value="&authMethod.label;" control="authMethodValue"/> + </hbox> + <textbox id="authMethodValue" readonly="true" class="plain"/> + </row> + <row align="center"> + <hbox pack="end"> + <label id="connectionSecurityLabel" value="&connectionSecurity.label;" + control="useSecureConnectionValue"/> + </hbox> + <textbox id="useSecureConnectionValue" readonly="true" class="plain"/> + </row> + </rows> + </grid> + </stack> + </hbox> + <separator flex="1"/> + </vbox> +</page> diff --git a/mailnews/base/prefs/content/amUtils.js b/mailnews/base/prefs/content/amUtils.js new file mode 100644 index 000000000..2b101f9f1 --- /dev/null +++ b/mailnews/base/prefs/content/amUtils.js @@ -0,0 +1,205 @@ +/* -*- 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource:///modules/MailUtils.js"); +Components.utils.import("resource:///modules/iteratorUtils.jsm"); + +function BrowseForLocalFolders() +{ + const nsIFilePicker = Components.interfaces.nsIFilePicker; + const nsIFile = Components.interfaces.nsIFile; + + var currentFolderTextBox = document.getElementById("server.localPath"); + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + + fp.init(window, + document.getElementById("browseForLocalFolder") + .getAttribute("filepickertitle"), + nsIFilePicker.modeGetFolder); + + var currentFolder = Components.classes["@mozilla.org/file/local;1"] + .createInstance(nsIFile); + currentFolder.initWithPath(currentFolderTextBox.value); + fp.displayDirectory = currentFolder; + + if (fp.show() != nsIFilePicker.returnOK) + return; + + // Retrieve the selected folder. + let selectedFolder = fp.file; + + // Check if the folder can be used for mail storage. + if (!top.checkDirectoryIsUsable(selectedFolder)) + return; + + currentFolderTextBox.value = selectedFolder.path; +} + +/** + * Return server/folder name formatted with server name if needed. + * + * @param aTargetFolder nsIMsgFolder to format name for + * If target.isServer then only its name is returned. + * Otherwise return the name as "<foldername> on <servername>". + */ +function prettyFolderName(aTargetFolder) +{ + if (aTargetFolder.isServer) + return aTargetFolder.prettyName; + + return document.getElementById("bundle_messenger") + .getFormattedString("verboseFolderFormat", + [aTargetFolder.prettyName, + aTargetFolder.server.prettyName]); +} + +/** + * Checks validity of junk target server name and folder. + * + * @param aTargetURI the URI specification to check + * @param aIsServer true if the URI specifies only a server (without folder) + * + * @return the value of aTargetURI if it is valid (usable), otherwise null + */ +function checkJunkTargetFolder(aTargetURI, aIsServer) +{ + try { + // Does the target account exist? + let targetServer = MailUtils.getFolderForURI(aTargetURI + (aIsServer ? "/Junk" : ""), + !aIsServer).server; + + // If the target server has deferred storage, Junk can't be stored into it. + if (targetServer.rootFolder != targetServer.rootMsgFolder) + return null; + } catch (e) { + return null; + } + + return aTargetURI; +} + +/** + * Finds a usable target for storing Junk mail. + * If the passed in server URI is not usable, choose Local Folders. + * + * @param aTargetURI the URI of a server or folder to try first + * @param aIsServer true if the URI specifies only a server (without folder) + * + * @return the server/folder URI of a usable target for storing Junk + */ +function chooseJunkTargetFolder(aTargetURI, aIsServer) +{ + let server = null; + + if (aTargetURI) { + server = MailUtils.getFolderForURI(aTargetURI, false).server; + if (!server.canCreateFoldersOnServer || !server.canSearchMessages || + (server.rootFolder != server.rootMsgFolder)) + server = null; + } + if (!server) + server = MailServices.accounts.localFoldersServer; + + return server.serverURI + (!aIsServer ? "/Junk" : ""); +} + +/** + * Fixes junk target folders if they point to an invalid/unusable (e.g. deferred) + * folder/account. Only returns the new safe values. It is up to the caller + * to push them to the proper elements/prefs. + * + * @param aSpamActionTargetAccount The value of the server.*.spamActionTargetAccount pref value (URI). + * @param aSpamActionTargetFolder The value of the server.*.spamActionTargetFolder pref value (URI). + * @param aProposedTarget The URI of a new target to try. + * @param aMoveTargetModeValue The value of the server.*.moveTargetMode pref value (0/1). + * @param aServerSpamSettings The nsISpamSettings object of any server + * (used just for the MOVE_TARGET_MODE_* constants). + * @param aMoveOnSpam The server.*.moveOnSpam pref value (bool). + * + * @return an array containing: + * newTargetAccount new safe junk target account + * newTargetAccount new safe junk target folder + * newMoveOnSpam new moveOnSpam value + */ +function sanitizeJunkTargets(aSpamActionTargetAccount, + aSpamActionTargetFolder, + aProposedTarget, + aMoveTargetModeValue, + aServerSpamSettings, + aMoveOnSpam) +{ + // Check if folder targets are valid. + aSpamActionTargetAccount = checkJunkTargetFolder(aSpamActionTargetAccount, true); + if (!aSpamActionTargetAccount) { + // If aSpamActionTargetAccount is not valid, + // reset to default behavior to NOT move junk messages... + if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_ACCOUNT) + aMoveOnSpam = false; + + // ... and find a good default target. + aSpamActionTargetAccount = chooseJunkTargetFolder(aProposedTarget, true); + } + + aSpamActionTargetFolder = checkJunkTargetFolder(aSpamActionTargetFolder, false); + if (!aSpamActionTargetFolder) { + // If aSpamActionTargetFolder is not valid, + // reset to default behavior to NOT move junk messages... + if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_FOLDER) + aMoveOnSpam = false; + + // ... and find a good default target. + aSpamActionTargetFolder = chooseJunkTargetFolder(aProposedTarget, false); + } + + return [ aSpamActionTargetAccount, aSpamActionTargetFolder, aMoveOnSpam ]; +} + +/** + * Opens Preferences (Options) dialog on the Advanced pane, General tab + * so that the user sees where the global receipts settings can be found. + * + * @param aTBPaneId Thunderbird pref paneID to open. + * @param aTBTabId Thunderbird tabID to open. + * @param aTBOtherArgs Other arguments to send to the pref tab. + * @param aSMPaneId Seamonkey pref pane to open. + */ +function openPrefsFromAccountManager(aTBPaneId, aTBTabId, aTBOtherArgs, aSMPaneId) { + let win = Services.wm.getMostRecentWindow("mail:3pane") || + Services.wm.getMostRecentWindow("mail:messageWindow") || + Services.wm.getMostRecentWindow("msgcompose"); + if (!win) + return; + + // If openOptionsDialog() exists, we are in Thunderbird. + if (typeof win.openOptionsDialog == "function") + win.openOptionsDialog(aTBPaneId, aTBTabId, aTBOtherArgs); + // If goPreferences() exists, we are in Seamonkey. + if (typeof win.goPreferences == "function") + win.goPreferences(aSMPaneId); +} + +/** + * Check if the given account name already exists in any account. + * + * @param aAccountName The account name string to look for. + * @param aAccountKey Optional. The key of an account that is skipped when + * searching the name. If unset, do not skip any account. + */ +function accountNameExists(aAccountName, aAccountKey) +{ + for (let account in fixIterator(MailServices.accounts.accounts, + Components.interfaces.nsIMsgAccount)) + { + if (account.key != aAccountKey && account.incomingServer && + aAccountName == account.incomingServer.prettyName) { + return true; + } + } + + return false; +} diff --git a/mailnews/base/prefs/content/aw-accname.js b/mailnews/base/prefs/content/aw-accname.js new file mode 100644 index 000000000..6fce2e878 --- /dev/null +++ b/mailnews/base/prefs/content/aw-accname.js @@ -0,0 +1,73 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gPrefsBundle; + +function acctNamePageValidate() +{ + var accountname = document.getElementById("prettyName").value; + var canAdvance = accountname ? true : false; + + // Check if this accountname already exists. If so, return false so that + // user can enter a different unique account name. + if (canAdvance && accountNameExists(accountname)) + canAdvance = false; + + document.documentElement.canAdvance = canAdvance; +} + +function acctNamePageUnload() { + var pageData = parent.GetPageData(); + + // fix for bug #255473 + // allow for multiple RSS accounts. + // if our isp.rdf file defines "wizardAutoGenerateUniqueHostname" + // we generate a unique hostname until we have one that doesn't exist + // for RSS accounts, in rss.rdf, userName, hostName and serverType + // default to the same thing, so we need to do this to allow for + // multiple RSS accounts. Note, they can all have the same pretty name. + if (gCurrentAccountData && + gCurrentAccountData.wizardAutoGenerateUniqueHostname) + { + var serverType = parent.getCurrentServerType(pageData); + var userName = parent.getCurrentUserName(pageData); + var hostName = parent.getCurrentHostname(pageData); + var hostNamePref = hostName; + var i = 2; + while (parent.AccountExists(userName, hostName, serverType)) + { + // If "Feeds" exists, try "Feeds-2", then "Feeds-3", etc. + hostName = hostNamePref + "-" + i; + i++; + } + setPageData(pageData, "server", "hostname", hostName); + } + + var accountname = document.getElementById("prettyName").value; + setPageData(pageData, "accname", "prettyName", accountname); + // Set this to true so we know the user has set the name. + setPageData(pageData, "accname", "userset", true); + return true; +} + +function acctNamePageInit() +{ + gPrefsBundle = document.getElementById("bundle_prefs"); + var accountNameInput = document.getElementById("prettyName"); + if (accountNameInput.value=="") { + var pageData = parent.GetPageData(); + var type = parent.getCurrentServerType(pageData); + var accountName; + + if (gCurrentAccountData && gCurrentAccountData.wizardAccountName) + accountName = gCurrentAccountData.wizardAccountName; + else if (type == "nntp") + accountName = pageData.newsserver.hostname.value; + else + accountName = pageData.identity.email.value; + accountNameInput.value = accountName; + } + acctNamePageValidate(); +} diff --git a/mailnews/base/prefs/content/aw-accounttype.js b/mailnews/base/prefs/content/aw-accounttype.js new file mode 100644 index 000000000..64b828594 --- /dev/null +++ b/mailnews/base/prefs/content/aw-accounttype.js @@ -0,0 +1,114 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +function setAccountTypeData() +{ + var rg = document.getElementById("acctyperadio"); + var selectedItemId = rg.selectedItem.id; + var mail = selectedItemId == "mailaccount"; + var news = selectedItemId == "newsaccount"; + + var pageData = parent.GetPageData(); + setPageData(pageData, "accounttype", "mailaccount", mail); + setPageData(pageData, "accounttype", "newsaccount", news); + + // Other account type, e.g. Movemail + setPageData(pageData, "accounttype", "otheraccount", !(news || mail)); +} + +function acctTypePageUnload() { + gCurrentAccountData = null; + setAccountTypeData(); + initializeIspData(); + setupWizardPanels(); + return true; +} + +function setupWizardPanels() { + if (gCurrentAccountData && gCurrentAccountData.useOverlayPanels) { + if ("testingIspServices" in this) { + if ("SetPageMappings" in this && testingIspServices()) { + SetPageMappings(document.documentElement.currentPage.id, "done"); + } + } + } + + var pageData = parent.GetPageData(); + + // We default this to false, even though we could set it to true if we + // are going to display the page. However as the accname page will set + // it to true for us, we'll just default it to false and not work it out + // twice. + setPageData(pageData, "accname", "userset", false); + + // If we need to skip wizardpanels, set the wizard to jump to the + // summary page i.e., last page. Otherwise, set the flow based + // on type of account (mail or news) user is creating. + var skipPanels = ""; + try { + if (gCurrentAccountData.wizardSkipPanels) + skipPanels = gCurrentAccountData.wizardSkipPanels.toString().toLowerCase(); + } catch(ex) {} + + // "done" is the only required panel for all accounts. We used to require an identity panel but not anymore. + // initialize wizardPanels with the optional mail/news panels + var wizardPanels, i; + var isMailAccount = pageData.accounttype.mailaccount; + var isNewsAccount = pageData.accounttype.newsaccount; + if (skipPanels == "true") // Support old syntax of true/false for wizardSkipPanels + wizardPanels = new Array("identitypage"); + else if (isMailAccount && isMailAccount.value) + wizardPanels = new Array("identitypage", "incomingpage", "outgoingpage", "accnamepage"); + else if (isNewsAccount && isNewsAccount.value) + wizardPanels = new Array("identitypage", "newsserver", "accnamepage"); + else { // An account created by an extension and XUL overlays + var button = document.getElementById("acctyperadio").selectedItem; + wizardPanels = button.value.split(/ *, */); + } + + // Create a hash table of the panels to skip + var skipArray = skipPanels.split(","); + var skipHash = new Array(); + for (i = 0; i < skipArray.length; i++) + skipHash[skipArray[i]] = skipArray[i]; + + // Remove skipped panels + i = 0; + while (i < wizardPanels.length) { + if (wizardPanels[i] in skipHash) + wizardPanels.splice(i, 1); + else + i++; + } + + wizardPanels.push("done"); + + // Set up order of panels + for (i = 0; i < (wizardPanels.length-1); i++) + setNextPage(wizardPanels[i], wizardPanels[i+1]); + + // make the account type page go to the very first of our approved wizard panels...this is usually going to + // be accounttype --> identitypage unless we were configured to skip the identity page + setNextPage("accounttype",wizardPanels[0]); +} + +function initializeIspData() +{ + let mailAccount = document.getElementById("mailaccount"); + if (!mailAccount || !mailAccount.selected) { + parent.SetCurrentAccountData(null); + } + + // now reflect the datasource up into the parent + var accountSelection = document.getElementById("acctyperadio"); + + var ispName = accountSelection.selectedItem.id; + + dump("initializing ISP data for " + ispName + "\n"); + + if (!ispName || ispName == "") return; + + parent.PrefillAccountForIsp(ispName); +} diff --git a/mailnews/base/prefs/content/aw-done.js b/mailnews/base/prefs/content/aw-done.js new file mode 100644 index 000000000..79fb7d000 --- /dev/null +++ b/mailnews/base/prefs/content/aw-done.js @@ -0,0 +1,215 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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/. */ + +Components.utils.import("resource:///modules/mailServices.js"); + +var gPrefsBundle; + +function donePageInit() { + var pageData = parent.GetPageData(); + var currentAccountData = gCurrentAccountData; + + if ("testingIspServices" in this) { + if (testingIspServices()) { + if ("setOtherISPServices" in this) { + setOtherISPServices(); + } + + if (currentAccountData && currentAccountData.useOverlayPanels && currentAccountData.createNewAccount) { + var backButton = document.documentElement.getButton("back"); + backButton.setAttribute("disabled", true); + + var cancelButton = document.documentElement.getButton("cancel"); + cancelButton.setAttribute("disabled", true); + + setPageData(pageData, "identity", "email", gEmailAddress); + setPageData(pageData, "identity", "fullName", gUserFullName); + setPageData(pageData, "login", "username", gScreenName); + } + } + } + + gPrefsBundle = document.getElementById("bundle_prefs"); + var showMailServerDetails = true; + + if (currentAccountData) { + // find out if we need to hide server details + showMailServerDetails = currentAccountData.showServerDetailsOnWizardSummary; + // Change the username field description to email field label in aw-identity + if (currentAccountData.emailIDFieldTitle) + setUserNameDescField(currentAccountData.emailIDFieldTitle); + } + + // Find out if we need to hide details for incoming servers + var hideIncoming = (gCurrentAccountData && gCurrentAccountData.wizardHideIncoming); + + var email = ""; + if (pageData.identity && pageData.identity.email) { + // fixup the email + email = pageData.identity.email.value; + if (email.split('@').length < 2 && + currentAccountData && + currentAccountData.domain) + email += "@" + currentAccountData.domain; + } + setDivTextFromForm("identity.email", email); + + var userName=""; + if (pageData.login && pageData.login.username) + userName = pageData.login.username.value; + if (!userName && email) { + if (currentAccountData && currentAccountData.incomingServerUserNameRequiresDomain == undefined) + currentAccountData.incomingServerUserNameRequiresDomain = false; + userName = getUsernameFromEmail(email, currentAccountData && + currentAccountData.incomingServerUserNameRequiresDomain); + } + // Hide the "username" field if we don't want to show information + // on the incoming server. + setDivTextFromForm("server.username", hideIncoming ? null : userName); + + var smtpUserName=""; + if (pageData.login && pageData.login.smtpusername) + smtpUserName = pageData.login.smtpusername.value; + if (!smtpUserName && email) + smtpUserName = getUsernameFromEmail(email, currentAccountData && + currentAccountData.smtpUserNameRequiresDomain); + setDivTextFromForm("smtpServer.username", smtpUserName); + + if (pageData.accname && pageData.accname.prettyName) { + // If currentAccountData has a pretty name (e.g. news link or from + // isp data) only set the user name with the pretty name if the + // pretty name originally came from ispdata + if ((currentAccountData && + currentAccountData.incomingServer.prettyName) && + (pageData.ispdata && + pageData.ispdata.supplied && + pageData.ispdata.supplied.value)) + { + // Get the polished account name - if the user has modified the + // account name then use that, otherwise use the userName. + pageData.accname.prettyName.value = + gPrefsBundle.getFormattedString("accountName", + [currentAccountData.incomingServer.prettyName, + (pageData.accname.userset && pageData.accname.userset.value) ? pageData.accname.prettyName.value : userName]); + } + // else just use the default supplied name + + setDivTextFromForm("account.name", pageData.accname.prettyName.value); + } else { + setDivTextFromForm("account.name", ""); + } + + // Show mail servers (incoming&outgoing) details + // based on current account data. ISP can set + // rdf value of literal showServerDetailsOnWizardSummary + // to false to hide server details + if (showMailServerDetails && !serverIsNntp(pageData)) { + var incomingServerName=""; + if (pageData.server && pageData.server.hostname) { + incomingServerName = pageData.server.hostname.value; + if (!incomingServerName && + currentAccountData && + currentAccountData.incomingServer.hostname) + incomingServerName = currentAccountData.incomingServer.hostName; + } + // Hide the incoming server name field if the user specified + // wizardHideIncoming in the ISP defaults file + setDivTextFromForm("server.name", hideIncoming ? null : incomingServerName); + setDivTextFromForm("server.port", pageData.server.port ? pageData.server.port.value : null); + var incomingServerType=""; + if (pageData.server && pageData.server.servertype) { + incomingServerType = pageData.server.servertype.value; + if (!incomingServerType && + currentAccountData && + currentAccountData.incomingServer.type) + incomingServerType = currentAccountData.incomingServer.type; + } + setDivTextFromForm("server.type", incomingServerType.toUpperCase()); + + var smtpServerName=""; + if (pageData.server && pageData.server.smtphostname) { + let smtpServer = MailServices.smtp.defaultServer; + smtpServerName = pageData.server.smtphostname.value; + if (!smtpServerName && smtpServer && smtpServer.hostname) + smtpServerName = smtpServer.hostname; + } + setDivTextFromForm("smtpServer.name", smtpServerName); + } + else { + setDivTextFromForm("server.name", null); + setDivTextFromForm("server.type", null); + setDivTextFromForm("server.port", null); + setDivTextFromForm("smtpServer.name", null); + } + + if (serverIsNntp(pageData)) { + var newsServerName=""; + if (pageData.newsserver && pageData.newsserver.hostname) + newsServerName = pageData.newsserver.hostname.value; + if (newsServerName) { + // No need to show username for news account + setDivTextFromForm("server.username", null); + } + setDivTextFromForm("newsServer.name", newsServerName); + setDivTextFromForm("server.port", null); + } + else { + setDivTextFromForm("newsServer.name", null); + } + + var isPop = false; + if (pageData.server && pageData.server.servertype) { + isPop = (pageData.server.servertype.value == "pop3"); + } + + hideShowDownloadMsgsUI(isPop); +} + +function hideShowDownloadMsgsUI(isPop) +{ + // only show the "download messages now" UI + // if this is a pop account, we are online, and this was opened + // from the 3 pane + var downloadMsgs = document.getElementById("downloadMsgs"); + if (isPop) { + if (!Services.io.offline) { + if (window.opener.location.href == "chrome://messenger/content/messenger.xul") { + downloadMsgs.hidden = false; + return; + } + } + } + + // else hide it + downloadMsgs.hidden = true; +} + +function setDivTextFromForm(divid, value) { + + // collapse the row if the div has no value + var div = document.getElementById(divid); + if (!value) { + div.setAttribute("collapsed","true"); + return; + } + else { + div.removeAttribute("collapsed"); + } + + // otherwise fill in the .text element + div = document.getElementById(divid+".text"); + if (!div) return; + + // set the value + div.setAttribute("value", value); +} + +function setUserNameDescField(name) +{ + if (name) { + var userNameField = document.getElementById("server.username.label"); + userNameField.setAttribute("value", name); + } +} diff --git a/mailnews/base/prefs/content/aw-identity.js b/mailnews/base/prefs/content/aw-identity.js new file mode 100644 index 000000000..ed99c4400 --- /dev/null +++ b/mailnews/base/prefs/content/aw-identity.js @@ -0,0 +1,212 @@ +/* -*- 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/. */ + +var gCurrentDomain; +var gPrefsBundle; + +function identityPageValidate() +{ + var canAdvance = false; + var name = document.getElementById("fullName").value; + let email = document.getElementById("email").value.trim(); + + if (name && email) { + canAdvance = gCurrentDomain ? emailNameIsLegal(email) : emailNameAndDomainAreLegal(email); + + if (gCurrentDomain && canAdvance) { + // For prefilled ISP data we must check if the account already exists as + // there is no second chance. The email is the username since the user + // fills in [______]@example.com. + var pageData = parent.GetPageData(); + var serverType = parent.getCurrentServerType(pageData); + var hostName = parent.getCurrentHostname(pageData); + var usernameWithDomain = email + "@" + gCurrentDomain; + if (parent.AccountExists(email, hostName, serverType) || + parent.AccountExists(usernameWithDomain, hostName, serverType)) + canAdvance = false; + } + } + + document.documentElement.canAdvance = canAdvance; +} + +function identityPageUnload() +{ + var pageData = parent.GetPageData(); + var name = document.getElementById("fullName").value; + let email = document.getElementById("email").value.trim(); + setPageData(pageData, "identity", "fullName", name); + setPageData(pageData, "identity", "email", email); + + return true; +} + +// This is for the case when the code appends the domain +// unnecessarily. +// This simply gets rid of "@domain" from "foo@domain" + +function fixPreFilledEmail() +{ + var emailElement = document.getElementById("email"); + var email = emailElement.value; + var emailArray = email.split('@'); + + if (gCurrentDomain) { + // check if user entered an @ sign even though we have a domain + if (emailArray.length >= 2) { + email = emailArray[0]; + emailElement.value = email; + } + } +} + +/** + * This function checks for common illegal characters. + * It shouldn't be too strict, since we do more extensive tests later. + */ +function emailNameIsLegal(aString) +{ + return aString && !/[^!-?A-~]/.test(aString); +} + +function emailNameAndDomainAreLegal(aString) +{ + return /^[!-?A-~]+\@[A-Za-z0-9.-]+$/.test(aString); +} + +function identityPageInit() +{ + gCurrentDomain = null; + gPrefsBundle = document.getElementById("bundle_prefs"); + clearEmailTextItems(); + setEmailDescriptionText(); + checkForDomain(); + checkForFullName(); + checkForEmail(); + fixPreFilledEmail(); + identityPageValidate(); +} + +function clearEmailTextItems() +{ + var emailDescText = document.getElementById("emailDescText"); + + if (emailDescText.hasChildNodes()) + emailDescText.firstChild.remove(); + + var postEmailText = document.getElementById("postEmailText"); + postEmailText.setAttribute("value", ""); + postEmailText.hidden = true; +} + +// Use email example data that ISP has provided. ISP data, if avaialble +// for the choice user has made, will be read into CurrentAccountData. +// Default example data from properties will be used when the info is missing. +function setEmailDescriptionText() +{ + var emailDescText = document.getElementById("emailDescText"); + var emailFieldLabel = document.getElementById("emailFieldLabel"); + var currentAccountData = parent.gCurrentAccountData; + + var displayText = null; + var emailFieldLabelData = null; + var setDefaultEmailDescStrings = true; + + // Set the default field label + emailFieldLabel.setAttribute("value", gPrefsBundle.getString("emailFieldText")); + + // Get values for customized data from current account + if (currentAccountData) + { + var emailProvider = currentAccountData.emailProviderName; + var sampleEmail = currentAccountData.sampleEmail; + var sampleUserName = currentAccountData.sampleUserName; + var emailIDDesc = currentAccountData.emailIDDescription; + var emailIDTitle = currentAccountData.emailIDFieldTitle; + + if (emailProvider && + sampleEmail && + sampleUserName && + emailIDDesc && + emailIDTitle) + { + // Get email description data + displayText = gPrefsBundle.getFormattedString("customizedEmailText", + [emailProvider, + emailIDDesc, + sampleEmail, + sampleUserName]); + + // Set emailfield label + emailFieldLabelData = emailIDTitle; + emailFieldLabel.setAttribute("value", emailFieldLabelData); + + // Need to display customized data. Turn off default settings. + setDefaultEmailDescStrings = false; + } + } + + if (setDefaultEmailDescStrings) + { + // Check for obtained values and set with default values if needed + var username = gPrefsBundle.getString("exampleEmailUserName"); + var domain = gPrefsBundle.getString("exampleEmailDomain"); + + displayText = gPrefsBundle.getFormattedString("defaultEmailText", + [username, domain]); + } + + // Create a text nodes with text to be displayed + var emailDescTextNode = document.createTextNode(displayText); + + // Display the dynamically generated text for email description + emailDescText.appendChild(emailDescTextNode); +} + +// retrieve the current domain from the parent wizard window, +// and update the UI to add the @domain static text +function checkForDomain() +{ + var accountData = parent.gCurrentAccountData; + if (!accountData || !accountData.domain) + return; + + // save in global variable + gCurrentDomain = accountData.domain; + var postEmailText = document.getElementById("postEmailText"); + postEmailText.setAttribute("value", "@" + gCurrentDomain); + postEmailText.hidden = false; +} + +function checkForFullName() { + var name = document.getElementById("fullName"); + if (name.value=="") { + try { + var userInfo = Components.classes["@mozilla.org/userinfo;1"].getService(Components.interfaces.nsIUserInfo); + name.value = userInfo.fullname; + } + catch (ex) { + // dump ("checkForFullName failed: " + ex + "\n"); + } + } +} + +function checkForEmail() +{ + var email = document.getElementById("email"); + var pageData = parent.GetPageData(); + if (pageData && pageData.identity && pageData.identity.email) { + email.value = pageData.identity.email.value; + } + if (email.value=="") { + try { + var userInfo = Components.classes["@mozilla.org/userinfo;1"].getService(Components.interfaces.nsIUserInfo); + email.value = userInfo.emailAddress; + } + catch (ex) { + // dump ("checkForEmail failed: " + ex + "\n"); + } + } +} diff --git a/mailnews/base/prefs/content/aw-incoming.js b/mailnews/base/prefs/content/aw-incoming.js new file mode 100644 index 000000000..fcc063fed --- /dev/null +++ b/mailnews/base/prefs/content/aw-incoming.js @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +var gOnMailServersPage; +var gOnNewsServerPage; +var gHideIncoming; +var gProtocolInfo = null; + +function incomingPageValidate() +{ + var canAdvance = true; + var hostName; + + if (gOnMailServersPage) { + hostName = document.getElementById("incomingServer").value; + if (!gHideIncoming && !isLegalHostNameOrIP(cleanUpHostName(hostName))) + canAdvance = false; + } + if (gOnNewsServerPage) { + hostName = document.getElementById("newsServer").value; + if (!isLegalHostNameOrIP(cleanUpHostName(hostName))) + canAdvance = false; + } + + if (canAdvance) { + var pageData = parent.GetPageData(); + var serverType = parent.getCurrentServerType(pageData); + var username = document.getElementById("username").value; + if (gProtocolInfo && gProtocolInfo.requiresUsername && !username || + parent.AccountExists(username, hostName, serverType)) + canAdvance = false; + } + + document.documentElement.canAdvance = canAdvance; +} + +function incomingPageUnload() +{ + var pageData = parent.GetPageData(); + + if (gOnMailServersPage) { + // If we have hidden the incoming server dialogs, we don't want + // to set the server to an empty value here + if (!gHideIncoming) { + var incomingServerName = document.getElementById("incomingServer"); + setPageData(pageData, "server", "hostname", cleanUpHostName(incomingServerName.value)); + } + var serverport = document.getElementById("serverPort").value; + setPageData(pageData, "server", "port", serverport); + var username = document.getElementById("username").value; + setPageData(pageData, "login", "username", username); + } + else if (gOnNewsServerPage) { + var newsServerName = document.getElementById("newsServer"); + setPageData(pageData, "newsserver", "hostname", cleanUpHostName(newsServerName.value)); + } + + return true; +} + +function incomingPageInit() { + gOnMailServersPage = (document.documentElement.currentPage.id == "incomingpage"); + gOnNewsServerPage = (document.documentElement.currentPage.id == "newsserver"); + if (gOnNewsServerPage) + { + var newsServer = document.getElementById("newsServer"); + var pageData = parent.GetPageData(); + try + { + newsServer.value = pageData.newsserver.hostname.value; + } + catch (ex){} + } + + gHideIncoming = false; + if (gCurrentAccountData && gCurrentAccountData.wizardHideIncoming) + gHideIncoming = true; + + var incomingServerbox = document.getElementById("incomingServerbox"); + var serverTypeBox = document.getElementById("serverTypeBox"); + if (incomingServerbox && serverTypeBox) { + if (gHideIncoming) { + incomingServerbox.setAttribute("hidden", "true"); + serverTypeBox.setAttribute("hidden", "true"); + } + else { + incomingServerbox.removeAttribute("hidden"); + serverTypeBox.removeAttribute("hidden"); + } + } + + // Server type selection (pop3 or imap) is for mail accounts only + var pageData = parent.GetPageData(); + var isMailAccount = pageData.accounttype.mailaccount.value; + var isOtherAccount = pageData.accounttype.otheraccount.value; + if (isMailAccount && !gHideIncoming) { + var serverTypeRadioGroup = document.getElementById("servertype"); + /* + * Check to see if the radiogroup has any value. If there is no + * value, this must be the first time user visting this page in the + * account setup process. So, the default is set to pop3. If there + * is a value (it's used automatically), user has already visited + * page and server type selection is done. Once user visits the page, + * the server type value from then on will persist (whether the selection + * came from the default or the user action). + */ + if (!serverTypeRadioGroup.value) { + /* + * if server type was set to imap in isp data, then + * we preset the server type radio group accordingly, + * otherwise, use pop3 as the default. + */ + var serverTypeRadioItem = document.getElementById(pageData.server && + pageData.server.servertype && pageData.server.servertype.value == "imap" ? + "imap" : "pop3"); + serverTypeRadioGroup.selectedItem = serverTypeRadioItem; // Set pop3 server type as default selection + } + var leaveMessages = document.getElementById("leaveMessagesOnServer"); + var deferStorage = document.getElementById("deferStorage"); + setServerType(); + setServerPrefs(leaveMessages); + setServerPrefs(deferStorage); + } + else if (isOtherAccount) { + document.getElementById("deferStorageBox").hidden = true; + } + + if (pageData.server && pageData.server.hostname) { + var incomingServerTextBox = document.getElementById("incomingServer"); + if (incomingServerTextBox && incomingServerTextBox.value == "") + incomingServerTextBox.value = pageData.server.hostname.value; + } + + // pageData.server is not a real nsMsgIncomingServer so it does not have + // protocolInfo property implemented. + let type = parent.getCurrentServerType(pageData); + gProtocolInfo = Components.classes["@mozilla.org/messenger/protocol/info;1?type=" + type] + .getService(Components.interfaces.nsIMsgProtocolInfo); + var loginNameInput = document.getElementById("username"); + + if (loginNameInput.value == "") { + if (gProtocolInfo.requiresUsername) { + // since we require a username, use the uid from the email address + loginNameInput.value = parent.getUsernameFromEmail(pageData.identity.email.value, gCurrentAccountData && + gCurrentAccountData.incomingServerUserNameRequiresDomain); + } + } + incomingPageValidate(); +} + +function setServerType() +{ + var pageData = parent.GetPageData(); + var serverType = document.getElementById("servertype").value; + var deferStorageBox = document.getElementById("deferStorageBox"); + var leaveMessages = document.getElementById("leaveMsgsOnSrvrBox"); + var port = serverType == "pop3" ? 110 : 143; + + document.getElementById("serverPort").value = port; + document.getElementById("defaultPortValue").value = port; + + deferStorageBox.hidden = serverType == "imap"; + leaveMessages.hidden = serverType == "imap"; + setPageData(pageData, "server", "servertype", serverType); + setPageData(pageData, "server", "port", port); + incomingPageValidate(); +} + +function setServerPrefs(aThis) +{ + setPageData(parent.GetPageData(), "server", aThis.id, aThis.checked); +} diff --git a/mailnews/base/prefs/content/aw-outgoing.js b/mailnews/base/prefs/content/aw-outgoing.js new file mode 100644 index 000000000..8caffc274 --- /dev/null +++ b/mailnews/base/prefs/content/aw-outgoing.js @@ -0,0 +1,151 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/hostnameUtils.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +var gProtocolInfo = null; +var gPrefsBundle; + +function outgoingPageValidate() { + let canAdvance = true; + + let smtpServer = document.getElementById("smtphostname").value; + let usingDefaultSMTP = document.getElementById("noSmtp").hidden; + if (!usingDefaultSMTP && !isLegalHostNameOrIP(cleanUpHostName(smtpServer))) + canAdvance = false; + + document.documentElement.canAdvance = canAdvance; +} + +function outgoingPageUnload() { + var pageData = parent.GetPageData(); + var username = document.getElementById("username").value; + let smtpserver = document.getElementById("smtphostname").value; + setPageData(pageData, "server", "smtphostname", cleanUpHostName(smtpserver)); + + // If SMTP username box is blank it is because the + // incoming and outgoing server names were the same, + // so set to be same as incoming username + var smtpusername = document.getElementById("smtpusername").value || username; + + setPageData(pageData, "login", "smtpusername", smtpusername); + + return true; +} + +function outgoingPageInit() { + gPrefsBundle = document.getElementById("bundle_prefs"); + var pageData = parent.GetPageData(); + + var smtpServer = null; + var smtpCreateNewServer = gCurrentAccountData && gCurrentAccountData.smtpCreateNewServer; + + // Don't use the default smtp server if smtp server creation was explicitly + // requested in isp rdf. + // If we're reusing the default smtp we should not set the smtp hostname. + if (MailServices.smtp.defaultServer && !smtpCreateNewServer) { + smtpServer = MailServices.smtp.defaultServer; + setPageData(pageData, "identity", "smtpServerKey", ""); + } + + var noSmtpBox = document.getElementById("noSmtp"); + var haveSmtpBox = document.getElementById("haveSmtp"); + + var boxToHide; + var boxToShow; + + if (pageData.server && pageData.server.smtphostname && smtpCreateNewServer) { + var smtpTextBox = document.getElementById("smtphostname"); + if (smtpTextBox && smtpTextBox.value == "") + smtpTextBox.value = pageData.server.smtphostname.value; + } + + if (smtpServer && smtpServer.hostname) { + // we have a hostname, so modify and show the static text and + // store the value of the default smtp server in the textbox. + modifyStaticText(smtpServer.hostname, "1") + boxToShow = haveSmtpBox; + boxToHide = noSmtpBox; + } + else { + // no default hostname yet + boxToShow = noSmtpBox; + boxToHide = haveSmtpBox; + } + + if (boxToHide) + boxToHide.setAttribute("hidden", "true"); + + if (boxToShow) + boxToShow.removeAttribute("hidden"); + + var smtpNameInput = document.getElementById("smtpusername"); + smtpServer = MailServices.smtp.defaultServer; + if (smtpServer && smtpServer.hostname && smtpServer.username) { + // we have a default SMTP server, so modify and show the static text + // and store the username for the default server in the textbox. + modifyStaticText(smtpServer.username, "2") + hideShowLoginSettings(2, 1, 3); + smtpNameInput.value = smtpServer.username; + } + else { + // no default SMTP server yet, so need to compare + // incoming and outgoing server names + var smtpServerName = pageData.server.smtphostname.value; + var incomingServerName = pageData.server.hostname.value; + if (smtpServerName == incomingServerName) { + // incoming and outgoing server names are the same, so show + // the static text and make sure textbox blank for later tests. + modifyStaticText(smtpServerName, "3") + hideShowLoginSettings(3, 1, 2); + smtpNameInput.value = ""; + } + else { + // incoming and outgoing server names are different, so set smtp + // username's textbox to be the same as incoming's one, unless already set. + hideShowLoginSettings(1, 2, 3); + smtpNameInput.value = smtpNameInput.value || loginNameInput.value; + } + } + outgoingPageValidate(); +} + +function modifyStaticText(smtpMod, smtpBox) +{ + // modify the value in the smtp display if we already have a + // smtp server so that the single string displays the hostname + // or username for the smtp server. + var smtpStatic = document.getElementById("smtpStaticText"+smtpBox); + if (smtpStatic && smtpStatic.hasChildNodes()) + smtpStatic.childNodes[0].nodeValue = smtpStatic.getAttribute("prefix") + + smtpMod + smtpStatic.getAttribute("suffix"); +} + +function hideShowLoginSettings(aEle, bEle, cEle) +{ + document.getElementById("loginSet" + aEle).hidden = false; + document.getElementById("loginSet" + bEle).hidden = true; + document.getElementById("loginSet" + cEle).hidden = true; +} + +var savedPassword=""; + +function onSavePassword(target) { + dump("savePassword changed! (" + target.checked + ")\n"); + var passwordField = document.getElementById("server.password"); + if (!passwordField) return; + + if (target.checked) { + passwordField.removeAttribute("disabled"); + passwordField.value = savedPassword; + } + else { + passwordField.setAttribute("disabled", "true"); + savedPassword = passwordField.value; + passwordField.value = ""; + } + +} diff --git a/mailnews/base/prefs/content/ispUtils.js b/mailnews/base/prefs/content/ispUtils.js new file mode 100644 index 000000000..7735d8a47 --- /dev/null +++ b/mailnews/base/prefs/content/ispUtils.js @@ -0,0 +1,166 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +// using the rdf service extensively here +var rdf = Components.classes["@mozilla.org/rdf/rdf-service;1"].getService(Components.interfaces.nsIRDFService); + +// all the RDF resources we'll be retrieving +var NC = "http://home.netscape.com/NC-rdf#"; +var Server = rdf.GetResource(NC + "Server"); +var SmtpServer = rdf.GetResource(NC + "SmtpServer"); +var ServerHost = rdf.GetResource(NC + "ServerHost"); +var ServerType = rdf.GetResource(NC + "ServerType"); +var PrefixIsUsername = rdf.GetResource(NC + "PrefixIsUsername"); +var UseAuthenticatedSmtp= rdf.GetResource(NC + "UseAuthenticatedSmtp"); + +// this is possibly expensive, not sure what to do here +var ispDefaults; + +var nsIRDFResource = Components.interfaces.nsIRDFResource; +var nsIRDFLiteral = Components.interfaces.nsIRDFLiteral; + +var ispRoot = rdf.GetResource("NC:ispinfo"); + +// given an ISP's domain URI, look up all relevant information about it +function getIspDefaultsForUri(domainURI) +{ + if (!ispDefaults) + ispDefaults = rdf.GetDataSource("rdf:ispdefaults"); + + var domainRes = rdf.GetResource(domainURI); + + var result = dataSourceToObject(ispDefaults, domainRes); + + if (!result) return null; + + // The domainURI should be in the format domain:example.com. (Where + // example.com is the domain name to use for all email addresses). If + // it does not match this pattern, then it is possible no domain + // has been specified, so we should leave it uninitialized. + if (domainURI.startsWith("domain:")) { + // add this extra attribute which is the domain itself + var domainData = domainURI.split(':'); + if (domainData.length > 1) { + // To faciltate distributing two different account types for one ISP, + // it's possible to add parameters to the domain URI + // - e.g. domain:example.com?type=imap. + // This is necessary so RDF doesn't think they're the same. + + // Save the domain, but only the part up to the (possible) question mark. + result.domain = domainData[1].replace(/\?.*/, ""); + } + } + return result; +} + +// construct an ISP's domain URI based on it's domain +// (i.e. turns example.com -> domain:example.com) +function getIspDefaultsForDomain(domain) { + let domainURI = "domain:" + domain; + return getIspDefaultsForUri(domainURI); +} + +// Given an email address (like "joe@example.com") look up +function getIspDefaultsForEmail(email) { + + var emailData = getEmailInfo(email); + + var ispData = null; + if (emailData) + ispData = getIspDefaultsForDomain(emailData.domain); + + prefillIspData(ispData, email); + + return ispData; +} + +// given an email address, split it into username and domain +// return in an associative array +function getEmailInfo(email) { + if (!email) return null; + + var result = new Object; + + var emailData = email.split('@'); + + if (emailData.length != 2) { + dump("bad e-mail address!\n"); + return null; + } + + // all the variables we'll be returning + result.username = emailData[0]; + result.domain = emailData[1]; + + return result; +} + +function prefillIspData(ispData, email, fullName) { + if (!ispData) return; + + // make sure these objects exist + if (!ispData.identity) ispData.identity = new Object; + if (!ispData.incomingServer) ispData.incomingServer = new Object; + + // fill in e-mail if it's not already there + if (email && !ispData.identity.email) + ispData.identity.email = email; + + var emailData = getEmailInfo(email); + if (emailData) { + + // fill in the username (assuming the ISP doesn't prevent it) + if (!ispData.incomingServer.userName && + !ispData.incomingServer.noDefaultUsername) + ispData.incomingServer.username = emailData.username; + } +} + +// this function will extract an entire datasource into a giant +// associative array for easy retrieval from JS +var NClength = NC.length; +function dataSourceToObject(datasource, root) +{ + var result = null; + var arcs = datasource.ArcLabelsOut(root); + + while (arcs.hasMoreElements()) { + var arc = arcs.getNext().QueryInterface(nsIRDFResource); + + var arcName = arc.Value; + arcName = arcName.substring(NClength, arcName.length); + + if (!result) result = new Object; + + var target = datasource.GetTarget(root, arc, true); + + var value; + var targetHasChildren = false; + try { + target = target.QueryInterface(nsIRDFResource); + targetHasChildren = true; + } catch (ex) { + target = target.QueryInterface(nsIRDFLiteral); + } + + if (targetHasChildren) + value = dataSourceToObject(datasource, target); + else { + value = target.Value; + + // fixup booleans/numbers/etc + if (value == "true") value = true; + else if (value == "false") value = false; + else { + var num = Number(value); + if (!isNaN(num)) value = num; + } + } + + // add this value + result[arcName] = value; + } + return result; +} diff --git a/mailnews/base/prefs/content/removeAccount.js b/mailnews/base/prefs/content/removeAccount.js new file mode 100644 index 000000000..77bbe58d5 --- /dev/null +++ b/mailnews/base/prefs/content/removeAccount.js @@ -0,0 +1,148 @@ +/* -*- Mode: Java; tab-width: 2; 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/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +var gServer; +var gDialog; + +function onLoad(event) { + gServer = window.arguments[0].account.incomingServer; + gDialog = document.documentElement; + + let bundle = document.getElementById("bundle_removeAccount"); + let removeQuestion = bundle.getFormattedString("removeQuestion", + [gServer.prettyName]); + document.getElementById("accountName").textContent = removeQuestion; + + // Allow to remove account data if it has a local storage. + let localDirectory = gServer.localPath; + if (localDirectory && localDirectory.exists()) { + localDirectory.normalize(); + + // Do not allow removal if localPath is outside of profile folder. + let profilePath = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile); + profilePath.normalize(); + + // TODO: bug 77652, decide what to do for deferred accounts. + // And inform the user if the account localPath is outside the profile. + if ((gServer.isDeferredTo || + (gServer instanceof Components.interfaces.nsIPop3IncomingServer && + gServer.deferredToAccount)) || + !profilePath.contains(localDirectory)) { + document.getElementById("removeData").disabled = true; + } + } else { + document.getElementById("removeDataPossibility").collapsed = true; + } + + enableRemove(); + window.sizeToContent(); +} + +function enableRemove() { + gDialog.getButton("accept").disabled = + (!document.getElementById("removeAccount").checked && + !document.getElementById("removeData").checked); +} + +/** + * Show the local directory. + */ +function openLocalDirectory() { + let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath"); + let localDir = gServer.localPath.path; + try { + new nsLocalFile(localDir).reveal(); + } catch(e) { + // Reveal may fail e.g. on Linux, then just show the path as a string. + document.getElementById("localDirectory").value = localDir; + document.getElementById("localDirectory").collapsed = false; + } +} + +function showInfo() { + let descs = document.querySelectorAll("vbox.indent"); + for (let desc of descs) { + desc.collapsed = false; + } + + // TODO: bug 1238271, this should use showFor attributes if possible. + if (gServer.type == "imap" || gServer.type == "nntp") { + document.getElementById("serverAccount").collapsed = false; + } else { + document.getElementById("localAccount").collapsed = false; + } + + window.sizeToContent(); + gDialog.getButton("disclosure").disabled = true; + gDialog.getButton("disclosure").blur(); +} + +function removeAccount() { + let removeAccount = document.getElementById("removeAccount").checked; + let removeData = document.getElementById("removeData").checked; + try { + // Remove the requested account data. + if (removeAccount) { + // Remove account + // Need to save these before the account and its server is removed. + let serverUri = gServer.type + "://" + gServer.hostName; + let serverUsername = gServer.username; + + MailServices.accounts.removeAccount(window.arguments[0].account, removeData); + window.arguments[0].result = true; + + // Remove password information. + let logins = Services.logins.findLogins({}, serverUri, null, serverUri); + for (let i = 0; i < logins.length; i++) { + if (logins[i].username == serverUsername) { + Services.logins.removeLogin(logins[i]); + break; + } + } + } else if (removeData) { + // Remove files only. + // TODO: bug 1302193 + window.arguments[0].result = false; + } + + document.getElementById("status").selectedPanel = + document.getElementById("success"); + } catch (ex) { + document.getElementById("status").selectedPanel = + document.getElementById("failure"); + Components.utils.reportError("Failure to remove account: " + ex); + window.arguments[0].result = false; + } +} + +function onAccept() { + // If Cancel is disabled, we already tried to remove the account + // and can only close the dialog. + if (gDialog.getButton("cancel").disabled) + return true; + + gDialog.getButton("accept").disabled = true; + gDialog.getButton("cancel").disabled = true; + gDialog.getButton("disclosure").disabled = true; + + // Change the "Remove" to an "OK" button by clearing the custom label. + gDialog.removeAttribute("buttonlabelaccept"); + gDialog.removeAttribute("buttonaccesskeyaccept"); + gDialog.getButton("accept").removeAttribute("label"); + gDialog.getButton("accept").removeAttribute("accesskey"); + gDialog.buttons = "accept"; + + document.getElementById("infoPane").selectedIndex = 1; + window.sizeToContent(); + + removeAccount(); + + gDialog.getButton("accept").disabled = false; + return false; +} diff --git a/mailnews/base/prefs/content/removeAccount.xul b/mailnews/base/prefs/content/removeAccount.xul new file mode 100644 index 000000000..c1d565973 --- /dev/null +++ b/mailnews/base/prefs/content/removeAccount.xul @@ -0,0 +1,82 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/dialogs.css" type="text/css"?> + +<!DOCTYPE dialog [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % removalDTD SYSTEM "chrome://messenger/locale/removeAccount.dtd"> +%removalDTD; +]> + +<dialog id="removeAccountDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&dialogTitle;" + width="600" + buttons="accept,disclosure,cancel" + buttonlabelaccept="&removeButton.label;" + buttonaccesskeyaccept="&removeButton.accesskey;" + defaultButton="cancel" + onload="onLoad();" + ondialogdisclosure="showInfo();" + ondialogaccept="return onAccept();"> + <stringbundle id="bundle_removeAccount" + src="chrome://messenger/locale/removeAccount.properties"/> + <script type="application/javascript" + src="chrome://messenger/content/removeAccount.js"/> + <deck id="infoPane"> + <vbox flex="1"> + <label id="accountName"></label> + <separator class="thin"/> + <checkbox id="removeAccount" + label="&removeAccount.label;" + checked="true" + disabled="true" + accesskey="&removeAccount.accesskey;" + oncommand="enableRemove();"/> + <vbox class="indent" collapsed="true"> + <description> + &removeAccount.desc; + </description> + </vbox> + <vbox id="removeDataPossibility" collapsed="false"> + <checkbox id="removeData" + label="&removeData.label;" + checked="false" + accesskey="&removeData.accesskey;" + oncommand="enableRemove();"/> + <vbox class="indent" collapsed="true"> + <description id="localAccount" collapsed="true"> + &removeDataLocalAccount.desc; + </description> + <description id="serverAccount" collapsed="true"> + &removeDataServerAccount.desc; + </description> + <hbox align="center"> + <button id="showLocalDirectory" + label="&showData.label;" + accesskey="&showData.accesskey;" + oncommand="openLocalDirectory();"/> + <label id="localDirectory" collapsed="true"/> + </hbox> + </vbox> + </vbox> + </vbox> + <vbox align="center"> + <spacer flex="1"/> + <deck id="status"> + <vbox align="center"> + <label>&progressPending;</label> + <progressmeter mode="undetermined"/> + </vbox> + <label id="success">&progressSuccess;</label> + <label id="failure">&progressFailure;</label> + </deck> + <spacer flex="1"/> + </vbox> + </deck> +</dialog> diff --git a/mailnews/base/prefs/content/smtpEditOverlay.js b/mailnews/base/prefs/content/smtpEditOverlay.js new file mode 100644 index 000000000..d8d216065 --- /dev/null +++ b/mailnews/base/prefs/content/smtpEditOverlay.js @@ -0,0 +1,184 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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/. */ + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/mailServices.js"); + +// be real hacky with document.getElementById until document.controls works +// with the new XUL widgets + +var gSmtpUsername; +var gSmtpDescription; +var gSmtpUsernameLabel; +var gSmtpHostname; +var gSmtpPort; +var gSmtpAuthMethod; +var gSmtpSocketType; +var gPort; +var gDefaultPort; +var Ci = Components.interfaces; + +function initSmtpSettings(server) { + gSmtpUsername = document.getElementById("smtp.username"); + gSmtpDescription = document.getElementById("smtp.description"); + gSmtpUsernameLabel = document.getElementById("smtp.username.label"); + gSmtpHostname = document.getElementById("smtp.hostname"); + gSmtpPort = document.getElementById("smtp.port"); + gSmtpAuthMethod = document.getElementById("smtp.authMethod"); + gSmtpSocketType = document.getElementById("smtp.socketType"); + gDefaultPort = document.getElementById("smtp.defaultPort"); + gPort = document.getElementById("smtp.port"); + + if (server) { + gSmtpHostname.value = server.hostname; + gSmtpDescription.value = server.description; + gSmtpPort.value = server.port; + gSmtpUsername.value = server.username; + gSmtpAuthMethod.value = server.authMethod; + gSmtpSocketType.value = (server.socketType < 4) ? server.socketType : 1; + } else { + // New server, load default values. + gSmtpAuthMethod.value = Services.prefs.getIntPref("mail.smtpserver.default.authMethod"); + gSmtpSocketType.value = Services.prefs.getIntPref("mail.smtpserver.default.try_ssl"); + } + + // Although sslChanged will set a label for cleartext password, + // we need to use the long label so that we can size the dialog. + setLabelFromStringBundle("authMethod-no", "authNo"); + setLabelFromStringBundle("authMethod-password-encrypted", + "authPasswordEncrypted"); + setLabelFromStringBundle("authMethod-password-cleartext", + "authPasswordCleartextInsecurely"); + setLabelFromStringBundle("authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("authMethod-ntlm", "authNTLM"); +#ifdef MOZ_MAILNEWS_OAUTH2 + setLabelFromStringBundle("authMethod-oauth2", "authOAuth2"); +#endif + setLabelFromStringBundle("authMethod-anysecure", "authAnySecure"); + setLabelFromStringBundle("authMethod-any", "authAny"); + + sizeToContent(); + + sslChanged(false); + authMethodChanged(false); + + if (MailServices.smtp.defaultServer) + onLockPreference(); + + // Hide deprecated/hidden auth options, unless selected + hideUnlessSelected(document.getElementById("authMethod-anysecure")); + hideUnlessSelected(document.getElementById("authMethod-any")); + + // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't + // allow users to choose it anymore. Hide the option unless the user already + // has it set. + hideUnlessSelected(document.getElementById("connectionSecurityType-1")); +} + +function hideUnlessSelected(element) +{ + element.hidden = !element.selected; +} + +function setLabelFromStringBundle(elementID, stringName) +{ + document.getElementById(elementID).label = + document.getElementById("bundle_messenger").getString(stringName); +} + +// Disables xul elements that have associated preferences locked. +function onLockPreference() +{ + try { + let allPrefElements = { + hostname: gSmtpHostname, + description: gSmtpDescription, + port: gSmtpPort, + authMethod: gSmtpAuthMethod, + try_ssl: gSmtpSocketType + }; + disableIfLocked(allPrefElements); + } catch (e) { // non-fatal + Components.utils.reportError("Error while getting locked prefs: " + e); + } +} + +/** + * Does the work of disabling an element given the array which contains xul id/prefstring pairs. + * + * @param prefstrArray array of XUL elements to check + * + * TODO: try to merge this with disableIfLocked function in am-offline.js (bug 755885) + */ +function disableIfLocked(prefstrArray) +{ + let finalPrefString = "mail.smtpserver." + + MailServices.smtp.defaultServer.key + "."; + let smtpPrefBranch = Services.prefs.getBranch(finalPrefString); + + for (let prefstring in prefstrArray) + if (smtpPrefBranch.prefIsLocked(prefstring)) + prefstrArray[prefstring].disabled = true; +} + +function saveSmtpSettings(server) +{ + //dump("Saving to " + server + "\n"); + if (server) { + server.hostname = cleanUpHostName(gSmtpHostname.value); + server.description = gSmtpDescription.value; + server.port = gSmtpPort.value; + server.authMethod = gSmtpAuthMethod.value; + server.username = gSmtpUsername.value; + server.socketType = gSmtpSocketType.value; + } +} + +function authMethodChanged(userAction) +{ + var noUsername = gSmtpAuthMethod.value == Ci.nsMsgAuthMethod.none; + gSmtpUsername.disabled = noUsername; + gSmtpUsernameLabel.disabled = noUsername; +} + +/** + * Resets the default port to SMTP or SMTPS, dependending on + * the |gSmtpSocketType| value, and sets the port to use to this default, + * if that's appropriate. + * + * @param userAction false for dialog initialization, + * true for user action. + */ +function sslChanged(userAction) +{ + const DEFAULT_SMTP_PORT = "587"; + const DEFAULT_SMTPS_PORT = "465"; + var socketType = gSmtpSocketType.value; + var otherDefaultPort; + var prevDefaultPort = gDefaultPort.value; + + if (socketType == Ci.nsMsgSocketType.SSL) { + gDefaultPort.value = DEFAULT_SMTPS_PORT; + otherDefaultPort = DEFAULT_SMTP_PORT; + } else { + gDefaultPort.value = DEFAULT_SMTP_PORT; + otherDefaultPort = DEFAULT_SMTPS_PORT; + } + + // If the port is not set, + // or the user is causing the default port to change, + // and the port is set to the default for the other protocol, + // then set the port to the default for the new protocol. + if ((gPort.value == 0) || + (userAction && (gDefaultPort.value != prevDefaultPort) && + (gPort.value == otherDefaultPort))) + gPort.value = gDefaultPort.value; + + // switch "insecure password" label + setLabelFromStringBundle("authMethod-password-cleartext", + socketType == Ci.nsMsgSocketType.SSL || + socketType == Ci.nsMsgSocketType.alwaysSTARTTLS ? + "authPasswordCleartextViaSSL" : "authPasswordCleartextInsecurely"); +} diff --git a/mailnews/base/prefs/content/smtpEditOverlay.xul b/mailnews/base/prefs/content/smtpEditOverlay.xul new file mode 100644 index 000000000..0bc900a3d --- /dev/null +++ b/mailnews/base/prefs/content/smtpEditOverlay.xul @@ -0,0 +1,126 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/smtpEditOverlay.dtd"> + +<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://messenger/content/smtpEditOverlay.js"/> + + <vbox id="smtpServerEditor"> + <groupbox> + <caption label="&settings.caption;"/> + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&serverDescription.label;" + accesskey="&serverDescription.accesskey;" + control="smtp.description"/> + <textbox id="smtp.description" + flex="1" + preftype="string" + prefstring="mail.smtpserver.%serverkey%.description"/> + </row> + <row align="center"> + <label value="&serverName.label;" + accesskey="&serverName.accesskey;" + control="smtp.hostname"/> + <textbox id="smtp.hostname" + flex="1" + preftype="string" + class="uri-element" + prefstring="mail.smtpserver.%serverkey%.hostname"/> + </row> + <row align="center"> + <label value="&serverPort.label;" + accesskey="&serverPort.accesskey;" + control="smtp.port"/> + <hbox align="center"> + <textbox id="smtp.port" + type="number" + min="0" + max="65535" + size="5" + preftype="int" + prefstring="mail.smtpserver.%serverkey%.port"/> + <label value="&serverPortDefault.label;"/> + <label id="smtp.defaultPort"/> + </hbox> + </row> + </rows> + </grid> + </groupbox> + + <separator class="thin"/> + + <groupbox> + <caption label="&security.caption;"/> + + <grid flex="1"> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label value="&connectionSecurity.label;" + accesskey="&connectionSecurity.accesskey;" + control="smtp.socketType"/> + <menulist id="smtp.socketType" oncommand="sslChanged(true);" + prefstring="mail.smtpserver.%serverkey%.try_ssl"> + <menupopup id="smtp.socketTypePopup"> + <menuitem value="0" label="&connectionSecurityType-0.label;"/> + <menuitem id="connectionSecurityType-1" + value="1" label="&connectionSecurityType-1.label;" + disabled="true" hidden="true"/> + <menuitem value="2" label="&connectionSecurityType-2.label;"/> + <menuitem value="3" label="&connectionSecurityType-3.label;"/> + </menupopup> + </menulist> + </row> + <row align="center"> + <label value="&authMethod.label;" + accesskey="&authMethod.accesskey;" + control="server.authMethod"/> + <!-- same in am-server.xul/js --> + <menulist id="smtp.authMethod" + oncommand="authMethodChanged(true);" + wsm_persist="true" + preftype="int" type="number" + prefstring="mail.smtpserver.%serverkey%.authMethod"> + <menupopup id="smtp.authMethodPopup"> + <menuitem id="authMethod-no" value="1"/> + <menuitem id="authMethod-password-cleartext" value="3"/> + <menuitem id="authMethod-password-encrypted" value="4"/> + <menuitem id="authMethod-kerberos" value="5"/> + <menuitem id="authMethod-ntlm" value="6"/> +#ifdef MOZ_MAILNEWS_OAUTH2 + <menuitem id="authMethod-oauth2" value="10"/> +#endif + <menuitem id="authMethod-anysecure" value="8"/> + <menuitem id="authMethod-any" value="9"/> + </menupopup> + </menulist> + </row> + <row id="smtp.username.box" align="center"> + <label id="smtp.username.label" value="&userName.label;" + accesskey="&userName.accesskey;" + control="smtp.username"/> + <textbox id="smtp.username" flex="1" + preftype="string" + prefstring="mail.smtpserver.%serverkey%.username"/> + </row> + </rows> + </grid> + </groupbox> + </vbox> +</overlay> |