From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/base/content/.eslintrc.js | 5 + comm/mailnews/base/content/dateFormat.js | 227 ++++ comm/mailnews/base/content/folder-menupopup.js | 1238 ++++++++++++++++++++ comm/mailnews/base/content/folderProps.js | 480 ++++++++ comm/mailnews/base/content/folderProps.xhtml | 338 ++++++ comm/mailnews/base/content/jsTreeView.js | 239 ++++ comm/mailnews/base/content/junkCommands.js | 449 +++++++ comm/mailnews/base/content/junkLog.js | 48 + comm/mailnews/base/content/junkLog.xhtml | 57 + comm/mailnews/base/content/markByDate.js | 120 ++ comm/mailnews/base/content/markByDate.xhtml | 79 ++ .../base/content/menulist-charsetpicker.js | 86 ++ comm/mailnews/base/content/msgAccountCentral.js | 238 ++++ comm/mailnews/base/content/msgAccountCentral.xhtml | 309 +++++ .../base/content/msgSelectOfflineFolders.js | 189 +++ .../base/content/msgSelectOfflineFolders.xhtml | 89 ++ comm/mailnews/base/content/msgSynchronize.js | 192 +++ comm/mailnews/base/content/msgSynchronize.xhtml | 76 ++ comm/mailnews/base/content/newFolderDialog.js | 82 ++ comm/mailnews/base/content/newFolderDialog.xhtml | 107 ++ comm/mailnews/base/content/newmailalert.js | 109 ++ comm/mailnews/base/content/newmailalert.xhtml | 55 + comm/mailnews/base/content/newsError.js | 48 + comm/mailnews/base/content/newsError.xhtml | 57 + comm/mailnews/base/content/renameFolderDialog.js | 43 + .../mailnews/base/content/renameFolderDialog.xhtml | 63 + comm/mailnews/base/content/retention.js | 52 + comm/mailnews/base/content/shutdownWindow.js | 97 ++ comm/mailnews/base/content/shutdownWindow.xhtml | 53 + comm/mailnews/base/content/subscribe.js | 496 ++++++++ comm/mailnews/base/content/subscribe.xhtml | 235 ++++ .../mailnews/base/content/virtualFolderListEdit.js | 206 ++++ .../base/content/virtualFolderListEdit.xhtml | 84 ++ .../base/content/virtualFolderProperties.js | 383 ++++++ .../base/content/virtualFolderProperties.xhtml | 110 ++ 35 files changed, 6739 insertions(+) create mode 100644 comm/mailnews/base/content/.eslintrc.js create mode 100644 comm/mailnews/base/content/dateFormat.js create mode 100644 comm/mailnews/base/content/folder-menupopup.js create mode 100644 comm/mailnews/base/content/folderProps.js create mode 100644 comm/mailnews/base/content/folderProps.xhtml create mode 100644 comm/mailnews/base/content/jsTreeView.js create mode 100644 comm/mailnews/base/content/junkCommands.js create mode 100644 comm/mailnews/base/content/junkLog.js create mode 100644 comm/mailnews/base/content/junkLog.xhtml create mode 100644 comm/mailnews/base/content/markByDate.js create mode 100644 comm/mailnews/base/content/markByDate.xhtml create mode 100644 comm/mailnews/base/content/menulist-charsetpicker.js create mode 100644 comm/mailnews/base/content/msgAccountCentral.js create mode 100644 comm/mailnews/base/content/msgAccountCentral.xhtml create mode 100644 comm/mailnews/base/content/msgSelectOfflineFolders.js create mode 100644 comm/mailnews/base/content/msgSelectOfflineFolders.xhtml create mode 100644 comm/mailnews/base/content/msgSynchronize.js create mode 100644 comm/mailnews/base/content/msgSynchronize.xhtml create mode 100644 comm/mailnews/base/content/newFolderDialog.js create mode 100644 comm/mailnews/base/content/newFolderDialog.xhtml create mode 100644 comm/mailnews/base/content/newmailalert.js create mode 100644 comm/mailnews/base/content/newmailalert.xhtml create mode 100644 comm/mailnews/base/content/newsError.js create mode 100644 comm/mailnews/base/content/newsError.xhtml create mode 100644 comm/mailnews/base/content/renameFolderDialog.js create mode 100644 comm/mailnews/base/content/renameFolderDialog.xhtml create mode 100644 comm/mailnews/base/content/retention.js create mode 100644 comm/mailnews/base/content/shutdownWindow.js create mode 100644 comm/mailnews/base/content/shutdownWindow.xhtml create mode 100644 comm/mailnews/base/content/subscribe.js create mode 100644 comm/mailnews/base/content/subscribe.xhtml create mode 100644 comm/mailnews/base/content/virtualFolderListEdit.js create mode 100644 comm/mailnews/base/content/virtualFolderListEdit.xhtml create mode 100644 comm/mailnews/base/content/virtualFolderProperties.js create mode 100644 comm/mailnews/base/content/virtualFolderProperties.xhtml (limited to 'comm/mailnews/base/content') diff --git a/comm/mailnews/base/content/.eslintrc.js b/comm/mailnews/base/content/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/base/content/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/base/content/dateFormat.js b/comm/mailnews/base/content/dateFormat.js new file mode 100644 index 0000000000..600aa873fa --- /dev/null +++ b/comm/mailnews/base/content/dateFormat.js @@ -0,0 +1,227 @@ +/* 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/. */ + +/* Utilities to show and parse user-entered date values used in filter and search rules. */ + +"use strict"; + +const formatYMD = 1; +const formatYDM = 2; +const formatMDY = 3; +const formatMYD = 4; +const formatDMY = 5; +const formatDYM = 6; +const formatMIN = 1; +const formatMAX = 6; + +var gSearchDateFormat = 0; +var gSearchDateSeparator; +var gSearchDateLeadingZeros; + +/** + * Get the short date format option of the current locale. + * This supports the common case which the date separator is + * either '/', '-', '.' and using Christian year. + */ +function initLocaleShortDateFormat() { + try { + const dateFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + }); + var aDate = new Date(1999, 11, 2); + // Short formats can be space-separated, like 02 Dec 1999. + var dateString = dateFormatter + .format(aDate) + .replace(" 2", "2") + .replace(/ /g, "/"); + + // find out the separator + var possibleSeparators = "/-."; + var arrayOfStrings; + for (let i = 0; i < possibleSeparators.length; ++i) { + arrayOfStrings = dateString.split(possibleSeparators[i]); + if (arrayOfStrings.length == 3) { + gSearchDateSeparator = possibleSeparators[i]; + break; + } + } + + // check the format option + if (arrayOfStrings.length != 3) { + // no successful split + console.error( + `initLocaleShortDateFormat: could not analyze date format of ${dateString}, defaulting to yyyy/mm/dd` + ); + } else { + // The date will contain a zero if the system settings include leading zeros. + gSearchDateLeadingZeros = dateString.includes("0"); + + // Match 2 as number, since that will match both "2" and "02". + // Let's not look for 12 since it could be Dec instead. + if (arrayOfStrings[0] == 2) { + // 02.12.1999 or 02.1999.12 + gSearchDateFormat = arrayOfStrings[1] == "1999" ? formatDYM : formatDMY; + } else if (arrayOfStrings[1] == 2) { + // 12.02.1999 or 1999.02.12 + gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYDM : formatMDY; + } else { + // implies arrayOfStrings[2] == 2 + // 12.1999.02 or 1999.12.02 + gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYMD : formatMYD; + } + } + } catch (e) { + console.error("initLocaleShortDateFormat: caught an exception: " + e); + gSearchDateFormat = 0; + } +} + +function initializeSearchDateFormat() { + if (gSearchDateFormat > 0) { + return; + } + + // get a search date format option and a separator + try { + gSearchDateFormat = Services.prefs.getComplexValue( + "mailnews.search_date_format", + Ci.nsIPrefLocalizedString + ).data; + + gSearchDateFormat = parseInt(gSearchDateFormat); + + // if the option is 0 then try to use the format of the current locale + if (gSearchDateFormat == 0) { + initLocaleShortDateFormat(); + } else { + // initialize the search date format based on preferences + if (gSearchDateFormat < formatMIN || gSearchDateFormat > formatMAX) { + gSearchDateFormat = formatYMD; + } + + gSearchDateSeparator = Services.prefs.getComplexValue( + "mailnews.search_date_separator", + Ci.nsIPrefLocalizedString + ).data; + + gSearchDateLeadingZeros = + Services.prefs.getComplexValue( + "mailnews.search_date_leading_zeros", + Ci.nsIPrefLocalizedString + ).data == "true"; + } + } catch (e) { + console.error("initializeSearchDateFormat: caught an exception: " + e); + gSearchDateFormat = 0; + } + + if (gSearchDateFormat == 0) { + // Set to yyyy/mm/dd in case we couldn't determine in any way. + gSearchDateFormat = formatYMD; + gSearchDateSeparator = "/"; + gSearchDateLeadingZeros = true; + } +} + +function convertPRTimeToString(tm) { + var time = new Date(); + // PRTime is in microseconds, JavaScript time is in milliseconds + // so divide by 1000 when converting + time.setTime(tm / 1000); + + return convertDateToString(time); +} + +function convertDateToString(time) { + initializeSearchDateFormat(); + + var year = time.getFullYear(); + var month = time.getMonth() + 1; // since js month is 0-11 + if (gSearchDateLeadingZeros && month < 10) { + month = "0" + month; + } + var date = time.getDate(); // day + if (gSearchDateLeadingZeros && date < 10) { + date = "0" + date; + } + + var dateStr; + var sep = gSearchDateSeparator; + + switch (gSearchDateFormat) { + case formatYMD: + dateStr = year + sep + month + sep + date; + break; + case formatYDM: + dateStr = year + sep + date + sep + month; + break; + case formatMDY: + dateStr = month + sep + date + sep + year; + break; + case formatMYD: + dateStr = month + sep + year + sep + date; + break; + case formatDMY: + dateStr = date + sep + month + sep + year; + break; + case formatDYM: + dateStr = date + sep + year + sep + month; + break; + default: + dump("valid search date format option is 1-6\n"); + } + + return dateStr; +} + +function convertStringToPRTime(str) { + initializeSearchDateFormat(); + + var arrayOfStrings = str.split(gSearchDateSeparator); + var year, month, date; + + // set year, month, date based on the format option + switch (gSearchDateFormat) { + case formatYMD: + year = arrayOfStrings[0]; + month = arrayOfStrings[1]; + date = arrayOfStrings[2]; + break; + case formatYDM: + year = arrayOfStrings[0]; + month = arrayOfStrings[2]; + date = arrayOfStrings[1]; + break; + case formatMDY: + year = arrayOfStrings[2]; + month = arrayOfStrings[0]; + date = arrayOfStrings[1]; + break; + case formatMYD: + year = arrayOfStrings[1]; + month = arrayOfStrings[0]; + date = arrayOfStrings[2]; + break; + case formatDMY: + year = arrayOfStrings[2]; + month = arrayOfStrings[1]; + date = arrayOfStrings[0]; + break; + case formatDYM: + year = arrayOfStrings[1]; + month = arrayOfStrings[2]; + date = arrayOfStrings[0]; + break; + default: + dump("valid search date format option is 1-6\n"); + } + + month -= 1; // since js month is 0-11 + + var time = new Date(year, month, date); + + // JavaScript time is in milliseconds, PRTime is in microseconds + // so multiply by 1000 when converting + return time.getTime() * 1000; +} diff --git a/comm/mailnews/base/content/folder-menupopup.js b/comm/mailnews/base/content/folder-menupopup.js new file mode 100644 index 0000000000..61a912a4fd --- /dev/null +++ b/comm/mailnews/base/content/folder-menupopup.js @@ -0,0 +1,1238 @@ +/* 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/. */ + +"use strict"; + +/* globals MozElements MozXULElement PanelUI */ + +// This file implements `folder-menupopup` custom elements used in traditional +// menus. + +// Wrap in a block to prevent leaking to window scope. +{ + const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" + ); + + const LazyModules = {}; + + ChromeUtils.defineModuleGetter( + LazyModules, + "FeedUtils", + "resource:///modules/FeedUtils.jsm" + ); + ChromeUtils.defineModuleGetter( + LazyModules, + "FolderUtils", + "resource:///modules/FolderUtils.jsm" + ); + ChromeUtils.defineModuleGetter( + LazyModules, + "MailUtils", + "resource:///modules/MailUtils.jsm" + ); + + /** + * Creates an element, sets attributes on it, including always setting the + * "generated" attribute to "true", and returns the element. The "generated" + * attribute is used to determine which elements to remove when clearing + * the menu. + * + * @param {string} tagName - The tag name of the element to generate. + * @param {object} [attributes] - Optional attributes to set on the element. + * @param {object} [isObject] - The optional "is" object to use when creating + * the element, typically `{is: "folder-menupopup"}`. + */ + function generateElement(tagName, attributes, isObject) { + const element = document.createXULElement(tagName, isObject); + element.setAttribute("generated", "true"); + + if (attributes) { + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + } + return element; + } + + /** + * A function to add shared code to the classes for the `folder-menupopup` + * custom element. Takes a "Base" class, and returns a class that extends + * the "Base" class. + * + * @param {Class} Base - A class to be extended with shared functionality. + * @returns {Class} A class that extends the first class. + */ + let FolderMenu = Base => + class extends Base { + constructor() { + super(); + + window.addEventListener( + "unload", + () => { + // Clean up when being destroyed. + this._removeListener(); + this._teardown(); + }, + { once: true } + ); + + // If non-null, the subFolders of this nsIMsgFolder will be used to + // populate this menu. If this is null, the menu will be populated + // using the root-folders for all accounts. + this._parentFolder = null; + + this._stringBundle = Services.strings.createBundle( + "chrome://messenger/locale/folderWidgets.properties" + ); + + // Various filtering modes can be used with this menu-binding. To use + // one of them, append the mode="foo" attribute to the element. When + // building the menu, we will then use this._filters[mode] as a filter + // function to eliminate folders that should not be shown. + // note: extensions should feel free to plug in here. + this._filters = { + // Returns true if messages can be filed in the folder. + filing(folder) { + if (!folder.server.canFileMessagesOnServer) { + return false; + } + + return folder.canFileMessages || folder.hasSubFolders; + }, + + // Returns true if we can get mail for this folder. (usually this just + // means the "root" fake folder). + getMail(folder) { + if (folder.isServer && folder.server.type != "none") { + return true; + } + if (folder.server.type == "nntp" || folder.server.type == "rss") { + return true; + } + return false; + }, + + // Returns true if we can add filters to this folder/account. + filters(folder) { + // We can always filter news. + if (folder.server.type == "nntp") { + return true; + } + + return folder.server.canHaveFilters; + }, + + subscribe(folder) { + return folder.canSubscribe; + }, + + newFolder(folder) { + return ( + folder.canCreateSubfolders && + folder.server.canCreateFoldersOnServer + ); + }, + + deferred(folder) { + return ( + folder.server.canCreateFoldersOnServer && !folder.supportsOffline + ); + }, + + // Folders that are not in a deferred account. + notDeferred(folder) { + let server = folder.server; + return !( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ); + }, + + // Folders that can be searched. + search(folder) { + if ( + !folder.server.canSearchMessages || + folder.getFlag(Ci.nsMsgFolderFlags.Virtual) + ) { + return false; + } + return true; + }, + + // Folders that can subscribe feeds. + feeds(folder) { + if ( + folder.server.type != "rss" || + folder.getFlag(Ci.nsMsgFolderFlags.Trash) || + folder.getFlag(Ci.nsMsgFolderFlags.Virtual) + ) { + return false; + } + return true; + }, + + junk(folder) { + // Don't show servers (nntp & any others) which do not allow search or filing + // I don't really understand why canSearchMessages is needed, but it was included in + // earlier code, so I include it as well. + if ( + !folder.server.canFileMessagesOnServer || + !folder.server.canSearchMessages + ) { + return false; + } + // Show parents that might have usable subfolders, or usable folders. + return folder.hasSubFolders || folder.canFileMessages; + }, + }; + + // Is this list containing only servers (accounts) and no real folders? + this._serversOnly = true; + + /** + * Our listener to let us know when folders change/appear/disappear so + * we can know to rebuild ourselves. + * + * @implements {nsIFolderListener} + */ + this._listener = { + _menu: this, + _clearMenu(menu) { + // I'm not quite sure why this isn't always a function (bug 514445). + if (menu._teardown) { + menu._teardown(); + } + }, + + _setCssSelectorsForItem(item) { + const child = this._getChildForItem(item); + if (child) { + this._menu._setCssSelectors(child._folder, child); + } + }, + + _folderAddedOrRemoved(folder) { + if (this._filterFunction && !this._filterFunction(folder)) { + return; + } + // xxx we can optimize this later + this._clearMenu(this._menu); + }, + + onFolderAdded(parentFolder, child) { + this._folderAddedOrRemoved(child); + }, + onMessageAdded(parentFolder, msg) {}, + onFolderRemoved(parentFolder, child) { + this._folderAddedOrRemoved(child); + }, + onMessageRemoved(parentFolder, msg) {}, + + // xxx I stole this listener list from nsMsgFolderDatasource.cpp, but + // someone should really document what events are fired when, so that + // we make sure we're updating at the right times. + onFolderPropertyChanged(item, property, old, newItem) {}, + onFolderIntPropertyChanged(item, property, old, aNew) { + if (item instanceof Ci.nsIMsgFolder) { + if (property == "FolderFlag") { + if ( + this._menu.getAttribute("showFavorites") != "true" || + !this._menu._initializedSpecials.has("favorites") + ) { + return; + } + + if ( + (old & Ci.nsMsgFolderFlags.Favorite) != + (aNew & Ci.nsMsgFolderFlags.Favorite) + ) { + setTimeout(this._clearMenu, 0, this._menu); + } + } + } + this._setCssSelectorsForItem(item); + }, + onFolderBoolPropertyChanged(item, property, old, newItem) { + this._setCssSelectorsForItem(item); + }, + onFolderUnicharPropertyChanged(item, property, old, newItem) { + this._setCssSelectorsForItem(item); + }, + onFolderPropertyFlagChanged(item, property, old, newItem) {}, + + onFolderEvent(folder, eventName) { + if (eventName == "MRMTimeChanged") { + if ( + this._menu.getAttribute("showRecent") != "true" || + !this._menu._initializedSpecials.has("recent") || + !this._menu.childWrapper.firstElementChild + ) { + return; + } + + const recentMenuItem = this._menu.childWrapper.firstElementChild; + const recentSubMenu = + this._menu._getSubMenuForMenuItem(recentMenuItem); + + // If this folder is already in the recent menu, return. + if ( + !recentSubMenu || + this._getChildForItem(folder, recentSubMenu) + ) { + return; + } + } else if (eventName == "RenameCompleted") { + // Special casing folder renames here, since they require more work + // since sort-order may have changed. + if (!this._getChildForItem(folder)) { + return; + } + } else { + return; + } + // Folder renamed, or new recent folder, so rebuild. + setTimeout(this._clearMenu, 0, this._menu); + }, + + /** + * Helper function to check and see whether we have a menuitem for this + * particular nsIMsgFolder. + * + * @param {nsIMsgFolder} item - The folder to check. + * @param {Element} [menu] - Optional menu to look in, defaults to this._menu. + * @returns {Element|null} The menuitem for that folder, or null if no + * child for that folder exists. + */ + _getChildForItem(item, menu = this._menu) { + if ( + !menu || + !menu.childWrapper.hasChildNodes() || + !(item instanceof Ci.nsIMsgFolder) + ) { + return null; + } + for (let child of menu.childWrapper.children) { + if (child._folder && child._folder.URI == item.URI) { + return child; + } + } + return null; + }, + }; + + // True if we have already built our menu items and are now just + // listening for changes. + this._initialized = false; + + // A Set listing which of our special menus are already built. + // E.g. "recent", "favorites". + this._initializedSpecials = new Set(); + + // The format for displaying names of folders. + this._displayformat = null; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + // Call the connectedCallback of the "base" class this mixin class is extending. + super.connectedCallback(); + + // Get the displayformat if set. + if (this.parentNode && this.parentNode.localName == "menulist") { + this._displayformat = this.parentNode.getAttribute("displayformat"); + } + } + + set parentFolder(val) { + this._parentFolder = val; + this._teardown(); + } + + get parentFolder() { + return this._parentFolder; + } + + /** + * Make sure we remove our listener when the window is being destroyed + * or the widget torn down. + */ + _removeListener() { + if (!this._initialized) { + return; + } + MailServices.mailSession.RemoveFolderListener(this._listener); + } + + /** + * Call this if you do not know whether the menu items have been built, + * but know that they need to be built now if they haven't been yet. + */ + _ensureInitialized() { + if (this._initialized) { + return; + } + + // The excludeServers attribute is a comma separated list of server keys. + const excludeServers = this.hasAttribute("excludeServers") + ? this.getAttribute("excludeServers").split(",") + : []; + + // Extensions and other consumers can add to these modes too, see the + // note on the _filters field. (Note: empty strings ("") are falsy in JS.) + const mode = this.getAttribute("mode"); + + const filterFunction = mode ? this._filters[mode] : folder => true; + + const folders = this._getFolders( + this._parentFolder, + excludeServers, + mode ? filterFunction : null + ); + + this._listener._filterFunction = filterFunction; + + this._build(folders, mode); + + // Lastly, we add a listener to get notified of changes in the folder + // structure. + MailServices.mailSession.AddFolderListener( + this._listener, + Ci.nsIFolderListener.all + ); + + this._initialized = true; + } + + /** + * Get the folders that will appear in the menu. + * + * @param {Element} parentFolder - The parent menu popup/view element. + * @param {string[]} excludeServers - Server keys for the servers to exclude. + * @param {Function} [filterFunction] - Function for filtering the folders. + */ + _getFolders(parentFolder, excludeServers, filterFunction) { + let folders; + + // If we have a parent folder, just get the subFolders for that parent. + if (parentFolder) { + folders = parentFolder.subFolders; + } else { + // If we don't have a parent, then we assume we should build the + // top-level accounts. (Actually we build the fake root folders for + // those accounts.) + let accounts = LazyModules.FolderUtils.allAccountsSorted(true); + + // Now generate our folder list. Note that we'll special case this + // situation elsewhere, to avoid destroying the sort order we just made. + folders = accounts.map(acct => acct.incomingServer.rootFolder); + } + + if (filterFunction) { + folders = folders.filter(filterFunction); + } + + if (excludeServers.length > 0) { + folders = folders.filter( + folder => !excludeServers.includes(folder.server.key) + ); + } + return folders; + } + + /** + * Actually constructs the menu items based on the folders given. + * + * @param {nsIMsgFolder[]} folders - An array of nsIMsgFolders to use for building. + * @param {string} [mode] - The filtering mode. See comment on _filters field. + */ + _build(folders, mode) { + let globalInboxFolder = null; + + // See if this is the toplevel menu (usually with accounts). + if (!this._parentFolder) { + this._addTopLevelMenuItems(); + + // If we are showing the accounts for deferring, move Local Folders to the top. + if (mode == "deferred") { + globalInboxFolder = + MailServices.accounts.localFoldersServer.rootFolder; + let localFoldersIndex = folders.indexOf(globalInboxFolder); + if (localFoldersIndex != -1) { + folders.splice(localFoldersIndex, 1); + folders.unshift(globalInboxFolder); + } + } + // If we're the root of the folder hierarchy, then we actually don't + // want to sort the folders, but rather the accounts to which the + // folders belong. Since that sorting was already done, we don't need + // to do anything for that case here. + } else { + this._maybeAddParentFolderMenuItem(mode); + + // Sort the list of folders. We give first priority to the sortKey + // property if it is available, otherwise a case-insensitive + // comparison of names. + folders = folders.sort((a, b) => a.compareSortKeys(b)); + } + + this._addFoldersMenuItems(folders, mode, globalInboxFolder); + if (!this._parentFolder) { + this._addTopLevelBottomMenuItems(); + } + } + + /** + * Add menu items that only appear at top level, like "Recent". + */ + _addTopLevelMenuItems() { + const showRecent = this.getAttribute("showRecent") == "true"; + const showFavorites = this.getAttribute("showFavorites") == "true"; + + if (showRecent) { + this.childWrapper.appendChild( + this._buildSpecialMenu({ + special: "recent", + label: this.getAttribute("recentLabel"), + accessKey: this.getAttribute("recentAccessKey"), + }) + ); + } + if (showFavorites) { + this.childWrapper.appendChild( + this._buildSpecialMenu({ + special: "favorites", + label: this.getAttribute("favoritesLabel"), + accessKey: this.getAttribute("favoritesAccessKey"), + }) + ); + } + if (showRecent || showFavorites) { + this.childWrapper.appendChild(this._buildSeparator()); + } + } + + /** + * Add menu items that only appear at top level (but last), like "". + */ + _addTopLevelBottomMenuItems() { + if (this.getAttribute("showLast") != "true") { + return; + } + const folderURI = Services.prefs.getStringPref( + "mail.last_msg_movecopy_target_uri" + ); + const folder = + folderURI && LazyModules.MailUtils.getExistingFolder(folderURI); + if (!folder) { + return; + } + + this.childWrapper.appendChild(this._buildSeparator()); + const attributes = { + label: `${folder.prettyName} - ${folder.server.prettyName}`, + ...this._getCssSelectorAttributes(folder), + }; + this.childWrapper.appendChild(this._buildMenuItem(attributes, folder)); + } + + /** + * Populate a "recent" or "favorites" special submenu with either the + * recently used or favorite folders, to allow for easy access. + * + * @param {Element} menu - The menu or toolbarbutton element for which one + * wants to populate the special sub menu. + * @param {Element} submenu - The submenu element, typically a menupopup. + */ + _populateSpecialSubmenu(menu, submenu) { + let specialType = menu.getAttribute("special"); + if (this._initializedSpecials.has(specialType)) { + return; + } + + // Iterate through all folders in all accounts matching the current filter. + let specialFolders = MailServices.accounts.allFolders; + if (this._listener._filterFunction) { + specialFolders = specialFolders.filter( + this._listener._filterFunction + ); + } + + switch (specialType) { + case "recent": + // Find the most recently modified ones. + specialFolders = LazyModules.FolderUtils.getMostRecentFolders( + specialFolders, + Services.prefs.getIntPref("mail.folder_widget.max_recent"), + "MRMTime" + ); + break; + case "favorites": + specialFolders = specialFolders.filter(folder => + folder.getFlag(Ci.nsMsgFolderFlags.Favorite) + ); + break; + } + + // Cache the pretty names so that they do not need to be fetched + // with quadratic complexity when sorting by name. + let specialFoldersMap = specialFolders.map(folder => { + return { + folder, + name: folder.prettyName, + }; + }); + + // Because we're scanning across multiple accounts, we can end up with + // several folders with the same name. Find those dupes. + let dupeNames = new Set(); + for (let i = 0; i < specialFoldersMap.length; i++) { + for (let j = i + 1; j < specialFoldersMap.length; j++) { + if (specialFoldersMap[i].name == specialFoldersMap[j].name) { + dupeNames.add(specialFoldersMap[i].name); + } + } + } + + for (let folderItem of specialFoldersMap) { + // If this folder name appears multiple times in the recent list, + // append the server name to disambiguate. + // TODO: + // - maybe this could use verboseFolderFormat from messenger.properties + // instead of hardcoded " - ". + // - disambiguate folders with same name in same account + // (in different subtrees). + let label = folderItem.name; + if (dupeNames.has(label)) { + label += " - " + folderItem.folder.server.prettyName; + } + + folderItem.label = label; + } + + // Make sure the entries are sorted alphabetically. + specialFoldersMap.sort((a, b) => + LazyModules.FolderUtils.folderNameCompare(a.label, b.label) + ); + + // Create entries for each of the recent folders. + for (let folderItem of specialFoldersMap) { + let attributes = { + label: folderItem.label, + ...this._getCssSelectorAttributes(folderItem.folder), + }; + + submenu.childWrapper.appendChild( + this._buildMenuItem(attributes, folderItem.folder) + ); + } + + if (specialFoldersMap.length == 0) { + menu.setAttribute("disabled", "true"); + } + + this._initializedSpecials.add(specialType); + } + + /** + * Add a menu item that refers back to the parent folder when there is a + * showFileHereLabel attribute or no mode attribute. However don't + * add such a menu item if one of the following conditions is met: + * (-) There is no parent folder. + * (-) Folder is server and showAccountsFileHere is explicitly false. + * (-) Current folder has a mode, the parent folder can be selected, + * no messages can be filed into the parent folder (e.g. when the + * parent folder is a news group or news server) and the folder + * mode is not equal to newFolder. + * The menu item will have the value of the fileHereLabel attribute as + * label or if the attribute does not exist the name of the parent + * folder instead. + * + * @param {string} mode - The mode attribute. + */ + _maybeAddParentFolderMenuItem(mode) { + let folder = this._parentFolder; + if ( + folder && + (this.getAttribute("showFileHereLabel") == "true" || !mode) + ) { + let showAccountsFileHere = this.getAttribute("showAccountsFileHere"); + if ( + (!folder.isServer || showAccountsFileHere != "false") && + (!mode || + mode == "newFolder" || + folder.noSelect || + folder.canFileMessages || + showAccountsFileHere == "true") + ) { + let attributes = {}; + + if (this.hasAttribute("fileHereLabel")) { + attributes.label = this.getAttribute("fileHereLabel"); + attributes.accesskey = this.getAttribute("fileHereAccessKey"); + } else { + attributes.label = folder.prettyName; + Object.assign(attributes, this._getCssSelectorAttributes(folder)); + } + + if (folder.noSelect) { + attributes.disabled = "true"; + } + + this.childWrapper.appendChild( + this._buildMenuItem(attributes, folder) + ); + this.childWrapper.appendChild(this._buildSeparator()); + } + } + } + + /** + * Add menu items, one for each folder. + * + * @param {nsIMsgFolder[]} folders - Array of folder objects. + * @param {string} mode - The mode attribute. + * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder. + */ + _addFoldersMenuItems(folders, mode, globalInboxFolder) { + // disableServers attribute is a comma separated list of server keys. + const disableServers = this.hasAttribute("disableServers") + ? this.getAttribute("disableServers").split(",") + : []; + + // We need to call this, or hasSubFolders will always return false. + // Remove this workaround when Bug 502900 is fixed. + LazyModules.MailUtils.discoverFolders(); + this._serversOnly = true; + + let [shouldExpand, labels] = this._getShouldExpandAndLabels(); + + for (let folder of folders) { + if (!folder.isServer) { + this._serversOnly = false; + } + + let attributes = { + label: this._getFolderLabel(mode, globalInboxFolder, folder), + ...this._getCssSelectorAttributes(folder), + }; + + if (disableServers.includes(folder.server.key)) { + attributes.disabled = "true"; + } + + if (!folder.hasSubFolders || !shouldExpand(folder.server.type)) { + // There are no subfolders, create a simple menu item. + this.childWrapper.appendChild( + this._buildMenuItem(attributes, folder) + ); + } else { + // There are subfolders, create a menu item with a submenu. + // xxx this is slightly problematic in that we haven't confirmed + // whether any of the subfolders will pass the filter. + + this._serversOnly = false; + + let submenuAttributes = {}; + + [ + "class", + "type", + "fileHereLabel", + "showFileHereLabel", + "oncommand", + "showAccountsFileHere", + "mode", + "disableServers", + "position", + ].forEach(attribute => { + if (this.hasAttribute(attribute)) { + submenuAttributes[attribute] = this.getAttribute(attribute); + } + }); + + const [menuItem, submenu] = this._buildMenuItemWithSubmenu( + attributes, + true, + folder, + submenuAttributes + ); + + // If there are labels, we add an item and separator to the submenu. + if (labels) { + const serverAttributes = { label: labels[folder.server.type] }; + + submenu.childWrapper.appendChild( + this._buildMenuItem(serverAttributes, folder, this) + ); + + submenu.childWrapper.appendChild(this._buildSeparator()); + } + + this.childWrapper.appendChild(menuItem); + } + } + } + + /** + * Return the label to use for a folder. + * + * @param {string} mode - The mode, e.g. "deferred". + * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder. + * @param {nsIMsgFolder} folder - The folder for which we are getting a label. + * @returns {string} The label to use for the folder. + */ + _getFolderLabel(mode, globalInboxFolder, folder) { + if ( + mode == "deferred" && + folder.isServer && + folder.server.rootFolder == globalInboxFolder + ) { + return this._stringBundle.formatStringFromName("globalInbox", [ + folder.prettyName, + ]); + } + return folder.prettyName; + } + + /** + * Let the user have a list of subfolders for all account types, none of + * them, or only some of them. Returns an array containing a function that + * determines whether to show subfolders for a given account type, and an + * object mapping account types to label names (may be null). + * + * @returns {any[]} - An array; [0] is the shouldExpand function, [1] is + * the labels object. + */ + _getShouldExpandAndLabels() { + let shouldExpand; + let labels = null; + if ( + this.getAttribute("expandFolders") == "true" || + !this.hasAttribute("expandFolders") + ) { + shouldExpand = () => true; + } else if (this.getAttribute("expandFolders") == "false") { + shouldExpand = () => false; + } else { + // We want a subfolder list for only some servers. We also may need + // to create headers to select the servers. If so, then headlabels + // is a comma-delimited list of labels corresponding to the server + // types specified in expandFolders. + let types = this.getAttribute("expandFolders").split(/ *, */); + // Set the labels. labels[type] = label + if (this.hasAttribute("headlabels")) { + let labelNames = this.getAttribute("headlabels").split(/ *, */); + labels = {}; + // If the length isn't equal, don't give them any of the labels, + // since any combination will probably be wrong. + if (labelNames.length == types.length) { + for (let index in types) { + labels[types[index]] = labelNames[index]; + } + } + } + shouldExpand = e => types.includes(e); + } + return [shouldExpand, labels]; + } + + /** + * Set attributes on a menu, menuitem, or toolbarbutton element to allow + * for CSS styling. + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem. + * @param {Element} menuNode - The actual DOM node to set attributes on. + */ + _setCssSelectors(folder, menuNode) { + const cssAttributes = this._getCssSelectorAttributes(folder); + + Object.entries(cssAttributes).forEach(([key, value]) => + menuNode.setAttribute(key, value) + ); + } + + /** + * Returns attributes to be set on a menu, menuitem, or toolbarbutton + * element to allow for CSS styling. + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu item. + * @returns {object} Contains the CSS selector attributes. + */ + _getCssSelectorAttributes(folder) { + let attributes = {}; + + // First the SpecialFolder attribute. + attributes.SpecialFolder = + LazyModules.FolderUtils.getSpecialFolderString(folder); + + // Now the biffState. + let biffStates = ["NewMail", "NoMail", "UnknownMail"]; + for (let state of biffStates) { + if (folder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) { + attributes.BiffState = state; + break; + } + } + + attributes.IsServer = folder.isServer; + attributes.IsSecure = folder.server.isSecure; + attributes.ServerType = folder.server.type; + attributes.IsFeedFolder = + !!LazyModules.FeedUtils.getFeedUrlsInFolder(folder); + + return attributes; + } + + /** + * This function returns a formatted display name for a menulist + * selected folder. The desired format is set as the 'displayformat' + * attribute of the folderpicker's , one of: + * 'name' (default) - Folder + * 'verbose' - Folder on Account + * 'path' - Account/Folder/Subfolder + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem. + * @returns {string} The display name. + */ + getDisplayName(folder) { + if (folder.isServer) { + return folder.prettyName; + } + + if (this._displayformat == "verbose") { + return this._stringBundle.formatStringFromName( + "verboseFolderFormat", + [folder.prettyName, folder.server.prettyName] + ); + } + + if (this._displayformat == "path") { + return ( + LazyModules.FeedUtils.getFolderPrettyPath(folder) || folder.name + ); + } + + return folder.name; + } + + /** + * Makes a given folder selected. + * TODO: This function does not work yet for the appmenu. However, as of + * June 2019, this functionality is not used in the appmenu. + * + * @param {nsIMsgFolder} inputFolder - The folder to select (if none, + * then Choose Folder). If inputFolder is not in this popup, but is + * instead a descendant of a member of the popup, that ancestor will be + * selected. + * @returns {boolean} Is true if any usable folder was found, otherwise false. + */ + selectFolder(inputFolder) { + // Set the label of the menulist element as if folder had been selected. + function setupParent(folder, menulist, noFolders) { + let menupopup = menulist.menupopup; + if (folder) { + menulist.setAttribute("label", menupopup.getDisplayName(folder)); + } else if (noFolders) { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("noFolders") + ); + } else if (menupopup._serversOnly) { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("chooseAccount") + ); + } else { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("chooseFolder") + ); + } + menulist.setAttribute("value", folder ? folder.URI : ""); + menulist.setAttribute("IsServer", folder ? folder.isServer : false); + menulist.setAttribute( + "IsSecure", + folder ? folder.server.isSecure : false + ); + menulist.setAttribute( + "ServerType", + folder ? folder.server.type : "none" + ); + menulist.setAttribute( + "SpecialFolder", + folder + ? LazyModules.FolderUtils.getSpecialFolderString(folder) + : "none" + ); + menulist.setAttribute( + "IsFeedFolder", + Boolean(folder && LazyModules.FeedUtils.getFeedUrlsInFolder(folder)) + ); + } + + let folder; + if (inputFolder) { + for (let child of this.children) { + if ( + child && + child._folder && + !child.disabled && + (child._folder.URI == inputFolder.URI || + (child.tagName == "menu" && + child._folder.isAncestorOf(inputFolder))) + ) { + if (child._folder.URI == inputFolder.URI) { + this.parentNode.selectedItem = child; + } + folder = inputFolder; + break; + } + } + } + + // If the caller specified a folder to select and it was not + // found, or if the caller didn't pass a folder (meaning a logical + // and valid folder wasn't determined), don't blow up but reset + // attributes and set a nice Choose Folder label so the user may + // select a valid folder per the filter for this picker. If there are + // no children, then no folder passed the filter; disable the menulist + // as there's nothing to choose from. + let noFolders; + if (!this.childElementCount) { + this.parentNode.setAttribute("disabled", true); + noFolders = true; + } else { + this.parentNode.removeAttribute("disabled"); + noFolders = false; + } + + setupParent(folder, this.parentNode, noFolders); + return !!folder; + } + + /** + * Removes all menu items from this menu, removes their submenus. This + * function is called when a change that affects this menu is detected + * by the listener. + */ + _teardown() { + if (!this._initialized) { + return; + } + const children = this.childWrapper.children; + // We iterate in reverse order because children is live so it changes + // as we remove child nodes. + for (let i = children.length - 1; i >= 0; i--) { + const item = children[i]; + if (item.getAttribute("generated") != "true") { + continue; + } + const submenu = this._getSubMenuForMenuItem(item); + + if (submenu && "_teardown" in submenu) { + submenu._teardown(); + submenu.remove(); + } + item.remove(); + } + + this._removeListener(); + + this._initialized = false; + this._initializedSpecials.clear(); + } + }; + + /** + * The MozFolderMenupopup widget is used as a menupopup that contains menu + * items and submenus for all folders from every account (or some subset of + * folders and accounts). It is also used to provide a menu with a menuitem + * for each account. Each menu item gets displayed with the folder or + * account name and icon. + * + * @augments {MozElements.MozMenuPopup} + */ + let MozFolderMenuPopup = FolderMenu( + class extends MozElements.MozMenuPopup { + constructor() { + super(); + + // To improve performance, only build the menu when it is shown. + this.addEventListener( + "popupshowing", + event => { + this._ensureInitialized(); + }, + true + ); + + // Because the menu items in a panelview go inside a child vbox but are + // direct children of a menupopup, we set up a consistent way to append + // and access menu items for both cases. + this.childWrapper = this; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.setAttribute("is", "folder-menupopup"); + + // Find out if we are in a wrapper (customize toolbars mode is active). + let inWrapper = false; + let node = this; + while (XULElement.isInstance(node)) { + if (node.id.startsWith("wrapper-")) { + inWrapper = true; + break; + } + node = node.parentNode; + } + + if (!inWrapper) { + if (this.hasAttribute("original-width")) { + // If we were in a wrapper before and have a width stored, restore it now. + if (this.getAttribute("original-width") == "none") { + this.removeAttribute("width"); + } else { + this.setAttribute("width", this.getAttribute("original-width")); + } + + this.removeAttribute("original-width"); + } + + // If we are a child of a menulist, and we aren't in a wrapper, we + // need to build our content right away, otherwise the menulist + // won't have proper sizing. + if (this.parentNode && this.parentNode.localName == "menulist") { + this._ensureInitialized(); + } + } else { + // But if we're in a wrapper, remove our children, because we're + // getting re-created when the toolbar customization closes. + this._teardown(); + + // Store our current width and set a safe small width when we show + // in a wrapper. + if (!this.hasAttribute("original-width")) { + this.setAttribute( + "original-width", + this.hasAttribute("width") ? this.getAttribute("width") : "none" + ); + this.setAttribute("width", "100"); + } + } + } + + /** + * Given a menu item, return the menupopup that it opens. + * + * @param {Element} menu - The menu item, typically a `menu` element. + * @returns {Element|null} The `menupopup` element or null if none found. + */ + _getSubMenuForMenuItem(menu) { + return menu.querySelector("menupopup"); + } + + /** + * Returns a `menuseparator` element for use in a `menupopup`. + */ + _buildSeparator() { + return generateElement("menuseparator"); + } + + /** + * Builds a menu item (`menuitem`) element that does not open a submenu + * (i.e. not a `menu` element). + * + * @param {object} [attributes] - Attributes to set on the element. + * @param {nsIMsgFolder} folder - The folder associated with the menu item. + * @returns {Element} A `menuitem`. + */ + _buildMenuItem(attributes, folder) { + const menuitem = generateElement("menuitem", attributes); + menuitem.classList.add("folderMenuItem", "menuitem-iconic"); + menuitem._folder = folder; + return menuitem; + } + + /** + * Builds a menu item (`menu`) element and an associated submenu + * (`menupopup`) element. + * + * @param {object} attributes - Attributes to set on the `menu` element. + * @param {boolean} folderSubmenu - Whether the submenu is to be a + * `folder-menupopup` element. + * @param {nsIMsgFolder} [folder] - The folder associated with the menu item. + * @param {object} submenuAttributes - Attributes to set on the `menupopup` element. + * @returns {Element[]} Array containing the `menu` and + * `menupopup` elements. + */ + _buildMenuItemWithSubmenu( + attributes, + folderSubmenu, + folder, + submenuAttributes + ) { + const menu = generateElement("menu", attributes); + menu.classList.add("folderMenuItem", "menu-iconic"); + + const isObject = folderSubmenu ? { is: "folder-menupopup" } : null; + + const menupopup = generateElement( + "menupopup", + submenuAttributes, + isObject + ); + + if (folder) { + menu._folder = folder; + menupopup._parentFolder = folder; + } + + if (!menupopup.childWrapper) { + menupopup.childWrapper = menupopup; + } + + menu.appendChild(menupopup); + + return [menu, menupopup]; + } + + /** + * Build a special menu item (`menu`) and an empty submenu (`menupopup`) + * for it. The submenu is populated just before it is shown by + * `_populateSpecialSubmenu`. + * + * The submenu (`menupopup`) is just a standard element, not a custom + * element (`folder-menupopup`). + * + * @param {object} [attributes] - Attributes to set on the menu item element. + * @returns {Element} The menu item (`menu`) element. + */ + _buildSpecialMenu(attributes) { + const [menu, menupopup] = this._buildMenuItemWithSubmenu(attributes); + + menupopup.addEventListener( + "popupshowing", + event => { + this._populateSpecialSubmenu(menu, menupopup); + }, + { once: true } + ); + + return menu; + } + } + ); + + customElements.define("folder-menupopup", MozFolderMenuPopup, { + extends: "menupopup", + }); +} diff --git a/comm/mailnews/base/content/folderProps.js b/comm/mailnews/base/content/folderProps.js new file mode 100644 index 0000000000..324bfcac40 --- /dev/null +++ b/comm/mailnews/base/content/folderProps.js @@ -0,0 +1,480 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from retention.js */ +/* global BigInt */ + +var { FolderTreeProperties } = ChromeUtils.import( + "resource:///modules/FolderTreeProperties.jsm" +); +var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm"); + +var gMsgFolder; +var gLockedPref = null; + +var gDefaultColor = ""; + +window.addEventListener("load", folderPropsOnLoad); +document.addEventListener("dialogaccept", folderPropsOKButton); +document.addEventListener("dialogcancel", folderCancelButton); + +/** + * The folderPropsSink is the class that gets notified of an imap folder's + * properties. + * + * @implements {nsIMsgImapFolderProps} + */ +var gFolderPropsSink = { + setFolderType(folderTypeString) { + var typeLabel = document.getElementById("folderType.text"); + if (typeLabel) { + typeLabel.setAttribute("value", folderTypeString); + } + // get the element for the folder type label and set value on it. + }, + + setFolderTypeDescription(folderDescription) { + var folderTypeLabel = document.getElementById("folderDescription.text"); + if (folderTypeLabel) { + folderTypeLabel.setAttribute("value", folderDescription); + } + }, + + setFolderPermissions(folderPermissions) { + var permissionsLabel = document.getElementById("folderPermissions.text"); + var descTextNode = document.createTextNode(folderPermissions); + permissionsLabel.appendChild(descTextNode); + }, + + serverDoesntSupportACL() { + var typeLabel = document.getElementById("folderTypeLabel"); + if (typeLabel) { + typeLabel.setAttribute("hidden", "true"); + } + var permissionsLabel = document.getElementById("permissionsDescLabel"); + if (permissionsLabel) { + permissionsLabel.setAttribute("hidden", "true"); + } + }, + + setQuotaStatus(folderQuotaStatus) { + var quotaStatusLabel = document.getElementById("folderQuotaStatus"); + if (quotaStatusLabel) { + quotaStatusLabel.setAttribute("value", folderQuotaStatus); + } + }, + + showQuotaData(showData) { + var quotaStatusLabel = document.getElementById("folderQuotaStatus"); + var folderQuotaData = document.getElementById("folderQuotaData"); + + if (quotaStatusLabel && folderQuotaData) { + quotaStatusLabel.hidden = showData; + folderQuotaData.hidden = !showData; + } + }, + + setQuotaData(folderQuota) { + let quotaDetails = document.getElementById("quotaDetails"); + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + for (let quota of folderQuota) { + let li = document.createElement("li"); + let name = document.createElement("span"); + name.textContent = quota.name; + li.appendChild(name); + + let progress = document.createElement("progress"); + progress.classList.add("quota-percentage"); + progress.setAttribute("value", quota.usage); + progress.setAttribute("max", quota.limit); + + li.appendChild(progress); + + let percentage = document.createElement("span"); + document.l10n.setAttributes(percentage, "quota-percent-used", { + percent: Number((100n * BigInt(quota.usage)) / BigInt(quota.limit)), + }); + li.appendChild(percentage); + + li.appendChild(document.createTextNode(" — ")); + + let details = document.createElement("span"); + if (/STORAGE/i.test(quota.name)) { + let usage = messenger.formatFileSize(quota.usage * 1024); + let limit = messenger.formatFileSize(quota.limit * 1024); + details.textContent = `${usage} / ${limit}`; + } else { + details.textContent = `${quota.usage} / ${quota.limit}`; + } + li.appendChild(details); + + quotaDetails.appendChild(li); + } + }, +}; + +function doEnabling() { + var nameTextbox = document.getElementById("name"); + document.querySelector("dialog").getButton("accept").disabled = + !nameTextbox.value; +} + +function folderPropsOKButton(event) { + if (gMsgFolder) { + if ( + document.getElementById("offline.selectForOfflineFolder").checked || + document.getElementById("offline.selectForOfflineNewsgroup").checked + ) { + gMsgFolder.setFlag(Ci.nsMsgFolderFlags.Offline); + } else { + gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.Offline); + } + + if (document.getElementById("folderCheckForNewMessages").checked) { + gMsgFolder.setFlag(Ci.nsMsgFolderFlags.CheckNew); + } else { + gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.CheckNew); + } + + let glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch"); + if (!glodaCheckbox.hidden) { + if (glodaCheckbox.checked) { + // We pass true here so that folders such as trash and junk can still + // have a priority set. + Gloda.resetFolderIndexingPriority(gMsgFolder, true); + } else { + Gloda.setFolderIndexingPriority( + gMsgFolder, + Gloda.getFolderForFolder(gMsgFolder).kIndexingNeverPriority + ); + } + } + + var retentionSettings = saveCommonRetentionSettings( + gMsgFolder.retentionSettings + ); + retentionSettings.useServerDefaults = document.getElementById( + "retention.useDefault" + ).checked; + gMsgFolder.retentionSettings = retentionSettings; + + let color = document.getElementById("color").value; + if (color == gDefaultColor) { + color = undefined; + } + FolderTreeProperties.setColor(gMsgFolder.URI, color); + // Tell 3-pane tabs to update the folder's color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color); + } + + try { + // This throws an exception when an illegal folder name was entered. + top.okCallback( + document.getElementById("name").value, + window.arguments[0].name + ); + } catch (e) { + event.preventDefault(); + } +} + +function folderCancelButton(event) { + // Clear any previewed color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-preview"); +} + +function folderPropsOnLoad() { + let styles = getComputedStyle(document.body); + let folderColors = { + Inbox: styles.getPropertyValue("--folder-color-inbox"), + Sent: styles.getPropertyValue("--folder-color-sent"), + Outbox: styles.getPropertyValue("--folder-color-outbox"), + Drafts: styles.getPropertyValue("--folder-color-draft"), + Trash: styles.getPropertyValue("--folder-color-trash"), + Archive: styles.getPropertyValue("--folder-color-archive"), + Templates: styles.getPropertyValue("--folder-color-template"), + Spam: styles.getPropertyValue("--folder-color-spam"), + Virtual: styles.getPropertyValue("--folder-color-folder-filter"), + RSS: styles.getPropertyValue("--folder-color-rss"), + Newsgroup: styles.getPropertyValue("--folder-color-newsletter"), + }; + gDefaultColor = styles.getPropertyValue("--folder-color-folder"); + + // look in arguments[0] for parameters + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].title) { + document.title = window.arguments[0].title; + } + if (window.arguments[0].okCallback) { + top.okCallback = window.arguments[0].okCallback; + } + } + + if (window.arguments[0].folder) { + // Fill in folder name, based on what they selected in the folder pane. + gMsgFolder = window.arguments[0].folder; + } + + if (window.arguments[0].name) { + // Initialize name textbox with the given name and remember this + // value so we can tell whether the folder needs to be renamed + // when the dialog is accepted. + var nameTextbox = document.getElementById("name"); + nameTextbox.value = window.arguments[0].name; + } + + const serverType = window.arguments[0].serverType; + + // Do this first, because of gloda we may want to override some of the hidden + // statuses. + hideShowControls(serverType); + + if (gMsgFolder) { + // We really need a functioning database, so we'll detect problems + // and create one if we have to. + try { + gMsgFolder.msgDatabase; + } catch (e) { + gMsgFolder.updateFolder(window.arguments[0].msgWindow); + } + + // Check the current folder name against known folder names to set the + // correct default color, if needed. + let selectedFolderName = ""; + + switch (window.arguments[0].serverType) { + case "rss": + selectedFolderName = "RSS"; + break; + case "nntp": + selectedFolderName = "Newsgroup"; + break; + default: + selectedFolderName = window.arguments[0].name; + break; + } + + if (Object.keys(folderColors).includes(selectedFolderName)) { + gDefaultColor = folderColors[selectedFolderName]; + } + + let colorInput = document.getElementById("color"); + colorInput.value = + FolderTreeProperties.getColor(gMsgFolder.URI) || gDefaultColor; + colorInput.addEventListener("input", event => { + // Preview the chosen color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + colorInput.value + ); + }); + let resetColorButton = document.getElementById("resetColor"); + resetColorButton.addEventListener("click", function () { + colorInput.value = gDefaultColor; + // Preview the default color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + gDefaultColor + ); + }); + + var locationTextbox = document.getElementById("location"); + + // Decode the displayed mailbox:// URL as it's useful primarily for debugging, + // whereas imap and news urls are sent around. + locationTextbox.value = + serverType == "imap" || serverType == "nntp" + ? gMsgFolder.folderURL + : decodeURI(gMsgFolder.folderURL); + + if (gMsgFolder.canRename) { + document.getElementById("name").removeAttribute("readonly"); + } + + if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + if (serverType == "imap" || serverType == "pop3") { + document.getElementById( + "offline.selectForOfflineFolder" + ).checked = true; + } + + if (serverType == "nntp") { + document.getElementById( + "offline.selectForOfflineNewsgroup" + ).checked = true; + } + } else { + if (serverType == "imap" || serverType == "pop3") { + document.getElementById( + "offline.selectForOfflineFolder" + ).checked = false; + } + + if (serverType == "nntp") { + document.getElementById( + "offline.selectForOfflineNewsgroup" + ).checked = false; + } + } + + // set check for new mail checkbox + document.getElementById("folderCheckForNewMessages").checked = + gMsgFolder.getFlag(Ci.nsMsgFolderFlags.CheckNew); + + // if gloda indexing is off, hide the related checkbox + var glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch"); + var glodaEnabled = Services.prefs.getBoolPref( + "mailnews.database.global.indexer.enabled" + ); + if ( + !glodaEnabled || + gMsgFolder.flags & + (Ci.nsMsgFolderFlags.Queue | Ci.nsMsgFolderFlags.Newsgroup) + ) { + glodaCheckbox.hidden = true; + } else { + // otherwise, the user can choose whether this file gets indexed + let glodaFolder = Gloda.getFolderForFolder(gMsgFolder); + glodaCheckbox.checked = + glodaFolder.indexingPriority != glodaFolder.kIndexingNeverPriority; + } + } + + if (serverType == "imap") { + let imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + imapFolder.fillInFolderProps(gFolderPropsSink); + + let users = [...imapFolder.getOtherUsersWithAccess()]; + if (users.length) { + document.getElementById("folderOtherUsers").hidden = false; + document.getElementById("folderOtherUsersText").textContent = + users.join(", "); + } + + // Disable "Repair Folder" when offline as that would cause the offline store + // to get deleted and redownloaded. + document.getElementById("folderRebuildSummaryButton").disabled = + gMsgFolder.supportsOffline && Services.io.offline; + } + + var retentionSettings = gMsgFolder.retentionSettings; + initCommonRetentionSettings(retentionSettings); + document.getElementById("retention.useDefault").checked = + retentionSettings.useServerDefaults; + + // set folder sizes + let numberOfMsgs = gMsgFolder.getTotalMessages(false); + if (numberOfMsgs >= 0) { + document.getElementById("numberOfMessages").value = numberOfMsgs; + } + + try { + let sizeOnDisk = Cc["@mozilla.org/messenger;1"] + .createInstance(Ci.nsIMessenger) + .formatFileSize(gMsgFolder.sizeOnDisk, true); + document.getElementById("sizeOnDisk").value = sizeOnDisk; + } catch (e) {} + + onCheckKeepMsg(); + onUseDefaultRetentionSettings(); + + // select the initial tab + if (window.arguments[0].tabID) { + document.getElementById("folderPropTabBox").selectedTab = + document.getElementById(window.arguments[0].tabID); + } +} + +function hideShowControls(serverType) { + let controls = document.querySelectorAll("[hidefor]"); + var len = controls.length; + for (var i = 0; i < len; i++) { + var control = controls[i]; + var hideFor = control.getAttribute("hidefor"); + if (!hideFor) { + throw new Error("hidefor empty"); + } + + // hide unsupported server type + // adding support for hiding multiple server types using hideFor="server1,server2" + var hideForBool = false; + var hideForTokens = hideFor.split(","); + for (var j = 0; j < hideForTokens.length; j++) { + if (hideForTokens[j] == serverType) { + hideForBool = true; + break; + } + } + control.hidden = hideForBool; + } + + // hide the privileges button if the imap folder doesn't have an admin url + // maybe should leave this hidden by default and only show it in this case instead + try { + var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + var privilegesButton = document.getElementById("imap.FolderPrivileges"); + if (privilegesButton) { + if (!imapFolder.hasAdminUrl) { + privilegesButton.setAttribute("hidden", "true"); + } + } + } + } catch (ex) {} + + if (gMsgFolder) { + // Hide "check for new mail" checkbox if this is an Inbox. + if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Inbox)) { + document.getElementById("folderCheckForNewMessages").hidden = true; + } + // Retention policy doesn't apply to Drafts/Templates/Outbox. + if ( + gMsgFolder.isSpecialFolder( + Ci.nsMsgFolderFlags.Drafts | + Ci.nsMsgFolderFlags.Templates | + Ci.nsMsgFolderFlags.Queue, + true + ) + ) { + document.getElementById("Retention").hidden = true; + } + } +} + +function onOfflineFolderDownload() { + // we need to create a progress window and pass that in as the second parameter here. + gMsgFolder.downloadAllForOffline(null, window.arguments[0].msgWindow); +} + +function onFolderPrivileges() { + var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + imapFolder.folderPrivileges(window.arguments[0].msgWindow); + } + // let's try closing the modal dialog to see if it fixes the various problems running this url + window.close(); +} + +function onUseDefaultRetentionSettings() { + var useDefault = document.getElementById("retention.useDefault").checked; + document.getElementById("retention.keepMsg").disabled = useDefault; + document.getElementById("retention.keepNewMsgMinLabel").disabled = useDefault; + document.getElementById("retention.keepOldMsgMinLabel").disabled = useDefault; + + var keepMsg = document.getElementById("retention.keepMsg").value; + const nsIMsgRetentionSettings = Ci.nsIMsgRetentionSettings; + document.getElementById("retention.keepOldMsgMin").disabled = + useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByAge; + document.getElementById("retention.keepNewMsgMin").disabled = + useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByNumHeaders; +} + +function RebuildSummaryInformation() { + window.arguments[0].rebuildSummaryCallback(); +} diff --git a/comm/mailnews/base/content/folderProps.xhtml b/comm/mailnews/base/content/folderProps.xhtml new file mode 100644 index 0000000000..2c0376cc2a --- /dev/null +++ b/comm/mailnews/base/content/folderProps.xhtml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + &folderProps.windowtitle.label; + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/jsTreeView.js b/comm/mailnews/base/content/jsTreeView.js new file mode 100644 index 0000000000..704fd2475f --- /dev/null +++ b/comm/mailnews/base/content/jsTreeView.js @@ -0,0 +1,239 @@ +/* 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/. */ + +/* exported PROTO_TREE_VIEW */ + +/** + * This file contains a prototype object designed to make the implementation of + * nsITreeViews in javascript simpler. This object requires that consumers + * override the _rebuild function. This function must set the _rowMap object to + * an array of objects fitting the following interface: + * + * readonly attribute string id - a unique identifier for the row/object + * readonly attribute integer level - the hierarchy level of the row + * attribute boolean open - whether or not this item's children are exposed + * string getText(aColName) - return the text to display for this row in the + * specified column + * string getProperties() - return the css-selectors + * attribute array children - return an array of child-objects also meeting this + * interface + */ + +function PROTO_TREE_VIEW() { + this._tree = null; + this._rowMap = []; + this._persistOpenMap = []; +} + +PROTO_TREE_VIEW.prototype = { + get rowCount() { + return this._rowMap.length; + }, + + /** + * CSS files will cue off of these. Note that we reach into the rowMap's + * items so that custom data-displays can define their own properties + */ + getCellProperties(aRow, aCol) { + return this._rowMap[aRow].getProperties(aCol); + }, + + /** + * The actual text to display in the tree + */ + getCellText(aRow, aCol) { + return this._rowMap[aRow].getText(aCol.id); + }, + + getCellValue(aRow, aCol) { + return this._rowMap[aRow].getValue(aCol.id); + }, + + /** + * The jstv items take care of assigning this when building children lists + */ + getLevel(aIndex) { + return this._rowMap[aIndex].level; + }, + + /** + * This is easy since the jstv items assigned the _parent property when making + * the child lists + */ + getParentIndex(aIndex) { + return this._rowMap.indexOf(this._rowMap[aIndex]._parent); + }, + + /** + * This is duplicative for our normal jstv views, but custom data-displays may + * want to do something special here + */ + getRowProperties(aRow) { + return this._rowMap[aRow].getProperties(); + }, + + /** + * If an item in our list has the same level and parent as us, it's a sibling + */ + hasNextSibling(aIndex, aNextIndex) { + let targetLevel = this._rowMap[aIndex].level; + for (let i = aNextIndex + 1; i < this._rowMap.length; i++) { + if (this._rowMap[i].level == targetLevel) { + return true; + } + if (this._rowMap[i].level < targetLevel) { + return false; + } + } + return false; + }, + + /** + * If we have a child-list with at least one element, we are a container. + */ + isContainer(aIndex) { + return this._rowMap[aIndex].children.length > 0; + }, + + isContainerEmpty(aIndex) { + // If the container has no children, the container is empty. + return !this._rowMap[aIndex].children.length; + }, + + /** + * Just look at the jstv item here + */ + isContainerOpen(aIndex) { + return this._rowMap[aIndex].open; + }, + + isEditable(aRow, aCol) { + // We don't support editing rows in the tree yet. + return false; + }, + + isSeparator(aIndex) { + // There are no separators in our trees + return false; + }, + + isSorted() { + // We do our own customized sorting + return false; + }, + + setTree(aTree) { + this._tree = aTree; + }, + + recursivelyAddToMap(aChild, aNewIndex) { + // When we add sub-children, we're going to need to increase our index + // for the next add item at our own level. + let currentCount = this._rowMap.length; + if (aChild.children.length && aChild.open) { + for (let [i, child] of this._rowMap[aNewIndex].children.entries()) { + let index = aNewIndex + i + 1; + this._rowMap.splice(index, 0, child); + aNewIndex += this.recursivelyAddToMap(child, index); + } + } + return this._rowMap.length - currentCount; + }, + + /** + * Opens or closes a container with children. The logic here is a bit hairy, so + * be very careful about changing anything. + */ + toggleOpenState(aIndex) { + // Ok, this is a bit tricky. + this._rowMap[aIndex]._open = !this._rowMap[aIndex].open; + + if (!this._rowMap[aIndex].open) { + // We're closing the current container. Remove the children + + // Note that we can't simply splice out children.length, because some of + // them might have children too. Find out how many items we're actually + // going to splice + let level = this._rowMap[aIndex].level; + let row = aIndex + 1; + while (row < this._rowMap.length && this._rowMap[row].level > level) { + row++; + } + let count = row - aIndex - 1; + this._rowMap.splice(aIndex + 1, count); + + // Remove us from the persist map + let index = this._persistOpenMap.indexOf(this._rowMap[aIndex].id); + if (index != -1) { + this._persistOpenMap.splice(index, 1); + } + + // Notify the tree of changes + if (this._tree) { + this._tree.rowCountChanged(aIndex + 1, -count); + } + } else { + // We're opening the container. Add the children to our map + + // Note that these children may have been open when we were last closed, + // and if they are, we also have to add those grandchildren to the map + let oldCount = this._rowMap.length; + this.recursivelyAddToMap(this._rowMap[aIndex], aIndex); + + // Add this container to the persist map + let id = this._rowMap[aIndex].id; + if (!this._persistOpenMap.includes(id)) { + this._persistOpenMap.push(id); + } + + // Notify the tree of changes + if (this._tree) { + this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount); + } + } + + // Invalidate the toggled row, so that the open/closed marker changes + if (this._tree) { + this._tree.invalidateRow(aIndex); + } + }, + + // We don't implement any of these at the moment + canDrop(aIndex, aOrientation) {}, + drop(aRow, aOrientation) {}, + selectionChanged() {}, + setCellText(aRow, aCol, aValue) {}, + setCellValue(aRow, aCol, aValue) {}, + getColumnProperties(aCol) { + return ""; + }, + getImageSrc(aRow, aCol) {}, + getProgressMode(aRow, aCol) {}, + cycleCell(aRow, aCol) {}, + cycleHeader(aCol) {}, + + _tree: null, + + /** + * An array of jstv items, where each item corresponds to a row in the tree + */ + _rowMap: null, + + /** + * This is a javascript map of which containers we had open, so that we can + * persist their state over-time. It is designed to be used as a JSON object. + */ + _persistOpenMap: null, + + _restoreOpenStates() { + // Note that as we iterate through here, .length may grow + for (let i = 0; i < this._rowMap.length; i++) { + if (this._persistOpenMap.includes(this._rowMap[i].id)) { + this.toggleOpenState(i); + } + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; diff --git a/comm/mailnews/base/content/junkCommands.js b/comm/mailnews/base/content/junkCommands.js new file mode 100644 index 0000000000..1554d54256 --- /dev/null +++ b/comm/mailnews/base/content/junkCommands.js @@ -0,0 +1,449 @@ +/* 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 use for junk processing commands + */ + +/* + * TODO: These functions make the false assumption that a view only contains + * a single folder. This is not true for XF saved searches. + * + * globals prerequisites used: + * + * top.window.MsgStatusFeedback + */ + +/* globals gDBView, gViewWrapper */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +ChromeUtils.defineModuleGetter( + this, + "MailUtils", + "resource:///modules/MailUtils.jsm" +); + +/** + * Determines the actions that should be carried out on the messages + * that are being marked as junk + * + * @param {nsIMsgFolder} aFolder - The folder with messages being marked as junk. + * @returns {object} result an object with two properties. + * @returns {boolean} result.markRead - Whether the messages should be marked + * as read. + * @returns {?nsIMsgFolder} result.junkTargetFolder - Where the messages should + * be moved, or null if they should not be moved. + */ +function determineActionsForJunkMsgs(aFolder) { + var actions = { markRead: false, junkTargetFolder: null }; + var spamSettings = aFolder.server.spamSettings; + + // note we will do moves/marking as read even if the spam + // feature is disabled, since the user has asked to use it + // despite the disabling + + actions.markRead = spamSettings.markAsReadOnSpam; + actions.junkTargetFolder = null; + + // move only when the corresponding setting is activated + // and the currently viewed folder is not the junk folder. + if (spamSettings.moveOnSpam && !aFolder.getFlag(Ci.nsMsgFolderFlags.Junk)) { + var spamFolderURI = spamSettings.spamFolderURI; + if (!spamFolderURI) { + // XXX TODO + // we should use nsIPromptService to inform the user of the problem, + // e.g. when the junk folder was accidentally deleted. + dump("determineActionsForJunkMsgs: no spam folder found, not moving."); + } else { + actions.junkTargetFolder = MailUtils.getOrCreateFolder(spamFolderURI); + } + } + + return actions; +} + +/** + * Performs required operations on a list of newly-classified junk messages. + * + * @param {nsIMsgFolder} aFolder - The folder with messages being marked as + * junk. + * @param {nsIMsgDBHdr[]} aJunkMsgHdrs - New junk messages. + * @param {nsIMsgDBHdr[]} aGoodMsgHdrs - New good messages. + */ +async function performActionsOnJunkMsgs(aFolder, aJunkMsgHdrs, aGoodMsgHdrs) { + return new Promise((resolve, reject) => { + if (aFolder instanceof Ci.nsIMsgImapMailFolder) { + // need to update IMAP custom flags + if (aJunkMsgHdrs.length) { + let junkMsgKeys = aJunkMsgHdrs.map(hdr => hdr.messageKey); + aFolder.storeCustomKeywords(null, "Junk", "NonJunk", junkMsgKeys); + } + + if (aGoodMsgHdrs.length) { + let goodMsgKeys = aGoodMsgHdrs.map(hdr => hdr.messageKey); + aFolder.storeCustomKeywords(null, "NonJunk", "Junk", goodMsgKeys); + } + } + if (!aJunkMsgHdrs.length) { + resolve(); + return; + } + + let actionParams = determineActionsForJunkMsgs(aFolder); + if (actionParams.markRead) { + aFolder.markMessagesRead(aJunkMsgHdrs, true); + } + + if (!actionParams.junkTargetFolder) { + resolve(); + return; + } + + // @implements {nsIMsgCopyServiceListener} + let listener = { + QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]), + OnStartCopy() {}, + OnProgress(progress, progressMax) {}, + SetMessageKey(key) {}, + GetMessageId() {}, + OnStopCopy(status) { + if (Components.isSuccessCode(status)) { + resolve(); + return; + } + let uri = actionParams.junkTargetFolder.URI; + reject(new Error(`Moving junk to ${uri} failed.`)); + }, + }; + MailServices.copy.copyMessages( + aFolder, + aJunkMsgHdrs, + actionParams.junkTargetFolder, + true /* isMove */, + listener, + top.msgWindow, + true /* allow undo */ + ); + }); +} + +/** + * Helper object storing the list of pending messages to process, + * and implementing junk processing callback. + * + * @param {nsIMsgFolder} aFolder - The folder with messages to be analyzed for junk. + * @param {integer} aTotalMessages - Number of messages to process, used for + * progress report only. + */ + +function MessageClassifier(aFolder, aTotalMessages) { + this.mFolder = aFolder; + this.mJunkMsgHdrs = []; + this.mGoodMsgHdrs = []; + this.mMessages = {}; + this.mMessageQueue = []; + this.mTotalMessages = aTotalMessages; + this.mProcessedMessages = 0; + this.firstMessage = true; + this.lastStatusTime = Date.now(); +} + +/** + * @implements {nsIJunkMailClassificationListener} + */ +MessageClassifier.prototype = { + /** + * Starts the message classification process for a message. If the message + * sender's address is whitelisted, the message is skipped. + * + * @param {nsIMsgDBHdr} aMsgHdr - The header of the message to classify. + * @param {nsISpamSettings} aSpamSettings - The object with information about + * whitelists + */ + analyzeMessage(aMsgHdr, aSpamSettings) { + var junkscoreorigin = aMsgHdr.getStringProperty("junkscoreorigin"); + if (junkscoreorigin == "user") { + // don't override user-set junk status + return; + } + + // check whitelisting + if (aSpamSettings.checkWhiteList(aMsgHdr)) { + // message is ham from whitelist + var db = aMsgHdr.folder.msgDatabase; + db.setStringProperty( + aMsgHdr.messageKey, + "junkscore", + Ci.nsIJunkMailPlugin.IS_HAM_SCORE + ); + db.setStringProperty(aMsgHdr.messageKey, "junkscoreorigin", "whitelist"); + this.mGoodMsgHdrs.push(aMsgHdr); + return; + } + + let messageURI = aMsgHdr.folder.generateMessageURI(aMsgHdr.messageKey); + this.mMessages[messageURI] = aMsgHdr; + if (this.firstMessage) { + this.firstMessage = false; + MailServices.junk.classifyMessage(messageURI, top.msgWindow, this); + } else { + this.mMessageQueue.push(messageURI); + } + }, + + /** + * Callback function from nsIJunkMailPlugin with classification results. + * + * @param {string} aClassifiedMsgURI - URI of classified message. + * @param {integer} aClassification - Junk classification (0: UNCLASSIFIED, 1: GOOD, 2: JUNK) + * @param {integer} aJunkPercent - 0 - 100 indicator of junk likelihood, + * with 100 meaning probably junk. + * @see {nsIJunkMailClassificationListener} + */ + async onMessageClassified(aClassifiedMsgURI, aClassification, aJunkPercent) { + if (!aClassifiedMsgURI) { + // Ignore end of batch. + return; + } + var score = + aClassification == Ci.nsIJunkMailPlugin.JUNK + ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE + : Ci.nsIJunkMailPlugin.IS_HAM_SCORE; + const statusDisplayInterval = 1000; // milliseconds between status updates + + // set these props via the db (instead of the message header + // directly) so that the nsMsgDBView knows to update the UI + // + var msgHdr = this.mMessages[aClassifiedMsgURI]; + var db = msgHdr.folder.msgDatabase; + db.setStringProperty(msgHdr.messageKey, "junkscore", score); + db.setStringProperty(msgHdr.messageKey, "junkscoreorigin", "plugin"); + db.setStringProperty(msgHdr.messageKey, "junkpercent", aJunkPercent); + + if (aClassification == Ci.nsIJunkMailPlugin.JUNK) { + this.mJunkMsgHdrs.push(msgHdr); + } else if (aClassification == Ci.nsIJunkMailPlugin.GOOD) { + this.mGoodMsgHdrs.push(msgHdr); + } + + var nextMsgURI = this.mMessageQueue.shift(); + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + + if (nextMsgURI) { + ++this.mProcessedMessages; + if (Date.now() > this.lastStatusTime + statusDisplayInterval) { + this.lastStatusTime = Date.now(); + var percentDone = 0; + if (this.mTotalMessages) { + percentDone = Math.round( + (this.mProcessedMessages * 100) / this.mTotalMessages + ); + } + top.window.MsgStatusFeedback.showStatusString( + bundle.formatStringFromName("junkAnalysisPercentComplete", [ + percentDone + "%", + ]) + ); + } + MailServices.junk.classifyMessage(nextMsgURI, top.msgWindow, this); + } else { + top.window.MsgStatusFeedback.showStatusString( + bundle.GetStringFromName("processingJunkMessages") + ); + await performActionsOnJunkMsgs( + this.mFolder, + this.mJunkMsgHdrs, + this.mGoodMsgHdrs + ); + setTimeout(() => { + top.window.MsgStatusFeedback.showStatusString(""); + }, 500); + } + }, +}; + +/** + * Filter all messages in the current folder for junk + */ +async function filterFolderForJunk() { + await processFolderForJunk(true); +} + +/** + * Filter selected messages in the current folder for junk + */ +async function analyzeMessagesForJunk() { + await processFolderForJunk(false); +} + +/** + * Filter messages in the current folder for junk + * + * @param {boolean} aAll - true to filter all messages, else filter selection. + */ +async function processFolderForJunk(aAll) { + let indices; + if (aAll) { + // need to expand all threads, so we analyze everything + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + var treeView = gDBView.QueryInterface(Ci.nsITreeView); + var count = treeView.rowCount; + if (!count) { + return; + } + } else { + indices = + AppConstants.MOZ_APP_NAME == "seamonkey" + ? window.GetSelectedIndices(gDBView) + : window.threadTree?.selectedIndices; + if (!indices || !indices.length) { + return; + } + } + let totalMessages = aAll ? count : indices.length; + + // retrieve server and its spam settings via the header of an arbitrary message + let tmpMsgURI; + for (let i = 0; i < totalMessages; i++) { + let index = aAll ? i : indices[i]; + try { + tmpMsgURI = gDBView.getURIForViewIndex(index); + break; + } catch (e) { + // dummy headers will fail, so look for another + continue; + } + } + if (!tmpMsgURI) { + return; + } + + let tmpMsgHdr = + MailServices.messageServiceFromURI(tmpMsgURI).messageURIToMsgHdr(tmpMsgURI); + let spamSettings = tmpMsgHdr.folder.server.spamSettings; + + // create a classifier instance to classify messages in the folder. + let msgClassifier = new MessageClassifier(tmpMsgHdr.folder, totalMessages); + + for (let i = 0; i < totalMessages; i++) { + let index = aAll ? i : indices[i]; + try { + let msgURI = gDBView.getURIForViewIndex(index); + let msgHdr = + MailServices.messageServiceFromURI(msgURI).messageURIToMsgHdr(msgURI); + msgClassifier.analyzeMessage(msgHdr, spamSettings); + } catch (ex) { + // blow off errors here - dummy headers will fail + } + } + if (msgClassifier.firstMessage) { + // the async plugin was not used, maybe all whitelisted? + await performActionsOnJunkMsgs( + msgClassifier.mFolder, + msgClassifier.mJunkMsgHdrs, + msgClassifier.mGoodMsgHdrs + ); + } +} + +/** + * Delete junk messages in the current folder. This provides the guarantee that + * the method will be synchronous if no messages are deleted. + * + * @returns {integer} The number of messages deleted. + */ +function deleteJunkInFolder() { + // use direct folder commands if possible so we don't mess with the selection + let selectedFolder = gViewWrapper.displayedFolder; + if (!selectedFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { + let junkMsgHdrs = []; + for (let msgHdr of gDBView.msgFolder.messages) { + let junkScore = msgHdr.getStringProperty("junkscore"); + if (junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE) { + junkMsgHdrs.push(msgHdr); + } + } + + if (junkMsgHdrs.length) { + gDBView.msgFolder.deleteMessages( + junkMsgHdrs, + top.msgWindow, + false, + false, + null, + true + ); + } + return junkMsgHdrs.length; + } + + // Folder is virtual, let the view do the work (but we lose selection) + + // need to expand all threads, so we find everything + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + + var treeView = gDBView.QueryInterface(Ci.nsITreeView); + var count = treeView.rowCount; + if (!count) { + return 0; + } + + var treeSelection = treeView.selection; + + var clearedSelection = false; + + // select the junk messages + var messageUri; + let numMessagesDeleted = 0; + for (let i = 0; i < count; ++i) { + try { + messageUri = gDBView.getURIForViewIndex(i); + } catch (ex) { + continue; // blow off errors for dummy rows + } + let msgHdr = + MailServices.messageServiceFromURI(messageUri).messageURIToMsgHdr( + messageUri + ); + let junkScore = msgHdr.getStringProperty("junkscore"); + var isJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE; + // if the message is junk, select it. + if (isJunk) { + // only do this once + if (!clearedSelection) { + // clear the current selection + // since we will be deleting all selected messages + treeSelection.clearSelection(); + clearedSelection = true; + treeSelection.selectEventsSuppressed = true; + } + treeSelection.rangedSelect(i, i, true /* augment */); + numMessagesDeleted++; + } + } + + // if we didn't clear the selection + // there was no junk, so bail. + if (!clearedSelection) { + return 0; + } + + treeSelection.selectEventsSuppressed = false; + // delete the selected messages + // + // We'll leave no selection after the delete + if ("gNextMessageViewIndexAfterDelete" in window) { + window.gNextMessageViewIndexAfterDelete = 0xffffffff; // nsMsgViewIndex_None + } + gDBView.doCommand(Ci.nsMsgViewCommandType.deleteMsg); + treeSelection.clearSelection(); + return numMessagesDeleted; +} diff --git a/comm/mailnews/base/content/junkLog.js b/comm/mailnews/base/content/junkLog.js new file mode 100644 index 0000000000..7f2b73edf7 --- /dev/null +++ b/comm/mailnews/base/content/junkLog.js @@ -0,0 +1,48 @@ +/* -*- Mode: JavaScript; 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 { MailE10SUtils } = ChromeUtils.import( + "resource:///modules/MailE10SUtils.jsm" +); + +var gLogView; +var gLogFile; + +window.addEventListener("DOMContentLoaded", onLoad); + +function onLoad() { + gLogView = document.getElementById("logView"); + gLogView.browsingContext.allowJavascript = false; // for security, disable JS + + gLogView.addEventListener("load", () => { + addStyling(); + }); + + gLogFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + gLogFile.append("junklog.html"); + if (gLogFile.exists()) { + MailE10SUtils.loadURI(gLogView, Services.io.newFileURI(gLogFile).spec); + } else { + addStyling(); // set style for initial about:blank + } +} + +function clearLog() { + if (gLogFile.exists()) { + gLogFile.remove(false); + gLogView.setAttribute("src", "about:blank"); // we don't have a log file to show + } +} + +function addStyling() { + let style = gLogView.contentDocument.createElement("style"); + gLogView.contentDocument.head.appendChild(style); + style.sheet.insertRule( + `@media (prefers-color-scheme: dark) { + :root { scrollbar-color: rgba(249, 249, 250, .4) rgba(20, 20, 25, .3);} + body { color: #f9f9fa; } + }` + ); +} diff --git a/comm/mailnews/base/content/junkLog.xhtml b/comm/mailnews/base/content/junkLog.xhtml new file mode 100644 index 0000000000..28c7071c4e --- /dev/null +++ b/comm/mailnews/base/content/junkLog.xhtml @@ -0,0 +1,57 @@ + + + + + + + + + + &adaptiveJunkLog.title; + + + + + + + + + diff --git a/comm/mailnews/base/content/markByDate.js b/comm/mailnews/base/content/markByDate.js new file mode 100644 index 0000000000..672897f1e1 --- /dev/null +++ b/comm/mailnews/base/content/markByDate.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from dateFormat.js */ + +var MILLISECONDS_PER_HOUR = 60 * 60 * 1000; +var MICROSECONDS_PER_DAY = 1000 * MILLISECONDS_PER_HOUR * 24; + +window.addEventListener("load", onLoad); + +document.addEventListener("dialogaccept", onAccept); + +function onLoad() { + var upperDateBox = document.getElementById("upperDate"); + // focus the upper bound control - this is where we expect most users to enter + // a date + upperDateBox.focus(); + + // and give it an initial date - "yesterday" + var initialDate = new Date(); + initialDate.setHours(0); + initialDate.setTime(initialDate.getTime() - MILLISECONDS_PER_HOUR); + // note that this is sufficient - though it is at the end of the previous day, + // we convert it to a date string, and then the time part is truncated + upperDateBox.value = convertDateToString(initialDate); + upperDateBox.select(); // allows to start overwriting immediately +} + +function onAccept() { + // get the times as entered by the user + var lowerDateString = document.getElementById("lowerDate").value; + // the fallback for the lower bound, if not entered, is the "beginning of + // time" (1970-01-01), which actually is simply 0 :) + var prLower = lowerDateString ? convertStringToPRTime(lowerDateString) : 0; + + var upperDateString = document.getElementById("upperDate").value; + var prUpper; + if (upperDateString == "") { + // for the upper bound, the fallback is "today". + var dateThisMorning = new Date(); + dateThisMorning.setMilliseconds(0); + dateThisMorning.setSeconds(0); + dateThisMorning.setMinutes(0); + dateThisMorning.setHours(0); + // Javascript time is in milliseconds, PRTime is in microseconds + prUpper = dateThisMorning.getTime() * 1000; + } else { + prUpper = convertStringToPRTime(upperDateString); + } + + // for the upper date, we have to do a correction: + // if the user enters a date, then she means (hopefully) that all messages sent + // at this day should be marked, too, but the PRTime calculated from this would + // point to the beginning of the day. So we need to increment it by + // [number of micro seconds per day]. This will denote the first microsecond of + // the next day then, which is later used as exclusive boundary + prUpper += MICROSECONDS_PER_DAY; + + markInDatabase(prLower, prUpper); +} + +/** + * M arks all headers in the database, whose time is between the two given + * times, as read. + * + * @param {integer} lower - PRTime for the lower bound (inclusive). + * @param {integer} upper - PRTime for the upper bound (exclusive). + */ +function markInDatabase(lower, upper) { + let messageFolder; + let messageDatabase; + // extract the database + if (window.arguments && window.arguments[0]) { + messageFolder = window.arguments[0]; + messageDatabase = messageFolder.msgDatabase; + } + + if (!messageDatabase) { + dump("markByDate::markInDatabase: there /is/ no database to operate on!\n"); + return; + } + + let searchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + let searchTerms = []; + searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, messageFolder); + + const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib; + const nsMsgSearchOp = Ci.nsMsgSearchOp; + + let searchTerm = searchSession.createTerm(); + searchTerm.attrib = nsMsgSearchAttrib.Date; + searchTerm.op = nsMsgSearchOp.IsBefore; + let value = searchTerm.value; + value.attrib = nsMsgSearchAttrib.Date; + value.date = upper; + searchTerm.value = value; + searchTerms.push(searchTerm); + + if (lower) { + searchTerm = searchSession.createTerm(); + searchTerm.booleanAnd = true; + searchTerm.attrib = nsMsgSearchAttrib.Date; + searchTerm.op = nsMsgSearchOp.IsAfter; + value = searchTerm.value; + value.attrib = nsMsgSearchAttrib.Date; + value.date = lower; + searchTerm.value = value; + searchTerms.push(searchTerm); + } + + let msgEnumerator = messageDatabase.getFilterEnumerator(searchTerms); + let headers = [...msgEnumerator]; + + if (headers.length) { + messageFolder.markMessagesRead(headers, true); + } +} diff --git a/comm/mailnews/base/content/markByDate.xhtml b/comm/mailnews/base/content/markByDate.xhtml new file mode 100644 index 0000000000..7928bf2b59 --- /dev/null +++ b/comm/mailnews/base/content/markByDate.xhtml @@ -0,0 +1,79 @@ + + + + + + + + + + + + &messageMarkByDate.label; + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/menulist-charsetpicker.js b/comm/mailnews/base/content/menulist-charsetpicker.js new file mode 100644 index 0000000000..eb354621a7 --- /dev/null +++ b/comm/mailnews/base/content/menulist-charsetpicker.js @@ -0,0 +1,86 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +// The menulist CE is defined lazily. Create one now to get menulist defined, +// allowing us to inherit from it. +if (!customElements.get("menulist")) { + delete document.createXULElement("menulist"); +} + +// Wrap in a block to prevent leaking to window scope. +{ + /** + * MozMenulistCharsetpicker is a menulist widget that is automatically + * populated with charset selections. + * + * @augments {MozMenuList} + */ + class MozMenulistCharsetpickerViewing extends customElements.get("menulist") { + /** + * Get the charset values to show in the list. + * + * @abstract + * @returns {string[]} an array of character encoding names + */ + get charsetValues() { + return [ + "UTF-8", + "Big5", + "EUC-KR", + "gbk", + "KOI8-R", + "ISO-2022-JP", + "ISO-8859-1", + "ISO-8859-2", + "ISO-8859-7", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + ]; + } + + connectedCallback() { + super.connectedCallback(); + if (this.delayConnectedCallback()) { + return; + } + + if (this.menupopup) { + return; + } + + let charsetBundle = Services.strings.createBundle( + "chrome://messenger/locale/charsetTitles.properties" + ); + this.charsetValues + .map(item => { + let strCharset = charsetBundle.GetStringFromName( + item.toLowerCase() + ".title" + ); + return { label: strCharset, value: item }; + }) + .sort((a, b) => { + if (a.value == "UTF-8" || a.label < b.label) { + return -1; + } else if (b.value == "UTF-8" || a.label > b.label) { + return 1; + } + return 0; + }) + .forEach(item => { + this.appendItem(item.label, item.value); + }); + } + } + customElements.define( + "menulist-charsetpicker-viewing", + MozMenulistCharsetpickerViewing, + { extends: "menulist" } + ); +} diff --git a/comm/mailnews/base/content/msgAccountCentral.js b/comm/mailnews/base/content/msgAccountCentral.js new file mode 100644 index 0000000000..7574c19da3 --- /dev/null +++ b/comm/mailnews/base/content/msgAccountCentral.js @@ -0,0 +1,238 @@ +/* -*- 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/. */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); +var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm"); + +var gSelectedServer = null; +var gSelectedFolder = null; + +window.addEventListener("DOMContentLoaded", OnInit); + +/** + * Set up the whole page depending on the selected folder/account. + * The folder is passed in via the document URL. + */ +function OnInit() { + let el = document.getElementById("setupTitle"); + + document.l10n.setAttributes(el, "setup-title", { + accounts: MailServices.accounts.accounts.length, + }); + + // Selected folder URI is passed as folderURI argument in the query string. + let folderURI = decodeURIComponent( + document.location.search.replace("?folderURI=", "") + ); + gSelectedFolder = folderURI ? MailUtils.getExistingFolder(folderURI) : null; + gSelectedServer = gSelectedFolder ? gSelectedFolder.server : null; + + if (gSelectedServer) { + // Display and collapse items presented to the user based on account type + updateAccountCentralUI(); + } else { + // If there is no gSelectedServer, we are in a brand new profile. + document.getElementById("headerFirstRun").hidden = false; + document.getElementById("headerExistingAccounts").hidden = true; + document.getElementById("version").textContent = Services.appinfo.version; + + // Update the style of the account setup buttons and area. + let accountSection = document.getElementById("accountSetupSection"); + for (let btn of accountSection.querySelectorAll(".btn-hub")) { + btn.classList.remove("btn-inline"); + } + accountSection.classList.remove("zebra"); + + document.getElementById("accountFeaturesSection").hidden = true; + } + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); +} + +/** + * Show items in the AccountCentral page depending on the capabilities + * of the given server. + */ +function updateAccountCentralUI() { + // Set the account name. + document.getElementById("accountName").textContent = + gSelectedServer.prettyName; + + // Update the account logo. + document + .getElementById("accountLogo") + .setAttribute("type", gSelectedServer.type); + + let exceptions = []; + let protocolInfo = null; + try { + protocolInfo = gSelectedServer.protocolInfo; + } catch (e) { + exceptions.push(e); + } + + // Is this a RSS account? + let isRssAccount = gSelectedServer?.type == "rss"; + + // Is this an NNTP account? + let isNNTPAccount = gSelectedServer?.type == "nntp"; + + // Is this a Local Folders account? + const isLocalFoldersAccount = gSelectedServer?.type == "none"; + + document + .getElementById("readButton") + .toggleAttribute("hidden", !getReadMessagesFolder()); + + // It can compose messages. + let showComposeMsgLink = false; + try { + showComposeMsgLink = protocolInfo && protocolInfo.showComposeMsgLink; + document + .getElementById("composeButton") + .toggleAttribute("hidden", !showComposeMsgLink); + } catch (e) { + exceptions.push(e); + } + + // It can subscribe to a newsgroup. + document + .getElementById("nntpSubscriptionButton") + .toggleAttribute("hidden", !isNNTPAccount); + + // It can subscribe to an RSS feed. + document + .getElementById("rssSubscriptionButton") + .toggleAttribute("hidden", !isRssAccount); + + // It can search messages. + let canSearchMessages = false; + try { + canSearchMessages = gSelectedServer && gSelectedServer.canSearchMessages; + document + .getElementById("searchButton") + .toggleAttribute("hidden", !canSearchMessages); + } catch (e) { + exceptions.push(e); + } + + // It can create filters. + let canHaveFilters = false; + try { + canHaveFilters = gSelectedServer && gSelectedServer.canHaveFilters; + document + .getElementById("filterButton") + .toggleAttribute("hidden", !canHaveFilters); + } catch (e) { + exceptions.push(e); + } + + // It can have End-to-end Encryption. + document + .getElementById("e2eButton") + .toggleAttribute( + "hidden", + isNNTPAccount || isRssAccount || isLocalFoldersAccount + ); + + // Check if we collected any exception. + while (exceptions.length) { + console.error( + "Error in setting AccountCentral Items: " + exceptions.pop() + "\n" + ); + } +} + +/** + * For the selected server, check for new messges and display first + * suitable folder (genrally Inbox) for reading. + */ +function readMessages() { + const folder = getReadMessagesFolder(); + top.MsgGetMessage([folder]); + parent.displayFolder(folder); +} + +/** + * Find the folder Read Messages should use. + * + * @returns {?nsIMsgFolder} folder to use, if we have a suitable one. + */ +function getReadMessagesFolder() { + const folder = MailUtils.getInboxFolder(gSelectedServer); + if (folder) { + return folder; + } + // For feeds and nntp, show the first non-trash folder. Don't use Outbox. + return gSelectedServer.rootFolder.descendants.find( + f => + !(f.flags & Ci.nsMsgFolderFlags.Trash) && + !(f.flags & Ci.nsMsgFolderFlags.Queue) + ); +} + +/** + * Open the AccountManager to view the settings for a given account. + * + * @param {string} selectPage - The xhtml file name for the viewing page, + * null for the account main page, other pages are 'am-server.xhtml', + * 'am-copies.xhtml', 'am-offline.xhtml', 'am-addressing.xhtml', + * 'am-smtp.xhtml' + */ +function viewSettings(selectPage) { + window.browsingContext.topChromeWindow.MsgAccountManager( + selectPage, + gSelectedServer + ); +} + +/** + * Bring up the search interface for selected account. + */ +function searchMessages() { + top.document + .getElementById("tabmail") + .currentAbout3Pane.commandController.doCommand("cmd_searchMessages"); +} + +/** + * Open the filters window. + */ +function createMsgFilters() { + window.browsingContext.topChromeWindow.MsgFilters(null, gSelectedFolder); +} + +/** + * Open the subscribe dialog. + */ +function subscribe() { + if (!gSelectedServer) { + return; + } + if (gSelectedServer.type == "rss") { + window.browsingContext.topChromeWindow.openSubscriptionsDialog( + gSelectedServer.rootFolder + ); + } else { + window.browsingContext.topChromeWindow.MsgSubscribe(gSelectedFolder); + } +} + +/** + * Open the target's url on an external browser. + * + * @param {Event} event - The keypress or click event. + */ +function openLink(event) { + event.preventDefault(); + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadURI(Services.io.newURI(event.target.href)); +} diff --git a/comm/mailnews/base/content/msgAccountCentral.xhtml b/comm/mailnews/base/content/msgAccountCentral.xhtml new file mode 100644 index 0000000000..843840c088 --- /dev/null +++ b/comm/mailnews/base/content/msgAccountCentral.xhtml @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.js b/comm/mailnews/base/content/msgSelectOfflineFolders.js new file mode 100644 index 0000000000..18f304bae7 --- /dev/null +++ b/comm/mailnews/base/content/msgSelectOfflineFolders.js @@ -0,0 +1,189 @@ +/* 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/. */ + +/* globals PROTO_TREE_VIEW */ + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + FolderUtils: "resource:///modules/FolderUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +var gFolderTreeView = new PROTO_TREE_VIEW(); + +var gSelectOffline = { + _treeElement: null, + _rollbackMap: new Map(), + + load() { + for (let account of FolderUtils.allAccountsSorted(true)) { + let server = account.incomingServer; + if ( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ) { + continue; + } + if (!server.rootFolder.supportsOffline) { + continue; + } + + gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder)); + } + + this._treeElement = document.getElementById("synchronizeTree"); + // TODO: Expand relevant rows. + this._treeElement.view = gFolderTreeView; + }, + + onKeyPress(aEvent) { + // For now, only do something on space key. + if (aEvent.charCode != aEvent.DOM_VK_SPACE) { + return; + } + + let selection = this._treeElement.view.selection; + let start = {}; + let end = {}; + let numRanges = selection.getRangeCount(); + + for (let range = 0; range < numRanges; range++) { + selection.getRangeAt(range, start, end); + for (let i = start.value; i <= end.value; i++) { + this._toggle(i); + } + } + }, + + onClick(aEvent) { + // We only care about button 0 (left click) events. + if (aEvent.button != 0) { + return; + } + + // We don't want to toggle when clicking on header or tree (scrollbar) or + // on treecol. + if (aEvent.target.nodeName != "treechildren") { + return; + } + + let treeCellInfo = this._treeElement.getCellAt( + aEvent.clientX, + aEvent.clientY + ); + + if (treeCellInfo.row == -1 || treeCellInfo.col.id != "syncCol") { + return; + } + + this._toggle(treeCellInfo.row); + }, + + _toggle(aRow) { + let folder = gFolderTreeView._rowMap[aRow]._folder; + + if (folder.isServer) { + return; + } + + // Save our current state for rollback, if necessary. + if (!this._rollbackMap.has(folder)) { + this._rollbackMap.set( + folder, + folder.getFlag(Ci.nsMsgFolderFlags.Offline) + ); + } + + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); + gFolderTreeView._tree.invalidateRow(aRow); + }, + + onCancel() { + for (let [folder, value] of this._rollbackMap) { + if (value != folder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); + } + } + }, +}; + +window.addEventListener("load", () => gSelectOffline.load()); +document.addEventListener("dialogcancel", () => gSelectOffline.onCancel()); + +/** + * A tree row representing a single folder. + */ +class FolderRow { + constructor(folder, parent = null) { + this._folder = folder; + this._open = false; + this._level = parent ? parent.level + 1 : 0; + this._parent = parent; + this._children = null; + } + + get id() { + return this._folder.URI; + } + + get text() { + return this.getText("folderNameCol"); + } + + getText(aColName) { + switch (aColName) { + case "folderNameCol": + return this._folder.abbreviatedName; + default: + return ""; + } + } + + get open() { + return this._open; + } + + get level() { + return this._level; + } + + getProperties(column) { + let properties = ""; + switch (column?.id) { + case "folderNameCol": + // From folderUtils.jsm. + properties = FolderUtils.getFolderProperties(this._folder, this.open); + break; + case "syncCol": + if (this._folder.isServer) { + return "isServer-true"; + } + properties = "syncCol"; + if (this._folder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + properties += " synchronize-true"; + } + break; + } + return properties; + } + + get children() { + if (this._children === null) { + this._children = []; + for (let subFolder of this._folder.subFolders) { + if (subFolder.supportsOffline) { + this._children.push(new FolderRow(subFolder, this)); + } + } + this._children.sort((a, b) => a._folder.compareSortKeys(b._folder)); + } + return this._children; + } +} diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml new file mode 100644 index 0000000000..88e0ecbd39 --- /dev/null +++ b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + &MsgSelect.label; + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/msgSynchronize.js b/comm/mailnews/base/content/msgSynchronize.js new file mode 100644 index 0000000000..4393a2d301 --- /dev/null +++ b/comm/mailnews/base/content/msgSynchronize.js @@ -0,0 +1,192 @@ +/* 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 { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +var gSynchronizeTree = null; +var gParentMsgWindow; +var gMsgWindow; + +var gInitialFolderStates = {}; + +window.addEventListener("DOMContentLoaded", onLoad); + +document.addEventListener("dialogaccept", syncOkButton); + +function onLoad() { + gParentMsgWindow = window.arguments?.[0]?.msgWindow; + + document.getElementById("syncMail").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_mail" + ); + document.getElementById("syncNews").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_news" + ); + document.getElementById("sendMessage").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_send_unsent" + ); + document.getElementById("workOffline").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_work_offline" + ); +} + +function syncOkButton() { + var syncMail = document.getElementById("syncMail").checked; + var syncNews = document.getElementById("syncNews").checked; + var sendMessage = document.getElementById("sendMessage").checked; + var workOffline = document.getElementById("workOffline").checked; + + Services.prefs.setBoolPref("mailnews.offline_sync_mail", syncMail); + Services.prefs.setBoolPref("mailnews.offline_sync_news", syncNews); + Services.prefs.setBoolPref("mailnews.offline_sync_send_unsent", sendMessage); + Services.prefs.setBoolPref("mailnews.offline_sync_work_offline", workOffline); + + if (syncMail || syncNews || sendMessage || workOffline) { + var offlineManager = Cc[ + "@mozilla.org/messenger/offline-manager;1" + ].getService(Ci.nsIMsgOfflineManager); + if (offlineManager) { + offlineManager.synchronizeForOffline( + syncNews, + syncMail, + sendMessage, + workOffline, + gParentMsgWindow + ); + } + } +} + +function OnSelect() { + top.window.openDialog( + "chrome://messenger/content/msgSelectOfflineFolders.xhtml", + "", + "centerscreen,chrome,modal,titlebar,resizable=yes" + ); + return true; +} + +// All the code below is only used by Seamonkey. + +function selectOkButton() { + return true; +} + +function selectCancelButton() { + for (var resourceValue in gInitialFolderStates) { + let folder = MailUtils.getExistingFolder(resourceValue); + if (gInitialFolderStates[resourceValue]) { + folder.setFlag(Ci.nsMsgFolderFlags.Offline); + } else { + folder.clearFlag(Ci.nsMsgFolderFlags.Offline); + } + } + return true; +} + +function SortSynchronizePane(column, sortKey) { + var node = FindInWindow(window, column); + if (!node) { + dump("Couldn't find sort column\n"); + return; + } + + node.setAttribute("sort", sortKey); + node.setAttribute("sortDirection", "natural"); + var col = gSynchronizeTree.columns[column]; + gSynchronizeTree.view.cycleHeader(col); +} + +function FindInWindow(currentWindow, id) { + var item = currentWindow.document.getElementById(id); + if (item) { + return item; + } + + for (var i = 0; i < currentWindow.frames.length; i++) { + var frameItem = FindInWindow(currentWindow.frames[i], id); + if (frameItem) { + return frameItem; + } + } + + return null; +} + +function onSynchronizeClick(event) { + // we only care about button 0 (left click) events + if (event.button != 0) { + return; + } + + let treeCellInfo = gSynchronizeTree.getCellAt(event.clientX, event.clientY); + if (treeCellInfo.row == -1) { + return; + } + + if (treeCellInfo.childElt == "twisty") { + var folderResource = GetFolderResource(gSynchronizeTree, treeCellInfo.row); + var folder = folderResource.QueryInterface(Ci.nsIMsgFolder); + + if (!gSynchronizeTree.view.isContainerOpen(treeCellInfo.row)) { + var serverType = folder.server.type; + // imap is the only server type that does folder discovery + if (serverType != "imap") { + return; + } + + if (folder.isServer) { + var server = folder.server; + server.performExpand(gMsgWindow); + } else { + var imapFolder = folderResource.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + imapFolder.performExpand(gMsgWindow); + } + } + } + } else if (treeCellInfo.col.id == "syncCol") { + UpdateNode( + GetFolderResource(gSynchronizeTree, treeCellInfo.row), + treeCellInfo.row + ); + } +} + +function onSynchronizeTreeKeyPress(event) { + // for now, only do something on space key + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSynchronizeTree.view.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + UpdateNode(GetFolderResource(gSynchronizeTree, k), k); + } + } +} + +function UpdateNode(resource, row) { + var folder = resource.QueryInterface(Ci.nsIMsgFolder); + + if (folder.isServer) { + return; + } + + if (!(resource.Value in gInitialFolderStates)) { + gInitialFolderStates[resource.Value] = folder.getFlag( + Ci.nsMsgFolderFlags.Offline + ); + } + + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); +} + +function GetFolderResource(aTree, aIndex) { + return aTree.view.getResourceAtIndex(aIndex); +} diff --git a/comm/mailnews/base/content/msgSynchronize.xhtml b/comm/mailnews/base/content/msgSynchronize.xhtml new file mode 100644 index 0000000000..27df730cac --- /dev/null +++ b/comm/mailnews/base/content/msgSynchronize.xhtml @@ -0,0 +1,76 @@ + + + + + + + + + + + &MsgSynchronize.label; + + + + + + &MsgSyncDesc.label; + &MsgSyncDirections.label; + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/newFolderDialog.js b/comm/mailnews/base/content/newFolderDialog.js new file mode 100644 index 0000000000..2476f198b0 --- /dev/null +++ b/comm/mailnews/base/content/newFolderDialog.js @@ -0,0 +1,82 @@ +/* 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 FOLDERS = 1; +var MESSAGES = 2; +var dialog; + +window.addEventListener("DOMContentLoaded", onLoad); +document.addEventListener("dialogaccept", onOK); + +function onLoad() { + var windowArgs = window.arguments[0]; + + dialog = {}; + + dialog.nameField = document.getElementById("name"); + dialog.nameField.focus(); + + // call this when OK is pressed + dialog.okCallback = windowArgs.okCallback; + + // pre select the folderPicker, based on what they selected in the folder pane + dialog.folder = windowArgs.folder; + try { + document + .getElementById("MsgNewFolderPopup") + .selectFolder(windowArgs.folder); + } catch (ex) { + // selected a child folder + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", windowArgs.folder.prettyName); + } + + // can folders contain both folders and messages? + if (windowArgs.dualUseFolders) { + dialog.folderType = FOLDERS | MESSAGES; + + // hide the section when folder contain both folders and messages. + var newFolderTypeBox = document.getElementById("newFolderTypeBox"); + newFolderTypeBox.setAttribute("hidden", "true"); + } else { + // set our folder type by calling the default selected type's oncommand + document.getElementById("folderGroup").selectedItem.doCommand(); + } + + doEnabling(); +} + +function onFolderSelect(event) { + dialog.folder = event.target._folder; + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", dialog.folder.prettyName); +} + +function onOK() { + var name = dialog.nameField.value; + + // do name validity check? + + // make sure name ends in "/" if folder to create can only contain folders + if (dialog.folderType == FOLDERS && !name.endsWith("/")) { + dialog.okCallback(name + "/", dialog.folder); + } else { + dialog.okCallback(name, dialog.folder); + } +} + +function onFoldersOnly() { + dialog.folderType = FOLDERS; +} + +function onMessagesOnly() { + dialog.folderType = MESSAGES; +} + +function doEnabling() { + document.querySelector("dialog").getButton("accept").disabled = + !dialog.nameField.value; +} diff --git a/comm/mailnews/base/content/newFolderDialog.xhtml b/comm/mailnews/base/content/newFolderDialog.xhtml new file mode 100644 index 0000000000..bc141b95f3 --- /dev/null +++ b/comm/mailnews/base/content/newFolderDialog.xhtml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + +%newFolderDTD; ]> + + + + &newFolderDialog.title; + + + + + + + + + + diff --git a/comm/mailnews/base/content/newmailalert.js b/comm/mailnews/base/content/newmailalert.js new file mode 100644 index 0000000000..898c8d6724 --- /dev/null +++ b/comm/mailnews/base/content/newmailalert.js @@ -0,0 +1,109 @@ +/* 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 { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +var gAlertListener = null; + +// NOTE: We must wait until "load" instead of "DOMContentLoaded" because +// otherwise the window height and width is not set in time for +// window.moveTo. +window.addEventListener("load", onAlertLoad); + +function prefillAlertInfo() { + // unwrap all the args.... + // arguments[0] --> The real nsIMsgFolder with new mail. + // arguments[1] --> The keys of new messages. + // arguments[2] --> The nsIObserver to receive window closed event. + let [folder, newMsgKeys, listener] = window.arguments; + newMsgKeys = newMsgKeys.wrappedJSObject; + gAlertListener = listener.QueryInterface(Ci.nsIObserver); + + // Generate an account label string based on the root folder. + var label = document.getElementById("alertTitle"); + var totalNumNewMessages = newMsgKeys.length; + let message = document + .getElementById("bundle_messenger") + .getString("newMailAlert_message"); + label.value = PluralForm.get(totalNumNewMessages, message) + .replace("#1", folder.server.rootFolder.prettyName) + .replace("#2", totalNumNewMessages); + + // handles rendering of new messages. + var folderSummaryInfoEl = document.getElementById("folderSummaryInfo"); + folderSummaryInfoEl.maxMsgHdrsInPopup = 6; + folderSummaryInfoEl.render(folder, newMsgKeys); +} + +function onAlertLoad() { + let dragSession = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService) + .getCurrentSession(); + if (dragSession && dragSession.sourceNode) { + // If a drag session is active, adjusting this window's dimensions causes + // the drag session to be abruptly terminated. To avoid interrupting the + // user, wait until the drag is finished and then set up and show the alert. + dragSession.sourceNode.addEventListener("dragend", () => doOnAlertLoad()); + } else { + doOnAlertLoad(); + } +} + +function doOnAlertLoad() { + prefillAlertInfo(); + + if (!document.getElementById("folderSummaryInfo").hasMessages()) { + closeAlert(); // no mail, so don't bother showing the alert... + return; + } + + // resize the alert based on our current content + let alertTextBox = document.getElementById("alertTextBox"); + let alertImageBox = document.getElementById("alertImageBox"); + alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px"; + + // Show in bottom right, offset by 10px. + // We wait one cycle until the window has resized. + setTimeout(() => { + let x = screen.availLeft + screen.availWidth - window.outerWidth - 10; + let y = screen.availTop + screen.availHeight - window.outerHeight - 10; + window.moveTo(x, y); + }); + + let openTime = Services.prefs.getIntPref("alerts.totalOpenTime"); + var alertContainer = document.getElementById("alertContainer"); + // Don't fade in if the prefers-reduced-motion is true. + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + alertContainer.setAttribute("noanimation", true); + setTimeout(closeAlert, openTime); + return; + } + + alertContainer.addEventListener("animationend", function hideAlert(event) { + if (event.animationName == "fade-in") { + alertContainer.removeEventListener("animationend", hideAlert); + setTimeout(fadeOutAlert, openTime); + } + }); + + alertContainer.setAttribute("fade-in", true); +} + +function fadeOutAlert() { + var alertContainer = document.getElementById("alertContainer"); + alertContainer.addEventListener("animationend", function fadeOut(event) { + if (event.animationName == "fade-out") { + alertContainer.removeEventListener("animationend", fadeOut); + closeAlert(); + } + }); + alertContainer.setAttribute("fade-out", true); +} + +function closeAlert() { + window.close(); + gAlertListener.observe(null, "newmailalert-closed", ""); +} diff --git a/comm/mailnews/base/content/newmailalert.xhtml b/comm/mailnews/base/content/newmailalert.xhtml new file mode 100644 index 0000000000..1346be9c9a --- /dev/null +++ b/comm/mailnews/base/content/newmailalert.xhtml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/newsError.js b/comm/mailnews/base/content/newsError.js new file mode 100644 index 0000000000..693083a166 --- /dev/null +++ b/comm/mailnews/base/content/newsError.js @@ -0,0 +1,48 @@ +/* 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/. */ + +// Error url must be formatted like this: +// about:newserror?r=response&m=messageid&k=messagekey&f=folderuri +// "r" is required; "m" and "f" are optional, but "k" always comes with "m". + +var folderUri; + +function initPage() { + let uri = document.documentURI; + let query = uri.slice(uri.indexOf("?") + 1); + let params = {}; + for (let piece of query.split("&")) { + let [key, value] = piece.split("="); + params[key] = decodeURIComponent(value); + } + + document.getElementById("ngResp").textContent = params.r; + + if ("m" in params) { + document.getElementById("msgId").textContent = params.m; + document.getElementById("msgKey").textContent = params.k; + } else { + document.getElementById("messageIdDesc").hidden = true; + } + + if ("f" in params) { + folderUri = params.f; + } else { + document.getElementById("errorTryAgain").hidden = true; + } +} + +function removeExpired() { + document.location.href = folderUri + "?list-ids"; +} + +let errorTryAgain = document.getElementById("errorTryAgain"); +errorTryAgain.addEventListener("click", function () { + removeExpired(); +}); + +// This must be called in this way, +// see mozilla-central/docshell/resources/content/netError.js after which +// this is modelled. +initPage(); diff --git a/comm/mailnews/base/content/newsError.xhtml b/comm/mailnews/base/content/newsError.xhtml new file mode 100644 index 0000000000..519d7c3897 --- /dev/null +++ b/comm/mailnews/base/content/newsError.xhtml @@ -0,0 +1,57 @@ + + + + +%htmlDTD; + +%netErrorDTD; ]> + + + + &newsError.title; + + + + + +
+
+

&articleNotFound.title;

+
+
+
+

&articleNotFound.desc;

+
+
+
    +
  • &serverResponded.title;
  • +
  • &articleExpired.title;
  • +
  • + &trySearching.title; <> () +
  • +
+
+
+ + +
+ + + + + + + + &rename.label; + + + + diff --git a/comm/mailnews/base/content/retention.js b/comm/mailnews/base/content/retention.js new file mode 100644 index 0000000000..ac40f173b3 --- /dev/null +++ b/comm/mailnews/base/content/retention.js @@ -0,0 +1,52 @@ +/* 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/. */ + +/* globals gLockedPref */ // From either folderProps.js or am-offline.js. + +function initCommonRetentionSettings(retentionSettings) { + document.getElementById("retention.keepMsg").value = + retentionSettings.retainByPreference; + document.getElementById("retention.keepOldMsgMin").value = + retentionSettings.daysToKeepHdrs > 0 + ? retentionSettings.daysToKeepHdrs + : 30; + document.getElementById("retention.keepNewMsgMin").value = + retentionSettings.numHeadersToKeep > 0 + ? retentionSettings.numHeadersToKeep + : 2000; + + document.getElementById("retention.applyToFlagged").checked = + !retentionSettings.applyToFlaggedMessages; +} + +function saveCommonRetentionSettings(aRetentionSettings) { + aRetentionSettings.retainByPreference = + document.getElementById("retention.keepMsg").value; + + aRetentionSettings.daysToKeepHdrs = document.getElementById( + "retention.keepOldMsgMin" + ).value; + aRetentionSettings.numHeadersToKeep = document.getElementById( + "retention.keepNewMsgMin" + ).value; + + aRetentionSettings.applyToFlaggedMessages = !document.getElementById( + "retention.applyToFlagged" + ).checked; + + return aRetentionSettings; +} + +function onCheckKeepMsg() { + if (gLockedPref && gLockedPref["retention.keepMsg"]) { + // if the pref associated with the radiobutton is locked, as indicated + // by the gLockedPref, skip this function. All elements in this + // radiogroup have been locked by the function onLockPreference. + return; + } + + var keepMsg = document.getElementById("retention.keepMsg").value; + document.getElementById("retention.keepOldMsgMin").disabled = keepMsg != 2; + document.getElementById("retention.keepNewMsgMin").disabled = keepMsg != 3; +} diff --git a/comm/mailnews/base/content/shutdownWindow.js b/comm/mailnews/base/content/shutdownWindow.js new file mode 100644 index 0000000000..68c7f6b6f0 --- /dev/null +++ b/comm/mailnews/base/content/shutdownWindow.js @@ -0,0 +1,97 @@ +/* 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 curTaskIndex = 0; +var numTasks = 0; +var stringBundle; + +var msgShutdownService = Cc[ + "@mozilla.org/messenger/msgshutdownservice;1" +].getService(Ci.nsIMsgShutdownService); + +window.addEventListener("DOMContentLoaded", onLoad); +document.addEventListener("dialogcancel", onCancel); + +function onLoad() { + numTasks = msgShutdownService.getNumTasks(); + + stringBundle = document.getElementById("bundle_shutdown"); + document.title = stringBundle.getString("shutdownDialogTitle"); + + updateTaskProgressLabel(1); + updateProgressMeter(0); + + msgShutdownService.startShutdownTasks(); +} + +function updateProgressLabel(inTaskName) { + var curTaskLabel = document.getElementById("shutdownStatus_label"); + curTaskLabel.value = inTaskName; +} + +function updateTaskProgressLabel(inCurTaskNum) { + var taskProgressLabel = document.getElementById("shutdownTask_label"); + taskProgressLabel.value = stringBundle.getFormattedString("taskProgress", [ + inCurTaskNum, + numTasks, + ]); +} + +function updateProgressMeter(inPercent) { + var taskProgressmeter = document.getElementById("shutdown_progressmeter"); + taskProgressmeter.value = inPercent; +} + +function onCancel() { + msgShutdownService.cancelShutdownTasks(); +} + +function nsMsgShutdownTaskListener() { + msgShutdownService.setShutdownListener(this); +} + +nsMsgShutdownTaskListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + window.close(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + updateProgressMeter((aCurTotalProgress / aMaxTotalProgress) * 100); + updateTaskProgressLabel(aCurTotalProgress + 1); + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + // we can ignore this notification + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aMessage) { + updateProgressLabel(aMessage); + } + }, + + onSecurityChange(aWebProgress, aRequest, state) { + // we can ignore this notification + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) { + // we can ignore this notification + }, +}; + +var MsgShutdownTaskListener = new nsMsgShutdownTaskListener(); diff --git a/comm/mailnews/base/content/shutdownWindow.xhtml b/comm/mailnews/base/content/shutdownWindow.xhtml new file mode 100644 index 0000000000..32237dc44f --- /dev/null +++ b/comm/mailnews/base/content/shutdownWindow.xhtml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/subscribe.js b/comm/mailnews/base/content/subscribe.js new file mode 100644 index 0000000000..46eb51ce52 --- /dev/null +++ b/comm/mailnews/base/content/subscribe.js @@ -0,0 +1,496 @@ +/* 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/. */ + +/* globals msgWindow, nsMsgStatusFeedback */ // From mailWindow.js + +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +var gSubscribeTree = null; +var gSubscribeBody = null; +var okCallback = null; +var gChangeTable = {}; +var gServerURI = null; +var gSubscribableServer = null; +var gNameField = null; +var gNameFieldLabel = null; +var gStatusFeedback; +var gSearchView = null; +var gSearchTree = null; +var gSubscribeBundle; + +window.addEventListener("DOMContentLoaded", SubscribeOnLoad); +window.addEventListener("unload", SubscribeOnUnload); + +document.addEventListener("dialogaccept", subscribeOK); +document.addEventListener("dialogcancel", subscribeCancel); + +function Stop() { + if (gSubscribableServer) { + gSubscribableServer.stopPopulating(msgWindow); + } +} + +function SetServerTypeSpecificTextValues() { + if (!gServerURI) { + return; + } + + let serverType = MailUtils.getExistingFolder(gServerURI).server.type; + + // Set the server specific ui elements. + let subscribeLabelString = gSubscribeBundle.getString( + "subscribeLabel-" + serverType + ); + let currentListTab = "currentListTab-" + serverType; + let currentListTabLabel = gSubscribeBundle.getString( + currentListTab + ".label" + ); + let currentListTabAccesskey = gSubscribeBundle.getString( + currentListTab + ".accesskey" + ); + + document + .getElementById("currentListTab") + .setAttribute("label", currentListTabLabel); + document + .getElementById("currentListTab") + .setAttribute("accesskey", currentListTabAccesskey); + document.getElementById("newGroupsTab").collapsed = serverType != "nntp"; // show newGroupsTab only for nntp servers + document + .getElementById("subscribeLabel") + .setAttribute("value", subscribeLabelString); +} + +function onServerClick(aFolder) { + gServerURI = aFolder.server.serverURI; + let serverMenu = document.getElementById("serverMenu"); + serverMenu.menupopup.selectFolder(aFolder); + + SetServerTypeSpecificTextValues(); + ShowCurrentList(); +} + +var MySubscribeListener = { + OnDonePopulating() { + gStatusFeedback._stopMeteors(); + document.getElementById("stopButton").disabled = true; + document.getElementById("refreshButton").disabled = false; + document.getElementById("currentListTab").disabled = false; + document.getElementById("newGroupsTab").disabled = false; + }, +}; + +function SetUpTree(forceToServer, getOnlyNew) { + if (!gServerURI) { + return; + } + + var server = MailUtils.getExistingFolder(gServerURI).server; + try { + CleanUpSearchView(); + gSubscribableServer = server.QueryInterface(Ci.nsISubscribableServer); + + // Enable (or disable) the search related UI. + EnableSearchUI(); + + // Clear out the text field when switching server. + gNameField.value = ""; + + // Since there is no text, switch to the Subscription view. + toggleSubscriptionView(false); + + gSubscribeTree.view = gSubscribableServer.folderView; + gSubscribableServer.subscribeListener = MySubscribeListener; + + document.getElementById("currentListTab").disabled = true; + document.getElementById("newGroupsTab").disabled = true; + document.getElementById("refreshButton").disabled = true; + + gStatusFeedback._startMeteors(); + gStatusFeedback.setStatusString(""); + gStatusFeedback.showStatusString( + gSubscribeBundle.getString("pleaseWaitString") + ); + document.getElementById("stopButton").disabled = false; + + gSubscribableServer.startPopulating(msgWindow, forceToServer, getOnlyNew); + } catch (e) { + if (e.result == 0x80550014) { + // NS_MSG_ERROR_OFFLINE + gStatusFeedback.setStatusString( + gSubscribeBundle.getString("offlineState") + ); + } else { + console.error("Failed to populate subscribe tree: " + e); + gStatusFeedback.setStatusString( + gSubscribeBundle.getString("errorPopulating") + ); + } + Stop(); + } +} + +function SubscribeOnUnload() { + try { + CleanUpSearchView(); + } catch (ex) { + dump("Failed to remove the subscribe tree: " + ex + "\n"); + } + + msgWindow.closeWindow(); +} + +function EnableSearchUI() { + if (gSubscribableServer.supportsSubscribeSearch) { + gNameField.removeAttribute("disabled"); + gNameFieldLabel.removeAttribute("disabled"); + } else { + gNameField.setAttribute("disabled", true); + gNameFieldLabel.setAttribute("disabled", true); + } +} + +function SubscribeOnLoad() { + gSubscribeBundle = document.getElementById("bundle_subscribe"); + + gSubscribeTree = document.getElementById("subscribeTree"); + gSubscribeBody = document.getElementById("subscribeTreeBody"); + gSearchTree = document.getElementById("searchTree"); + gSearchTree = document.getElementById("searchTree"); + gNameField = document.getElementById("namefield"); + gNameFieldLabel = document.getElementById("namefieldlabel"); + + // eslint-disable-next-line no-global-assign + msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow + ); + msgWindow.domWindow = window; + gStatusFeedback = new nsMsgStatusFeedback(); + msgWindow.statusFeedback = gStatusFeedback; + msgWindow.rootDocShell.allowAuth = true; + msgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL; + + // look in arguments[0] for parameters + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].okCallback) { + top.okCallback = window.arguments[0].okCallback; + } + } + + var serverMenu = document.getElementById("serverMenu"); + + gServerURI = null; + let folder = + "folder" in window.arguments[0] ? window.arguments[0].folder : null; + if (folder && folder.server instanceof Ci.nsISubscribableServer) { + serverMenu.menupopup.selectFolder(folder.server.rootMsgFolder); + try { + CleanUpSearchView(); + gSubscribableServer = folder.server.QueryInterface( + Ci.nsISubscribableServer + ); + // Enable (or disable) the search related UI. + EnableSearchUI(); + gServerURI = folder.server.serverURI; + } catch (ex) { + // dump("not a subscribable server\n"); + CleanUpSearchView(); + gSubscribableServer = null; + gServerURI = null; + } + } + + if (!gServerURI) { + // dump("subscribe: no uri\n"); + // dump("xxx todo: use the default news server. right now, I'm just using the first server\n"); + + serverMenu.selectedIndex = 0; + + if (serverMenu.selectedItem) { + gServerURI = serverMenu.selectedItem.getAttribute("id"); + } else { + // dump("xxx todo none of your servers are subscribable\n"); + // dump("xxx todo fix this by disabling subscribe if no subscribable server or, add a CREATE SERVER button, like in 4.x\n"); + return; + } + } + + SetServerTypeSpecificTextValues(); + + ShowCurrentList(); + + gNameField.focus(); +} + +function subscribeOK() { + if (top.okCallback) { + top.okCallback(top.gChangeTable); + } + Stop(); + if (gSubscribableServer) { + gSubscribableServer.subscribeCleanup(); + } +} + +function subscribeCancel() { + Stop(); + if (gSubscribableServer) { + gSubscribableServer.subscribeCleanup(); + } +} + +function SetState(name, state) { + var changed = gSubscribableServer.setState(name, state); + if (changed) { + StateChanged(name, state); + } +} + +function StateChanged(name, state) { + if (gServerURI in gChangeTable) { + if (name in gChangeTable[gServerURI]) { + var oldValue = gChangeTable[gServerURI][name]; + if (oldValue != state) { + delete gChangeTable[gServerURI][name]; + } + } else { + gChangeTable[gServerURI][name] = state; + } + } else { + gChangeTable[gServerURI] = {}; + gChangeTable[gServerURI][name] = state; + } +} + +function InSearchMode() { + return !document.getElementById("searchView").hidden; +} + +function SearchOnClick(event) { + // We only care about button 0 (left click) events. + if (event.button != 0 || event.target.localName != "treechildren") { + return; + } + + let treeCellInfo = gSearchTree.getCellAt(event.clientX, event.clientY); + if (treeCellInfo.row == -1 || treeCellInfo.row > gSearchView.rowCount - 1) { + return; + } + + if (treeCellInfo.col.id == "subscribedColumn2") { + if (event.detail != 2) { + // Single clicked on the check box + // (in the "subscribedColumn2" column) reverse state. + // If double click, do nothing. + ReverseStateFromRow(treeCellInfo.row); + } + } else if (event.detail == 2) { + // Double clicked on a row, reverse state. + ReverseStateFromRow(treeCellInfo.row); + } + + // Invalidate the row. + InvalidateSearchTreeRow(treeCellInfo.row); +} + +function ReverseStateFromRow(aRow) { + // To determine if the row is subscribed or not, + // we get the properties for the "subscribedColumn2" cell in the row + // and look for the "subscribed" property. + // If the "subscribed" string is in the list of properties + // we are subscribed. + let col = gSearchTree.columns.nameColumn2; + let name = gSearchView.getCellValue(aRow, col); + let isSubscribed = gSubscribableServer.isSubscribed(name); + SetStateFromRow(aRow, !isSubscribed); +} + +function SetStateFromRow(row, state) { + var col = gSearchTree.columns.nameColumn2; + var name = gSearchView.getCellValue(row, col); + SetState(name, state); +} + +function SetSubscribeState(state) { + try { + // We need to iterate over the tree selection, and set the state for + // all rows in the selection. + var inSearchMode = InSearchMode(); + var view = inSearchMode ? gSearchView : gSubscribeTree.view; + var colId = inSearchMode ? "nameColumn2" : "nameColumn"; + + var sel = view.selection; + for (var i = 0; i < sel.getRangeCount(); ++i) { + var start = {}, + end = {}; + sel.getRangeAt(i, start, end); + for (var k = start.value; k <= end.value; ++k) { + if (inSearchMode) { + SetStateFromRow(k, state); + } else { + let name = view.getCellValue(k, gSubscribeTree.columns[colId]); + SetState(name, state, k); + } + } + } + + if (inSearchMode) { + // Force a repaint. + InvalidateSearchTree(); + } else { + gSubscribeTree.invalidate(); + } + } catch (ex) { + dump("SetSubscribedState failed: " + ex + "\n"); + } +} + +function ReverseStateFromNode(row) { + let name = gSubscribeTree.view.getCellValue( + row, + gSubscribeTree.columns.nameColumn + ); + SetState(name, !gSubscribableServer.isSubscribed(name), row); +} + +function SubscribeOnClick(event) { + // We only care about button 0 (left click) events. + if (event.button != 0 || event.target.localName != "treechildren") { + return; + } + + let treeCellInfo = gSubscribeTree.getCellAt(event.clientX, event.clientY); + if ( + treeCellInfo.row == -1 || + treeCellInfo.row > gSubscribeTree.view.rowCount - 1 + ) { + return; + } + + if (event.detail == 2) { + // Only toggle subscribed state when double clicking something + // that isn't a container. + if (!gSubscribeTree.view.isContainer(treeCellInfo.row)) { + ReverseStateFromNode(treeCellInfo.row); + } + } else if (event.detail == 1) { + // If the user single clicks on the subscribe check box, we handle it here. + if (treeCellInfo.col.id == "subscribedColumn") { + ReverseStateFromNode(treeCellInfo.row); + } + } +} + +function Refresh() { + // Clear out the textfield's entry. + gNameField.value = ""; + + var newGroupsTab = document.getElementById("newGroupsTab"); + SetUpTree(true, newGroupsTab.selected); +} + +function ShowCurrentList() { + // Clear out the textfield's entry on call of Refresh(). + gNameField.value = ""; + + // Make sure the current list tab is selected. + document.getElementById("subscribeTabs").selectedIndex = 0; + + // Try loading the hostinfo before talk to server. + SetUpTree(false, false); +} + +function ShowNewGroupsList() { + // Clear out the textfield's entry. + gNameField.value = ""; + + // Make sure the new groups tab is selected. + document.getElementById("subscribeTabs").selectedIndex = 1; + + // Force it to talk to the server and get new groups. + SetUpTree(true, true); +} + +function InvalidateSearchTreeRow(row) { + gSearchTree.invalidateRow(row); +} + +function InvalidateSearchTree() { + gSearchTree.invalidate(); +} + +/** + * Toggle the tree panel in the dialog between search view and subscribe view. + * + * @param {boolean} toggle - If true, show the search view else show the + * subscribe view. + */ +function toggleSubscriptionView(toggle) { + document.getElementById("subscribeView").hidden = toggle; + document.getElementById("searchView").hidden = !toggle; +} + +function Search() { + let searchValue = gNameField.value; + if ( + searchValue.length && + gSubscribableServer && + gSubscribableServer.supportsSubscribeSearch + ) { + toggleSubscriptionView(true); + gSubscribableServer.setSearchValue(searchValue); + + if (!gSearchView && gSubscribableServer) { + gSearchView = gSubscribableServer.QueryInterface(Ci.nsITreeView); + gSearchView.selection = null; + gSearchTree.view = gSearchView; + } + return; + } + toggleSubscriptionView(false); +} + +function CleanUpSearchView() { + if (gSearchView) { + gSearchView.selection = null; + gSearchView = null; + } +} + +function onSearchTreeKeyPress(event) { + // For now, only do something on space key. + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSearchView.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + ReverseStateFromRow(k); + } + + // Force a repaint. + InvalidateSearchTree(); + } +} + +function onSubscribeTreeKeyPress(event) { + // For now, only do something on space key. + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSubscribeTree.view.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + ReverseStateFromNode(k); + } + } +} diff --git a/comm/mailnews/base/content/subscribe.xhtml b/comm/mailnews/base/content/subscribe.xhtml new file mode 100644 index 0000000000..f2838b95e2 --- /dev/null +++ b/comm/mailnews/base/content/subscribe.xhtml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + &subscribeDialog.title; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/virtualFolderListEdit.js b/comm/mailnews/base/content/virtualFolderListEdit.js new file mode 100644 index 0000000000..775d3ba1c7 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderListEdit.js @@ -0,0 +1,206 @@ +/* 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/. */ + +/* globals PROTO_TREE_VIEW */ + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + FolderUtils: "resource:///modules/FolderUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +window.addEventListener("DOMContentLoaded", () => { + gSelectVirtual.load(); +}); + +var gFolderTreeView = new PROTO_TREE_VIEW(); + +var gSelectVirtual = { + _treeElement: null, + _selectedList: new Set(), + + load() { + if (window.arguments[0].searchFolderURIs) { + let srchFolderUriArray = window.arguments[0].searchFolderURIs.split("|"); + for (let uri of srchFolderUriArray) { + this._selectedList.add(MailUtils.getOrCreateFolder(uri)); + } + } + + // Add the top level of the folder tree. + for (let account of FolderUtils.allAccountsSorted(true)) { + let server = account.incomingServer; + if ( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ) { + continue; + } + + gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder)); + } + + // Recursively expand the tree to show all selected folders. + function expandToSelected(row, i) { + hiddenFolders.delete(row._folder); + for (let folder of hiddenFolders) { + if (row._folder.isAncestorOf(folder)) { + gFolderTreeView.toggleOpenState(i); + for (let j = row.children.length - 1; j >= 0; j--) { + expandToSelected(row.children[j], i + j + 1); + } + break; + } + } + } + + let hiddenFolders = new Set(gSelectVirtual._selectedList); + for (let i = gFolderTreeView.rowCount - 1; i >= 0; i--) { + expandToSelected(gFolderTreeView._rowMap[i], i); + } + + this._treeElement = document.getElementById("folderPickerTree"); + this._treeElement.view = gFolderTreeView; + }, + + onKeyPress(aEvent) { + // For now, only do something on space key. + if (aEvent.charCode != aEvent.DOM_VK_SPACE) { + return; + } + + let selection = this._treeElement.view.selection; + let start = {}; + let end = {}; + let numRanges = selection.getRangeCount(); + + for (let range = 0; range < numRanges; range++) { + selection.getRangeAt(range, start, end); + for (let i = start.value; i <= end.value; i++) { + this._toggle(i); + } + } + }, + + onClick(aEvent) { + // We only care about button 0 (left click) events. + if (aEvent.button != 0) { + return; + } + + // We don't want to toggle when clicking on header or tree (scrollbar) or + // on treecol. + if (aEvent.target.nodeName != "treechildren") { + return; + } + + let treeCellInfo = this._treeElement.getCellAt( + aEvent.clientX, + aEvent.clientY + ); + if (treeCellInfo.row == -1 || treeCellInfo.col.id != "selectedCol") { + return; + } + + this._toggle(treeCellInfo.row); + }, + + _toggle(aRow) { + let folder = gFolderTreeView._rowMap[aRow]._folder; + if (this._selectedList.has(folder)) { + this._selectedList.delete(folder); + } else { + this._selectedList.add(folder); + } + + gFolderTreeView._tree.invalidateRow(aRow); + }, + + onAccept() { + // XXX We should just pass the folder objects around... + let uris = [...this._selectedList.values()] + .map(folder => folder.URI) + .join("|"); + + if (window.arguments[0].okCallback) { + window.arguments[0].okCallback(uris); + } + }, +}; + +document.addEventListener("dialogaccept", () => gSelectVirtual.onAccept()); + +/** + * A tree row representing a single folder. + */ +class FolderRow { + constructor(folder, parent = null) { + this._folder = folder; + this._open = false; + this._level = parent ? parent.level + 1 : 0; + this._parent = parent; + this._children = null; + } + + get id() { + return this._folder.URI; + } + + get text() { + return this.getText("folderNameCol"); + } + + getText(aColName) { + switch (aColName) { + case "folderNameCol": + return this._folder.abbreviatedName; + default: + return ""; + } + } + + get open() { + return this._open; + } + + get level() { + return this._level; + } + + getProperties(column) { + let properties = ""; + switch (column?.id) { + case "folderNameCol": + // From folderUtils.jsm. + properties = FolderUtils.getFolderProperties(this._folder, this.open); + break; + case "selectedCol": + properties = "selectedColumn"; + if (gSelectVirtual._selectedList.has(this._folder)) { + properties += " selected-true"; + } + break; + } + return properties; + } + + get children() { + if (this._children === null) { + this._children = []; + for (let subFolder of this._folder.subFolders) { + if (!subFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { + this._children.push(new FolderRow(subFolder, this)); + } + } + this._children.sort((a, b) => a._folder.compareSortKeys(b._folder)); + } + return this._children; + } +} diff --git a/comm/mailnews/base/content/virtualFolderListEdit.xhtml b/comm/mailnews/base/content/virtualFolderListEdit.xhtml new file mode 100644 index 0000000000..326892734a --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderListEdit.xhtml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + &virtualFolderListTitle.title; + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/virtualFolderProperties.js b/comm/mailnews/base/content/virtualFolderProperties.js new file mode 100644 index 0000000000..10429ec3d6 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderProperties.js @@ -0,0 +1,383 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../search/content/searchTerm.js */ + +var gPickedFolder; +var gMailView = null; +var msgWindow; // important, don't change the name of this variable. it's really a global used by commandglue.js +var gSearchTermSession; // really an in memory temporary filter we use to read in and write out the search terms +var gSearchFolderURIs = ""; +var gMessengerBundle = null; +var gFolderBundle = null; +var gDefaultColor = ""; +var gMsgFolder; + +var { FolderTreeProperties } = ChromeUtils.import( + "resource:///modules/FolderTreeProperties.jsm" +); +var { FolderUtils } = ChromeUtils.import("resource:///modules/FolderUtils.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); +var { VirtualFolderHelper } = ChromeUtils.import( + "resource:///modules/VirtualFolderWrapper.jsm" +); + +window.addEventListener("DOMContentLoaded", onLoad); + +document.addEventListener("dialogaccept", onOK); +document.addEventListener("dialogcancel", onCancel); + +function onLoad() { + var windowArgs = window.arguments[0]; + var acceptButton = document.querySelector("dialog").getButton("accept"); + + gMessengerBundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + + gFolderBundle = Services.strings.createBundle( + "chrome://messenger/locale/folderWidgets.properties" + ); + + // call this when OK is pressed + msgWindow = windowArgs.msgWindow; // eslint-disable-line no-global-assign + + initializeSearchWidgets(); + + setSearchScope(Ci.nsMsgSearchScope.offlineMail); + if (windowArgs.editExistingFolder) { + acceptButton.label = document + .querySelector("dialog") + .getAttribute("editFolderAcceptButtonLabel"); + acceptButton.accesskey = document + .querySelector("dialog") + .getAttribute("editFolderAcceptButtonAccessKey"); + InitDialogWithVirtualFolder(windowArgs.folder); + } else { + // we are creating a new virtual folder + acceptButton.label = document + .querySelector("dialog") + .getAttribute("newFolderAcceptButtonLabel"); + acceptButton.accesskey = document + .querySelector("dialog") + .getAttribute("newFolderAcceptButtonAccessKey"); + // it is possible that we were given arguments to pre-fill the dialog with... + gSearchTermSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + + if (windowArgs.searchTerms) { + // then add them to our search session + for (let searchTerm of windowArgs.searchTerms) { + gSearchTermSession.appendTerm(searchTerm); + } + } + if (windowArgs.folder) { + // pre select the folderPicker, based on what they selected in the folder pane + gPickedFolder = windowArgs.folder; + try { + document + .getElementById("msgNewFolderPopup") + .selectFolder(windowArgs.folder); + } catch (ex) { + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", windowArgs.folder.prettyName); + } + + // if the passed in URI is not a server then pre-select it as the folder to search + if (!windowArgs.folder.isServer) { + gSearchFolderURIs = windowArgs.folder.URI; + } + } + + let folderNameField = document.getElementById("name"); + folderNameField.removeAttribute("hidden"); + folderNameField.focus(); + if (windowArgs.newFolderName) { + folderNameField.value = windowArgs.newFolderName; + } + if (windowArgs.searchFolderURIs) { + gSearchFolderURIs = windowArgs.searchFolderURIs; + } + + setupSearchRows(gSearchTermSession.searchTerms); + doEnabling(); // we only need to disable/enable the OK button for new virtual folders + } + + if (typeof windowArgs.searchOnline != "undefined") { + document.getElementById("searchOnline").checked = windowArgs.searchOnline; + } + updateOnlineSearchState(); + updateFoldersCount(); +} + +function setupSearchRows(aSearchTerms) { + if (aSearchTerms && aSearchTerms.length > 0) { + // Load the search terms for the folder. + initializeSearchRows(Ci.nsMsgSearchScope.offlineMail, aSearchTerms); + } else { + onMore(null); + } +} + +function updateOnlineSearchState() { + var enableCheckbox = false; + var checkbox = document.getElementById("searchOnline"); + // only enable the checkbox for selection, for online servers + var srchFolderUriArray = gSearchFolderURIs.split("|"); + if (srchFolderUriArray[0]) { + var realFolder = MailUtils.getOrCreateFolder(srchFolderUriArray[0]); + enableCheckbox = realFolder.server.offlineSupportLevel; // anything greater than 0 is an online server like IMAP or news + } + + if (enableCheckbox) { + checkbox.removeAttribute("disabled"); + } else { + checkbox.setAttribute("disabled", true); + checkbox.checked = false; + } +} + +function InitDialogWithVirtualFolder(aVirtualFolder) { + let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder( + window.arguments[0].folder + ); + gMsgFolder = window.arguments[0].folder; + + let styles = getComputedStyle(document.body); + let folderColors = { + Inbox: styles.getPropertyValue("--folder-color-inbox"), + Sent: styles.getPropertyValue("--folder-color-sent"), + Outbox: styles.getPropertyValue("--folder-color-outbox"), + Drafts: styles.getPropertyValue("--folder-color-draft"), + Trash: styles.getPropertyValue("--folder-color-trash"), + Archive: styles.getPropertyValue("--folder-color-archive"), + Templates: styles.getPropertyValue("--folder-color-template"), + Junk: styles.getPropertyValue("--folder-color-spam"), + Virtual: styles.getPropertyValue("--folder-color-folder-filter"), + RSS: styles.getPropertyValue("--folder-color-rss"), + Newsgroup: styles.getPropertyValue("--folder-color-newsletter"), + }; + gDefaultColor = styles.getPropertyValue("--folder-color-folder"); + + // when editing an existing folder, hide the folder picker that stores the parent location of the folder + document.getElementById("msgNewFolderPicker").collapsed = true; + let items = document.getElementsByClassName("chooseFolderLocation"); + for (let item of items) { + item.setAttribute("hidden", true); + } + let folderNameField = document.getElementById("existingName"); + folderNameField.removeAttribute("hidden"); + + // Show the icon color options. + document.getElementById("iconColorContainer").collapsed = false; + + let folderType = FolderUtils.getSpecialFolderString(gMsgFolder); + if (folderType in folderColors) { + gDefaultColor = folderColors[folderType]; + } + + let colorInput = document.getElementById("color"); + colorInput.value = + FolderTreeProperties.getColor(aVirtualFolder.URI) || gDefaultColor; + colorInput.addEventListener("input", event => { + // Preview the chosen color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + colorInput.value + ); + }); + let resetColorButton = document.getElementById("resetColor"); + resetColorButton.addEventListener("click", function () { + colorInput.value = gDefaultColor; + // Preview the default color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + gDefaultColor + ); + }); + + gSearchFolderURIs = virtualFolderWrapper.searchFolderURIs; + updateFoldersCount(); + document.getElementById("searchOnline").checked = + virtualFolderWrapper.onlineSearch; + gSearchTermSession = virtualFolderWrapper.searchTermsSession; + + setupSearchRows(gSearchTermSession.searchTerms); + + // set the name of the folder + let name = gFolderBundle.formatStringFromName("verboseFolderFormat", [ + aVirtualFolder.prettyName, + aVirtualFolder.server.prettyName, + ]); + folderNameField.setAttribute("value", name); + // update the window title based on the name of the saved search + document.title = gMessengerBundle.formatStringFromName( + "editVirtualFolderPropertiesTitle", + [aVirtualFolder.prettyName] + ); +} + +function onFolderPick(aEvent) { + gPickedFolder = aEvent.target._folder; + document.getElementById("msgNewFolderPopup").selectFolder(gPickedFolder); +} + +function onOK(event) { + var name = document.getElementById("name").value; + var searchOnline = document.getElementById("searchOnline").checked; + + if (!gSearchFolderURIs) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("alertNoSearchFoldersSelected") + ); + event.preventDefault(); + return; + } + + if (window.arguments[0].editExistingFolder) { + // update the search terms + gSearchTermSession.searchTerms = saveSearchTerms( + gSearchTermSession.searchTerms, + gSearchTermSession + ); + // save the settings + let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder( + window.arguments[0].folder + ); + virtualFolderWrapper.searchTerms = gSearchTermSession.searchTerms; + virtualFolderWrapper.searchFolders = gSearchFolderURIs; + virtualFolderWrapper.onlineSearch = searchOnline; + virtualFolderWrapper.cleanUpMessageDatabase(); + + MailServices.accounts.saveVirtualFolders(); + + let color = document.getElementById("color").value; + if (color == gDefaultColor) { + color = undefined; + } + FolderTreeProperties.setColor(gMsgFolder.URI, color); + // Tell 3-pane tabs to update the folder's color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color); + + if (window.arguments[0].onOKCallback) { + window.arguments[0].onOKCallback(); + } + + return; + } + + var uri = gPickedFolder.URI; + if (name && uri) { + // create a new virtual folder + // check to see if we already have a folder with the same name and alert the user if so... + var parentFolder = MailUtils.getOrCreateFolder(uri); + + // sanity check the name based on the logic used by nsMsgBaseUtils.cpp. It can't start with a '.', it can't end with a '.', '~' or ' '. + // it can't contain a ';' or '#'. + if (/^\.|[\.\~ ]$|[\;\#]/.test(name)) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("folderCreationFailed") + ); + event.preventDefault(); + return; + } else if (parentFolder.containsChildNamed(name)) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("folderExists") + ); + event.preventDefault(); + return; + } + + gSearchTermSession.searchTerms = saveSearchTerms( + gSearchTermSession.searchTerms, + gSearchTermSession + ); + VirtualFolderHelper.createNewVirtualFolder( + name, + parentFolder, + gSearchFolderURIs, + gSearchTermSession.searchTerms, + searchOnline + ); + } +} + +function onCancel(event) { + if (gMsgFolder) { + // Clear any previewed color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-preview"); + } +} + +function doEnabling() { + var acceptButton = document.querySelector("dialog").getButton("accept"); + acceptButton.disabled = !document.getElementById("name").value; +} + +function chooseFoldersToSearch() { + // if we have some search folders already, then root the folder picker dialog off the account + // for those folders. Otherwise fall back to the preselectedfolderURI which is the parent folder + // for this new virtual folder. + window.openDialog( + "chrome://messenger/content/virtualFolderListEdit.xhtml", + "", + "chrome,titlebar,modal,centerscreen,resizable", + { + searchFolderURIs: gSearchFolderURIs, + okCallback: onFolderListDialogCallback, + } + ); +} + +// callback routine from chooseFoldersToSearch +function onFolderListDialogCallback(searchFolderURIs) { + gSearchFolderURIs = searchFolderURIs; + updateFoldersCount(); + updateOnlineSearchState(); // we may have changed the server type we are searching... +} + +function updateFoldersCount() { + let srchFolderUriArray = gSearchFolderURIs.split("|"); + let folderCount = gSearchFolderURIs ? srchFolderUriArray.length : 0; + let foldersList = document.getElementById("chosenFoldersCount"); + foldersList.textContent = PluralForm.get( + folderCount, + gMessengerBundle.GetStringFromName("virtualFolderSourcesChosen") + ).replace("#1", folderCount); + if (folderCount > 0) { + let folderNames = []; + for (let folderURI of srchFolderUriArray) { + let folder = MailUtils.getOrCreateFolder(folderURI); + let name = this.gMessengerBundle.formatStringFromName( + "verboseFolderFormat", + [folder.prettyName, folder.server.prettyName] + ); + folderNames.push(name); + } + foldersList.setAttribute("tooltiptext", folderNames.join("\n")); + } else { + foldersList.removeAttribute("tooltiptext"); + } +} + +function onEnterInSearchTerm() { + // stub function called by the core search widget code... + // nothing for us to do here +} diff --git a/comm/mailnews/base/content/virtualFolderProperties.xhtml b/comm/mailnews/base/content/virtualFolderProperties.xhtml new file mode 100644 index 0000000000..07b8283f84 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderProperties.xhtml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + %folderDTD; + + %folderPropsDTD; + + %searchTermDTD; +]> + + + &virtualFolderProperties.title; + + + + + + + + + + + + + + + + + -- cgit v1.2.3