summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/content
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/base/content
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/base/content')
-rw-r--r--comm/mailnews/base/content/.eslintrc.js5
-rw-r--r--comm/mailnews/base/content/dateFormat.js227
-rw-r--r--comm/mailnews/base/content/folder-menupopup.js1238
-rw-r--r--comm/mailnews/base/content/folderProps.js480
-rw-r--r--comm/mailnews/base/content/folderProps.xhtml338
-rw-r--r--comm/mailnews/base/content/jsTreeView.js239
-rw-r--r--comm/mailnews/base/content/junkCommands.js449
-rw-r--r--comm/mailnews/base/content/junkLog.js48
-rw-r--r--comm/mailnews/base/content/junkLog.xhtml57
-rw-r--r--comm/mailnews/base/content/markByDate.js120
-rw-r--r--comm/mailnews/base/content/markByDate.xhtml79
-rw-r--r--comm/mailnews/base/content/menulist-charsetpicker.js86
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.js238
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.xhtml309
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.js189
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.xhtml89
-rw-r--r--comm/mailnews/base/content/msgSynchronize.js192
-rw-r--r--comm/mailnews/base/content/msgSynchronize.xhtml76
-rw-r--r--comm/mailnews/base/content/newFolderDialog.js82
-rw-r--r--comm/mailnews/base/content/newFolderDialog.xhtml107
-rw-r--r--comm/mailnews/base/content/newmailalert.js109
-rw-r--r--comm/mailnews/base/content/newmailalert.xhtml55
-rw-r--r--comm/mailnews/base/content/newsError.js48
-rw-r--r--comm/mailnews/base/content/newsError.xhtml57
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.js43
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.xhtml63
-rw-r--r--comm/mailnews/base/content/retention.js52
-rw-r--r--comm/mailnews/base/content/shutdownWindow.js97
-rw-r--r--comm/mailnews/base/content/shutdownWindow.xhtml53
-rw-r--r--comm/mailnews/base/content/subscribe.js496
-rw-r--r--comm/mailnews/base/content/subscribe.xhtml235
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.js206
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.xhtml84
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.js383
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.xhtml110
35 files changed, 6739 insertions, 0 deletions
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 "<last>".
+ */
+ _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 <menulist>, 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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderProps.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/folderProps.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&folderProps.windowtitle.label;</title>
+ <link rel="localization" href="messenger/folderprops.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/retention.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/folderProps.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="folderPropertiesDialog">
+ <tabbox id="folderPropTabBox">
+ <tabs id="folderPropTabs">
+ <tab id="GeneralTab" label="&generalInfo.label;" />
+ <tab id="Retention" label="&retention.label;" />
+ <tab
+ id="SynchronizationTab"
+ hidefor="pop3,rss,none"
+ label="&folderSynchronizationTab.label;"
+ />
+ <tab
+ id="SharingTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderSharingTab.label;"
+ />
+ <tab
+ id="QuotaTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderQuotaTab.label;"
+ />
+ </tabs>
+ <tabpanels id="folderPropTabPanels">
+ <vbox id="GeneralPanel">
+ <hbox id="nameBox" align="center" class="input-container">
+ <label
+ id="nameLabel"
+ value="&folderProps.name.label;"
+ control="name"
+ accesskey="&folderProps.name.accesskey;"
+ />
+ <hbox align="center" class="input-container">
+ <html:input
+ id="name"
+ type="text"
+ readonly="readonly"
+ oninput="doEnabling();"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ />
+ <label
+ id="colorLabel"
+ value="&folderProps.color.label;"
+ control="color"
+ accesskey="&folderProps.color.accesskey;"
+ />
+ <html:input
+ id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"
+ />
+ <button
+ id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"
+ />
+ </hbox>
+ <label
+ id="locationLabel"
+ value="&folderProps.location.label;"
+ control="location"
+ accesskey="&folderProps.location.accesskey;"
+ />
+ <html:input
+ id="location"
+ type="text"
+ readonly="readonly"
+ class="uri-element input-inline"
+ aria-labelledby="locationLabel"
+ />
+ </hbox>
+ <vbox>
+ <spacer height="2" />
+ <hbox align="center">
+ <label value="&numberOfMessages.label;" />
+ <label id="numberOfMessages" value="&numberUnknown.label;" />
+ <spacer flex="1" />
+ <label value="&sizeOnDisk.label;" />
+ <label id="sizeOnDisk" value="&sizeUnknown.label;" />
+ </hbox>
+ <spacer height="2" />
+ </vbox>
+ <checkbox
+ id="folderIncludeInGlobalSearch"
+ hidefor="nntp"
+ label="&folderIncludeInGlobalSearch.label;"
+ accesskey="&folderIncludeInGlobalSearch.accesskey;"
+ />
+ <checkbox
+ hidefor="pop3,none,nntp"
+ id="folderCheckForNewMessages"
+ label="&folderCheckForNewMessages2.label;"
+ accesskey="&folderCheckForNewMessages2.accesskey;"
+ />
+ <separator class="thin" />
+ <hbox>
+ <description id="folderRebuildSummaryExplanation" flex="1">
+ &folderRebuildSummaryFile.explanation;
+ </description>
+ <vbox>
+ <button
+ id="folderRebuildSummaryButton"
+ label="&folderRebuildSummaryFile2.label;"
+ oncommand="RebuildSummaryInformation();"
+ accesskey="&folderRebuildSummaryFile2.accesskey;"
+ tooltiptext="&folderRebuildSummaryFileTip2.label;"
+ align="center"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+
+ <vbox id="RetentionPanel" align="start">
+ <description hidefor="imap,pop3" class="desc"
+ >&retentionCleanup.label;</description
+ >
+ <description hidefor="pop3,rss,none,nntp" class="desc"
+ >&retentionCleanupImap.label;</description
+ >
+ <description hidefor="imap,rss,none,nntp" class="desc"
+ >&retentionCleanupPop.label;</description
+ >
+
+ <hbox align="center" class="indent">
+ <checkbox
+ wsm_persist="true"
+ id="retention.useDefault"
+ accesskey="&retentionUseAccount.accesskey;"
+ label="&retentionUseAccount.label;"
+ checked="true"
+ oncommand="onUseDefaultRetentionSettings()"
+ />
+ </hbox>
+ <vbox class="indent">
+ <hbox class="indent">
+ <radiogroup
+ wsm_persist="true"
+ id="retention.keepMsg"
+ aria-labelledby="retention.useDefault"
+ >
+ <radio
+ wsm_persist="true"
+ value="1"
+ accesskey="&retentionKeepAll.accesskey;"
+ label="&retentionKeepAll.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepNewMsg"
+ accesskey="&retentionKeepRecent.accesskey;"
+ value="3"
+ label="&retentionKeepRecent.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepNewMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="2000"
+ wsm_persist="true"
+ aria-labelledby="keepNewMsg retention.keepNewMsgMin retention.keepNewMsgMinLabel"
+ />
+ <label
+ value="&message.label;"
+ control="retention.keepNewMsgMin"
+ id="retention.keepNewMsgMinLabel"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepMsg"
+ accesskey="&retentionDeleteMsg.accesskey;"
+ value="2"
+ label="&retentionDeleteMsg.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepOldMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="30"
+ wsm_persist="true"
+ aria-labelledby="keepMsg retention.keepOldMsgMin retention.keepOldMsgMinLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="retention.keepOldMsgMin"
+ id="retention.keepOldMsgMinLabel"
+ />
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <hbox class="indent">
+ <checkbox
+ id="retention.applyToFlagged"
+ wsm_persist="true"
+ label="&retentionApplyToFlagged.label;"
+ accesskey="&retentionApplyToFlagged.accesskey;"
+ observes="retention.keepMsg"
+ checked="true"
+ />
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <vbox id="SyncPanel" align="start">
+ <vbox>
+ <checkbox
+ hidefor="nntp"
+ wsm_persist="true"
+ id="offline.selectForOfflineFolder"
+ label="&offlineFolder.check.label;"
+ accesskey="&offlineFolder.check.accesskey;"
+ />
+ <checkbox
+ hidefor="imap"
+ wsm_persist="true"
+ id="offline.selectForOfflineNewsgroup"
+ label="&selectofflineNewsgroup.check.label;"
+ accesskey="&selectofflineNewsgroup.check.accesskey;"
+ />
+ </vbox>
+ <button
+ hidefor="nntp"
+ label="&offlineFolder.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineFolder.button.accesskey;"
+ id="offline.offlineFolderDownloadButton"
+ orient="right"
+ />
+ <button
+ hidefor="imap"
+ label="&offlineNewsgroup.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineNewsgroup.button.accesskey;"
+ id="offline.offlineNewsgroupDownloadButton"
+ orient="right"
+ />
+ </vbox>
+
+ <vbox id="SharingPanel">
+ <hbox align="start">
+ <label value="&folderType.label;" id="folderTypeLabel" />
+ <label value="" id="folderType.text" />
+ </hbox>
+ <vbox align="start">
+ <label value="" id="folderDescription.text" />
+ <label value=" " />
+ <label
+ value="&permissionsDesc.label;"
+ id="permissionsDescLabel"
+ />
+
+ <description id="folderPermissions.text"></description>
+ </vbox>
+ <vbox id="folderOtherUsers" align="start" hidden="true">
+ <label value=" " />
+ <label value="&folderOtherUsers.label;" />
+ <description id="folderOtherUsersText"></description>
+ </vbox>
+ <spacer flex="1" />
+ <vbox align="start">
+ <button
+ hidefor="pop3,none,rss,nntp"
+ label="&privileges.button.label;"
+ oncommand="onFolderPrivileges();"
+ accesskey="&privileges.button.accesskey;"
+ id="imap.FolderPrivileges"
+ orient="right"
+ />
+ </vbox>
+ </vbox>
+
+ <vbox id="quotaPanel">
+ <label id="folderQuotaStatus" flex="1" />
+ <hbox id="folderQuotaData" hidden="true">
+ <html:ul id="quotaDetails"></html:ul>
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/junkLog.dtd">
+
+<html
+ id="viewLogWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="screenX screenY width height"
+ width="600"
+ height="375"
+ scrolling="false"
+>
+ <head>
+ <title>&adaptiveJunkLog.title;</title>
+ <script defer="defer" src="chrome://messenger/content/junkLog.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ >
+ <vbox flex="1">
+ <hbox>
+ <label value="&adaptiveJunkLogInfo.label;" />
+ <spacer flex="1" />
+ <button
+ label="&clearLog.label;"
+ accesskey="&clearLog.accesskey;"
+ oncommand="clearLog();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox flex="1">
+ <browser
+ id="logView"
+ type="content"
+ disablehistory="true"
+ disablesecurity="true"
+ src="about:blank"
+ autofind="false"
+ flex="1"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/markByDate.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&messageMarkByDate.label;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dateFormat.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/markByDate.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel">
+ <hbox align="center" pack="end">
+ <label
+ id="lowerDateLabel"
+ control="lowerDate"
+ value="&markByDateLower.label;"
+ accesskey="&markByDateLower.accesskey;"
+ />
+ <html:input
+ id="lowerDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="lowerDateLabel"
+ size="11"
+ />
+ </hbox>
+ <hbox align="center" pack="end">
+ <label
+ id="upperDateLabel"
+ control="upperDate"
+ value="&markByDateUpper.label;"
+ accesskey="&markByDateUpper.accesskey;"
+ />
+ <html:input
+ id="upperDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="upperDateLabel"
+ size="11"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountCentral.css" type="text/css"?>
+
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ windowtype="mail:accountcentral"
+ lightweightthemes="true"
+>
+ <head>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountCentral.ftl" />
+
+ <script
+ defer="defer"
+ src="chrome://communicator/content/utilityOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgAccountCentral.js"
+ ></script>
+ </head>
+
+ <body>
+ <main id="accountCentral">
+ <header
+ id="headerFirstRun"
+ class="account-central-header"
+ hidden="hidden"
+ >
+ <img
+ id="brandLogo"
+ src="chrome://branding/content/about-logo.svg"
+ alt=""
+ />
+ <aside>
+ <h1
+ class="account-central-title"
+ data-l10n-id="account-central-title"
+ ></h1>
+ <div class="account-central-version">
+ <span id="version"></span>
+ <a
+ id="releasenotes"
+ data-l10n-id="release-notes"
+ onclick="openAboutDialog();"
+ onkeypress="if (event.key == 'Enter') { openAboutDialog(); }"
+ >
+ <img
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ alt=""
+ />
+ </a>
+ </div>
+ </aside>
+ </header>
+
+ <header
+ id="headerExistingAccounts"
+ class="account-central-header summary-header"
+ >
+ <span id="accountLogo"></span>
+ <aside>
+ <h1 id="accountName"></h1>
+ </aside>
+ <aside class="settings-btn-container">
+ <button
+ id="settingsButton"
+ type="button"
+ data-l10n-id="account-settings"
+ class="btn-link"
+ onclick="viewSettings(null);"
+ ></button>
+ </aside>
+ </header>
+
+ <section id="accountFeaturesSection" class="account-central-section">
+ <aside class="row-container account-options">
+ <button
+ id="readButton"
+ type="button"
+ data-l10n-id="read"
+ class="btn-link btn-inline"
+ onclick="readMessages();"
+ ></button>
+ <button
+ id="nntpSubscriptionButton"
+ type="button"
+ data-l10n-id="nntp-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="rssSubscriptionButton"
+ type="button"
+ data-l10n-id="rss-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="composeButton"
+ type="button"
+ data-l10n-id="compose"
+ class="btn-link btn-inline"
+ onclick="window.browsingContext.topChromeWindow.MsgNewMessage(event);"
+ hidden="hidden"
+ ></button>
+ <button
+ id="searchButton"
+ type="button"
+ data-l10n-id="search"
+ class="btn-link btn-inline"
+ onclick="searchMessages();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="filterButton"
+ type="button"
+ data-l10n-id="filter"
+ class="btn-link btn-inline"
+ onclick="createMsgFilters();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="e2eButton"
+ type="button"
+ data-l10n-id="e2e"
+ class="btn-link btn-inline"
+ onclick="viewSettings('am-e2e.xhtml');"
+ hidden="hidden"
+ ></button>
+ </aside>
+ </section>
+
+ <section
+ id="accountSetupSection"
+ class="account-central-section setup-section zebra"
+ >
+ <h2 id="setupTitle" class="section-title"></h2>
+
+ <aside class="row-container">
+ <button
+ id="setupEmail"
+ type="button"
+ data-l10n-id="email-label"
+ class="btn-hub btn-inline"
+ onclick="top.openAccountSetupTab();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="email-description"></p>
+ </div>
+
+ <button
+ id="setupAddressBook"
+ type="button"
+ data-l10n-id="addressbook-label"
+ class="btn-hub btn-inline"
+ onclick="top.addNewAddressBook('CARDDAV');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="addressbook-description"></p>
+ </div>
+
+ <button
+ id="setupCalendar"
+ type="button"
+ data-l10n-id="calendar-label"
+ class="btn-hub btn-inline"
+ onclick="window.browsingContext.topChromeWindow.cal.window.openCalendarWizard(window);"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="calendar-description"></p>
+ </div>
+
+ <button
+ id="setupChat"
+ type="button"
+ data-l10n-id="chat-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddIMAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="chat-description"></p>
+ </div>
+
+ <button
+ id="setupFilelink"
+ type="button"
+ data-l10n-id="filelink-label"
+ class="btn-hub btn-inline"
+ onclick="top.openOptionsDialog('paneCompose', 'compositionAttachmentsCategory');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="filelink-description"></p>
+ </div>
+
+ <button
+ id="setupFeeds"
+ type="button"
+ data-l10n-id="feeds-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddFeedAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="feeds-description"></p>
+ </div>
+
+ <button
+ id="setupNewsgroups"
+ type="button"
+ data-l10n-id="newsgroups-label"
+ class="btn-hub btn-inline"
+ onclick="top.openNewsgroupAccountWizard();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="newsgroups-description"></p>
+ </div>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="import-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="import-paragraph2"></p>
+ </aside>
+ <aside class="row-container">
+ <button
+ id="importButton"
+ type="button"
+ data-l10n-id="import-label"
+ class="btn-hub btn-inline"
+ onclick="top.toImport();"
+ ></button>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="about-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="about-paragraph"></p>
+ <p
+ id="donationParagraph"
+ data-l10n-id="about-paragraph-consider-donation"
+ >
+ <a
+ href="https://give.thunderbird.net/en-US/?utm_source=account_hub_tb_release&amp;utm_medium=referral&amp;utm_content=paragraph_text"
+ class="donation-link"
+ data-l10n-name="donation-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </p>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="resources-title"></h2>
+
+ <aside class="row-container">
+ <a
+ id="featuresLink"
+ href="https://www.thunderbird.net/en-US/features/"
+ class="resource-link"
+ data-l10n-id="explore-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="supportLink"
+ href="https://support.mozilla.org/products/thunderbird"
+ class="resource-link"
+ data-l10n-id="support-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="involvedLink"
+ href="https://www.thunderbird.net/en-US/get-involved/"
+ class="resource-link"
+ data-l10n-id="involved-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="developerLink"
+ href="https://developer.thunderbird.net/"
+ class="resource-link"
+ data-l10n-id="developer-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </aside>
+ </section>
+ </main>
+ </body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/msgSelectOffline.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+
+<html
+ id="selectOffline"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:selectOffline"
+ lightweightthemes="true"
+ width="450"
+ height="400"
+ persist="width height screenX screenY"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSelect.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSelectOfflineFolders.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+
+ <label class="desc" control="synchronizeTree"
+ >&MsgSelectDesc.label;</label
+ >
+
+ <tree
+ id="synchronizeTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="offline"
+ onkeypress="gSelectOffline.onKeyPress(event);"
+ onclick="gSelectOffline.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ persist="hidden width"
+ label="&MsgSelectItems.label;"
+ primary="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="syncCol"
+ style="flex: 1 auto"
+ persist="hidden width"
+ label="&MsgSelectInd.label;"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+<html
+ id="msgSynchronize"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:synchronizeOffline"
+ lightweightthemes="true"
+ style="height: 22em"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSynchronize.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSynchronize.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="msg-synchronizer" style="width: 63ch">
+ <html:p>&MsgSyncDesc.label;</html:p>
+ <html:p>&MsgSyncDirections.label;</html:p>
+
+ <vbox class="indent" align="start">
+ <checkbox
+ id="syncMail"
+ hidable="true"
+ hidefor="pop3"
+ label="&syncTypeMail.label;"
+ accesskey="&syncTypeMail.accesskey;"
+ />
+ <checkbox
+ id="syncNews"
+ label="&syncTypeNews.label;"
+ accesskey="&syncTypeNews.accesskey;"
+ />
+ </vbox>
+ <vbox align="start">
+ <checkbox
+ id="sendMessage"
+ label="&sendMessage.label;"
+ accesskey="&sendMessage.accesskey;"
+ />
+ <checkbox
+ id="workOffline"
+ label="&workOffline.label;"
+ accesskey="&workOffline.accesskey;"
+ />
+ </vbox>
+ <separator class="thin" />
+ <hbox pack="end">
+ <button
+ id="select"
+ label="&selectButton.label;"
+ accesskey="&selectButton.accesskey;"
+ oncommand="OnSelect();"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % newFolderDTD SYSTEM "chrome://messenger/locale/newFolderDialog.dtd">
+%newFolderDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&newFolderDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <label
+ id="nameLabel"
+ value="&name.label;"
+ accesskey="&name.accesskey;"
+ control="name"
+ />
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+
+ <separator />
+
+ <label
+ value="&description.label;"
+ accesskey="&description.accesskey;"
+ control="msgNewFolderPicker"
+ />
+
+ <menulist
+ id="msgNewFolderPicker"
+ class="folderMenuItem"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="MsgNewFolderPopup"
+ showFileHereLabel="true"
+ class="menulist-menupopup"
+ mode="newFolder"
+ oncommand="onFolderSelect(event)"
+ />
+ </menulist>
+
+ <vbox id="newFolderTypeBox">
+ <separator class="thin" />
+
+ <label value="&folderRestriction1.label;" />
+ <label value="&folderRestriction2.label;" />
+
+ <separator class="thin" />
+
+ <radiogroup id="folderGroup" orient="horizontal" class="indent">
+ <radio oncommand="onFoldersOnly();" label="&foldersOnly.label;" />
+ <radio
+ oncommand="onMessagesOnly();"
+ label="&messagesOnly.label;"
+ selected="true"
+ />
+ </radiogroup>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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);
+
+ // <folder-summary> 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 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/newmailalert.css" type="text/css"?>
+
+<html
+ id="newMailAlertNotification"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="alert:alert"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newmailalert.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ role="alert"
+ >
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox id="alertContainer" align="start">
+ <hbox id="alertImageBox" align="center" pack="center">
+ <html:img
+ id="alertImage"
+ alt=""
+ src="chrome://branding/content/icon64.png"
+ srcset="chrome://branding/content/icon128.png 2x"
+ />
+ </hbox>
+
+ <vbox id="alertTextBox">
+ <label id="alertTitle" />
+ <separator id="alertSeparator" />
+ <folder-summary id="folderSummaryInfo" />
+ </vbox>
+
+ <toolbarbutton
+ id="closeButton"
+ class="close-icon"
+ onclick="closeAlert();"
+ />
+ </hbox>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+%htmlDTD;
+<!ENTITY % netErrorDTD
+ SYSTEM "chrome://messenger/locale/newsError.dtd">
+%netErrorDTD; ]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:" />
+ <title>&newsError.title;</title>
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/aboutNetError.css"
+ type="text/css"
+ media="all"
+ />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link
+ rel="icon"
+ type="image/png"
+ id="favicon"
+ href="chrome://global/skin/icons/warning.svg"
+ />
+ </head>
+ <body>
+ <div id="errorPageContainer">
+ <div id="errorTitle">
+ <h1 id="errorTitleText">&articleNotFound.title;</h1>
+ </div>
+ <div id="errorLongContent">
+ <div id="errorShortDesc">
+ <p id="errorShortDescText"><b>&articleNotFound.desc;</b></p>
+ </div>
+ <div id="errorLongDesc">
+ <ul>
+ <li>&serverResponded.title; <span id="ngResp" /></li>
+ <li>&articleExpired.title;</li>
+ <li id="messageIdDesc">
+ &trySearching.title; &lt;<span id="msgId" />&gt; (<span
+ id="msgKey"
+ />)
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- This button really means "remove all expired articles", but we use
+ the "errorTryAgain" id to piggyback on toolkit's CSS. -->
+ <button id="errorTryAgain">&removeExpiredArticles.title;</button>
+ </div>
+ <script src="chrome://messenger/content/newsError.js" />
+ </body>
+</html>
diff --git a/comm/mailnews/base/content/renameFolderDialog.js b/comm/mailnews/base/content/renameFolderDialog.js
new file mode 100644
index 0000000000..8525e0ba93
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.js
@@ -0,0 +1,43 @@
+/* -*- 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/. */
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOK);
+
+var dialog;
+function onLoad() {
+ var windowArgs = window.arguments[0];
+
+ dialog = {};
+
+ dialog.OKButton = document.querySelector("dialog").getButton("accept");
+
+ dialog.nameField = document.getElementById("name");
+ dialog.nameField.value = windowArgs.name;
+ dialog.nameField.select();
+ 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.preselectedFolderURI = windowArgs.preselectedURI;
+
+ doEnabling();
+}
+
+function onOK() {
+ dialog.okCallback(dialog.nameField.value, dialog.preselectedFolderURI);
+}
+
+function doEnabling() {
+ if (dialog.nameField.value) {
+ if (dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = false;
+ }
+ } else if (!dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = true;
+ }
+}
diff --git a/comm/mailnews/base/content/renameFolderDialog.xhtml b/comm/mailnews/base/content/renameFolderDialog.xhtml
new file mode 100644
index 0000000000..f81ba88db0
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: xml; 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/renameFolderDialog.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&renameFolderDialog.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/renameFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <html:label
+ id="nameLabel"
+ for="name"
+ style="display: inline-block"
+ accesskey="&rename.accesskey;"
+ >&rename.label;</html:label
+ >
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ width="600"
+ height="150"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/shutdownWindow.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="cancel">
+ <stringbundle
+ id="bundle_shutdown"
+ src="chrome://messenger/locale/shutdownWindow.properties"
+ />
+
+ <vbox align="center">
+ <label id="shutdownStatus_label" value="" />
+ <separator class="thin" />
+ </vbox>
+
+ <html:progress id="shutdown_progressmeter" max="100" />
+
+ <vbox align="center">
+ <label id="shutdownTask_label" value="" />
+ <separator class="thick" />
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/subscribe.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/searchBox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/subscribe.dtd">
+
+<html
+ id="subscribeWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="width height screenX screenY"
+ lightweightthemes="true"
+ windowtype="mailnews:subscribe"
+ scrolling="false"
+>
+ <head>
+ <title>&subscribeDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/mailWindow.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/subscribe.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_subscribe"
+ src="chrome://messenger/locale/subscribe.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox flex="1">
+ <hbox>
+ <vbox>
+ <hbox pack="end" align="center" flex="1">
+ <label
+ value="&server.label;"
+ accesskey="&server.accesskey;"
+ control="serverMenu"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="namefieldlabel"
+ accesskey="&namefield.accesskey;"
+ value="&namefield.label;"
+ control="namefield"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="serverMenu" flex="1" class="folderMenuItem">
+ <menupopup
+ is="folder-menupopup"
+ mode="subscribe"
+ expandFolders="false"
+ oncommand="onServerClick(event.target._folder);"
+ />
+ </menulist>
+ <search-textbox
+ id="namefield"
+ class="themeableSearchBox"
+ flex="1"
+ timeout="300"
+ aria-controls="subscribeTree"
+ oncommand="Search();"
+ />
+ </vbox>
+ </hbox>
+ <spacer />
+ <separator class="thin" />
+ <spacer />
+ <vbox flex="1">
+ <tabbox flex="1" handleCtrlTab="false">
+ <tabs id="subscribeTabs">
+ <tab
+ id="currentListTab"
+ selected="true"
+ onclick="if (!event.target.disabled) ShowCurrentList()"
+ oncommand="ShowCurrentList()"
+ />
+ <tab
+ id="newGroupsTab"
+ label="&newGroupsTab.label;"
+ accesskey="&newGroupsTab.accesskey;"
+ onclick="if (!event.target.disabled) ShowNewGroupsList()"
+ oncommand="ShowNewGroupsList()"
+ />
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="treepanel" flex="1" orient="vertical">
+ <label id="subscribeLabel" />
+ <hbox flex="1">
+ <hbox id="subscribeView" flex="1">
+ <tree
+ id="subscribeTree"
+ treelines="true"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ onkeypress="onSubscribeTreeKeyPress(event);"
+ onclick="SubscribeOnClick(event);"
+ >
+ <treecols id="theSubscribeColumns">
+ <treecol
+ id="nameColumn"
+ primary="true"
+ hideheader="true"
+ crop="center"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn"
+ type="checkbox"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="subscribeTreeBody" />
+ </tree>
+ </hbox>
+ <hbox id="searchView" flex="1" hidden="true">
+ <tree
+ id="searchTree"
+ flex="1"
+ disableKeyNavigation="true"
+ hidecolumnpicker="true"
+ onkeypress="onSearchTreeKeyPress(event);"
+ onclick="SearchOnClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="nameColumn2"
+ primary="true"
+ hideheader="true"
+ sortDirection="ascending"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn2"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="searchTreeBody" />
+ </tree>
+ </hbox>
+ <vbox>
+ <button
+ id="subscribe"
+ label="&subscribeButton.label;"
+ accesskey="&subscribeButton.accesskey;"
+ oncommand="SetSubscribeState(true)"
+ />
+ <button
+ id="unsubscribe"
+ label="&unsubscribeButton.label;"
+ accesskey="&unsubscribeButton.accesskey;"
+ oncommand="SetSubscribeState(false)"
+ />
+ <button
+ id="refreshButton"
+ label="&refreshButton.label;"
+ accesskey="&refreshButton.accesskey;"
+ oncommand="Refresh()"
+ />
+ <button
+ id="stopButton"
+ label="&stopButton.label;"
+ accesskey="&stopButton.accesskey;"
+ oncommand="Stop()"
+ disabled="true"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ <hbox>
+ <hbox id="statusContainerBox" flex="1">
+ <label id="statusText" class="statusbarpanel" crop="end" flex="1" />
+ <hbox
+ id="statusbar-progresspanel"
+ class="statusbarpanel statusbarpanel-progress"
+ collapsed="true"
+ pack="end"
+ flex="1"
+ >
+ <html:progress
+ class="progressmeter-statusbar"
+ id="statusbar-icon"
+ value="0"
+ max="100"
+ />
+ </hbox>
+ </hbox>
+ <hbox />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/mailWindow1.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/virtualFolderListDialog.dtd">
+
+<html
+ id="virtualFolderList"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ persist="width height screenX screenY"
+ width="400"
+ height="370"
+ windowtype="mailnews:virtualFolderList"
+ scrolling="false"
+>
+ <head>
+ <title>&virtualFolderListTitle.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/virtualFolderListEdit.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <label control="folderPickerTree">&virtualFolderDesc.label;</label>
+ <tree
+ id="folderPickerTree"
+ flex="1"
+ style="height: 290px"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="virtual"
+ onkeypress="gSelectVirtual.onKeyPress(event);"
+ onclick="gSelectVirtual.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ label="&folderName.label;"
+ style="flex: 10 10 auto"
+ primary="true"
+ crop="center"
+ />
+ <treecol
+ id="selectedCol"
+ label="&folderSearch.label;"
+ style="flex: 1 auto"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % folderDTD SYSTEM "chrome://messenger/locale/virtualFolderProperties.dtd">
+ %folderDTD;
+ <!ENTITY % folderPropsDTD SYSTEM "chrome://messenger/locale/folderProps.dtd">
+ %folderPropsDTD;
+ <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+ %searchTermDTD;
+]>
+<html id="virtualFolderProperties" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ windowtype="mailnews:virtualFolderProperties"
+ style="min-width: 60em; min-height: 30em;"
+ scrolling="false">
+<head>
+ <title>&virtualFolderProperties.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchWidgets.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchTerm.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dateFormat.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/virtualFolderProperties.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="virtualFolderPropertiesDialog"
+ buttons="accept,cancel"
+ newFolderAcceptButtonLabel="&newFolderButton.label;"
+ newFolderAcceptButtonAccessKey="&newFolderButton.accesskey;"
+ editFolderAcceptButtonLabel="&editFolderButton.label;"
+ editFolderAcceptButtonAccessKey="&editFolderButton.accesskey;"
+ style="min-width: 60em; min-height: 30em;">
+
+ <html:div class="grid-two-column-fr grid-items-center">
+ <label id="nameLabel" value="&name.label;" accesskey="&name.accesskey;"
+ control="name"/>
+ <hbox class="input-container">
+ <html:input id="name"
+ hidden="hidden"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"/>
+ <html:input id="existingName"
+ readonly="readonly"
+ hidden="hidden"
+ class="input-inline"
+ tabindex="0"/>
+ <hbox id="iconColorContainer" align="center" collapsed="true">
+ <label id="colorLabel" value="&folderProps.color.label;" control="color"
+ accesskey="&folderProps.color.accesskey;"/>
+ <html:input id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"/>
+ <button id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"/>
+ </hbox>
+ </hbox>
+ <label class="chooseFolderLocation" value="&description.label;"
+ accesskey="&description.accesskey;" control="msgNewFolderPicker"/>
+ <menulist id="msgNewFolderPicker" class="folderMenuItem chooseFolderLocation"
+ align="center" displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgNewFolderPopup" class="menulist-menupopup"
+ mode="newFolder" showFileHereLabel="true" oncommand="onFolderPick(event);"/>
+ </menulist>
+ <label value="&folderSelectionCaption.label;"/>
+ <hbox align="center">
+ <label id="chosenFoldersCount"/>
+ <spacer flex="1"/>
+ <button id="folderListPicker" label="&chooseFoldersButton.label;"
+ accesskey="&chooseFoldersButton.accesskey;"
+ oncommand="chooseFoldersToSearch();"/>
+ </hbox>
+ </html:div>
+
+ <checkbox id="searchOnline" label="&searchOnline.label;"
+ accesskey="&searchOnline.accesskey;"/>
+
+ <separator class="thin"/>
+
+ <vbox id="virtualFolderSearchTerms">
+ <label value="&searchTermCaption.label;"/>
+ <hbox id="virtualFolderSearchTermListBoxWrapper">
+ <vbox id="virtualFolderSearchTermListBox">
+#include ../../search/content/searchTerm.inc.xhtml
+ </hbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>