summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/preferences
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/mail/components/preferences
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.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/mail/components/preferences')
-rw-r--r--comm/mail/components/preferences/actionsshared.js23
-rw-r--r--comm/mail/components/preferences/applicationManager.js112
-rw-r--r--comm/mail/components/preferences/applicationManager.xhtml76
-rw-r--r--comm/mail/components/preferences/attachmentReminder.js100
-rw-r--r--comm/mail/components/preferences/attachmentReminder.xhtml54
-rw-r--r--comm/mail/components/preferences/chat.inc.xhtml198
-rw-r--r--comm/mail/components/preferences/chat.js193
-rw-r--r--comm/mail/components/preferences/colors.js15
-rw-r--r--comm/mail/components/preferences/colors.xhtml90
-rw-r--r--comm/mail/components/preferences/compose.inc.xhtml354
-rw-r--r--comm/mail/components/preferences/compose.js776
-rw-r--r--comm/mail/components/preferences/connection.js597
-rw-r--r--comm/mail/components/preferences/connection.xhtml264
-rw-r--r--comm/mail/components/preferences/cookies.js993
-rw-r--r--comm/mail/components/preferences/cookies.xhtml117
-rw-r--r--comm/mail/components/preferences/dockoptions.js11
-rw-r--r--comm/mail/components/preferences/dockoptions.xhtml59
-rw-r--r--comm/mail/components/preferences/downloads.js132
-rw-r--r--comm/mail/components/preferences/extensionControlled.js129
-rw-r--r--comm/mail/components/preferences/findInPage.js641
-rw-r--r--comm/mail/components/preferences/fonts.js196
-rw-r--r--comm/mail/components/preferences/fonts.xhtml337
-rw-r--r--comm/mail/components/preferences/general.inc.xhtml1096
-rw-r--r--comm/mail/components/preferences/general.js2962
-rw-r--r--comm/mail/components/preferences/jar.mn55
-rw-r--r--comm/mail/components/preferences/messagestyle.js259
-rw-r--r--comm/mail/components/preferences/messengerLanguages.js632
-rw-r--r--comm/mail/components/preferences/messengerLanguages.xhtml93
-rw-r--r--comm/mail/components/preferences/moz.build18
-rw-r--r--comm/mail/components/preferences/notifications.js25
-rw-r--r--comm/mail/components/preferences/notifications.xhtml71
-rw-r--r--comm/mail/components/preferences/offline.js31
-rw-r--r--comm/mail/components/preferences/offline.xhtml77
-rw-r--r--comm/mail/components/preferences/passwordManager.js819
-rw-r--r--comm/mail/components/preferences/passwordManager.xhtml186
-rw-r--r--comm/mail/components/preferences/permissions.js501
-rw-r--r--comm/mail/components/preferences/permissions.xhtml128
-rw-r--r--comm/mail/components/preferences/preferences.js453
-rw-r--r--comm/mail/components/preferences/preferences.xhtml256
-rw-r--r--comm/mail/components/preferences/preferencesTab.js162
-rw-r--r--comm/mail/components/preferences/privacy.inc.xhtml597
-rw-r--r--comm/mail/components/preferences/privacy.js562
-rw-r--r--comm/mail/components/preferences/receipts.js38
-rw-r--r--comm/mail/components/preferences/receipts.xhtml120
-rw-r--r--comm/mail/components/preferences/searchResults.inc.xhtml24
-rw-r--r--comm/mail/components/preferences/sync.inc.xhtml239
-rw-r--r--comm/mail/components/preferences/sync.js377
-rw-r--r--comm/mail/components/preferences/syncDialog.js38
-rw-r--r--comm/mail/components/preferences/syncDialog.xhtml210
-rw-r--r--comm/mail/components/preferences/tagDialog.xhtml26
-rw-r--r--comm/mail/components/preferences/test/browser/browser.ini20
-rw-r--r--comm/mail/components/preferences/test/browser/browser_chat.js74
-rw-r--r--comm/mail/components/preferences/test/browser/browser_cloudfile.js796
-rw-r--r--comm/mail/components/preferences/test/browser/browser_compose.js87
-rw-r--r--comm/mail/components/preferences/test/browser/browser_general.js380
-rw-r--r--comm/mail/components/preferences/test/browser/browser_openPreferences.js37
-rw-r--r--comm/mail/components/preferences/test/browser/browser_privacy.js454
-rw-r--r--comm/mail/components/preferences/test/browser/browser_sync.js419
-rw-r--r--comm/mail/components/preferences/test/browser/files/avatar.pngbin0 -> 11019 bytes
-rw-r--r--comm/mail/components/preferences/test/browser/files/icon.svg7
-rw-r--r--comm/mail/components/preferences/test/browser/files/management.html10
-rw-r--r--comm/mail/components/preferences/test/browser/head.js314
62 files changed, 18120 insertions, 0 deletions
diff --git a/comm/mail/components/preferences/actionsshared.js b/comm/mail/components/preferences/actionsshared.js
new file mode 100644
index 0000000000..aae709d957
--- /dev/null
+++ b/comm/mail/components/preferences/actionsshared.js
@@ -0,0 +1,23 @@
+/* -*- 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 FILEACTION_SAVE_TO_DISK = 1;
+var FILEACTION_OPEN_INTERNALLY = 2;
+var FILEACTION_OPEN_DEFAULT = 3;
+var FILEACTION_OPEN_CUSTOM = 4;
+function FileAction() {}
+FileAction.prototype = {
+ type: "",
+ extension: "",
+ hasExtension: true,
+ editable: true,
+ smallIcon: "",
+ bigIcon: "",
+ typeName: "",
+ action: "",
+ mimeInfo: null,
+ customHandler: "",
+ handleMode: false,
+};
diff --git a/comm/mail/components/preferences/applicationManager.js b/comm/mail/components/preferences/applicationManager.js
new file mode 100644
index 0000000000..5ebc0f077b
--- /dev/null
+++ b/comm/mail/components/preferences/applicationManager.js
@@ -0,0 +1,112 @@
+/* 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/. */
+
+// applications.js
+/* globals gGeneralPane */
+
+var gAppManagerDialog = {
+ _removed: [],
+
+ init() {
+ this.handlerInfo = window.arguments[0];
+ var bundle = document.getElementById("appManagerBundle");
+ gGeneralPane._prefsBundle = document.getElementById("bundlePreferences");
+ var description = this.handlerInfo.typeDescription;
+ var key =
+ this.handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo
+ ? "handleFile"
+ : "handleProtocol";
+ var contentText = bundle.getFormattedString(key, [description]);
+ contentText = bundle.getFormattedString("descriptionApplications", [
+ contentText,
+ ]);
+ document.getElementById("appDescription").textContent = contentText;
+
+ let list = document.getElementById("appList");
+ let listFragment = document.createDocumentFragment();
+ for (let app of this.handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!gGeneralPane.isValidHandlerApp(app)) {
+ continue;
+ }
+
+ let item = document.createXULElement("richlistitem");
+ item.classList.add("typeLabel");
+ listFragment.append(item);
+ item.app = app;
+
+ let image = document.createElement("img");
+ image.classList.add("typeIcon");
+ image.setAttribute("src", gGeneralPane._getIconURLForHandlerApp(app));
+ image.setAttribute("alt", "");
+ item.appendChild(image);
+
+ let label = document.createElement("span");
+ label.classList.add("typeDescription");
+ label.textContent = app.name;
+ item.appendChild(label);
+ }
+ list.append(listFragment);
+
+ // Triggers onSelect which populates label.
+ list.selectedIndex = 0;
+ },
+
+ onOK() {
+ if (!this._removed.length) {
+ // return early to avoid calling the |store| method.
+ return;
+ }
+
+ for (var i = 0; i < this._removed.length; ++i) {
+ this.handlerInfo.removePossibleApplicationHandler(this._removed[i]);
+ }
+
+ this.handlerInfo.store();
+ },
+
+ remove() {
+ var list = document.getElementById("appList");
+ this._removed.push(list.selectedItem.app);
+ var index = list.selectedIndex;
+ list.selectedItem.remove();
+ if (list.getRowCount() == 0) {
+ // The list is now empty, make the bottom part disappear
+ document.getElementById("appDetails").hidden = true;
+ } else {
+ // Select the item at the same index, if we removed the last
+ // item of the list, select the previous item
+ if (index == list.getRowCount()) {
+ --index;
+ }
+ list.selectedIndex = index;
+ }
+ },
+
+ onSelect() {
+ var list = document.getElementById("appList");
+ if (!list.selectedItem) {
+ document.getElementById("remove").disabled = true;
+ return;
+ }
+ document.getElementById("remove").disabled = false;
+ var app = list.selectedItem.app;
+ var address = "";
+ if (app instanceof Ci.nsILocalHandlerApp) {
+ address = app.executable.path;
+ } else if (app instanceof Ci.nsIWebHandlerApp) {
+ address = app.uriTemplate;
+ } else if (app instanceof Ci.nsIWebContentHandlerInfo) {
+ address = app.uri;
+ }
+ document.getElementById("appLocation").value = address;
+ var bundle = document.getElementById("appManagerBundle");
+ var appType =
+ app instanceof Ci.nsILocalHandlerApp
+ ? "descriptionLocalApp"
+ : "descriptionWebApp";
+ document.getElementById("appType").value = bundle.getString(appType);
+ },
+};
+
+document.addEventListener("dialogaccept", () => gAppManagerDialog.onOK());
diff --git a/comm/mail/components/preferences/applicationManager.xhtml b/comm/mail/components/preferences/applicationManager.xhtml
new file mode 100644
index 0000000000..10c9346c0f
--- /dev/null
+++ b/comm/mail/components/preferences/applicationManager.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 http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/preferences/applications.css"?>
+
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gAppManagerDialog.init();"
+ data-l10n-id="app-manager-window-dialog2"
+ style="min-width: 30em"
+>
+ <dialog id="appManager" buttons="accept,cancel">
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/general.js" />
+ <script src="chrome://messenger/content/preferences/applicationManager.js" />
+
+ <commandset id="appManagerCommandSet">
+ <command
+ id="cmd_delete"
+ oncommand="gAppManagerDialog.remove();"
+ disabled="true"
+ />
+ </commandset>
+
+ <keyset id="appManagerKeyset">
+ <key id="delete" keycode="VK_DELETE" command="cmd_delete" />
+ </keyset>
+
+ <stringbundle
+ id="appManagerBundle"
+ src="chrome://messenger/locale/preferences/applicationManager.properties"
+ />
+ <stringbundle
+ id="bundlePreferences"
+ src="chrome://messenger/locale/preferences/preferences.properties"
+ />
+
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/preferences/application-manager.ftl"
+ />
+ </linkset>
+
+ <description id="appDescription" />
+ <separator class="thin" />
+ <hbox flex="1">
+ <richlistbox
+ id="appList"
+ onselect="gAppManagerDialog.onSelect();"
+ flex="1"
+ style="min-height: 150px"
+ />
+ <vbox>
+ <button
+ id="remove"
+ data-l10n-id="remove-app-button"
+ command="cmd_delete"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ <vbox id="appDetails">
+ <separator class="thin" />
+ <label id="appType" />
+ <html:input id="appLocation" type="text" readonly="readonly" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/attachmentReminder.js b/comm/mail/components/preferences/attachmentReminder.js
new file mode 100644
index 0000000000..da01364b4e
--- /dev/null
+++ b/comm/mail/components/preferences/attachmentReminder.js
@@ -0,0 +1,100 @@
+/* -*- 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 gAttachmentReminderOptionsDialog = {
+ keywordListBox: null,
+
+ init() {
+ this.keywordListBox = document.getElementById("keywordList");
+ this.buildKeywordList();
+ },
+
+ buildKeywordList() {
+ var keywordsInCsv = Services.prefs.getComplexValue(
+ "mail.compose.attachment_reminder_keywords",
+ Ci.nsIPrefLocalizedString
+ );
+ if (!keywordsInCsv) {
+ return;
+ }
+ keywordsInCsv = keywordsInCsv.data;
+ var keywordsInArr = keywordsInCsv.split(",");
+ for (let i = 0; i < keywordsInArr.length; i++) {
+ if (keywordsInArr[i]) {
+ this.keywordListBox.appendItem(keywordsInArr[i], keywordsInArr[i]);
+ }
+ }
+ if (keywordsInArr.length) {
+ this.keywordListBox.selectedIndex = 0;
+ }
+ },
+
+ async addKeyword() {
+ var input = { value: "" }; // Default to empty.
+
+ let [title, message] = await document.l10n.formatValues([
+ { id: "new-keyword-title" },
+ { id: "new-keyword-label" },
+ ]);
+
+ var ok = Services.prompt.prompt(window, title, message, input, null, {
+ value: 0,
+ });
+ if (ok && input.value) {
+ let newKey = this.keywordListBox.appendItem(input.value, input.value);
+ this.keywordListBox.ensureElementIsVisible(newKey);
+ this.keywordListBox.selectItem(newKey);
+ }
+ },
+
+ async editKeyword() {
+ if (this.keywordListBox.selectedIndex < 0) {
+ return;
+ }
+ var keywordToEdit = this.keywordListBox.selectedItem;
+ var input = { value: keywordToEdit.getAttribute("value") };
+
+ let [title, message] = await document.l10n.formatValues([
+ { id: "edit-keyword-title" },
+ { id: "edit-keyword-label" },
+ ]);
+
+ var ok = Services.prompt.prompt(window, title, message, input, null, {
+ value: 0,
+ });
+ if (ok && input.value) {
+ this.keywordListBox.selectedItem.value = input.value;
+ this.keywordListBox.selectedItem.label = input.value;
+ }
+ },
+
+ removeKeyword() {
+ if (this.keywordListBox.selectedIndex < 0) {
+ return;
+ }
+ this.keywordListBox.selectedItem.remove();
+ },
+
+ saveKeywords() {
+ var keywordList = "";
+ for (var i = 0; i < this.keywordListBox.getRowCount(); i++) {
+ keywordList += this.keywordListBox
+ .getItemAtIndex(i)
+ .getAttribute("value");
+ if (i != this.keywordListBox.getRowCount() - 1) {
+ keywordList += ",";
+ }
+ }
+
+ Services.prefs.setStringPref(
+ "mail.compose.attachment_reminder_keywords",
+ keywordList
+ );
+ },
+};
+
+document.addEventListener("dialogaccept", () =>
+ gAttachmentReminderOptionsDialog.saveKeywords()
+);
diff --git a/comm/mail/components/preferences/attachmentReminder.xhtml b/comm/mail/components/preferences/attachmentReminder.xhtml
new file mode 100644
index 0000000000..7f2f7b19ce
--- /dev/null
+++ b/comm/mail/components/preferences/attachmentReminder.xhtml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="attachment-reminder-window"
+ onload="gAttachmentReminderOptionsDialog.init();"
+ style="min-width: 38em"
+>
+ <dialog id="attachmentReminderOptionsDialog" dlgbuttons="accept,cancel">
+ <script src="chrome://messenger/content/preferences/attachmentReminder.js" />
+
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/preferences/attachment-reminder.ftl"
+ />
+ </linkset>
+
+ <vbox>
+ <label control="keywordList" data-l10n-id="attachment-reminder-label" />
+ <hbox>
+ <richlistbox
+ id="keywordList"
+ flex="1"
+ ondblclick="gAttachmentReminderOptionsDialog.editKeyword();"
+ />
+ <vbox>
+ <button
+ data-l10n-id="keyword-new-button"
+ oncommand="gAttachmentReminderOptionsDialog.addKeyword();"
+ />
+ <button
+ data-l10n-id="keyword-edit-button"
+ oncommand="gAttachmentReminderOptionsDialog.editKeyword();"
+ />
+ <button
+ data-l10n-id="keyword-remove-button"
+ oncommand="gAttachmentReminderOptionsDialog.removeKeyword();"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/chat.inc.xhtml b/comm/mail/components/preferences/chat.inc.xhtml
new file mode 100644
index 0000000000..22b0cd3c4a
--- /dev/null
+++ b/comm/mail/components/preferences/chat.inc.xhtml
@@ -0,0 +1,198 @@
+# 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/.
+ <script src="chrome://messenger/content/preferences/chat.js"/>
+ <script src="chrome://messenger/content/preferences/messagestyle.js"/>
+
+ <stringbundle id="themesBundle"
+ src="chrome://messenger/locale/preferences/messagestyle.properties"/>
+ <html:template id="paneChat">
+ <hbox id="chatPaneCategory"
+ class="subcategory"
+ data-category="paneChat">
+ <html:h1 data-l10n-id="chat-pane-header"/>
+ </hbox>
+
+ <html:div data-category="paneChat">
+ <html:fieldset data-category="paneChat">
+ <html:legend data-l10n-id="chat-status-title"></html:legend>
+ <!-- Startup -->
+ <hbox align="center">
+ <label id="chatStartupAction"
+ data-l10n-id="startup-label"
+ control="messengerStartupAction"/>
+ <hbox>
+ <menulist id="messengerStartupAction" preference="messenger.startup.action">
+ <menupopup>
+ <menuitem data-l10n-id="offline-label" value="0"/>
+ <menuitem data-l10n-id="auto-connect-label" value="1"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ <separator/>
+
+ <!-- Status -->
+ <hbox align="center">
+ <checkbox id="reportIdle" data-l10n-id="idle-label"
+ preference="messenger.status.reportIdle"/>
+ <html:input id="timeBeforeAway" type="number"
+ class="size2 idle-reporting-enabled"
+ min="1" max="720"
+ preference="messenger.status.timeBeforeIdle"/>
+ <label data-l10n-id="idle-time-label" control="timeBeforeAway"/>
+ </hbox>
+ <vbox class="indent">
+ <hbox>
+ <checkbox id="autoAway"
+ data-l10n-id="away-message-label"
+ class="idle-reporting-enabled"
+ preference="messenger.status.awayWhenIdle"/>
+ <spacer flex="1"/>
+ </hbox>
+ <html:input id="defaultIdleAwayMessage"
+ type="text"
+ class="idle-reporting-enabled indent"
+ preference="messenger.status.defaultIdleAwayMessage"/>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneChat">
+ <html:fieldset data-category="paneChat">
+ <html:legend data-l10n-id="chat-notifications-title"></html:legend>
+ <hbox>
+ <checkbox id="sendTyping"
+ data-l10n-id="send-typing-label"
+ preference="purple.conversations.im.send_typing"/>
+ <spacer flex="1"/>
+ </hbox>
+
+ <separator/>
+
+ <hbox>
+ <label data-l10n-id="notification-label"/>
+ </hbox>
+ <hbox>
+ <checkbox id="desktopChatNotifications"
+ data-l10n-id="show-notification-label"
+ preference="mail.chat.show_desktop_notifications"/>
+ <hbox>
+ <menulist id="chatNotificationInfo" preference="mail.chat.notification_info">
+ <menupopup>
+ <menuitem data-l10n-id="notification-all" value="0"/>
+ <menuitem data-l10n-id="notification-name" value="1"/>
+ <menuitem data-l10n-id="notification-empty" value="2"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ <checkbox id="getAttention"
+ preference="messenger.options.getAttentionOnNewMessages"
+ data-l10n-id="notification-type-label"/>
+ <hbox align="center">
+ <checkbox id="chatNotification"
+ data-l10n-id="chat-play-sound-label"
+ preference="mail.chat.play_sound"/>
+ <spacer flex="1"/>
+ <button is="highlightable-button" id="playChatSound"
+ data-l10n-id="chat-play-button"
+ oncommand="gChatPane.previewSound();"/>
+ </hbox>
+ <radiogroup id="chatSoundType"
+ class="indent"
+ orient="vertical"
+ preference="mail.chat.play_sound.type"
+ aria-labelledby="chatNotification">
+ <hbox>
+ <radio id="chatSoundSystemSound"
+ data-l10n-id="chat-system-sound-label"
+ value="0"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <radio id="chatSoundCustom"
+ data-l10n-id="chat-custom-sound-label"
+ value="1"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox align="center" class="input-container">
+ <html:input id="chatSoundUrlLocation"
+ type="text"
+ class="input-filefield indent"
+ readonly="readonly"
+ preference="mail.chat.play_sound.url"
+ preference-editable="true"
+ aria-labelledby="chatSoundCustom"/>
+ <button is="highlightable-button" id="browseForChatSound"
+ data-l10n-id="chat-browse-sound-button"
+ oncommand="gChatPane.browseForSoundFile();"/>
+ </hbox>
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="chatPaneStylingCategory"
+ class="subcategory"
+ data-category="paneChat">
+ <html:h1 data-l10n-id="chat-pane-styling-header"/>
+ </hbox>
+
+ <html:div data-category="paneChat">
+ <html:fieldset data-category="paneChat">
+ <separator/>
+ <hbox align="center">
+ <label data-l10n-id="theme-label" control="messagestyle-themename"/>
+ <hbox flex="1">
+ <menulist id="messagestyle-themename"
+ flex="1" crop="end"
+ preference="messenger.options.messagesStyle.theme"
+ onselect="previewObserver.currentThemeChanged();">
+ <menupopup id="theme-menupopup">
+ <menuitem id="mail-menuitem"
+ data-l10n-id="style-mail"
+ value="mail"/>
+ <menuitem id="bubbles-menuitem"
+ data-l10n-id="style-bubbles"
+ value="bubbles"/>
+ <menuitem id="dark-menuitem"
+ data-l10n-id="style-dark"
+ value="dark"/>
+ <menuitem id="papersheets-menuitem"
+ data-l10n-id="style-paper"
+ value="papersheets"/>
+ <menuitem id="simple-menuitem"
+ data-l10n-id="style-simple"
+ value="simple"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="start">
+ <label data-l10n-id="preview-label"/>
+ <tooltip id="aHTMLTooltip" page="true"/>
+ <vbox id="previewBox" flex="1">
+ <vbox id="noPreviewScreen" flex="1" align="center" pack="center">
+ <hbox id="noPreviewBox" align="start">
+ <vbox id="noPreviewInnerBox" flex="1">
+ <label id="noPreviewTitle" data-l10n-id="no-preview-label"/>
+ <description id="noAccountDesc"
+ data-l10n-id="no-preview-description"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ <hbox align="center">
+ <label data-l10n-id="chat-variant-label" control="themevariant"/>
+ <hbox>
+ <menulist id="themevariant"
+ preference="messenger.options.messagesStyle.variant"
+ onselect="previewObserver.currentVariantChanged();"/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ </html:template>
diff --git a/comm/mail/components/preferences/chat.js b/comm/mail/components/preferences/chat.js
new file mode 100644
index 0000000000..e6e7b660c8
--- /dev/null
+++ b/comm/mail/components/preferences/chat.js
@@ -0,0 +1,193 @@
+/* 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";
+
+/* import-globals-from preferences.js */
+// messagestyle.js
+/* globals previewObserver */
+
+Preferences.addAll([
+ { id: "messenger.startup.action", type: "int" },
+ { id: "purple.conversations.im.send_typing", type: "bool" },
+ { id: "messenger.status.reportIdle", type: "bool" },
+ { id: "messenger.status.timeBeforeIdle", type: "int" },
+ { id: "messenger.status.awayWhenIdle", type: "bool" },
+ { id: "messenger.status.defaultIdleAwayMessage", type: "wstring" },
+ { id: "purple.logging.log_chats", type: "bool" },
+ { id: "purple.logging.log_ims", type: "bool" },
+ { id: "purple.logging.log_system", type: "bool" },
+ { id: "mail.chat.show_desktop_notifications", type: "bool" },
+ { id: "mail.chat.notification_info", type: "int" },
+ { id: "mail.chat.play_sound", type: "bool" },
+ { id: "mail.chat.play_sound.type", type: "int" },
+ { id: "mail.chat.play_sound.url", type: "string" },
+ { id: "messenger.options.getAttentionOnNewMessages", type: "bool" },
+ { id: "messenger.options.messagesStyle.theme", type: "string" },
+ { id: "messenger.options.messagesStyle.variant", type: "string" },
+]);
+
+var gChatPane = {
+ init() {
+ this.updateDisabledState();
+ this.updateMessageDisabledState();
+ this.updatePlaySound();
+ this.initPreview();
+
+ let element = document.getElementById("timeBeforeAway");
+ Preferences.addSyncFromPrefListener(
+ element,
+ () =>
+ Preferences.get("messenger.status.timeBeforeIdle")
+ .valueFromPreferences / 60
+ );
+ Preferences.addSyncToPrefListener(element, element => element.value * 60);
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("chatSoundUrlLocation"),
+ () => this.readSoundLocation()
+ );
+ },
+
+ initPreview() {
+ // We add this browser only when really necessary.
+ let previewBox = document.getElementById("previewBox");
+ if (previewBox.querySelector("browser")) {
+ return;
+ }
+
+ document.getElementById("noPreviewScreen").hidden = true;
+ let browser = document.createXULElement("browser", {
+ is: "conversation-browser",
+ });
+ browser.setAttribute("id", "previewbrowser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("tooltip", "aHTMLTooltip");
+ previewBox.appendChild(browser);
+ previewObserver.load();
+ },
+
+ updateDisabledState() {
+ let checked = Preferences.get("messenger.status.reportIdle").value;
+ document.querySelectorAll(".idle-reporting-enabled").forEach(e => {
+ e.disabled = !checked;
+ });
+ },
+
+ updateMessageDisabledState() {
+ let textbox = document.getElementById("defaultIdleAwayMessage");
+ textbox.toggleAttribute(
+ "disabled",
+ !Preferences.get("messenger.status.awayWhenIdle").value
+ );
+ },
+
+ convertURLToLocalFile(aFileURL) {
+ // convert the file url into a nsIFile
+ if (aFileURL) {
+ return Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getFileFromURLSpec(aFileURL);
+ }
+ return null;
+ },
+
+ readSoundLocation() {
+ let chatSoundUrlLocation = document.getElementById("chatSoundUrlLocation");
+ chatSoundUrlLocation.value = Preferences.get(
+ "mail.chat.play_sound.url"
+ ).value;
+ if (chatSoundUrlLocation.value) {
+ chatSoundUrlLocation.label = this.convertURLToLocalFile(
+ chatSoundUrlLocation.value
+ ).leafName;
+ chatSoundUrlLocation.style.backgroundImage =
+ "url(moz-icon://" + chatSoundUrlLocation.label + "?size=16)";
+ }
+ },
+
+ previewSound() {
+ let sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
+
+ let soundLocation =
+ document.getElementById("chatSoundType").value == 1
+ ? document.getElementById("chatSoundUrlLocation").value
+ : "";
+
+ // This should be in sync with the code in nsStatusBarBiffManager::PlayBiffSound.
+ if (!soundLocation.startsWith("file://")) {
+ if (Services.appinfo.OS == "Darwin") {
+ // OS X
+ sound.beep();
+ } else {
+ sound.playEventSound(Ci.nsISound.EVENT_NEW_MAIL_RECEIVED);
+ }
+ } else {
+ sound.play(Services.io.newURI(soundLocation));
+ }
+ },
+
+ browseForSoundFile() {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ // If we already have a sound file, then use the path for that sound file
+ // as the initial path in the dialog.
+ let localFile = this.convertURLToLocalFile(
+ document.getElementById("chatSoundUrlLocation").value
+ );
+ if (localFile) {
+ fp.displayDirectory = localFile.parent;
+ }
+
+ // XXX todo, persist the last sound directory and pass it in
+ fp.init(
+ window,
+ document
+ .getElementById("bundlePreferences")
+ .getString("soundFilePickerTitle"),
+ Ci.nsIFilePicker.modeOpen
+ );
+ fp.appendFilters(Ci.nsIFilePicker.filterAudio);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+
+ // convert the nsIFile into a nsIFile url
+ Preferences.get("mail.chat.play_sound.url").value = fp.fileURL.spec;
+ this.readSoundLocation(); // XXX We shouldn't have to be doing this by hand
+ this.updatePlaySound();
+ });
+ },
+
+ updatePlaySound() {
+ let soundsEnabled = Preferences.get("mail.chat.play_sound").value;
+ let soundTypeValue = Preferences.get("mail.chat.play_sound.type").value;
+ let soundUrlLocation = Preferences.get("mail.chat.play_sound.url").value;
+ let soundDisabled = !soundsEnabled || soundTypeValue != 1;
+
+ document.getElementById("chatSoundType").disabled = !soundsEnabled;
+ document.getElementById("chatSoundUrlLocation").disabled = soundDisabled;
+ document.getElementById("browseForChatSound").disabled = soundDisabled;
+ document.getElementById("playChatSound").disabled =
+ !soundsEnabled || (!soundUrlLocation && soundTypeValue != 0);
+ },
+};
+
+Preferences.get("messenger.status.reportIdle").on(
+ "change",
+ gChatPane.updateDisabledState
+);
+Preferences.get("messenger.status.awayWhenIdle").on(
+ "change",
+ gChatPane.updateMessageDisabledState
+);
+Preferences.get("mail.chat.play_sound").on("change", gChatPane.updatePlaySound);
+Preferences.get("mail.chat.play_sound.type").on(
+ "change",
+ gChatPane.updatePlaySound
+);
diff --git a/comm/mail/components/preferences/colors.js b/comm/mail/components/preferences/colors.js
new file mode 100644
index 0000000000..cd45f8ca44
--- /dev/null
+++ b/comm/mail/components/preferences/colors.js
@@ -0,0 +1,15 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "browser.display.document_color_use", type: "int" },
+ { id: "browser.anchor_color", type: "string" },
+ { id: "browser.visited_color", type: "string" },
+ { id: "browser.underline_anchors", type: "bool" },
+ { id: "browser.display.foreground_color", type: "string" },
+ { id: "browser.display.background_color", type: "string" },
+ { id: "browser.display.use_system_colors", type: "bool" },
+]);
diff --git a/comm/mail/components/preferences/colors.xhtml b/comm/mail/components/preferences/colors.xhtml
new file mode 100644
index 0000000000..827078ba4c
--- /dev/null
+++ b/comm/mail/components/preferences/colors.xhtml
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="colors-dialog-window2">
+ <dialog id="ColorsDialog"
+ dlgbuttons="accept,cancel">
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/colors.ftl"/>
+ </linkset>
+
+ <hbox>
+ <hbox flex="1">
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="colors-dialog-legend"></html:legend>
+ <hbox align="center">
+ <label data-l10n-id="text-color-label" control="foregroundtextmenu"/>
+ <spacer flex="1"/>
+ <html:input type="color" id="foregroundtextmenu" preference="browser.display.foreground_color"/>
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label data-l10n-id="background-color-label" control="backgroundmenu"/>
+ <spacer flex="1"/>
+ <html:input type="color" id="backgroundmenu" preference="browser.display.background_color"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="center">
+ <checkbox id="browserUseSystemColors" data-l10n-id="use-system-colors"
+ preference="browser.display.use_system_colors"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </hbox>
+ <hbox flex="1">
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="colors-link-legend"></html:legend>
+ <hbox align="center">
+ <label data-l10n-id="link-color-label" control="unvisitedlinkmenu"/>
+ <spacer flex="1"/>
+ <html:input type="color" id="unvisitedlinkmenu" preference="browser.anchor_color"/>
+ </hbox>
+ <hbox align="center" style="margin-top: 5px">
+ <label data-l10n-id="visited-link-color-label" control="visitedlinkmenu"/>
+ <spacer flex="1"/>
+ <html:input type="color" id="visitedlinkmenu" preference="browser.visited_color"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox align="center">
+ <checkbox id="browserUnderlineAnchors" data-l10n-id="underline-link-checkbox"
+ preference="browser.underline_anchors"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </hbox>
+ </hbox>
+#ifdef XP_WIN
+ <vbox align="start">
+#else
+ <vbox>
+#endif
+ <label data-l10n-id="override-color-label"
+ control="useDocumentColors"/>
+ <menulist id="useDocumentColors"
+ preference="browser.display.document_color_use">
+ <menupopup>
+ <menuitem data-l10n-id="override-color-always"
+ value="2" id="documentColorAlways"/>
+ <menuitem data-l10n-id="override-color-auto"
+ value="0" id="documentColorAutomatic"/>
+ <menuitem data-l10n-id="override-color-never"
+ value="1" id="documentColorNever"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+
+ <script src="chrome://global/content/preferencesBindings.js"/>
+ <script src="chrome://messenger/content/preferences/colors.js"/>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/compose.inc.xhtml b/comm/mail/components/preferences/compose.inc.xhtml
new file mode 100644
index 0000000000..3ba7063084
--- /dev/null
+++ b/comm/mail/components/preferences/compose.inc.xhtml
@@ -0,0 +1,354 @@
+# 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/.
+ <script src="chrome://messenger/content/preferences/compose.js"/>
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://messenger/content/preferences/downloads.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ <html:template id="paneCompose">
+ <hbox id="compositionMainCategory"
+ class="subcategory"
+ data-category="paneCompose">
+ <html:h1 data-l10n-id="composition-category-header"/>
+ </hbox>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <separator class="thin"/>
+ <hbox align="center">
+ <label data-l10n-id="forward-label" control="forwardMessageMode"/>
+ <hbox>
+ <menulist id="forwardMessageMode" preference="mail.forward_message_mode">
+ <menupopup>
+ <menuitem value="2" data-l10n-id="inline-label"/>
+ <menuitem value="0" data-l10n-id="as-attachment-label"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <separator orient="vertical" class="thin"/>
+ <checkbox id="addExtension" preference="mail.forward_add_extension"
+ data-l10n-id="extension-label"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" pack="start">
+ <checkbox id="autoSave" preference="mail.compose.autosave"
+ data-l10n-id="auto-save-label"/>
+ <html:input id="autoSaveInterval" type="number" class="size2"
+ min="1" max="35790"
+ preference="mail.compose.autosaveinterval"
+ aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/>
+ <label id="autoSaveEnd" data-l10n-id="auto-save-end"/>
+ </hbox>
+ <hbox>
+ <checkbox id="mailWarnOnSendAccelKey"
+ data-l10n-id="warn-on-send-accel-key"
+ preference="mail.warn_on_send_accel_key"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <checkbox id="addLinkPreviews"
+ data-l10n-id="add-link-previews"
+ preference="mail.compose.add_link_preview"/>
+ <spacer flex="1"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <html:legend data-l10n-id="composition-spelling-title"></html:legend>
+ <hbox>
+ <checkbox id="spellCheckBeforeSend"
+ data-l10n-id="spellcheck-label"
+ preference="mail.SpellCheckBeforeSend"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <checkbox id="inlineSpellCheck"
+ data-l10n-id="spellcheck-inline-label"
+ preference="mail.spellcheck.inline"/>
+ <spacer flex="1"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <vbox flex="1">
+ <label data-l10n-id="language-popup-label" control="dictionaryList"/>
+
+ <html:ul id="dictionaryList" class="indent">
+ <html:template id="dictionaryListItem">
+ <html:li>
+ <html:label>
+ <html:input type="checkbox" />
+ <html:span class="checkbox-label"></html:span>
+ </html:label>
+ </html:li>
+ </html:template>
+ </html:ul>
+
+ <label id="downloadDictionaries" class="text-link"
+ onclick="if (event.button == 0) { openDictionaryList('tab'); }"
+ data-l10n-id="download-dictionaries-link"/>
+
+ <spacer flex="1"/>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <html:legend data-l10n-id="compose-html-style-title"></html:legend>
+ <hbox>
+ <vbox flex="1">
+ <hbox align="center">
+ <label control="FontSelect" data-l10n-id="font-label"/>
+ <hbox flex="1">
+ <menulist id="FontSelect" preference="msgcompose.font_face"
+ sizetopopup="pref" crop="center" flex="1">
+ <menupopup>
+ <menuitem value="" label="&fontVarWidth.label;"/>
+ <menuitem value="monospace" label="&fontFixedWidth.label;"/>
+ <menuseparator/>
+ <menuitem value="Helvetica, Arial, sans-serif" label="&fontHelvetica.label;"/>
+ <menuitem value="Times New Roman, Times, serif" label="&fontTimes.label;"/>
+ <menuitem value="Courier New, Courier, monospace" label="&fontCourier.label;"/>
+ <menuseparator/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <label control="fontSizeSelect" data-l10n-id="font-size-label"/>
+ <hbox>
+ <menulist id="fontSizeSelect" preference="msgcompose.font_size" value="3">
+ <menupopup>
+ <menuitem value="1" label="&size-tinyCmd.label;"/>
+ <menuitem value="2" label="&size-smallCmd.label;"/>
+ <menuitem value="3" label="&size-mediumCmd.label;"/>
+ <menuitem value="4" label="&size-largeCmd.label;"/>
+ <menuitem value="5" label="&size-extraLargeCmd.label;"/>
+ <menuitem value="6" label="&size-hugeCmd.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox id="useReaderDefaults"
+ data-l10n-id="default-colors-label"
+ preference="msgcompose.default_colors"/>
+ </hbox>
+ <hbox align="center" class="indent">
+ <label id="textColorLabel"
+ control="textColorButton"
+ data-l10n-id="font-color-label"/>
+ <html:input type="color" id="textColorButton" preference="msgcompose.text_color"/>
+ <separator orient="vertical" class="thin"/>
+ <label id="backgroundColorLabel"
+ control="backgroundColorButton"
+ data-l10n-id="bg-color-label"/>
+ <html:input type="color" id="backgroundColorButton" preference="msgcompose.background_color"/>
+ </hbox>
+ </vbox>
+ <vbox>
+ <spacer flex="1"/>
+ <button is="highlightable-button"
+ data-l10n-id="restore-html-label"
+ oncommand="gComposePane.restoreHTMLDefaults();"/>
+ </vbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox id="defaultToParagraph"
+ data-l10n-id="default-format-label"
+ preference="mail.compose.default_to_paragraph"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <html:legend data-l10n-id="compose-send-format-title"></html:legend>
+ <radiogroup class="indent"
+ preference="mail.default_send_format">
+ <radio value="0"
+ aria-describedby="composeSendAutomaticDescription"
+ data-l10n-id="compose-send-automatic-option" />
+ <description id="composeSendAutomaticDescription"
+ class="indent tip-caption"
+ data-l10n-id="compose-send-automatic-description" />
+ <radio value="3"
+ aria-describedby="composeSendBothDescription"
+ data-l10n-id="compose-send-both-option" />
+ <description id="composeSendBothDescription"
+ class="indent tip-caption"
+ data-l10n-id="compose-send-both-description" />
+ <radio value="2"
+ aria-describedby="composeSendHTMLDescription"
+ data-l10n-id="compose-send-html-option" />
+ <description id="composeSendHTMLDescription"
+ class="indent tip-caption"
+ data-l10n-id="compose-send-html-description" />
+ <radio value="1"
+ aria-describedby="composeSendPlainDescription"
+ data-l10n-id="compose-send-plain-option" />
+ <description id="composeSendPlainDescription"
+ class="indent tip-caption"
+ data-l10n-id="compose-send-plain-description" />
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="compositionAddressingCategory"
+ class="subcategory"
+ data-category="paneCompose">
+ <html:h1 data-l10n-id="composition-addressing-header"/>
+ </hbox>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <!-- Address Autocomplete -->
+ <separator class="thin"/>
+
+ <description data-l10n-id="autocomplete-description"/>
+
+ <hbox align="center">
+ <checkbox id="addressingAutocomplete" data-l10n-id="ab-label"
+ preference="mail.enable_autocomplete"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="autocompleteLDAP" data-l10n-id="directories-label"
+ preference="ldap_2.autoComplete.useDirectory"/>
+ <hbox flex="1">
+ <menulist is="menulist-addrbooks" id="directoriesList"
+ aria-labelledby="autocompleteLDAP"
+ preference="ldap_2.autoComplete.directoryServer"
+ data-l10n-id="directories-none-label"
+ data-l10n-attrs="none"
+ remoteonly="true"
+ flex="1"/>
+ </hbox>
+ <button is="highlightable-button" id="editButton"
+ data-l10n-id="edit-directories-label"
+ oncommand="gComposePane.editDirectories();"
+ preference="pref.ldap.disable_button.edit_directories"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" pack="start">
+ <checkbox id="emailCollectionOutgoing" data-l10n-id="email-picker-label"
+ preference="mail.collect_email_address_outgoing"/>
+ <hbox flex="1">
+ <menulist is="menulist-addrbooks" id="localDirectoriesList"
+ aria-labelledby="emailCollectionOutgoing"
+ preference="mail.collect_addressbook"
+ localonly="true"
+ writable="true"
+ flex="1"/>
+ </hbox>
+ </hbox>
+
+ <hbox align="center" pack="start">
+ <label data-l10n-id="default-directory-label"
+ control="defaultStartupDirList"/>
+ <hbox flex="1">
+ <menulist is="menulist-addrbooks" id="defaultStartupDirList"
+ oncommand="gComposePane.setDefaultStartupDir(this.value);"
+ data-l10n-id="default-last-label"
+ data-l10n-attrs="none"
+ alladdressbooks="true"
+ mailinglists="true"
+ flex="1"/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="compositionAttachmentsCategory"
+ class="subcategory"
+ data-category="paneCompose">
+ <html:h1 data-l10n-id="composition-attachments-header"/>
+ </hbox>
+
+ <html:div data-category="paneCompose">
+ <html:fieldset data-category="paneCompose">
+ <hbox align="center">
+ <checkbox id="attachment_reminder_label"
+ data-l10n-id="attachment-label"
+ preference="mail.compose.attachment_reminder"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="attachment_reminder_button"
+ data-l10n-id="attachment-options-label"
+ oncommand="gComposePane.attachmentReminderOptionsDialog();"
+ search-l10n-ids="
+ attachment-reminder-window.title,
+ attachment-reminder-label,
+ keyword-new-button.label,
+ keyword-edit-button.label,
+ keyword-remove-button.label,
+ new-keyword-title,
+ new-keyword-label,
+ edit-keyword-title,
+ edit-keyword-label"/>
+ </hbox>
+ </hbox>
+ <vbox id="cloudFileBox">
+ <hbox id="cloudFileToggleAndThreshold" align="center">
+ <checkbox id="enableThreshold"
+ data-l10n-id="enable-cloud-share"
+ preference="mail.compose.big_attachments.notify"/>
+ <html:input id="cloudFileThreshold" type="number" min="0" class="size3"
+ preference="mail.compose.big_attachments.threshold_kb"/>
+ <label control="cloudFileThreshold" data-l10n-id="cloud-share-size"/>
+ </hbox>
+ <hbox style="height: 480px; flex: 1 auto;">
+ <vbox id="provider-listing">
+ <richlistbox id="cloudFileView" orient="vertical"
+ seltype="single"
+ onoverflow="gCloudFile.onListOverflow();"
+ onselect="gCloudFile.onSelectionChanged(event);">
+ </richlistbox>
+ <vbox id="addCloudFileAccountButtons">
+ </vbox>
+ <hbox>
+ <menulist id="addCloudFileAccount"
+ hidden="true"
+ data-l10n-id="add-cloud-account"
+ data-l10n-attrs="defaultlabel"
+ oncommand="gCloudFile.addCloudFileAccount(this.value);">
+ <menupopup id="addCloudFileAccountListItems"/>
+ </menulist>
+ </hbox>
+ <button is="highlightable-button" id="removeCloudFileAccount"
+ disabled="true"
+ data-l10n-id="remove-cloud-account"
+ oncommand="gCloudFile.removeCloudFileAccount();"/>
+ <label is="text-link"
+ id="moreProvidersLink"
+ href="https://addons.thunderbird.net/thunderbird/tag/filelink"
+ data-l10n-id="find-cloud-providers"/>
+ </vbox>
+ <separator class="thin" orient="vertical"/>
+ <vbox flex="1">
+ <vbox id="cloudFileDefaultPanel" flex="1">
+ <description data-l10n-id="cloud-account-description"/>
+ </vbox>
+ <vbox id="cloudFileSettingsWrapper" flex="1">
+ </vbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ </html:template>
diff --git a/comm/mail/components/preferences/compose.js b/comm/mail/components/preferences/compose.js
new file mode 100644
index 0000000000..7d9acf0622
--- /dev/null
+++ b/comm/mail/components/preferences/compose.js
@@ -0,0 +1,776 @@
+/* -*- 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/. */
+
+"use strict";
+
+/* import-globals-from preferences.js */
+
+var { InlineSpellChecker } = ChromeUtils.importESModule(
+ "resource://gre/modules/InlineSpellChecker.sys.mjs"
+);
+
+// CloudFile account tools used by gCloudFile.
+var { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+var { E10SUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/E10SUtils.sys.mjs"
+);
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+Preferences.addAll([
+ { id: "mail.forward_message_mode", type: "int" },
+ { id: "mail.forward_add_extension", type: "bool" },
+ { id: "mail.SpellCheckBeforeSend", type: "bool" },
+ { id: "mail.spellcheck.inline", type: "bool" },
+ { id: "mail.warn_on_send_accel_key", type: "bool" },
+ { id: "mail.compose.autosave", type: "bool" },
+ { id: "mail.compose.autosaveinterval", type: "int" },
+ { id: "mail.enable_autocomplete", type: "bool" },
+ { id: "ldap_2.autoComplete.useDirectory", type: "bool" },
+ { id: "ldap_2.autoComplete.directoryServer", type: "string" },
+ { id: "pref.ldap.disable_button.edit_directories", type: "bool" },
+ { id: "mail.collect_email_address_outgoing", type: "bool" },
+ { id: "mail.collect_addressbook", type: "string" },
+ { id: "spellchecker.dictionary", type: "unichar" },
+ { id: "msgcompose.default_colors", type: "bool" },
+ { id: "msgcompose.font_face", type: "string" },
+ { id: "msgcompose.font_size", type: "string" },
+ { id: "msgcompose.text_color", type: "string" },
+ { id: "msgcompose.background_color", type: "string" },
+ { id: "mail.compose.attachment_reminder", type: "bool" },
+ { id: "mail.compose.default_to_paragraph", type: "bool" },
+ { id: "mail.compose.big_attachments.notify", type: "bool" },
+ { id: "mail.compose.big_attachments.threshold_kb", type: "int" },
+ { id: "mail.default_send_format", type: "int" },
+ { id: "mail.compose.add_link_preview", type: "bool" },
+]);
+
+var gComposePane = {
+ mSpellChecker: null,
+
+ init() {
+ this.enableAutocomplete();
+
+ this.initLanguages();
+
+ this.populateFonts();
+
+ this.updateAutosave();
+
+ this.updateUseReaderDefaults();
+
+ this.updateAttachmentCheck();
+
+ this.updateEmailCollection();
+
+ this.initAbDefaultStartupDir();
+
+ this.setButtonColors();
+
+ // If BigFiles is disabled, hide the "Outgoing" tab, and the tab
+ // selectors, and bail out.
+ if (!Services.prefs.getBoolPref("mail.cloud_files.enabled")) {
+ // Hide the tab selector
+ let cloudFileBox = document.getElementById("cloudFileBox");
+ cloudFileBox.hidden = true;
+ return;
+ }
+
+ gCloudFile.init();
+ },
+
+ attachmentReminderOptionsDialog() {
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/attachmentReminder.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ updateAutosave() {
+ gComposePane.enableElement(
+ document.getElementById("autoSaveInterval"),
+ Preferences.get("mail.compose.autosave").value
+ );
+ },
+
+ updateUseReaderDefaults() {
+ let useReaderDefaultsChecked = Preferences.get(
+ "msgcompose.default_colors"
+ ).value;
+ gComposePane.enableElement(
+ document.getElementById("textColorLabel"),
+ !useReaderDefaultsChecked
+ );
+ gComposePane.enableElement(
+ document.getElementById("backgroundColorLabel"),
+ !useReaderDefaultsChecked
+ );
+ gComposePane.enableElement(
+ document.getElementById("textColorButton"),
+ !useReaderDefaultsChecked
+ );
+ gComposePane.enableElement(
+ document.getElementById("backgroundColorButton"),
+ !useReaderDefaultsChecked
+ );
+ },
+
+ updateAttachmentCheck() {
+ gComposePane.enableElement(
+ document.getElementById("attachment_reminder_button"),
+ Preferences.get("mail.compose.attachment_reminder").value
+ );
+ },
+
+ updateEmailCollection() {
+ gComposePane.enableElement(
+ document.getElementById("localDirectoriesList"),
+ Preferences.get("mail.collect_email_address_outgoing").value
+ );
+ },
+
+ enableElement(aElement, aEnable) {
+ let pref = aElement.getAttribute("preference");
+ let prefIsLocked = pref ? Preferences.get(pref).locked : false;
+ aElement.disabled = !aEnable || prefIsLocked;
+ },
+
+ enableAutocomplete() {
+ let acLDAPPref = Preferences.get("ldap_2.autoComplete.useDirectory").value;
+ gComposePane.enableElement(
+ document.getElementById("directoriesList"),
+ acLDAPPref
+ );
+ gComposePane.enableElement(
+ document.getElementById("editButton"),
+ acLDAPPref
+ );
+ },
+
+ editDirectories() {
+ gSubDialog.open(
+ "chrome://messenger/content/addressbook/pref-editdirectories.xhtml"
+ );
+ },
+
+ initAbDefaultStartupDir() {
+ if (!this.startupDirListener.inited) {
+ this.startupDirListener.load();
+ }
+
+ let dirList = document.getElementById("defaultStartupDirList");
+ if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) {
+ // Some directory is the default.
+ let startupURI = Services.prefs.getCharPref(
+ "mail.addr_book.view.startupURI"
+ );
+ dirList.value = startupURI;
+ } else {
+ // Choose item meaning there is no default startup directory any more.
+ dirList.value = "";
+ }
+ },
+
+ setButtonColors() {
+ document.getElementById("textColorButton").value = Preferences.get(
+ "msgcompose.text_color"
+ ).value;
+ document.getElementById("backgroundColorButton").value = Preferences.get(
+ "msgcompose.background_color"
+ ).value;
+ },
+
+ setDefaultStartupDir(aDirURI) {
+ if (aDirURI) {
+ // Some AB directory was selected. Set prefs to make this directory
+ // the default view when starting up the main AB.
+ Services.prefs.setCharPref("mail.addr_book.view.startupURI", aDirURI);
+ Services.prefs.setBoolPref(
+ "mail.addr_book.view.startupURIisDefault",
+ true
+ );
+ } else {
+ // Set pref that there's no default startup view directory any more.
+ Services.prefs.setBoolPref(
+ "mail.addr_book.view.startupURIisDefault",
+ false
+ );
+ }
+ },
+
+ async initLanguages() {
+ let languageList = document.getElementById("dictionaryList");
+ this.mSpellChecker = Cc["@mozilla.org/spellchecker/engine;1"].getService(
+ Ci.mozISpellCheckingEngine
+ );
+
+ // Get the list of dictionaries from the spellchecker.
+
+ let dictList = this.mSpellChecker.getDictionaryList();
+
+ // HACK: calling sortDictionaryList may fail the first time due to
+ // synchronous loading of the .ftl files. If we load the files and wait
+ // for a known value asynchronously, no such failure will happen.
+ await new Localization([
+ "toolkit/intl/languageNames.ftl",
+ "toolkit/intl/regionNames.ftl",
+ ]).formatValue("language-name-en");
+ let sortedList = new InlineSpellChecker().sortDictionaryList(dictList);
+ let activeDictionaries = Services.prefs
+ .getCharPref("spellchecker.dictionary")
+ .split(",");
+ let template = document.getElementById("dictionaryListItem");
+ languageList.replaceChildren(
+ ...sortedList.map(({ displayName, localeCode }) => {
+ let item = template.content.cloneNode(true).firstElementChild;
+ item.querySelector(".checkbox-label").textContent = displayName;
+ let input = item.querySelector("input");
+ input.setAttribute("value", localeCode);
+ input.addEventListener("change", event => {
+ let language = event.target.value;
+ let dicts = Services.prefs
+ .getCharPref("spellchecker.dictionary")
+ .split(",")
+ .filter(Boolean);
+ if (!event.target.checked) {
+ dicts = dicts.filter(item => item != language);
+ } else {
+ dicts.push(language);
+ }
+ Services.prefs.setCharPref(
+ "spellchecker.dictionary",
+ dicts.join(",")
+ );
+ });
+ input.checked = activeDictionaries.includes(localeCode);
+ return item;
+ })
+ );
+ },
+
+ populateFonts() {
+ var fontsList = document.getElementById("FontSelect");
+ try {
+ var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"].getService(
+ Ci.nsIFontEnumerator
+ );
+ var localFonts = enumerator.EnumerateAllFonts();
+ for (let i = 0; i < localFonts.length; ++i) {
+ // Remove Linux system generic fonts that collide with CSS generic fonts.
+ if (
+ localFonts[i] != "" &&
+ localFonts[i] != "serif" &&
+ localFonts[i] != "sans-serif" &&
+ localFonts[i] != "monospace"
+ ) {
+ fontsList.appendItem(localFonts[i], localFonts[i]);
+ }
+ }
+ } catch (e) {}
+ // Choose the item after the list is completely generated.
+ var preference = Preferences.get(fontsList.getAttribute("preference"));
+ fontsList.value = preference.value;
+ },
+
+ restoreHTMLDefaults() {
+ // reset throws an exception if the pref value is already the default so
+ // work around that with some try/catch exception handling
+ try {
+ Preferences.get("msgcompose.font_face").reset();
+ } catch (ex) {}
+
+ try {
+ Preferences.get("msgcompose.font_size").reset();
+ } catch (ex) {}
+
+ try {
+ Preferences.get("msgcompose.text_color").reset();
+ } catch (ex) {}
+
+ try {
+ Preferences.get("msgcompose.background_color").reset();
+ } catch (ex) {}
+
+ try {
+ Preferences.get("msgcompose.default_colors").reset();
+ } catch (ex) {}
+
+ this.updateUseReaderDefaults();
+ this.setButtonColors();
+ },
+
+ startupDirListener: {
+ inited: false,
+ domain: "mail.addr_book.view.startupURI",
+ observe(subject, topic, prefName) {
+ if (topic != "nsPref:changed") {
+ return;
+ }
+
+ // If the default startup directory prefs have changed,
+ // reinitialize the default startup dir picker to show the new value.
+ gComposePane.initAbDefaultStartupDir();
+ },
+ load() {
+ // Observe changes of our prefs.
+ Services.prefs.addObserver(this.domain, this);
+ // Unload the pref observer when preferences window is closed.
+ window.addEventListener("unload", () => this.unload(), true);
+ this.inited = true;
+ },
+
+ unload(event) {
+ Services.prefs.removeObserver(
+ gComposePane.startupDirListener.domain,
+ gComposePane.startupDirListener
+ );
+ },
+ },
+};
+
+var gCloudFile = {
+ _initialized: false,
+ _list: null,
+ _buttonContainer: null,
+ _listContainer: null,
+ _settings: null,
+ _tabpanel: null,
+ _settingsPanelWrap: null,
+ _defaultPanel: null,
+
+ get _strings() {
+ return Services.strings.createBundle(
+ "chrome://messenger/locale/preferences/applications.properties"
+ );
+ },
+
+ init() {
+ this._list = document.getElementById("cloudFileView");
+ this._buttonContainer = document.getElementById(
+ "addCloudFileAccountButtons"
+ );
+ this._addAccountButton = document.getElementById("addCloudFileAccount");
+ this._listContainer = document.getElementById(
+ "addCloudFileAccountListItems"
+ );
+ this._removeAccountButton = document.getElementById(
+ "removeCloudFileAccount"
+ );
+ this._defaultPanel = document.getElementById("cloudFileDefaultPanel");
+ this._settingsPanelWrap = document.getElementById(
+ "cloudFileSettingsWrapper"
+ );
+
+ this.updateThreshold();
+ this.rebuildView();
+
+ window.addEventListener("unload", this, { capture: false, once: true });
+
+ this._onAccountConfigured = this._onAccountConfigured.bind(this);
+ this._onProviderRegistered = this._onProviderRegistered.bind(this);
+ this._onProviderUnregistered = this._onProviderUnregistered.bind(this);
+ cloudFileAccounts.on("accountConfigured", this._onAccountConfigured);
+ cloudFileAccounts.on("providerRegistered", this._onProviderRegistered);
+ cloudFileAccounts.on("providerUnregistered", this._onProviderUnregistered);
+
+ let element = document.getElementById("cloudFileThreshold");
+ Preferences.addSyncFromPrefListener(element, () => this.readThreshold());
+ Preferences.addSyncToPrefListener(element, () => this.writeThreshold());
+
+ this._initialized = true;
+ },
+
+ destroy() {
+ // Remove any controllers or observers here.
+ cloudFileAccounts.off("accountConfigured", this._onAccountConfigured);
+ cloudFileAccounts.off("providerRegistered", this._onProviderRegistered);
+ cloudFileAccounts.off("providerUnregistered", this._onProviderUnregistered);
+ },
+
+ _onAccountConfigured(event, account) {
+ for (let item of this._list.children) {
+ if (item.value == account.accountKey) {
+ item.querySelector(".configuredWarning").hidden = account.configured;
+ }
+ }
+ },
+
+ _onProviderRegistered(event, provider) {
+ let accounts = cloudFileAccounts.getAccountsForType(provider.type);
+ accounts.sort(this._sortDisplayNames);
+
+ // Always add newly-enabled accounts to the end of the list, this makes
+ // it clearer to users what's happening.
+ for (let account of accounts) {
+ let item = this.makeRichListItemForAccount(account);
+ this._list.appendChild(item);
+ }
+
+ this._buttonContainer.appendChild(this.makeButtonForProvider(provider));
+ this._listContainer.appendChild(this.makeListItemForProvider(provider));
+ },
+
+ _onProviderUnregistered(event, type) {
+ for (let item of [...this._list.children]) {
+ // If the provider is unregistered, getAccount returns null.
+ if (!cloudFileAccounts.getAccount(item.value)) {
+ if (item.hasAttribute("selected")) {
+ this._defaultPanel.hidden = false;
+ this._settingsPanelWrap.hidden = true;
+ if (this._settings) {
+ this._settings.remove();
+ }
+ this._removeAccountButton.disabled = true;
+ }
+ item.remove();
+ }
+ }
+
+ for (let button of this._buttonContainer.children) {
+ if (button.getAttribute("value") == type) {
+ button.remove();
+ }
+ }
+
+ for (let item of this._listContainer.children) {
+ if (item.getAttribute("value") == type) {
+ item.remove();
+ }
+ }
+
+ if (this._buttonContainer.childElementCount < 1) {
+ this._buttonContainer.hidden = false;
+ this._addAccountButton.hidden = true;
+ }
+ },
+
+ makeRichListItemForAccount(aAccount) {
+ let rli = document.createXULElement("richlistitem");
+ rli.setAttribute("align", "center");
+ rli.classList.add("cloudfileAccount", "input-container");
+ rli.setAttribute("value", aAccount.accountKey);
+
+ let icon = document.createElement("img");
+ icon.classList.add("typeIcon");
+ if (aAccount.iconURL) {
+ icon.setAttribute("src", aAccount.iconURL);
+ }
+ icon.setAttribute("alt", "");
+ rli.appendChild(icon);
+
+ let label = document.createXULElement("label");
+ label.setAttribute("crop", "end");
+ label.setAttribute("flex", "1");
+ label.setAttribute(
+ "value",
+ cloudFileAccounts.getDisplayName(aAccount.accountKey)
+ );
+ label.addEventListener("click", this, true);
+ rli.appendChild(label);
+
+ let input = document.createElement("input");
+ input.setAttribute("type", "text");
+ input.setAttribute("hidden", "hidden");
+ input.addEventListener("blur", this);
+ input.addEventListener("keypress", this);
+ rli.appendChild(input);
+
+ let warningIcon = document.createElement("img");
+ warningIcon.setAttribute("class", "configuredWarning typeIcon");
+ warningIcon.setAttribute("src", "chrome://global/skin/icons/warning.svg");
+ // "title" provides the accessible name, not "alt".
+ warningIcon.setAttribute(
+ "title",
+ this._strings.GetStringFromName("notConfiguredYet")
+ );
+ if (aAccount.configured) {
+ warningIcon.hidden = true;
+ }
+ rli.appendChild(warningIcon);
+
+ return rli;
+ },
+
+ makeButtonForProvider(provider) {
+ let button = document.createXULElement("button");
+ button.setAttribute("value", provider.type);
+ button.setAttribute(
+ "label",
+ this._strings.formatStringFromName("addProvider", [provider.displayName])
+ );
+ button.setAttribute(
+ "oncommand",
+ `gCloudFile.addCloudFileAccount("${provider.type}")`
+ );
+ button.style.listStyleImage = `url("${provider.iconURL}")`;
+ return button;
+ },
+
+ makeListItemForProvider(provider) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.classList.add("menuitem-iconic");
+ menuitem.setAttribute("value", provider.type);
+ menuitem.setAttribute("label", provider.displayName);
+ menuitem.setAttribute("image", provider.iconURL);
+ return menuitem;
+ },
+
+ // Sort the accounts by displayName.
+ _sortDisplayNames(a, b) {
+ let aName = a.displayName.toLowerCase();
+ let bName = b.displayName.toLowerCase();
+ return aName.localeCompare(bName);
+ },
+
+ rebuildView() {
+ // Clear the list of entries.
+ while (this._list.hasChildNodes()) {
+ this._list.lastChild.remove();
+ }
+
+ let accounts = cloudFileAccounts.accounts;
+ accounts.sort(this._sortDisplayNames);
+
+ for (let account of accounts) {
+ let rli = this.makeRichListItemForAccount(account);
+ this._list.appendChild(rli);
+ }
+
+ while (this._buttonContainer.hasChildNodes()) {
+ this._buttonContainer.lastChild.remove();
+ }
+
+ let providers = cloudFileAccounts.providers;
+ providers.sort(this._sortDisplayNames);
+ for (let provider of providers) {
+ this._buttonContainer.appendChild(this.makeButtonForProvider(provider));
+ this._listContainer.appendChild(this.makeListItemForProvider(provider));
+ }
+ },
+
+ onSelectionChanged(aEvent) {
+ if (!this._initialized || aEvent.target != this._list) {
+ return;
+ }
+
+ // Get the selected item
+ let selection = this._list.selectedItem;
+ this._removeAccountButton.disabled = !selection;
+ if (!selection) {
+ this._defaultPanel.hidden = false;
+ this._settingsPanelWrap.hidden = true;
+ if (this._settings) {
+ this._settings.remove();
+ }
+ return;
+ }
+
+ this._showAccountInfo(selection.value);
+ },
+
+ _showAccountInfo(aAccountKey) {
+ let account = cloudFileAccounts.getAccount(aAccountKey);
+ this._defaultPanel.hidden = true;
+ this._settingsPanelWrap.hidden = false;
+
+ let url = account.managementURL + `?accountId=${account.accountKey}`;
+
+ let browser = document.createXULElement("browser");
+ browser.setAttribute("type", "content");
+ browser.setAttribute("remote", "true");
+ browser.setAttribute("remoteType", E10SUtils.EXTENSION_REMOTE_TYPE);
+ browser.setAttribute("forcemessagemanager", "true");
+ if (account.extension) {
+ browser.setAttribute(
+ "initialBrowsingContextGroupId",
+ account.extension.policy.browsingContextGroupId
+ );
+ }
+ browser.setAttribute("disableglobalhistory", "true");
+ browser.setAttribute("messagemanagergroup", "webext-browsers");
+ browser.setAttribute("autocompletepopup", "PopupAutoComplete");
+ browser.setAttribute("selectmenulist", "ContentSelectDropdown");
+
+ browser.setAttribute("flex", "1");
+ // Allows keeping dialog background color without hoops.
+ browser.setAttribute("transparent", "true");
+
+ // If we have a past browser, we replace it. Else append to the wrapper.
+ if (this._settings) {
+ this._settings.remove();
+ }
+
+ this._settingsPanelWrap.appendChild(browser);
+ this._settings = browser;
+
+ ExtensionParent.apiManager.emit("extension-browser-inserted", browser);
+ browser.messageManager.loadFrameScript(
+ "chrome://extensions/content/ext-browser-content.js",
+ false,
+ true
+ );
+
+ let options = account.browserStyle
+ ? { stylesheets: ExtensionParent.extensionStylesheets }
+ : {};
+ browser.messageManager.sendAsyncMessage("Extension:InitBrowser", options);
+
+ browser.fixupAndLoadURIString(url, {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ },
+
+ onListOverflow() {
+ if (this._buttonContainer.childElementCount > 1) {
+ this._buttonContainer.hidden = true;
+ this._addAccountButton.hidden = false;
+ }
+ },
+
+ addCloudFileAccount(aType) {
+ let account = cloudFileAccounts.createAccount(aType);
+ if (!account) {
+ return;
+ }
+
+ let rli = this.makeRichListItemForAccount(account);
+ this._list.appendChild(rli);
+ this._list.selectItem(rli);
+ this._addAccountButton.removeAttribute("image");
+ this._addAccountButton.setAttribute(
+ "label",
+ this._addAccountButton.getAttribute("defaultlabel")
+ );
+ this._removeAccountButton.disabled = false;
+ },
+
+ removeCloudFileAccount() {
+ // Get the selected account key
+ let selection = this._list.selectedItem;
+ if (!selection) {
+ return;
+ }
+
+ let accountKey = selection.value;
+ let accountName = cloudFileAccounts.getDisplayName(accountKey);
+ // Does the user really want to remove this account?
+ let confirmMessage = this._strings.formatStringFromName(
+ "dialog_removeAccount",
+ [accountName]
+ );
+
+ if (Services.prompt.confirm(null, "", confirmMessage)) {
+ this._list.clearSelection();
+ cloudFileAccounts.removeAccount(accountKey);
+ let rli = this._list.querySelector(
+ "richlistitem[value='" + accountKey + "']"
+ );
+ rli.remove();
+ this._defaultPanel.hidden = false;
+ this._settingsPanelWrap.hidden = true;
+ if (this._settings) {
+ this._settings.remove();
+ }
+ }
+ },
+
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.destroy();
+ break;
+ case "click": {
+ let label = aEvent.target;
+ let item = label.parentNode;
+ let input = item.querySelector("input");
+ if (!item.selected) {
+ return;
+ }
+ label.hidden = true;
+ input.value = label.value;
+ input.removeAttribute("hidden");
+ input.focus();
+ break;
+ }
+ case "blur": {
+ let input = aEvent.target;
+ let item = input.parentNode;
+ let label = item.querySelector("label");
+ cloudFileAccounts.setDisplayName(item.value, input.value);
+ label.value = input.value;
+ label.hidden = false;
+ input.setAttribute("hidden", "hidden");
+ break;
+ }
+ case "keypress": {
+ let input = aEvent.target;
+ let item = input.parentNode;
+ let label = item.querySelector("label");
+
+ if (aEvent.key == "Enter") {
+ cloudFileAccounts.setDisplayName(item.value, input.value);
+ label.value = input.value;
+ label.hidden = false;
+ input.setAttribute("hidden", "hidden");
+ gCloudFile._list.focus();
+
+ aEvent.preventDefault();
+ } else if (aEvent.key == "Escape") {
+ input.value = label.value;
+ label.hidden = false;
+ input.setAttribute("hidden", "hidden");
+ gCloudFile._list.focus();
+
+ aEvent.preventDefault();
+ }
+ }
+ }
+ },
+
+ readThreshold() {
+ let pref = Preferences.get("mail.compose.big_attachments.threshold_kb");
+ return pref.value / 1024;
+ },
+
+ writeThreshold() {
+ let threshold = document.getElementById("cloudFileThreshold");
+ let intValue = parseInt(threshold.value, 10);
+ return isNaN(intValue) ? 0 : intValue * 1024;
+ },
+
+ updateThreshold() {
+ document.getElementById("cloudFileThreshold").disabled = !Preferences.get(
+ "mail.compose.big_attachments.notify"
+ ).value;
+ },
+};
+
+Preferences.get("mail.compose.autosave").on(
+ "change",
+ gComposePane.updateAutosave
+);
+Preferences.get("mail.compose.attachment_reminder").on(
+ "change",
+ gComposePane.updateAttachmentCheck
+);
+Preferences.get("msgcompose.default_colors").on(
+ "change",
+ gComposePane.updateUseReaderDefaults
+);
+Preferences.get("ldap_2.autoComplete.useDirectory").on(
+ "change",
+ gComposePane.enableAutocomplete
+);
+Preferences.get("mail.collect_email_address_outgoing").on(
+ "change",
+ gComposePane.updateEmailCollection
+);
+Preferences.get("mail.compose.big_attachments.notify").on(
+ "change",
+ gCloudFile.updateThreshold
+);
diff --git a/comm/mail/components/preferences/connection.js b/comm/mail/components/preferences/connection.js
new file mode 100644
index 0000000000..686c2950cf
--- /dev/null
+++ b/comm/mail/components/preferences/connection.js
@@ -0,0 +1,597 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* import-globals-from ../../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from ./extensionControlled.js */
+
+Preferences.addAll([
+ // Add network.proxy.autoconfig_url before network.proxy.type so they're
+ // both initialized when network.proxy.type initialization triggers a call to
+ // gConnectionsDialog.updateReloadButton().
+ { id: "network.proxy.autoconfig_url", type: "string" },
+ { id: "network.proxy.type", type: "int" },
+ { id: "network.proxy.http", type: "string" },
+ { id: "network.proxy.http_port", type: "int" },
+ { id: "network.proxy.ssl", type: "string" },
+ { id: "network.proxy.ssl_port", type: "int" },
+ { id: "network.proxy.socks", type: "string" },
+ { id: "network.proxy.socks_port", type: "int" },
+ { id: "network.proxy.socks_version", type: "int" },
+ { id: "network.proxy.socks_remote_dns", type: "bool" },
+ { id: "network.proxy.no_proxies_on", type: "string" },
+ { id: "network.proxy.share_proxy_settings", type: "bool" },
+ { id: "signon.autologin.proxy", type: "bool" },
+ { id: "pref.advanced.proxies.disable_button.reload", type: "bool" },
+ { id: "network.proxy.backup.ssl", type: "string" },
+ { id: "network.proxy.backup.ssl_port", type: "int" },
+ { id: "network.trr.mode", type: "int" },
+ { id: "network.trr.uri", type: "string" },
+ { id: "network.trr.resolvers", type: "string" },
+ { id: "network.trr.custom_uri", type: "string" },
+]);
+
+window.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ Preferences.get("network.proxy.type").on(
+ "change",
+ gConnectionsDialog.proxyTypeChanged.bind(gConnectionsDialog)
+ );
+ Preferences.get("network.proxy.socks_version").on(
+ "change",
+ gConnectionsDialog.updateDNSPref.bind(gConnectionsDialog)
+ );
+
+ Preferences.get("network.trr.uri").on("change", () => {
+ gConnectionsDialog.updateDnsOverHttpsUI();
+ });
+
+ Preferences.get("network.trr.resolvers").on("change", () => {
+ gConnectionsDialog.initDnsOverHttpsUI();
+ });
+
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxyType"),
+ () => gConnectionsDialog.readProxyType()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxyHTTP"),
+ () => gConnectionsDialog.readHTTPProxyServer()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxyHTTP_Port"),
+ () => gConnectionsDialog.readHTTPProxyPort()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("shareAllProxies"),
+ () => gConnectionsDialog.updateProtocolPrefs()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxySSL"),
+ () => gConnectionsDialog.readProxyProtocolPref("ssl", false)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxySSL_Port"),
+ () => gConnectionsDialog.readProxyProtocolPref("ssl", true)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxySOCKS"),
+ () => gConnectionsDialog.readProxyProtocolPref("socks", false)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxySOCKS_Port"),
+ () => gConnectionsDialog.readProxyProtocolPref("socks", true)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("networkProxySOCKSVersion"),
+ () => gConnectionsDialog.updateDNSPref()
+ );
+
+ // XXX: We can't init the DNS-over-HTTPs UI until the syncfrompref for network.trr.mode
+ // has been called. The uiReady promise will be resolved after the first call to
+ // readDnsOverHttpsMode and the subsequent call to initDnsOverHttpsUI has happened.
+ gConnectionsDialog.uiReady = new Promise(resolve => {
+ gConnectionsDialog._areTrrPrefsReady = false;
+ gConnectionsDialog._handleTrrPrefsReady = resolve;
+ }).then(() => {
+ gConnectionsDialog.initDnsOverHttpsUI();
+ });
+
+ let element = document.getElementById("networkDnsOverHttps");
+ Preferences.addSyncFromPrefListener(element, () =>
+ gConnectionsDialog.readDnsOverHttpsMode()
+ );
+ Preferences.addSyncToPrefListener(element, () =>
+ gConnectionsDialog.writeDnsOverHttpsMode()
+ );
+ document.documentElement.addEventListener("beforeaccept", e =>
+ gConnectionsDialog.beforeAccept(e)
+ );
+
+ document
+ .getElementById("proxyExtensionDisable")
+ .addEventListener("click", disableControllingProxyExtension);
+ gConnectionsDialog.updateProxySettingsUI();
+ initializeProxyUI(gConnectionsDialog);
+ },
+ { once: true, capture: true }
+);
+
+var gConnectionsDialog = {
+ beforeAccept(event) {
+ let dnsOverHttpsResolverChoice = document.getElementById(
+ "networkDnsOverHttpsResolverChoices"
+ ).value;
+ if (dnsOverHttpsResolverChoice == "custom") {
+ let customValue = document
+ .getElementById("networkCustomDnsOverHttpsInput")
+ .value.trim();
+ if (customValue) {
+ Services.prefs.setStringPref("network.trr.uri", customValue);
+ } else {
+ Services.prefs.clearUserPref("network.trr.uri");
+ }
+ } else {
+ Services.prefs.setStringPref(
+ "network.trr.uri",
+ dnsOverHttpsResolverChoice
+ );
+ }
+
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ if (proxyTypePref.value == 2) {
+ this.doAutoconfigURLFixup();
+ return;
+ }
+
+ if (proxyTypePref.value != 1) {
+ return;
+ }
+
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+
+ // If the port is 0 and the proxy server is specified, focus on the port and cancel submission.
+ for (let prefName of ["http", "ssl", "socks"]) {
+ let proxyPortPref = Preferences.get(
+ "network.proxy." + prefName + "_port"
+ );
+ let proxyPref = Preferences.get("network.proxy." + prefName);
+ // Only worry about ports which are currently active. If the share option is on, then ignore
+ // all ports except the HTTP and SOCKS port
+ if (
+ proxyPref.value != "" &&
+ proxyPortPref.value == 0 &&
+ (prefName == "http" || prefName == "socks" || !shareProxiesPref.value)
+ ) {
+ document
+ .getElementById("networkProxy" + prefName.toUpperCase() + "_Port")
+ .focus();
+ event.preventDefault();
+ return;
+ }
+ }
+
+ // In the case of a shared proxy preference, backup the current values and update with the HTTP value
+ if (shareProxiesPref.value) {
+ var proxyServerURLPref = Preferences.get("network.proxy.ssl");
+ var proxyPortPref = Preferences.get("network.proxy.ssl_port");
+ var backupServerURLPref = Preferences.get("network.proxy.backup.ssl");
+ var backupPortPref = Preferences.get("network.proxy.backup.ssl_port");
+ backupServerURLPref.value =
+ backupServerURLPref.value || proxyServerURLPref.value;
+ backupPortPref.value = backupPortPref.value || proxyPortPref.value;
+ proxyServerURLPref.value = httpProxyURLPref.value;
+ proxyPortPref.value = httpProxyPortPref.value;
+ }
+
+ this.sanitizeNoProxiesPref();
+ },
+
+ checkForSystemProxy() {
+ if ("@mozilla.org/system-proxy-settings;1" in Cc) {
+ document.getElementById("systemPref").removeAttribute("hidden");
+ }
+ },
+
+ proxyTypeChanged() {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+
+ // Update http
+ var httpProxyURLPref = Preferences.get("network.proxy.http");
+ httpProxyURLPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var httpProxyPortPref = Preferences.get("network.proxy.http_port");
+ httpProxyPortPref.updateControlDisabledState(proxyTypePref.value != 1);
+
+ // Now update the other protocols
+ this.updateProtocolPrefs();
+
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ shareProxiesPref.updateControlDisabledState(proxyTypePref.value != 1);
+ var autologinProxyPref = Preferences.get("signon.autologin.proxy");
+ autologinProxyPref.updateControlDisabledState(proxyTypePref.value == 0);
+ var noProxiesPref = Preferences.get("network.proxy.no_proxies_on");
+ noProxiesPref.updateControlDisabledState(proxyTypePref.value == 0);
+
+ var autoconfigURLPref = Preferences.get("network.proxy.autoconfig_url");
+ autoconfigURLPref.updateControlDisabledState(proxyTypePref.value != 2);
+
+ this.updateReloadButton();
+
+ document.getElementById("networkProxyNoneLocalhost").hidden =
+ Services.prefs.getBoolPref(
+ "network.proxy.allow_hijacking_localhost",
+ false
+ );
+ },
+
+ updateDNSPref() {
+ var socksVersionPref = Preferences.get("network.proxy.socks_version");
+ var socksDNSPref = Preferences.get("network.proxy.socks_remote_dns");
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ var isDefinitelySocks4 =
+ proxyTypePref.value == 1 && socksVersionPref.value == 4;
+ socksDNSPref.updateControlDisabledState(
+ isDefinitelySocks4 || proxyTypePref.value == 0
+ );
+ return undefined;
+ },
+
+ updateReloadButton() {
+ // Disable the "Reload PAC" button if the selected proxy type is not PAC or
+ // if the current value of the PAC textbox does not match the value stored
+ // in prefs. Likewise, disable the reload button if PAC is not configured
+ // in prefs.
+
+ var typedURL = document.getElementById("networkProxyAutoconfigURL").value;
+ var proxyTypeCur = Preferences.get("network.proxy.type").value;
+
+ var pacURL = Services.prefs.getCharPref("network.proxy.autoconfig_url");
+ var proxyType = Services.prefs.getIntPref("network.proxy.type");
+
+ var disableReloadPref = Preferences.get(
+ "pref.advanced.proxies.disable_button.reload"
+ );
+ disableReloadPref.updateControlDisabledState(
+ proxyTypeCur != 2 || proxyType != 2 || typedURL != pacURL
+ );
+ },
+
+ readProxyType() {
+ this.proxyTypeChanged();
+ return undefined;
+ },
+
+ updateProtocolPrefs() {
+ var proxyTypePref = Preferences.get("network.proxy.type");
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ var proxyPrefs = ["ssl", "socks"];
+ for (var i = 0; i < proxyPrefs.length; ++i) {
+ var proxyServerURLPref = Preferences.get(
+ "network.proxy." + proxyPrefs[i]
+ );
+ var proxyPortPref = Preferences.get(
+ "network.proxy." + proxyPrefs[i] + "_port"
+ );
+
+ // Restore previous per-proxy custom settings, if present.
+ if (proxyPrefs[i] != "socks" && !shareProxiesPref.value) {
+ var backupServerURLPref = Preferences.get(
+ "network.proxy.backup." + proxyPrefs[i]
+ );
+ var backupPortPref = Preferences.get(
+ "network.proxy.backup." + proxyPrefs[i] + "_port"
+ );
+ if (backupServerURLPref.hasUserValue) {
+ proxyServerURLPref.value = backupServerURLPref.value;
+ backupServerURLPref.reset();
+ }
+ if (backupPortPref.hasUserValue) {
+ proxyPortPref.value = backupPortPref.value;
+ backupPortPref.reset();
+ }
+ }
+
+ proxyServerURLPref.updateElements();
+ proxyPortPref.updateElements();
+ let prefIsShared = proxyPrefs[i] != "socks" && shareProxiesPref.value;
+ proxyServerURLPref.updateControlDisabledState(
+ proxyTypePref.value != 1 || prefIsShared
+ );
+ proxyPortPref.updateControlDisabledState(
+ proxyTypePref.value != 1 || prefIsShared
+ );
+ }
+ var socksVersionPref = Preferences.get("network.proxy.socks_version");
+ socksVersionPref.updateControlDisabledState(proxyTypePref.value != 1);
+ this.updateDNSPref();
+ return undefined;
+ },
+
+ readProxyProtocolPref(aProtocol, aIsPort) {
+ if (aProtocol != "socks") {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ var pref = Preferences.get(
+ "network.proxy.http" + (aIsPort ? "_port" : "")
+ );
+ return pref.value;
+ }
+
+ var backupPref = Preferences.get(
+ "network.proxy.backup." + aProtocol + (aIsPort ? "_port" : "")
+ );
+ return backupPref.hasUserValue ? backupPref.value : undefined;
+ }
+ return undefined;
+ },
+
+ reloadPAC() {
+ Cc["@mozilla.org/network/protocol-proxy-service;1"]
+ .getService()
+ .reloadPAC();
+ },
+
+ doAutoconfigURLFixup() {
+ var autoURL = document.getElementById("networkProxyAutoconfigURL");
+ var autoURLPref = Preferences.get("network.proxy.autoconfig_url");
+ try {
+ autoURLPref.value = autoURL.value = Services.uriFixup.getFixupURIInfo(
+ autoURL.value,
+ 0
+ ).preferredURI.spec;
+ } catch (ex) {}
+ },
+
+ sanitizeNoProxiesPref() {
+ var noProxiesPref = Preferences.get("network.proxy.no_proxies_on");
+ // replace substrings of ; and \n with commas if they're neither immediately
+ // preceded nor followed by a valid separator character
+ noProxiesPref.value = noProxiesPref.value.replace(
+ /([^, \n;])[;\n]+(?![,\n;])/g,
+ "$1,"
+ );
+ // replace any remaining ; and \n since some may follow commas, etc.
+ noProxiesPref.value = noProxiesPref.value.replace(/[;\n]/g, "");
+ },
+
+ readHTTPProxyServer() {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ this.updateProtocolPrefs();
+ }
+ return undefined;
+ },
+
+ readHTTPProxyPort() {
+ var shareProxiesPref = Preferences.get(
+ "network.proxy.share_proxy_settings"
+ );
+ if (shareProxiesPref.value) {
+ this.updateProtocolPrefs();
+ }
+ return undefined;
+ },
+
+ getProxyControls() {
+ let controlGroup = document.getElementById("networkProxyType");
+ return [
+ ...controlGroup.querySelectorAll(":scope > radio"),
+ ...controlGroup.querySelectorAll("label"),
+ ...controlGroup.querySelectorAll("input"),
+ ...controlGroup.querySelectorAll("checkbox"),
+ ...document.querySelectorAll("#networkProxySOCKSVersion > radio"),
+ ...document.querySelectorAll("#ConnectionsDialogPane > checkbox"),
+ ];
+ },
+
+ // Update the UI to show/hide the extension controlled message for
+ // proxy settings.
+ async updateProxySettingsUI() {
+ let isLocked = API_PROXY_PREFS.some(pref =>
+ Services.prefs.prefIsLocked(pref)
+ );
+
+ function setInputsDisabledState(isControlled) {
+ for (let element of gConnectionsDialog.getProxyControls()) {
+ element.disabled = isControlled;
+ }
+ gConnectionsDialog.proxyTypeChanged();
+ }
+
+ if (isLocked) {
+ // An extension can't control this setting if any pref is locked.
+ hideControllingProxyExtension();
+ } else {
+ handleControllingProxyExtension().then(setInputsDisabledState);
+ }
+ },
+
+ get dnsOverHttpsResolvers() {
+ let rawValue = Preferences.get("network.trr.resolvers", "").value;
+ // if there's no default, we'll hold its position with an empty string
+ let defaultURI = Preferences.get("network.trr.uri", "").defaultValue;
+ let providers = [];
+ if (rawValue) {
+ try {
+ providers = JSON.parse(rawValue);
+ } catch (ex) {
+ console.error(
+ `Bad JSON data in pref network.trr.resolvers: ${rawValue}`
+ );
+ }
+ }
+ if (!Array.isArray(providers)) {
+ console.error(
+ `Expected a JSON array in network.trr.resolvers: ${rawValue}`
+ );
+ providers = [];
+ }
+ let defaultIndex = providers.findIndex(p => p.url == defaultURI);
+ if (defaultIndex == -1 && defaultURI) {
+ // the default value for the pref isn't included in the resolvers list
+ // so we'll make a stub for it. Without an id, we'll have to use the url as the label
+ providers.unshift({ url: defaultURI });
+ }
+ return providers;
+ },
+
+ isDnsOverHttpsLocked() {
+ return Services.prefs.prefIsLocked("network.trr.mode");
+ },
+
+ isDnsOverHttpsEnabled() {
+ // values outside 1:4 are considered falsey/disabled in this context
+ let trrPref = Preferences.get("network.trr.mode");
+ let enabled = trrPref.value > 0 && trrPref.value < 5;
+ return enabled;
+ },
+
+ readDnsOverHttpsMode() {
+ // called to update checked element property to reflect current pref value
+ let enabled = this.isDnsOverHttpsEnabled();
+ let uriPref = Preferences.get("network.trr.uri");
+ uriPref.updateControlDisabledState(!enabled || this.isDnsOverHttpsLocked());
+ // this is the first signal we get when the prefs are available, so
+ // lazy-init if appropriate
+ if (!this._areTrrPrefsReady) {
+ this._areTrrPrefsReady = true;
+ this._handleTrrPrefsReady();
+ } else {
+ this.updateDnsOverHttpsUI();
+ }
+ return enabled;
+ },
+
+ writeDnsOverHttpsMode() {
+ // called to update pref with user change
+ let trrModeCheckbox = document.getElementById("networkDnsOverHttps");
+ // we treat checked/enabled as mode 2
+ return trrModeCheckbox.checked ? 2 : 0;
+ },
+
+ updateDnsOverHttpsUI() {
+ // init and update of the UI must wait until the pref values are ready
+ if (!this._areTrrPrefsReady) {
+ return;
+ }
+ let [menu, customInput] = this.getDnsOverHttpsControls();
+ let customContainer = document.getElementById(
+ "customDnsOverHttpsContainer"
+ );
+ let customURI = Preferences.get("network.trr.custom_uri").value;
+ let currentURI = Preferences.get("network.trr.uri").value;
+ let resolvers = this.dnsOverHttpsResolvers;
+ let isCustom = menu.value == "custom";
+
+ if (this.isDnsOverHttpsEnabled()) {
+ this.toggleDnsOverHttpsUI(false);
+ if (isCustom) {
+ // if the current and custom_uri values mismatch, update the uri pref
+ if (
+ currentURI &&
+ !customURI &&
+ !resolvers.find(r => r.url == currentURI)
+ ) {
+ Services.prefs.setStringPref("network.trr.custom_uri", currentURI);
+ }
+ }
+ } else {
+ this.toggleDnsOverHttpsUI(true);
+ }
+
+ if (!menu.disabled && isCustom) {
+ customContainer.hidden = false;
+ customInput.disabled = false;
+ } else {
+ customContainer.hidden = true;
+ customInput.disabled = true;
+ }
+
+ // The height has likely changed, find our SubDialog and tell it to resize.
+ requestAnimationFrame(() => {
+ let dialogs = window.opener.gSubDialog._dialogs;
+ let dialog = dialogs.find(d => d._frame.contentDocument == document);
+ if (dialog) {
+ dialog.resizeVertically();
+ }
+ });
+ },
+
+ getDnsOverHttpsControls() {
+ return [
+ document.getElementById("networkDnsOverHttpsResolverChoices"),
+ document.getElementById("networkCustomDnsOverHttpsInput"),
+ document.getElementById("networkDnsOverHttpsResolverChoicesLabel"),
+ document.getElementById("networkCustomDnsOverHttpsInputLabel"),
+ ];
+ },
+
+ toggleDnsOverHttpsUI(disabled) {
+ for (let element of this.getDnsOverHttpsControls()) {
+ element.disabled = disabled;
+ }
+ },
+
+ initDnsOverHttpsUI() {
+ let resolvers = this.dnsOverHttpsResolvers;
+ let defaultURI = Preferences.get("network.trr.uri").defaultValue;
+ let currentURI = Preferences.get("network.trr.uri").value;
+ let menu = document.getElementById("networkDnsOverHttpsResolverChoices");
+
+ // populate the DNS-Over-HTTPs resolver list
+ menu.removeAllItems();
+ for (let resolver of resolvers) {
+ let item = menu.appendItem(undefined, resolver.url);
+ if (resolver.url == defaultURI) {
+ document.l10n.setAttributes(
+ item,
+ "connection-dns-over-https-url-item-default",
+ {
+ name: resolver.name || resolver.url,
+ }
+ );
+ } else {
+ item.label = resolver.name || resolver.url;
+ }
+ }
+ let lastItem = menu.appendItem(undefined, "custom");
+ document.l10n.setAttributes(
+ lastItem,
+ "connection-dns-over-https-url-custom"
+ );
+
+ // set initial selection in the resolver provider picker
+ let selectedIndex = currentURI
+ ? resolvers.findIndex(r => r.url == currentURI)
+ : 0;
+ if (selectedIndex == -1) {
+ // select the last "Custom" item
+ selectedIndex = menu.itemCount - 1;
+ }
+ menu.selectedIndex = selectedIndex;
+
+ if (this.isDnsOverHttpsLocked()) {
+ // disable all the options and the checkbox itself to disallow enabling them
+ this.toggleDnsOverHttpsUI(true);
+ document.getElementById("networkDnsOverHttps").disabled = true;
+ } else {
+ this.toggleDnsOverHttpsUI(false);
+ this.updateDnsOverHttpsUI();
+ document.getElementById("networkDnsOverHttps").disabled = false;
+ }
+ },
+};
diff --git a/comm/mail/components/preferences/connection.xhtml b/comm/mail/components/preferences/connection.xhtml
new file mode 100644
index 0000000000..1bbb822f66
--- /dev/null
+++ b/comm/mail/components/preferences/connection.xhtml
@@ -0,0 +1,264 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE window>
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="connection-dialog-window2"
+ style="min-width: 49em"
+ onload="gConnectionsDialog.checkForSystemProxy();"
+>
+ <dialog id="ConnectionsDialog" dlgbuttons="accept,cancel">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/preferences/connection.ftl"
+ />
+ <html:link
+ rel="localization"
+ href="messenger/preferences/preferences.ftl"
+ />
+ <html:link rel="localization" href="branding/brand.ftl" />
+ </linkset>
+
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/extensionControlled.js" />
+ <script src="chrome://messenger/content/preferences/connection.js" />
+
+ <!-- Need a wrapper div within the xul:dialog, which otherwise does not give
+ - enough height for the flex display.
+ - REMOVE when we use HTML only. -->
+ <html:div>
+ <html:div id="proxyExtensionContent" hidden="hidden">
+ <html:p id="proxyExtensionDescription">
+ <html:img data-l10n-name="extension-icon" />
+ </html:p>
+ <html:button
+ id="proxyExtensionDisable"
+ data-l10n-id="disable-extension-button"
+ >
+ </html:button>
+ </html:div>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="connection-proxy-legend"></html:legend>
+
+ <radiogroup id="networkProxyType" preference="network.proxy.type">
+ <radio value="0" data-l10n-id="proxy-type-no" />
+ <radio value="4" data-l10n-id="proxy-type-wpad" />
+ <radio
+ value="5"
+ data-l10n-id="proxy-type-system"
+ id="systemPref"
+ hidden="true"
+ />
+ <radio value="1" data-l10n-id="proxy-type-manual" />
+ <box id="proxy-grid" class="indent" flex="1">
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="proxy-http-label"
+ control="networkProxyHTTP"
+ />
+ </hbox>
+ <hbox align="center" class="input-container">
+ <html:input
+ id="networkProxyHTTP"
+ type="text"
+ preference="network.proxy.http"
+ />
+ <label
+ data-l10n-id="http-port-label"
+ control="networkProxyHTTP_Port"
+ />
+ <html:input
+ id="networkProxyHTTP_Port"
+ type="number"
+ class="size5"
+ max="65535"
+ preference="network.proxy.http_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox />
+ <hbox>
+ <checkbox
+ id="shareAllProxies"
+ data-l10n-id="proxy-http-sharing"
+ preference="network.proxy.share_proxy_settings"
+ class="align-no-label"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="proxy-https-label"
+ control="networkProxySSL"
+ />
+ </hbox>
+ <hbox align="center" class="input-container">
+ <html:input
+ id="networkProxySSL"
+ type="text"
+ preference="network.proxy.ssl"
+ />
+ <label
+ data-l10n-id="ssl-port-label"
+ control="networkProxySSL_Port"
+ />
+ <html:input
+ id="networkProxySSL_Port"
+ type="number"
+ class="size5"
+ max="65535"
+ preference="network.proxy.ssl_port"
+ />
+ </hbox>
+ </html:div>
+ <separator class="thin" />
+ <html:div class="proxy-grid-row">
+ <hbox pack="end">
+ <label
+ data-l10n-id="proxy-socks-label"
+ control="networkProxySOCKS"
+ />
+ </hbox>
+ <hbox align="center" class="input-container">
+ <html:input
+ id="networkProxySOCKS"
+ type="text"
+ preference="network.proxy.socks"
+ />
+ <label
+ data-l10n-id="socks-port-label"
+ control="networkProxySOCKS_Port"
+ />
+ <html:input
+ id="networkProxySOCKS_Port"
+ type="number"
+ class="size5"
+ max="65535"
+ preference="network.proxy.socks_port"
+ />
+ </hbox>
+ </html:div>
+ <html:div class="proxy-grid-row">
+ <spacer />
+ <radiogroup
+ id="networkProxySOCKSVersion"
+ orient="horizontal"
+ class="align-no-label"
+ preference="network.proxy.socks_version"
+ >
+ <radio
+ id="networkProxySOCKSVersion4"
+ value="4"
+ data-l10n-id="proxy-socks4-label"
+ />
+ <radio
+ id="networkProxySOCKSVersion5"
+ value="5"
+ data-l10n-id="proxy-socks5-label"
+ />
+ </radiogroup>
+ </html:div>
+ </box>
+ <radio value="2" data-l10n-id="proxy-type-auto" />
+ <hbox class="indent input-container" flex="1" align="center">
+ <html:input
+ id="networkProxyAutoconfigURL"
+ type="url"
+ preference="network.proxy.autoconfig_url"
+ oninput="gConnectionsDialog.updateReloadButton();"
+ />
+ <button
+ id="autoReload"
+ data-l10n-id="proxy-reload-label"
+ oncommand="gConnectionsDialog.reloadPAC();"
+ preference="pref.advanced.proxies.disable_button.reload"
+ />
+ </hbox>
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+ <separator class="thin" />
+ <label data-l10n-id="no-proxy-label" control="networkProxyNone" />
+ <html:textarea
+ id="networkProxyNone"
+ rows="2"
+ preference="network.proxy.no_proxies_on"
+ />
+ <label data-l10n-id="no-proxy-example" control="networkProxyNone" />
+ <label
+ id="networkProxyNoneLocalhost"
+ control="networkProxyNone"
+ data-l10n-id="connection-proxy-noproxy-localhost-desc-2"
+ />
+ <separator class="thin" />
+ <checkbox
+ id="autologinProxy"
+ data-l10n-id="proxy-password-prompt"
+ preference="signon.autologin.proxy"
+ />
+ <checkbox
+ id="networkProxySOCKSRemoteDNS"
+ preference="network.proxy.socks_remote_dns"
+ data-l10n-id="proxy-remote-dns"
+ />
+ <separator class="thin" />
+ <checkbox
+ id="networkDnsOverHttps"
+ data-l10n-id="proxy-enable-doh"
+ preference="network.trr.mode"
+ />
+ <box id="dnsOverHttps-grid" class="indent" flex="1">
+ <html:div class="dnsOverHttps-grid-row">
+ <hbox pack="end">
+ <label
+ id="networkDnsOverHttpsResolverChoicesLabel"
+ data-l10n-id="connection-dns-over-https-url-resolver"
+ control="networkDnsOverHttpsResolverChoices"
+ />
+ </hbox>
+ <menulist
+ id="networkDnsOverHttpsResolverChoices"
+ flex="1"
+ oncommand="gConnectionsDialog.updateDnsOverHttpsUI()"
+ />
+ </html:div>
+ <html:div
+ class="dnsOverHttps-grid-row"
+ id="customDnsOverHttpsContainer"
+ hidden="hidden"
+ >
+ <hbox>
+ <label
+ id="networkCustomDnsOverHttpsInputLabel"
+ data-l10n-id="connection-dns-over-https-custom-label"
+ control="networkCustomDnsOverHttpsInput"
+ />
+ </hbox>
+ <html:input
+ id="networkCustomDnsOverHttpsInput"
+ type="url"
+ style="flex: 1"
+ preference="network.trr.custom_uri"
+ />
+ </html:div>
+ </box>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/cookies.js b/comm/mail/components/preferences/cookies.js
new file mode 100644
index 0000000000..da06eb7e5a
--- /dev/null
+++ b/comm/mail/components/preferences/cookies.js
@@ -0,0 +1,993 @@
+/* -*- 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 { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ ContextualIdentityService:
+ "resource://gre/modules/ContextualIdentityService.sys.mjs",
+});
+
+var gCookiesWindow = {
+ _hosts: {},
+ _hostOrder: [],
+ _tree: null,
+ _bundle: null,
+
+ init() {
+ Services.obs.addObserver(this, "cookie-changed");
+ Services.obs.addObserver(this, "perm-changed");
+
+ this._bundle = document.getElementById("bundlePreferences");
+ this._tree = document.getElementById("cookiesList");
+
+ this._populateList(true);
+
+ document.getElementById("filter").focus();
+
+ if (!Services.prefs.getBoolPref("privacy.userContext.enabled")) {
+ document.getElementById("userContext").hidden = true;
+ document.getElementById("userContextLabel").hidden = true;
+ }
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, "cookie-changed");
+ Services.obs.removeObserver(this, "perm-changed");
+ },
+
+ _populateList(aInitialLoad) {
+ this._loadCookies();
+ this._tree.view = this._view;
+ if (aInitialLoad) {
+ this.sort("rawHost");
+ }
+ if (this._view.rowCount > 0) {
+ this._tree.view.selection.select(0);
+ }
+
+ if (aInitialLoad) {
+ if (
+ "arguments" in window &&
+ window.arguments[2] &&
+ window.arguments[2].filterString
+ ) {
+ this.setFilter(window.arguments[2].filterString);
+ }
+ } else if (document.getElementById("filter").value != "") {
+ this.filter();
+ }
+
+ this._saveState();
+ },
+
+ _cookieEquals(aCookieA, aCookieB, aStrippedHost) {
+ return (
+ aCookieA.rawHost == aStrippedHost &&
+ aCookieA.name == aCookieB.name &&
+ aCookieA.path == aCookieB.path &&
+ ChromeUtils.isOriginAttributesEqual(
+ aCookieA.originAttributes,
+ aCookieB.originAttributes
+ )
+ );
+ },
+
+ observe(aCookie, aTopic, aData) {
+ if (aTopic != "cookie-changed") {
+ return;
+ }
+
+ if (aCookie instanceof Ci.nsICookie) {
+ var strippedHost = this._makeStrippedHost(aCookie.host);
+ if (aData == "changed") {
+ this._handleCookieChanged(aCookie, strippedHost);
+ } else if (aData == "added") {
+ this._handleCookieAdded(aCookie, strippedHost);
+ }
+ } else if (aData == "cleared") {
+ this._hosts = {};
+ this._hostOrder = [];
+
+ var oldRowCount = this._view._rowCount;
+ this._view._rowCount = 0;
+ this._tree.rowCountChanged(0, -oldRowCount);
+ this._view.selection.clearSelection();
+ } else if (aData == "reload") {
+ // first, clear any existing entries
+ this.observe(aCookie, aTopic, "cleared");
+
+ // then, reload the list
+ this._populateList(false);
+ }
+
+ // We don't yet handle aData == "deleted" - it's a less common case
+ // and is rather complicated as selection tracking is difficult
+ },
+
+ _handleCookieChanged(changedCookie, strippedHost) {
+ var rowIndex = 0;
+ var cookieItem = null;
+ if (!this._view._filtered) {
+ for (var i = 0; i < this._hostOrder.length; ++i) {
+ // (var host in this._hosts) {
+ ++rowIndex;
+ var hostItem = this._hosts[this._hostOrder[i]]; // var hostItem = this._hosts[host];
+ if (this._hostOrder[i] == strippedHost) {
+ // host == strippedHost) {
+ // Host matches, look for the cookie within this Host collection
+ // and update its data
+ for (var j = 0; j < hostItem.cookies.length; ++j) {
+ ++rowIndex;
+ var currCookie = hostItem.cookies[j];
+ if (this._cookieEquals(currCookie, changedCookie, strippedHost)) {
+ currCookie.value = changedCookie.value;
+ currCookie.isSecure = changedCookie.isSecure;
+ currCookie.isDomain = changedCookie.isDomain;
+ currCookie.expires = changedCookie.expires;
+ cookieItem = currCookie;
+ break;
+ }
+ }
+ } else if (hostItem.open) {
+ rowIndex += hostItem.cookies.length;
+ }
+ }
+ } else {
+ // Just walk the filter list to find the item. It doesn't matter that
+ // we don't update the main Host collection when we do this, because
+ // when the filter is reset the Host collection is rebuilt anyway.
+ for (rowIndex = 0; rowIndex < this._view._filterSet.length; ++rowIndex) {
+ currCookie = this._view._filterSet[rowIndex];
+ if (this._cookieEquals(currCookie, changedCookie, strippedHost)) {
+ currCookie.value = changedCookie.value;
+ currCookie.isSecure = changedCookie.isSecure;
+ currCookie.isDomain = changedCookie.isDomain;
+ currCookie.expires = changedCookie.expires;
+ cookieItem = currCookie;
+ break;
+ }
+ }
+ }
+
+ // Make sure the tree display is up to date...
+ this._tree.invalidateRow(rowIndex);
+ // ... and if the cookie is selected, update the displayed metadata too
+ if (cookieItem != null && this._view.selection.currentIndex == rowIndex) {
+ this._updateCookieData(cookieItem);
+ }
+ },
+
+ _handleCookieAdded(changedCookie, strippedHost) {
+ var rowCountImpact = 0;
+ var addedHost = { value: 0 };
+ this._addCookie(strippedHost, changedCookie, addedHost);
+ if (!this._view._filtered) {
+ // The Host collection for this cookie already exists, and it's not open,
+ // so don't increment the rowCountImpact because the user is not going to
+ // see the additional rows as they're hidden.
+ if (addedHost.value || this._hosts[strippedHost].open) {
+ ++rowCountImpact;
+ }
+ } else {
+ // We're in search mode, and the cookie being added matches
+ // the search condition, so add it to the list.
+ var c = this._makeCookieObject(strippedHost, changedCookie);
+ if (this._cookieMatchesFilter(c)) {
+ this._view._filterSet.push(
+ this._makeCookieObject(strippedHost, changedCookie)
+ );
+ ++rowCountImpact;
+ }
+ }
+ // Now update the tree display at the end (we could/should re run the sort
+ // if any to get the position correct.)
+ var oldRowCount = this._rowCount;
+ this._view._rowCount += rowCountImpact;
+ this._tree.rowCountChanged(oldRowCount - 1, rowCountImpact);
+
+ document.getElementById("removeAllCookies").disabled = this._view._filtered;
+ },
+
+ _view: {
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+ _filtered: false,
+ _filterSet: [],
+ _filterValue: "",
+ _rowCount: 0,
+ _cacheValid: 0,
+ _cacheItems: [],
+ get rowCount() {
+ return this._rowCount;
+ },
+
+ _getItemAtIndex(aIndex) {
+ if (this._filtered) {
+ return this._filterSet[aIndex];
+ }
+
+ var start = 0;
+ var count = 0,
+ hostIndex = 0;
+
+ var cacheIndex = Math.min(this._cacheValid, aIndex);
+ if (cacheIndex > 0) {
+ var cacheItem = this._cacheItems[cacheIndex];
+ start = cacheItem.start;
+ count = hostIndex = cacheItem.count;
+ }
+
+ for (let i = start; i < gCookiesWindow._hostOrder.length; ++i) {
+ let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]];
+ if (!currHost) {
+ continue;
+ }
+ if (count == aIndex) {
+ return currHost;
+ }
+ hostIndex = count;
+
+ let cacheEntry = { start: i, count };
+ var cacheStart = count;
+
+ if (currHost.open) {
+ if (count < aIndex && aIndex <= count + currHost.cookies.length) {
+ // We are looking for an entry within this host's children,
+ // enumerate them looking for the index.
+ ++count;
+ for (let j = 0; j < currHost.cookies.length; ++j) {
+ if (count == aIndex) {
+ let cookie = currHost.cookies[j];
+ cookie.parentIndex = hostIndex;
+ return cookie;
+ }
+ ++count;
+ }
+ } else {
+ // A host entry was open, but we weren't looking for an index
+ // within that host entry's children, so skip forward over the
+ // entry's children. We need to add one to increment for the
+ // host value too.
+ count += currHost.cookies.length + 1;
+ }
+ } else {
+ ++count;
+ }
+
+ for (let j = cacheStart; j < count; j++) {
+ this._cacheItems[j] = cacheEntry;
+ }
+ this._cacheValid = count - 1;
+ }
+ return null;
+ },
+
+ _removeItemAtIndex(aIndex, aCount) {
+ var removeCount = aCount === undefined ? 1 : aCount;
+ if (this._filtered) {
+ // remove the cookies from the unfiltered set so that they
+ // don't reappear when the filter is changed. See bug 410863.
+ for (let i = aIndex; i < aIndex + removeCount; ++i) {
+ let item = this._filterSet[i];
+ let parent = gCookiesWindow._hosts[item.rawHost];
+ for (var j = 0; j < parent.cookies.length; ++j) {
+ if (item == parent.cookies[j]) {
+ parent.cookies.splice(j, 1);
+ break;
+ }
+ }
+ }
+ this._filterSet.splice(aIndex, removeCount);
+ return;
+ }
+
+ let item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return;
+ }
+ this._invalidateCache(aIndex - 1);
+ if (item.container) {
+ gCookiesWindow._hosts[item.rawHost] = null;
+ } else {
+ let parent = this._getItemAtIndex(item.parentIndex);
+ for (let i = 0; i < parent.cookies.length; ++i) {
+ var cookie = parent.cookies[i];
+ if (
+ item.rawHost == cookie.rawHost &&
+ item.name == cookie.name &&
+ item.path == cookie.path &&
+ ChromeUtils.isOriginAttributesEqual(
+ item.originAttributes,
+ cookie.originAttributes
+ )
+ ) {
+ parent.cookies.splice(i, removeCount);
+ }
+ }
+ }
+ },
+
+ _invalidateCache(aIndex) {
+ this._cacheValid = Math.min(this._cacheValid, aIndex);
+ },
+
+ getCellText(aIndex, aColumn) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return "";
+ }
+ if (aColumn.id == "domainCol") {
+ return item.rawHost;
+ }
+ if (aColumn.id == "nameCol") {
+ return "name" in item ? item.name : "";
+ }
+ } else if (aColumn.id == "domainCol") {
+ return this._filterSet[aIndex].rawHost;
+ } else if (aColumn.id == "nameCol") {
+ return "name" in this._filterSet[aIndex]
+ ? this._filterSet[aIndex].name
+ : "";
+ }
+ return "";
+ },
+
+ _selection: null,
+ get selection() {
+ return this._selection;
+ },
+ set selection(val) {
+ this._selection = val;
+ },
+ getRowProperties(aRow) {
+ return "";
+ },
+ getCellProperties(aRow, aColumn) {
+ return "";
+ },
+ getColumnProperties(aColumn) {
+ return "";
+ },
+ isContainer(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return false;
+ }
+ return item.container;
+ }
+ return false;
+ },
+ isContainerOpen(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return false;
+ }
+ return item.open;
+ }
+ return false;
+ },
+ isContainerEmpty(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return false;
+ }
+ return item.cookies.length == 0;
+ }
+ return false;
+ },
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted(aIndex) {
+ return false;
+ },
+ canDrop(aIndex, aOrientation) {
+ return false;
+ },
+ drop(aIndex, aOrientation) {},
+ getParentIndex(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ // If an item has no parent index (i.e. it is at the top level) this
+ // function MUST return -1 otherwise we will go into an infinite loop.
+ // Containers are always top level items in the cookies tree, so make
+ // sure to return the appropriate value here.
+ if (!item || item.container) {
+ return -1;
+ }
+ return item.parentIndex;
+ }
+ return -1;
+ },
+ hasNextSibling(aParentIndex, aIndex) {
+ if (!this._filtered) {
+ // |aParentIndex| appears to be bogus, but we can get the real
+ // parent index by getting the entry for |aIndex| and reading the
+ // parentIndex field.
+ // The index of the last item in this host collection is the
+ // index of the parent + the size of the host collection, and
+ // aIndex has a next sibling if it is less than this value.
+ var item = this._getItemAtIndex(aIndex);
+ if (item) {
+ if (item.container) {
+ for (var i = aIndex + 1; i < this.rowCount; ++i) {
+ var subsequent = this._getItemAtIndex(i);
+ if (subsequent.container) {
+ return true;
+ }
+ }
+ return false;
+ }
+ let parent = this._getItemAtIndex(item.parentIndex);
+ if (parent && parent.container) {
+ return aIndex < item.parentIndex + parent.cookies.length;
+ }
+ }
+ }
+ return aIndex < this.rowCount - 1;
+ },
+ hasPreviousSibling(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return false;
+ }
+ var parent = this._getItemAtIndex(item.parentIndex);
+ if (parent && parent.container) {
+ return aIndex > item.parentIndex + 1;
+ }
+ }
+ return aIndex > 0;
+ },
+ getLevel(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return 0;
+ }
+ return item.level;
+ }
+ return 0;
+ },
+ getImageSrc(aIndex, aColumn) {},
+ getProgressMode(aIndex, aColumn) {},
+ getCellValue(aIndex, aColumn) {},
+ setTree(aTree) {},
+ toggleOpenState(aIndex) {
+ if (!this._filtered) {
+ var item = this._getItemAtIndex(aIndex);
+ if (!item) {
+ return;
+ }
+ this._invalidateCache(aIndex);
+ var multiplier = item.open ? -1 : 1;
+ var delta = multiplier * item.cookies.length;
+ this._rowCount += delta;
+ item.open = !item.open;
+ gCookiesWindow._tree.rowCountChanged(aIndex + 1, delta);
+ gCookiesWindow._tree.invalidateRow(aIndex);
+ }
+ },
+ cycleHeader(aColumn) {},
+ selectionChanged() {},
+ cycleCell(aIndex, aColumn) {},
+ isEditable(aIndex, aColumn) {
+ return false;
+ },
+ setCellValue(aIndex, aColumn, aValue) {},
+ setCellText(aIndex, aColumn, aValue) {},
+ },
+
+ _makeStrippedHost(aHost) {
+ let formattedHost = aHost.startsWith(".")
+ ? aHost.substring(1, aHost.length)
+ : aHost;
+ return formattedHost.startsWith("www.")
+ ? formattedHost.substring(4, formattedHost.length)
+ : formattedHost;
+ },
+
+ _addCookie(aStrippedHost, aCookie, aHostCount) {
+ if (!(aStrippedHost in this._hosts) || !this._hosts[aStrippedHost]) {
+ this._hosts[aStrippedHost] = {
+ cookies: [],
+ rawHost: aStrippedHost,
+ level: 0,
+ open: false,
+ container: true,
+ };
+ this._hostOrder.push(aStrippedHost);
+ ++aHostCount.value;
+ }
+
+ var c = this._makeCookieObject(aStrippedHost, aCookie);
+ this._hosts[aStrippedHost].cookies.push(c);
+ },
+
+ _makeCookieObject(aStrippedHost, aCookie) {
+ let c = {
+ name: aCookie.name,
+ value: aCookie.value,
+ isDomain: aCookie.isDomain,
+ host: aCookie.host,
+ rawHost: aStrippedHost,
+ path: aCookie.path,
+ isSecure: aCookie.isSecure,
+ expires: aCookie.expires,
+ level: 1,
+ container: false,
+ originAttributes: aCookie.originAttributes,
+ };
+ return c;
+ },
+
+ _loadCookies() {
+ var hostCount = { value: 0 };
+ this._hosts = {};
+ this._hostOrder = [];
+ for (let cookie of Services.cookies.cookies) {
+ var strippedHost = this._makeStrippedHost(cookie.host);
+ this._addCookie(strippedHost, cookie, hostCount);
+ }
+ this._view._rowCount = hostCount.value;
+ },
+
+ formatExpiresString(aExpires) {
+ if (aExpires) {
+ var date = new Date(1000 * aExpires);
+ const dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "long",
+ timeStyle: "long",
+ });
+ return dateTimeFormatter.format(date);
+ }
+ return this._bundle.getString("expireAtEndOfSession");
+ },
+
+ _getUserContextString(aUserContextId) {
+ if (parseInt(aUserContextId, 10) == 0) {
+ return this._bundle.getString("defaultUserContextLabel");
+ }
+
+ return ContextualIdentityService.getUserContextLabel(aUserContextId);
+ },
+
+ _updateCookieData(aItem) {
+ var seln = this._view.selection;
+ var ids = [
+ "name",
+ "value",
+ "host",
+ "path",
+ "isSecure",
+ "expires",
+ "userContext",
+ ];
+ var properties;
+
+ if (aItem && !aItem.container && seln.count > 0) {
+ properties = {
+ name: aItem.name,
+ value: aItem.value,
+ host: aItem.host,
+ path: aItem.path,
+ expires: this.formatExpiresString(aItem.expires),
+ isDomain: aItem.isDomain
+ ? this._bundle.getString("domainColon")
+ : this._bundle.getString("hostColon"),
+ isSecure: aItem.isSecure
+ ? this._bundle.getString("forSecureOnly")
+ : this._bundle.getString("forAnyConnection"),
+ userContext: this._getUserContextString(
+ aItem.originAttributes.userContextId
+ ),
+ };
+ for (var i = 0; i < ids.length; ++i) {
+ document.getElementById(ids[i]).disabled = false;
+ }
+ } else {
+ var noneSelected = this._bundle.getString("noCookieSelected");
+ properties = {
+ name: noneSelected,
+ value: noneSelected,
+ host: noneSelected,
+ path: noneSelected,
+ expires: noneSelected,
+ isSecure: noneSelected,
+ userContext: noneSelected,
+ };
+ for (i = 0; i < ids.length; ++i) {
+ document.getElementById(ids[i]).disabled = true;
+ }
+ }
+ for (var property in properties) {
+ document.getElementById(property).value = properties[property];
+ }
+ },
+
+ onCookieSelected() {
+ var item;
+ var seln = this._tree.view.selection;
+ if (!this._view._filtered) {
+ item = this._view._getItemAtIndex(seln.currentIndex);
+ } else {
+ item = this._view._filterSet[seln.currentIndex];
+ }
+
+ this._updateCookieData(item);
+
+ var rangeCount = seln.getRangeCount();
+ var selectedCookieCount = 0;
+ for (var i = 0; i < rangeCount; ++i) {
+ var min = {};
+ var max = {};
+ seln.getRangeAt(i, min, max);
+ for (var j = min.value; j <= max.value; ++j) {
+ item = this._view._getItemAtIndex(j);
+ if (!item) {
+ continue;
+ }
+ if (item.container && !item.open) {
+ selectedCookieCount += item.cookies.length;
+ } else if (!item.container) {
+ ++selectedCookieCount;
+ }
+ }
+ }
+ item = this._view._getItemAtIndex(seln.currentIndex);
+ if (item && seln.count == 1 && item.container && item.open) {
+ selectedCookieCount += 2;
+ }
+
+ let buttonLabel = this._bundle.getString("removeSelectedCookies");
+ let removeSelectedCookies = document.getElementById(
+ "removeSelectedCookies"
+ );
+ removeSelectedCookies.label = PluralForm.get(
+ selectedCookieCount,
+ buttonLabel
+ ).replace("#1", selectedCookieCount);
+
+ removeSelectedCookies.disabled = !(seln.count > 0);
+ document.getElementById("removeAllCookies").disabled = this._view._filtered;
+ },
+
+ deleteCookie() {
+ // Selection Notes
+ // - Selection always moves to *NEXT* adjacent item unless item
+ // is last child at a given level in which case it moves to *PREVIOUS*
+ // item
+ //
+ // Selection Cases (Somewhat Complicated)
+ //
+ // 1) Single cookie selected, host has single child
+ // v cnn.com
+ // //// cnn.com ///////////// goksdjf@ ////
+ // > atwola.com
+ //
+ // Before SelectedIndex: 1 Before RowCount: 3
+ // After SelectedIndex: 0 After RowCount: 1
+ //
+ // 2) Host selected, host open
+ // v goats.com ////////////////////////////
+ // goats.com sldkkfjl
+ // goat.scom flksj133
+ // > atwola.com
+ //
+ // Before SelectedIndex: 0 Before RowCount: 4
+ // After SelectedIndex: 0 After RowCount: 1
+ //
+ // 3) Host selected, host closed
+ // > goats.com ////////////////////////////
+ // > atwola.com
+ //
+ // Before SelectedIndex: 0 Before RowCount: 2
+ // After SelectedIndex: 0 After RowCount: 1
+ //
+ // 4) Single cookie selected, host has many children
+ // v goats.com
+ // goats.com sldkkfjl
+ // //// goats.com /////////// flksjl33 ////
+ // > atwola.com
+ //
+ // Before SelectedIndex: 2 Before RowCount: 4
+ // After SelectedIndex: 1 After RowCount: 3
+ //
+ // 5) Single cookie selected, host has many children
+ // v goats.com
+ // //// goats.com /////////// flksjl33 ////
+ // goats.com sldkkfjl
+ // > atwola.com
+ //
+ // Before SelectedIndex: 1 Before RowCount: 4
+ // After SelectedIndex: 1 After RowCount: 3
+ var seln = this._view.selection;
+ var tbo = this._tree;
+
+ if (seln.count < 1) {
+ return;
+ }
+
+ var nextSelected = 0;
+ var rowCountImpact = 0;
+ var deleteItems = [];
+ if (!this._view._filtered) {
+ var ci = seln.currentIndex;
+ nextSelected = ci;
+ var invalidateRow = -1;
+ let item = this._view._getItemAtIndex(ci);
+ if (item.container) {
+ rowCountImpact -= (item.open ? item.cookies.length : 0) + 1;
+ deleteItems = deleteItems.concat(item.cookies);
+ if (!this._view.hasNextSibling(-1, ci)) {
+ --nextSelected;
+ }
+ this._view._removeItemAtIndex(ci);
+ } else {
+ var parent = this._view._getItemAtIndex(item.parentIndex);
+ --rowCountImpact;
+ if (parent.cookies.length == 1) {
+ --rowCountImpact;
+ deleteItems.push(item);
+ if (!this._view.hasNextSibling(-1, ci)) {
+ --nextSelected;
+ }
+ if (!this._view.hasNextSibling(-1, item.parentIndex)) {
+ --nextSelected;
+ }
+ this._view._removeItemAtIndex(item.parentIndex);
+ invalidateRow = item.parentIndex;
+ } else {
+ deleteItems.push(item);
+ if (!this._view.hasNextSibling(-1, ci)) {
+ --nextSelected;
+ }
+ this._view._removeItemAtIndex(ci);
+ }
+ }
+ this._view._rowCount += rowCountImpact;
+ tbo.rowCountChanged(ci, rowCountImpact);
+ if (invalidateRow != -1) {
+ tbo.invalidateRow(invalidateRow);
+ }
+ } else {
+ var rangeCount = seln.getRangeCount();
+ for (var i = 0; i < rangeCount; ++i) {
+ var min = {};
+ var max = {};
+ seln.getRangeAt(i, min, max);
+ nextSelected = min.value;
+ for (var j = min.value; j <= max.value; ++j) {
+ deleteItems.push(this._view._getItemAtIndex(j));
+ if (!this._view.hasNextSibling(-1, max.value)) {
+ --nextSelected;
+ }
+ }
+ var delta = max.value - min.value + 1;
+ this._view._removeItemAtIndex(min.value, delta);
+ rowCountImpact = -1 * delta;
+ this._view._rowCount += rowCountImpact;
+ tbo.rowCountChanged(min.value, rowCountImpact);
+ }
+ }
+
+ for (let item of deleteItems) {
+ Services.cookies.remove(
+ item.host,
+ item.name,
+ item.path,
+ item.originAttributes
+ );
+ }
+
+ if (nextSelected < 0) {
+ seln.clearSelection();
+ } else {
+ seln.select(nextSelected);
+ this._tree.focus();
+ }
+ },
+
+ deleteAllCookies() {
+ Services.cookies.removeAll();
+ this._tree.focus();
+ },
+
+ onCookieKeyPress(aEvent) {
+ if (aEvent.keyCode == 46) {
+ this.deleteCookie();
+ }
+ },
+
+ _lastSortProperty: "",
+ _lastSortAscending: false,
+ sort(aProperty) {
+ var ascending =
+ aProperty == this._lastSortProperty ? !this._lastSortAscending : true;
+
+ function sortByHost(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ }
+
+ // Sort the Non-Filtered Host Collections
+ if (aProperty == "rawHost") {
+ this._hostOrder.sort(sortByHost);
+ if (!ascending) {
+ this._hostOrder.reverse();
+ }
+ }
+
+ function sortByProperty(a, b) {
+ return a[aProperty]
+ .toLowerCase()
+ .localeCompare(b[aProperty].toLowerCase());
+ }
+ for (var host in this._hosts) {
+ var cookies = this._hosts[host].cookies;
+ cookies.sort(sortByProperty);
+ if (!ascending) {
+ cookies.reverse();
+ }
+ }
+ // Sort the Filtered List, if in Filtered mode
+ if (this._view._filtered) {
+ this._view._filterSet.sort(sortByProperty);
+ if (!ascending) {
+ this._view._filterSet.reverse();
+ }
+ }
+
+ this._view._invalidateCache(0);
+ this._view.selection.clearSelection();
+ this._view.selection.select(0);
+ this._tree.invalidate();
+ this._tree.ensureRowIsVisible(0);
+
+ this._lastSortAscending = ascending;
+ this._lastSortProperty = aProperty;
+ },
+
+ clearFilter() {
+ // Revert to single-select in the tree
+ this._tree.setAttribute("seltype", "single");
+
+ // Clear the Tree Display
+ this._view._filtered = false;
+ this._view._rowCount = 0;
+ this._tree.rowCountChanged(0, -this._view._filterSet.length);
+ this._view._filterSet = [];
+
+ // Just reload the list to make sure deletions are respected
+ this._loadCookies();
+ this._tree.view = this._view;
+
+ // Restore sort order
+ var sortby = this._lastSortProperty;
+ if (sortby == "") {
+ this._lastSortAscending = false;
+ this.sort("rawHost");
+ } else {
+ this._lastSortAscending = !this._lastSortAscending;
+ this.sort(sortby);
+ }
+
+ // Restore open state
+ for (var i = 0; i < this._openIndices.length; ++i) {
+ this._view.toggleOpenState(this._openIndices[i]);
+ }
+ this._openIndices = [];
+
+ // Restore selection
+ this._view.selection.clearSelection();
+ for (i = 0; i < this._lastSelectedRanges.length; ++i) {
+ var range = this._lastSelectedRanges[i];
+ this._view.selection.rangedSelect(range.min, range.max, true);
+ }
+ this._lastSelectedRanges = [];
+
+ document.getElementById("cookiesIntro").value =
+ this._bundle.getString("cookiesAll");
+ },
+
+ _cookieMatchesFilter(aCookie) {
+ return (
+ aCookie.rawHost.includes(this._view._filterValue) ||
+ aCookie.name.includes(this._view._filterValue) ||
+ aCookie.value.includes(this._view._filterValue)
+ );
+ },
+
+ _filterCookies(aFilterValue) {
+ this._view._filterValue = aFilterValue;
+ var cookies = [];
+ for (let i = 0; i < gCookiesWindow._hostOrder.length; ++i) {
+ let currHost = gCookiesWindow._hosts[gCookiesWindow._hostOrder[i]];
+ if (!currHost) {
+ continue;
+ }
+ for (var j = 0; j < currHost.cookies.length; ++j) {
+ var cookie = currHost.cookies[j];
+ if (this._cookieMatchesFilter(cookie)) {
+ cookies.push(cookie);
+ }
+ }
+ }
+ return cookies;
+ },
+
+ _lastSelectedRanges: [],
+ _openIndices: [],
+ _saveState() {
+ // Save selection
+ var seln = this._view.selection;
+ this._lastSelectedRanges = [];
+ var rangeCount = seln.getRangeCount();
+ for (var i = 0; i < rangeCount; ++i) {
+ var min = {};
+ var max = {};
+ seln.getRangeAt(i, min, max);
+ this._lastSelectedRanges.push({ min: min.value, max: max.value });
+ }
+
+ // Save open states
+ this._openIndices = [];
+ for (i = 0; i < this._view.rowCount; ++i) {
+ var item = this._view._getItemAtIndex(i);
+ if (item && item.container && item.open) {
+ this._openIndices.push(i);
+ }
+ }
+ },
+
+ filter() {
+ var filter = document.getElementById("filter").value;
+ if (filter == "") {
+ gCookiesWindow.clearFilter();
+ return;
+ }
+ var view = gCookiesWindow._view;
+ view._filterSet = gCookiesWindow._filterCookies(filter);
+ if (!view._filtered) {
+ // Save Display Info for the Non-Filtered mode when we first
+ // enter Filtered mode.
+ gCookiesWindow._saveState();
+ view._filtered = true;
+ }
+ // Move to multi-select in the tree
+ gCookiesWindow._tree.setAttribute("seltype", "multiple");
+
+ // Clear the display
+ var oldCount = view._rowCount;
+ view._rowCount = 0;
+ gCookiesWindow._tree.rowCountChanged(0, -oldCount);
+ // Set up the filtered display
+ view._rowCount = view._filterSet.length;
+ gCookiesWindow._tree.rowCountChanged(0, view.rowCount);
+
+ // if the view is not empty then select the first item
+ if (view.rowCount > 0) {
+ view.selection.select(0);
+ }
+
+ document.getElementById("cookiesIntro").value =
+ gCookiesWindow._bundle.getString("cookiesFiltered");
+ },
+
+ setFilter(aFilterString) {
+ document.getElementById("filter").value = aFilterString;
+ this.filter();
+ },
+
+ focusFilterBox() {
+ var filter = document.getElementById("filter");
+ filter.focus();
+ filter.select();
+ },
+};
diff --git a/comm/mail/components/preferences/cookies.xhtml b/comm/mail/components/preferences/cookies.xhtml
new file mode 100644
index 0000000000..63b6ba5a64
--- /dev/null
+++ b/comm/mail/components/preferences/cookies.xhtml
@@ -0,0 +1,117 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+
+<!DOCTYPE dialog>
+
+<window id="CookiesDialog"
+ class="windowDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="cookies-window-dialog2"
+ onload="gCookiesWindow.init();"
+ onunload="gCookiesWindow.uninit();"
+ persist="width height">
+
+ <script src="chrome://messenger/content/globalOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+ <script src="chrome://messenger/content/preferences/cookies.js"/>
+
+ <stringbundle id="bundlePreferences"
+ src="chrome://messenger/locale/preferences/preferences.properties"/>
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/cookies.ftl"/>
+ </linkset>
+
+ <keyset>
+ <key data-l10n-id="window-close-key" data-l10n-attrs="key"
+ modifiers="accel" oncommand="window.close();"/>
+ <key data-l10n-id="window-focus-search-key" data-l10n-attrs="key"
+ modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/>
+ <key data-l10n-id="window-focus-search-alt-key" data-l10n-attrs="key"
+ modifiers="accel" oncommand="gCookiesWindow.focusFilterBox();"/>
+ </keyset>
+
+ <vbox flex="1" class="contentPane largeDialogContainer">
+ <hbox align="center">
+ <label data-l10n-id="filter-search-label" control="filter"/>
+ <search-textbox id="filter"
+ flex="1"
+ aria-controls="cookiesList"
+ oncommand="gCookiesWindow.filter();"/>
+ </hbox>
+ <separator class="thin"/>
+ <label control="cookiesList" id="cookiesIntro" data-l10n-id="cookies-on-system-label"/>
+ <separator class="thin"/>
+ <tree id="cookiesList" flex="1" style="height: 10em;"
+ onkeypress="gCookiesWindow.onCookieKeyPress(event)"
+ onselect="gCookiesWindow.onCookieSelected();"
+ hidecolumnpicker="true" seltype="single">
+ <treecols>
+ <treecol id="domainCol" data-l10n-id="treecol-site-header"
+ primary="true"
+ persist="width"
+ onclick="gCookiesWindow.sort('rawHost');"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="nameCol" data-l10n-id="treecol-name-header"
+ persist="width"
+ onclick="gCookiesWindow.sort('name');"/>
+ </treecols>
+ <treechildren id="cookiesChildren"/>
+ </tree>
+ <hbox id="cookieInfoSettings" flex="1">
+ <vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="nameLabel" control="name" data-l10n-id="props-name-label"/>
+ </vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="valueLabel" control="value" data-l10n-id="props-value-label"/>
+ </vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="isDomain" control="host" data-l10n-id="props-domain-label"/>
+ </vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="pathLabel" control="path" data-l10n-id="props-path-label"/>
+ </vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="isSecureLabel" control="isSecure" data-l10n-id="props-secure-label"/>
+ </vbox>
+ <vbox flex="1" pack="center" align="end">
+ <label id="expiresLabel" control="expires" data-l10n-id="props-expires-label"/>
+ </vbox>
+ <vbox id="userContextLabel" flex="1" pack="center" align="end">
+ <label control="userContext" data-l10n-id="props-container-label"/>
+ </vbox>
+ </vbox>
+ <vbox flex="1">
+ <html:input id="name" type="text" readonly="readonly"/>
+ <html:input id="value" type="text" readonly="readonly"/>
+ <html:input id="host" type="text" readonly="readonly"/>
+ <html:input id="path" type="text" readonly="readonly"/>
+ <html:input id="isSecure" type="text" readonly="readonly"/>
+ <html:input id="expires" type="text" readonly="readonly"/>
+ <html:input id="userContext" type="text" readonly="readonly"/>
+ </vbox>
+ </hbox>
+ </vbox>
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <button id="removeSelectedCookies" disabled="true"
+ data-l10n-id="remove-cookie-button"
+ oncommand="gCookiesWindow.deleteCookie();"/>
+ <button id="removeAllCookies" disabled="true"
+ data-l10n-id="remove-all-cookies-button"
+ oncommand="gCookiesWindow.deleteAllCookies();"/>
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <button oncommand="window.close();"
+ data-l10n-id="cookie-close-button"/>
+#endif
+ </hbox>
+ </hbox>
+</window>
diff --git a/comm/mail/components/preferences/dockoptions.js b/comm/mail/components/preferences/dockoptions.js
new file mode 100644
index 0000000000..d518150551
--- /dev/null
+++ b/comm/mail/components/preferences/dockoptions.js
@@ -0,0 +1,11 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "mail.biff.animate_dock_icon", type: "bool" },
+ { id: "mail.biff.show_badge", type: "bool" },
+ { id: "mail.biff.use_new_count_in_badge", type: "bool" },
+]);
diff --git a/comm/mail/components/preferences/dockoptions.xhtml b/comm/mail/components/preferences/dockoptions.xhtml
new file mode 100644
index 0000000000..978cbe1e2d
--- /dev/null
+++ b/comm/mail/components/preferences/dockoptions.xhtml
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="dock-options-window-dialog2"
+ style="min-width: 33em;">
+ <dialog id="DockOptionsDialog"
+ dlgbuttons="accept,cancel">
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/dock-options.ftl"/>
+ </linkset>
+ <hbox orient="vertical">
+#ifdef XP_MACOSX
+ <checkbox id="newMailNotificationBounce"
+ data-l10n-id="bounce-system-dock-icon"
+ preference="mail.biff.animate_dock_icon"/>
+#endif
+#ifdef XP_WIN
+ <checkbox id="newMailBadge"
+ data-l10n-id="dock-options-show-badge"
+ preference="mail.biff.show_badge"/>
+#endif
+ <separator class="thin"/>
+ <html:div>
+ <html:fieldset>
+ <html:legend data-l10n-id="dock-icon-legend"></html:legend>
+ <vbox>
+ <separator class="thin"/>
+ <label data-l10n-id="dock-icon-show-label"/>
+ <radiogroup id="dockCount"
+ preference="mail.biff.use_new_count_in_badge"
+ class="indent" orient="vertical">
+ <radio id="dockCountAll" value="false"
+ data-l10n-id="count-unread-messages-radio"/>
+ <radio id="dockCountNew" value="true"
+ data-l10n-id="count-new-messages-radio"/>
+ </radiogroup>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+#ifdef XP_MACOSX
+ <separator/>
+ <description class="bold" data-l10n-id="notification-settings-info2"/>
+#endif
+ </hbox>
+
+ <script src="chrome://global/content/preferencesBindings.js"/>
+ <script src="chrome://messenger/content/preferences/dockoptions.js"/>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/downloads.js b/comm/mail/components/preferences/downloads.js
new file mode 100644
index 0000000000..ede1543492
--- /dev/null
+++ b/comm/mail/components/preferences/downloads.js
@@ -0,0 +1,132 @@
+/* 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 preferences.js */
+
+var { Downloads } = ChromeUtils.importESModule(
+ "resource://gre/modules/Downloads.sys.mjs"
+);
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+Preferences.addAll([
+ { id: "browser.download.useDownloadDir", type: "bool" },
+ { id: "browser.download.folderList", type: "int" },
+ { id: "browser.download.downloadDir", type: "file" },
+ { id: "browser.download.dir", type: "file" },
+ { id: "pref.downloads.disable_button.edit_actions", type: "bool" },
+]);
+
+var gDownloadDirSection = {
+ async chooseFolder() {
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ var bundlePreferences = document.getElementById("bundlePreferences");
+ var title = bundlePreferences.getString("chooseAttachmentsFolderTitle");
+ fp.init(window, title, Ci.nsIFilePicker.modeGetFolder);
+
+ var customDirPref = Preferences.get("browser.download.dir");
+ if (customDirPref.value) {
+ fp.displayDirectory = customDirPref.value;
+ }
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ let rv = await new Promise(resolve => fp.open(resolve));
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+
+ let file = fp.file.QueryInterface(Ci.nsIFile);
+ let currentDirPref = Preferences.get("browser.download.downloadDir");
+ customDirPref.value = currentDirPref.value = file;
+ let folderListPref = Preferences.get("browser.download.folderList");
+ folderListPref.value = await this._fileToIndex(file);
+ },
+
+ onReadUseDownloadDir() {
+ this.readDownloadDirPref();
+ var downloadFolder = document.getElementById("downloadFolder");
+ var chooseFolder = document.getElementById("chooseFolder");
+ var preference = Preferences.get("browser.download.useDownloadDir");
+ var dirPreference = Preferences.get("browser.download.dir");
+ downloadFolder.disabled = !preference.value || dirPreference.locked;
+ chooseFolder.disabled = !preference.value || dirPreference.locked;
+ return undefined;
+ },
+
+ async _fileToIndex(aFile) {
+ if (!aFile || aFile.equals(await this._getDownloadsFolder("Desktop"))) {
+ return 0;
+ } else if (aFile.equals(await this._getDownloadsFolder("Downloads"))) {
+ return 1;
+ }
+ return 2;
+ },
+
+ async _indexToFile(aIndex) {
+ switch (aIndex) {
+ case 0:
+ return this._getDownloadsFolder("Desktop");
+ case 1:
+ return this._getDownloadsFolder("Downloads");
+ }
+ var customDirPref = Preferences.get("browser.download.dir");
+ return customDirPref.value;
+ },
+
+ async _getDownloadsFolder(aFolder) {
+ switch (aFolder) {
+ case "Desktop":
+ return Services.dirsvc.get("Desk", Ci.nsIFile);
+ case "Downloads":
+ let downloadsDir = await Downloads.getSystemDownloadsDirectory();
+ return new FileUtils.File(downloadsDir);
+ }
+ throw new Error(
+ "ASSERTION FAILED: folder type should be 'Desktop' or 'Downloads'"
+ );
+ },
+
+ async readDownloadDirPref() {
+ var folderListPref = Preferences.get("browser.download.folderList");
+ var bundlePreferences = document.getElementById("bundlePreferences");
+ var downloadFolder = document.getElementById("downloadFolder");
+
+ var customDirPref = Preferences.get("browser.download.dir");
+ var customIndex = customDirPref.value
+ ? await this._fileToIndex(customDirPref.value)
+ : 0;
+ if (customIndex == 0) {
+ downloadFolder.value = bundlePreferences.getString("desktopFolderName");
+ } else if (customIndex == 1) {
+ downloadFolder.value = bundlePreferences.getString(
+ "myDownloadsFolderName"
+ );
+ } else {
+ downloadFolder.value = customDirPref.value
+ ? customDirPref.value.path
+ : "";
+ }
+
+ var currentDirPref = Preferences.get("browser.download.downloadDir");
+ var downloadDir =
+ currentDirPref.value || (await this._indexToFile(folderListPref.value));
+ if (downloadDir) {
+ let urlSpec = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getURLSpecFromDir(downloadDir);
+
+ downloadFolder.style.backgroundImage =
+ "url(moz-icon://" + urlSpec + "?size=16)";
+ }
+
+ return undefined;
+ },
+};
+
+Preferences.get("browser.download.dir").on(
+ "change",
+ gDownloadDirSection.readDownloadDirPref.bind(gDownloadDirSection)
+);
diff --git a/comm/mail/components/preferences/extensionControlled.js b/comm/mail/components/preferences/extensionControlled.js
new file mode 100644
index 0000000000..5dccb348bc
--- /dev/null
+++ b/comm/mail/components/preferences/extensionControlled.js
@@ -0,0 +1,129 @@
+/* - 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 preferences.js */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+const API_PROXY_PREFS = [
+ "network.proxy.type",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.ssl",
+ "network.proxy.ssl_port",
+ "network.proxy.socks",
+ "network.proxy.socks_port",
+ "network.proxy.socks_version",
+ "network.proxy.socks_remote_dns",
+ "network.proxy.no_proxies_on",
+ "network.proxy.autoconfig_url",
+ "signon.autologin.proxy",
+];
+
+/**
+ * Check if a pref is being managed by an extension.
+ *
+ * NOTE: We only currently handle proxy.settings.
+ */
+/**
+ * Get the addon extension that is controlling the proxy settings.
+ *
+ * @returns - The found addon, or undefined if none was found.
+ */
+async function getControllingProxyExtensionAddon() {
+ await ExtensionSettingsStore.initialize();
+ let id = ExtensionSettingsStore.getSetting("prefs", "proxy.settings")?.id;
+ if (id) {
+ return AddonManager.getAddonByID(id);
+ }
+ return undefined;
+}
+
+/**
+ * Show or hide the proxy extension message depending on whether or not the
+ * proxy settings are controlled by an extension.
+ *
+ * @returns {boolean} - Whether the proxy settings are controlled by an
+ * extension.
+ */
+async function handleControllingProxyExtension() {
+ let addon = await getControllingProxyExtensionAddon();
+ if (addon) {
+ showControllingProxyExtension(addon);
+ } else {
+ hideControllingProxyExtension();
+ }
+ return !!addon;
+}
+
+/**
+ * Show the proxy extension message.
+ *
+ * @param {object} addon - The addon extension that is currently controlling the
+ * proxy settings.
+ * @param {string} addon.name - The addon name.
+ * @param {string} [addon.iconUrl] - The addon icon source.
+ */
+function showControllingProxyExtension(addon) {
+ let description = document.getElementById("proxyExtensionDescription");
+ description
+ .querySelector("img")
+ .setAttribute(
+ "src",
+ addon.iconUrl || "chrome://mozapps/skin/extensions/extensionGeneric.svg"
+ );
+ document.l10n.setAttributes(
+ description,
+ "proxy-settings-controlled-by-extension",
+ { name: addon.name }
+ );
+
+ document.getElementById("proxyExtensionContent").hidden = false;
+}
+
+/**
+ * Hide the proxy extension message.
+ */
+function hideControllingProxyExtension() {
+ document.getElementById("proxyExtensionContent").hidden = true;
+}
+
+/**
+ * Disable the addon extension that is currently controlling the proxy settings.
+ */
+function disableControllingProxyExtension() {
+ getControllingProxyExtensionAddon().then(addon => addon?.disable());
+}
+
+/**
+ * Start listening to the proxy settings, and update the UI accordingly.
+ *
+ * @param {object} container - The proxy container.
+ * @param {Function} container.updateProxySettingsUI - A callback to call
+ * whenever the proxy settings change.
+ */
+function initializeProxyUI(container) {
+ let deferredUpdate = new DeferredTask(() => {
+ container.updateProxySettingsUI();
+ }, 10);
+ let proxyObserver = {
+ observe: (subject, topic, data) => {
+ if (API_PROXY_PREFS.includes(data)) {
+ deferredUpdate.arm();
+ }
+ },
+ };
+ Services.prefs.addObserver("", proxyObserver);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver("", proxyObserver);
+ });
+}
diff --git a/comm/mail/components/preferences/findInPage.js b/comm/mail/components/preferences/findInPage.js
new file mode 100644
index 0000000000..c69e8b50b6
--- /dev/null
+++ b/comm/mail/components/preferences/findInPage.js
@@ -0,0 +1,641 @@
+/* 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 extensionControlled.js */
+/* import-globals-from preferences.js */
+
+// A tweak to the standard <button> CE to use textContent on the <label>
+// inside the button, which allows the text to be highlighted when the user
+// is searching.
+
+const MozButton = customElements.get("button");
+class HighlightableButton extends MozButton {
+ static get inheritedAttributes() {
+ return Object.assign({}, super.inheritedAttributes, {
+ ".button-text": "text=label,accesskey,crop",
+ });
+ }
+}
+customElements.define("highlightable-button", HighlightableButton, {
+ extends: "button",
+});
+
+var gSearchResultsPane = {
+ listSearchTooltips: new Set(),
+ listSearchMenuitemIndicators: new Set(),
+ searchInput: null,
+ // A map of DOM Elements to a string of keywords used in search.
+ // XXX: We should invalidate this cache on `intl:app-locales-changed`.
+ searchKeywords: new WeakMap(),
+ inited: false,
+
+ init() {
+ if (this.inited) {
+ return;
+ }
+ this.inited = true;
+ this.searchInput = document.getElementById("searchInput");
+ this.searchInput.hidden = !Services.prefs.getBoolPref(
+ "browser.preferences.search"
+ );
+ if (!this.searchInput.hidden) {
+ this.searchInput.addEventListener("input", this);
+ this.searchInput.addEventListener("command", this);
+ window.addEventListener("DOMContentLoaded", () => {
+ this.searchInput.focus();
+ });
+ // Initialize other panes in an idle callback.
+ window.requestIdleCallback(() => this.initializeCategories());
+ }
+ let helpUrl =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "preferences";
+ let helpContainer = document.getElementById("need-help");
+ helpContainer.querySelector("a").href = helpUrl;
+ },
+
+ async handleEvent(event) {
+ // Ensure categories are initialized if idle callback didn't run soon enough.
+ await this.initializeCategories();
+ this.searchFunction(event);
+ },
+
+ /**
+ * Check that the text content contains the query string.
+ *
+ * @param {string} content the text content to be searched.
+ * @param {string} query the query string.
+ *
+ * @returns {boolean} true when the text content contains the query string else false.
+ */
+ queryMatchesContent(content, query) {
+ if (!content || !query) {
+ return false;
+ }
+ return content.toLowerCase().includes(query.toLowerCase());
+ },
+
+ categoriesInitialized: false,
+
+ /**
+ * Will attempt to initialize all uninitialized categories.
+ */
+ async initializeCategories() {
+ // Initializing all the JS for all the tabs.
+ if (!this.categoriesInitialized) {
+ this.categoriesInitialized = true;
+ // Each element of gCategoryInits is a name.
+ for (let [name, category] of gCategoryInits) {
+ if (
+ (name != "paneCalendar" && !category.inited) ||
+ (calendarDeactivator.isCalendarActivated && !category.inited)
+ ) {
+ await category.init();
+ }
+ }
+ let lastSelected = Services.xulStore.getValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected"
+ );
+ search(lastSelected, "data-category");
+ }
+ },
+
+ /**
+ * Finds and returns text nodes within node and all descendants.
+ * Iterates through all the sibilings of the node object and adds the sibilings
+ * to an array if sibling is a TEXT_NODE else checks the text nodes with in current node.
+ * Source - http://stackoverflow.com/questions/10730309/find-all-text-nodes-in-html-page
+ *
+ * @param {Node} node DOM element.
+ *
+ * @returns {Node[]} array of text nodes.
+ */
+ textNodeDescendants(node) {
+ if (!node) {
+ return [];
+ }
+ let all = [];
+ for (node = node.firstChild; node; node = node.nextSibling) {
+ if (node.nodeType === node.TEXT_NODE) {
+ all.push(node);
+ } else {
+ all = all.concat(this.textNodeDescendants(node));
+ }
+ }
+ return all;
+ },
+
+ /**
+ * This function is used to find words contained within the text nodes.
+ * We pass in the textNodes because they contain the text to be highlighted.
+ * We pass in the nodeSizes to tell exactly where highlighting need be done.
+ * When creating the range for highlighting, if the nodes are section is split
+ * by an access key, it is important to have the size of each of the nodes summed.
+ *
+ * @param {Node[]} textNodes List of DOM elements.
+ * @param {Node[]} nodeSizes Running size of text nodes. This will contain the same
+ * number of elements as textNodes. The first element is the size of first textNode element.
+ * For any nodes after, they will contain the summation of the nodes thus far in the array.
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = [[8], [9], [18]]
+ * This is used to determine the offset when highlighting.
+ * @param {string} textSearch Concatenation of textNodes's text content.
+ * Example:
+ * textNodes = [[This is ], [a], [n example]]
+ * nodeSizes = "This is an example"
+ * This is used when executing the regular expression.
+ * @param {string} searchPhrase word or words to search for.
+ *
+ * @returns {boolean} Returns true when atleast one instance of search phrase is found, otherwise false.
+ */
+ highlightMatches(textNodes, nodeSizes, textSearch, searchPhrase) {
+ if (!searchPhrase) {
+ return false;
+ }
+
+ let indices = [];
+ let i = -1;
+ while ((i = textSearch.indexOf(searchPhrase, i + 1)) >= 0) {
+ indices.push(i);
+ }
+
+ // Looping through each spot the searchPhrase is found in the concatenated string.dom-mutation-list.
+ for (let startValue of indices) {
+ let endValue = startValue + searchPhrase.length;
+ let startNode = null;
+ let endNode = null;
+ let nodeStartIndex = null;
+
+ // Determining the start and end node to highlight from.
+ for (let index = 0; index < nodeSizes.length; index++) {
+ let lengthNodes = nodeSizes[index];
+ // Determining the start node.
+ if (!startNode && lengthNodes >= startValue) {
+ startNode = textNodes[index];
+ nodeStartIndex = index;
+ // Calculating the offset when found query is not in the first node.
+ if (index > 0) {
+ startValue -= nodeSizes[index - 1];
+ }
+ }
+ // Determining the end node.
+ if (!endNode && lengthNodes >= endValue) {
+ endNode = textNodes[index];
+ // Calculating the offset when endNode is different from startNode
+ // or when endNode is not the first node.
+ if (index != nodeStartIndex || index > 0) {
+ endValue -= nodeSizes[index - 1];
+ }
+ }
+ }
+ let range = document.createRange();
+ range.setStart(startNode, startValue);
+ range.setEnd(endNode, endValue);
+ this.getFindSelection(startNode.ownerGlobal).addRange(range);
+ }
+
+ return !!indices.length;
+ },
+
+ /**
+ * Get the selection instance from given window.
+ *
+ * @param {object} win The window object points to frame's window.
+ */
+ getFindSelection(win) {
+ // Yuck. See bug 138068.
+ let docShell = win.docShell;
+
+ let controller = docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+
+ let selection = controller.getSelection(
+ Ci.nsISelectionController.SELECTION_FIND
+ );
+ selection.setColors("currentColor", "#ffe900", "currentColor", "#003eaa");
+
+ return selection;
+ },
+
+ /**
+ * Shows or hides content according to search input.
+ *
+ * @param {object} event to search for filted query in.
+ */
+ async searchFunction(event) {
+ let query = event.target.value.trim().toLowerCase();
+ if (this.query == query) {
+ return;
+ }
+
+ let subQuery = this.query && query.includes(this.query);
+ this.query = query;
+
+ this.getFindSelection(window).removeAllRanges();
+ this.removeAllSearchTooltips();
+ this.removeAllSearchMenuitemIndicators();
+
+ let srHeader = document.getElementById("header-searchResults");
+ let noResultsEl = document.getElementById("no-results-message");
+ if (this.query) {
+ // Showing the Search Results Tag.
+ await gotoPref("paneSearchResults");
+ srHeader.hidden = false;
+
+ let resultsFound = false;
+
+ // Building the range for highlighted areas.
+ let rootPreferencesChildren = [
+ ...document.querySelectorAll(
+ "#paneDeck > *:not([data-hidden-from-search],script,stringbundle,commandset,keyset,linkset)"
+ ),
+ ];
+
+ if (subQuery) {
+ // Since the previous query is a subset of the current query,
+ // there is no need to check elements that is hidden already.
+ rootPreferencesChildren = rootPreferencesChildren.filter(
+ el => !el.hidden
+ );
+ }
+
+ // Attach the bindings for all children if they were not already visible.
+ for (let child of rootPreferencesChildren) {
+ if (child.hidden) {
+ child.classList.add("visually-hidden");
+ child.hidden = false;
+ }
+ }
+
+ let ts = performance.now();
+ let FRAME_THRESHOLD = 10;
+
+ // Showing or Hiding specific section depending on if words in query are found.
+ for (let child of rootPreferencesChildren) {
+ if (performance.now() - ts > FRAME_THRESHOLD) {
+ // Creating tooltips for all the instances found.
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+ ts = await new Promise(resolve =>
+ window.requestAnimationFrame(resolve)
+ );
+ if (query !== this.query) {
+ return;
+ }
+ }
+
+ if (
+ !child.classList.contains("header") &&
+ !child.classList.contains("subcategory") &&
+ (await this.searchWithinNode(child, this.query))
+ ) {
+ child.classList.remove("visually-hidden");
+
+ // Show the preceding search-header if one exists.
+ let groupbox = child.closest("groupbox");
+ let groupHeader =
+ groupbox && groupbox.querySelector(".search-header");
+ if (groupHeader) {
+ groupHeader.hidden = false;
+ }
+
+ resultsFound = true;
+ } else {
+ child.classList.add("visually-hidden");
+ }
+ }
+
+ noResultsEl.hidden = !!resultsFound;
+ noResultsEl.setAttribute("query", this.query);
+ // XXX: This is potentially racy in case where Fluent retranslates the
+ // message and ereases the query within.
+ // The feature is not yet supported, but we should fix for it before
+ // we enable it. See bug 1446389 for details.
+ let msgQueryElem = document.getElementById("sorry-message-query");
+ msgQueryElem.textContent = this.query;
+ if (resultsFound) {
+ // Creating tooltips for all the instances found.
+ for (let anchorNode of this.listSearchTooltips) {
+ this.createSearchTooltip(anchorNode, this.query);
+ }
+ }
+ } else {
+ noResultsEl.hidden = true;
+ document.getElementById("sorry-message-query").textContent = "";
+ // Going back to General when cleared.
+ await gotoPref("paneGeneral");
+ srHeader.hidden = true;
+
+ // Hide some special second level headers in normal view.
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+ }
+
+ window.dispatchEvent(
+ new CustomEvent("PreferencesSearchCompleted", { detail: query })
+ );
+ },
+
+ /**
+ * Finding leaf nodes and checking their content for words to search,
+ * It is a recursive function.
+ *
+ * @param {Node} nodeObject DOM Element.
+ * @param {string} searchPhrase
+ *
+ * @returns {boolean} Returns true when found in at least one childNode, false otherwise.
+ */
+ async searchWithinNode(nodeObject, searchPhrase) {
+ let matchesFound = false;
+ if (
+ nodeObject.childElementCount == 0 ||
+ nodeObject.tagName == "button" ||
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description" ||
+ nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem"
+ ) {
+ let simpleTextNodes = this.textNodeDescendants(nodeObject);
+ for (let node of simpleTextNodes) {
+ let result = this.highlightMatches(
+ [node],
+ [node.length],
+ node.textContent.toLowerCase(),
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+
+ // Collecting data from anonymous content / label / description.
+ let nodeSizes = [];
+ let allNodeText = "";
+ let runningSize = 0;
+
+ let accessKeyTextNodes = [];
+
+ if (
+ nodeObject.tagName == "label" ||
+ nodeObject.tagName == "description"
+ ) {
+ accessKeyTextNodes.push(...simpleTextNodes);
+ }
+
+ for (let node of accessKeyTextNodes) {
+ runningSize += node.textContent.length;
+ allNodeText += node.textContent;
+ nodeSizes.push(runningSize);
+ }
+
+ // Access key are presented.
+ let complexTextNodesResult = this.highlightMatches(
+ accessKeyTextNodes,
+ nodeSizes,
+ allNodeText.toLowerCase(),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:button, have a 'label' attribute
+ // that contains the user-visible text.
+ let labelResult = this.queryMatchesContent(
+ nodeObject.getAttribute("label"),
+ searchPhrase
+ );
+
+ // Searching some elements, such as xul:label, store their user-visible
+ // text in a "value" attribute. Value will be skipped for menuitem since
+ // value in menuitem could represent index number to distinct each item.
+ let valueResult =
+ nodeObject.tagName !== "menuitem" && nodeObject.tagName !== "radio"
+ ? this.queryMatchesContent(
+ nodeObject.getAttribute("value"),
+ searchPhrase
+ )
+ : false;
+
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using l10n ids.
+ let keywordsResult =
+ nodeObject.hasAttribute("search-l10n-ids") &&
+ (await this.matchesSearchL10nIDs(nodeObject, searchPhrase));
+
+ if (!keywordsResult) {
+ // Searching some elements, such as xul:button, buttons to open subdialogs
+ // using searchkeywords attribute.
+ keywordsResult =
+ !keywordsResult &&
+ nodeObject.hasAttribute("searchkeywords") &&
+ this.queryMatchesContent(
+ nodeObject.getAttribute("searchkeywords"),
+ searchPhrase
+ );
+ }
+
+ // Creating tooltips for buttons.
+ if (
+ keywordsResult &&
+ (nodeObject.tagName === "button" || nodeObject.tagName == "menulist")
+ ) {
+ this.listSearchTooltips.add(nodeObject);
+ }
+
+ if (keywordsResult && nodeObject.tagName === "menuitem") {
+ nodeObject.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(nodeObject);
+ let menulist = nodeObject.closest("menulist");
+
+ menulist.setAttribute("indicator", "true");
+ this.listSearchMenuitemIndicators.add(menulist);
+ }
+
+ if (
+ (nodeObject.tagName == "menulist" ||
+ nodeObject.tagName == "menuitem") &&
+ (labelResult || valueResult || keywordsResult)
+ ) {
+ nodeObject.setAttribute("highlightable", "true");
+ }
+
+ matchesFound =
+ matchesFound ||
+ complexTextNodesResult ||
+ labelResult ||
+ valueResult ||
+ keywordsResult;
+ }
+
+ for (let i = 0; i < nodeObject.childNodes.length; i++) {
+ let result = await this.searchChildNodeIfVisible(
+ nodeObject,
+ i,
+ searchPhrase
+ );
+ matchesFound = matchesFound || result;
+ }
+ return matchesFound;
+ },
+
+ /**
+ * Search for a phrase within a child node if it is visible.
+ *
+ * @param {Node} nodeObject The parent DOM Element.
+ * @param {number} index The index for the childNode.
+ * @param {string} searchPhrase
+ *
+ * @returns {boolean} Returns true when found the specific childNode, false otherwise
+ */
+ async searchChildNodeIfVisible(nodeObject, index, searchPhrase) {
+ let result = false;
+ if (
+ !nodeObject.childNodes[index].hidden &&
+ nodeObject.getAttribute("data-hidden-from-search") !== "true"
+ ) {
+ result = await this.searchWithinNode(
+ nodeObject.childNodes[index],
+ searchPhrase
+ );
+ // Creating tooltips for menulist element.
+ if (result && nodeObject.tagName === "menulist") {
+ this.listSearchTooltips.add(nodeObject);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Search for a phrase in l10n messages associated with the element.
+ *
+ * @param {Node} nodeObject The parent DOM Element.
+ * @param {string} searchPhrase.
+ * @returns {boolean} true when the text content contains the query string else false.
+ */
+ async matchesSearchL10nIDs(nodeObject, searchPhrase) {
+ if (!this.searchKeywords.has(nodeObject)) {
+ // The `search-l10n-ids` attribute is a comma-separated list of
+ // l10n ids. It may also uses a dot notation to specify an attribute
+ // of the message to be used.
+ //
+ // Example: "containers-add-button.label, user-context-personal".
+ //
+ // The result is an array of arrays of l10n ids and optionally attribute names.
+ //
+ // Example: [["containers-add-button", "label"], ["user-context-personal"]]
+ const refs = nodeObject
+ .getAttribute("search-l10n-ids")
+ .split(",")
+ .map(s => s.trim().split("."))
+ .filter(s => !!s[0].length);
+
+ const messages = await document.l10n.formatMessages(
+ refs.map(ref => ({ id: ref[0] }))
+ );
+
+ // Map the localized messages taking value or a selected attribute and
+ // building a string of concatenated translated strings out of it.
+ let keywords = messages
+ .map((msg, i) => {
+ let [refId, refAttr] = refs[i];
+ if (!msg) {
+ console.error(`Missing search l10n id "${refId}"`);
+ return null;
+ }
+ if (refAttr) {
+ let attr =
+ msg.attributes && msg.attributes.find(a => a.name === refAttr);
+ if (!attr) {
+ console.error(`Missing search l10n id "${refId}.${refAttr}"`);
+ return null;
+ }
+ if (attr.value === "") {
+ console.error(
+ `Empty value added to search-l10n-ids "${refId}.${refAttr}"`
+ );
+ }
+ return attr.value;
+ }
+ if (msg.value === "") {
+ console.error(`Empty value added to search-l10n-ids "${refId}"`);
+ }
+ return msg.value;
+ })
+ .filter(keyword => keyword !== null)
+ .join(" ");
+
+ this.searchKeywords.set(nodeObject, keywords);
+ return this.queryMatchesContent(keywords, searchPhrase);
+ }
+
+ return this.queryMatchesContent(
+ this.searchKeywords.get(nodeObject),
+ searchPhrase
+ );
+ },
+
+ /**
+ * Inserting a div structure infront of the DOM element matched textContent.
+ * Then calculation the offsets to position the tooltip in the correct place.
+ *
+ * @param {Node} anchorNode DOM Element.
+ * @param {string} query Word or words that are being searched for.
+ */
+ createSearchTooltip(anchorNode, query) {
+ if (anchorNode.tooltipNode) {
+ return;
+ }
+ let searchTooltip = anchorNode.ownerDocument.createElement("span");
+ let searchTooltipText = anchorNode.ownerDocument.createElement("span");
+ searchTooltip.className = "search-tooltip";
+ searchTooltipText.textContent = query;
+ searchTooltip.appendChild(searchTooltipText);
+
+ // Set tooltipNode property to track corresponded tooltip node.
+ anchorNode.tooltipNode = searchTooltip;
+ anchorNode.parentElement.classList.add("search-tooltip-parent");
+ anchorNode.parentElement.appendChild(searchTooltip);
+
+ this.calculateTooltipPosition(anchorNode);
+ },
+
+ calculateTooltipPosition(anchorNode) {
+ let searchTooltip = anchorNode.tooltipNode;
+ // In order to get the up-to-date position of each of the nodes that we're
+ // putting tooltips on, we have to flush layout intentionally, and that
+ // this is the result of a XUL limitation (bug 1363730).
+ let tooltipRect = searchTooltip.getBoundingClientRect();
+ searchTooltip.style.setProperty(
+ "left",
+ `calc(50% - ${tooltipRect.width / 2}px)`
+ );
+ },
+
+ /**
+ * Remove all search tooltips.
+ */
+ removeAllSearchTooltips() {
+ for (let anchorNode of this.listSearchTooltips) {
+ anchorNode.parentElement.classList.remove("search-tooltip-parent");
+ if (anchorNode.tooltipNode) {
+ anchorNode.tooltipNode.remove();
+ }
+ anchorNode.tooltipNode = null;
+ }
+ this.listSearchTooltips.clear();
+ },
+
+ /**
+ * Remove all indicators on menuitem.
+ */
+ removeAllSearchMenuitemIndicators() {
+ for (let node of this.listSearchMenuitemIndicators) {
+ node.removeAttribute("indicator");
+ }
+ this.listSearchMenuitemIndicators.clear();
+ },
+};
diff --git a/comm/mail/components/preferences/fonts.js b/comm/mail/components/preferences/fonts.js
new file mode 100644
index 0000000000..d6a4c1308e
--- /dev/null
+++ b/comm/mail/components/preferences/fonts.js
@@ -0,0 +1,196 @@
+/* -*- 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/. */
+
+// toolkit/content/preferencesBindings.js
+/* globals Preferences */
+// toolkit/mozapps/preferences/fontbuilder.js
+/* globals FontBuilder */
+
+var kDefaultFontType = "font.default.%LANG%";
+var kFontNameFmtSerif = "font.name.serif.%LANG%";
+var kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+var kFontNameFmtMonospace = "font.name.monospace.%LANG%";
+var kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+var kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+var kFontNameListFmtMonospace = "font.name-list.monospace.%LANG%";
+var kFontSizeFmtVariable = "font.size.variable.%LANG%";
+var kFontSizeFmtFixed = "font.size.monospace.%LANG%";
+var kFontMinSizeFmt = "font.minimum-size.%LANG%";
+
+Preferences.addAll([
+ { id: "font.language.group", type: "wstring" },
+ { id: "browser.display.use_document_fonts", type: "int" },
+ { id: "mail.fixed_width_messages", type: "bool" },
+]);
+
+var gFontsDialog = {
+ _selectLanguageGroupPromise: Promise.resolve(),
+
+ init() {
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("selectLangs"),
+ () => gFontsDialog.readFontLanguageGroup()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("serif"),
+ element => FontBuilder.readFontSelection(element)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("sans-serif"),
+ element => FontBuilder.readFontSelection(element)
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("monospace"),
+ element => FontBuilder.readFontSelection(element)
+ );
+
+ let element = document.getElementById("useDocumentFonts");
+ Preferences.addSyncFromPrefListener(element, () =>
+ gFontsDialog.readUseDocumentFonts()
+ );
+ Preferences.addSyncToPrefListener(element, () =>
+ gFontsDialog.writeUseDocumentFonts()
+ );
+
+ element = document.getElementById("mailFixedWidthMessages");
+ Preferences.addSyncFromPrefListener(element, () =>
+ gFontsDialog.readFixedWidthForPlainText()
+ );
+ Preferences.addSyncToPrefListener(element, () =>
+ gFontsDialog.writeFixedWidthForPlainText()
+ );
+ },
+
+ _selectLanguageGroup(aLanguageGroup) {
+ this._selectLanguageGroupPromise = (async () => {
+ // Avoid overlapping language group selections by awaiting the resolution
+ // of the previous one. We do this because this function is re-entrant,
+ // as inserting <preference> elements into the DOM sometimes triggers a call
+ // back into this function. And since this function is also asynchronous,
+ // that call can enter this function before the previous run has completed,
+ // which would corrupt the font menulists. Awaiting the previous call's
+ // resolution avoids that fate.
+ await this._selectLanguageGroupPromise;
+
+ var prefs = [
+ {
+ format: kDefaultFontType,
+ type: "string",
+ element: "defaultFontType",
+ fonttype: null,
+ },
+ {
+ format: kFontNameFmtSerif,
+ type: "fontname",
+ element: "serif",
+ fonttype: "serif",
+ },
+ {
+ format: kFontNameFmtSansSerif,
+ type: "fontname",
+ element: "sans-serif",
+ fonttype: "sans-serif",
+ },
+ {
+ format: kFontNameFmtMonospace,
+ type: "fontname",
+ element: "monospace",
+ fonttype: "monospace",
+ },
+ {
+ format: kFontNameListFmtSerif,
+ type: "unichar",
+ element: null,
+ fonttype: "serif",
+ },
+ {
+ format: kFontNameListFmtSansSerif,
+ type: "unichar",
+ element: null,
+ fonttype: "sans-serif",
+ },
+ {
+ format: kFontNameListFmtMonospace,
+ type: "unichar",
+ element: null,
+ fonttype: "monospace",
+ },
+ {
+ format: kFontSizeFmtVariable,
+ type: "int",
+ element: "sizeVar",
+ fonttype: null,
+ },
+ {
+ format: kFontSizeFmtFixed,
+ type: "int",
+ element: "sizeMono",
+ fonttype: null,
+ },
+ {
+ format: kFontMinSizeFmt,
+ type: "int",
+ element: "minSize",
+ fonttype: null,
+ },
+ ];
+ for (var i = 0; i < prefs.length; ++i) {
+ var name = prefs[i].format.replace(/%LANG%/, aLanguageGroup);
+ var preference = Preferences.get(name);
+ if (!preference) {
+ preference = Preferences.add({ id: name, type: prefs[i].type });
+ }
+
+ if (!prefs[i].element) {
+ continue;
+ }
+
+ var element = document.getElementById(prefs[i].element);
+ if (element) {
+ element.setAttribute("preference", preference.id);
+
+ if (prefs[i].fonttype) {
+ await FontBuilder.buildFontList(
+ aLanguageGroup,
+ prefs[i].fonttype,
+ element
+ );
+ }
+ preference.setElementValue(element);
+ }
+ }
+ })().catch(console.error);
+ },
+
+ readFontLanguageGroup() {
+ var languagePref = Preferences.get("font.language.group");
+ this._selectLanguageGroup(languagePref.value);
+ return undefined;
+ },
+
+ readUseDocumentFonts() {
+ var preference = Preferences.get("browser.display.use_document_fonts");
+ return preference.value == 1;
+ },
+
+ writeUseDocumentFonts() {
+ var useDocumentFonts = document.getElementById("useDocumentFonts");
+ return useDocumentFonts.checked ? 1 : 0;
+ },
+
+ readFixedWidthForPlainText() {
+ var preference = Preferences.get("mail.fixed_width_messages");
+ return preference.value == 1;
+ },
+
+ writeFixedWidthForPlainText() {
+ var mailFixedWidthMessages = document.getElementById(
+ "mailFixedWidthMessages"
+ );
+ return mailFixedWidthMessages.checked;
+ },
+};
+
+window.addEventListener("load", () => gFontsDialog.init());
diff --git a/comm/mail/components/preferences/fonts.xhtml b/comm/mail/components/preferences/fonts.xhtml
new file mode 100644
index 0000000000..4bb793a04d
--- /dev/null
+++ b/comm/mail/components/preferences/fonts.xhtml
@@ -0,0 +1,337 @@
+<?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"?>
+
+<!DOCTYPE html>
+<html
+ id="FontsDialog"
+ 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"
+ type="child"
+ persist="lastSelected"
+ scrolling="false"
+ style="min-width: 60ch"
+>
+ <head>
+ <title data-l10n-id="fonts-dialog-title"></title>
+ <link rel="localization" href="messenger/preferences/fonts.ftl" />
+ <script
+ defer="defer"
+ src="chrome://global/content/preferencesBindings.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://mozapps/content/preferences/fontbuilder.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/menulist-charsetpicker.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/preferences/fonts.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel" style="min-height: 100vh">
+ <keyset>
+ <key
+ data-l10n-id="fonts-window-close"
+ modifiers="accel"
+ oncommand="Preferences.close(event)"
+ />
+ </keyset>
+
+ <html:fieldset>
+ <!-- title row -->
+ <hbox>
+ <hbox align="center" pack="end">
+ <label
+ id="fontsTitle"
+ control="selectLangs"
+ data-l10n-id="fonts-language-legend"
+ />
+ </hbox>
+ <menulist id="selectLangs" flex="1" preference="font.language.group">
+ <menupopup>
+ <menuitem value="ar" data-l10n-id="font-language-group-arabic" />
+ <menuitem
+ value="x-armn"
+ data-l10n-id="font-language-group-armenian"
+ />
+ <menuitem
+ value="x-beng"
+ data-l10n-id="font-language-group-bengali"
+ />
+ <menuitem
+ value="zh-CN"
+ data-l10n-id="font-language-group-simpl-chinese"
+ />
+ <menuitem
+ value="zh-HK"
+ data-l10n-id="font-language-group-trad-chinese-hk"
+ />
+ <menuitem
+ value="zh-TW"
+ data-l10n-id="font-language-group-trad-chinese"
+ />
+ <menuitem
+ value="x-cyrillic"
+ data-l10n-id="font-language-group-cyrillic"
+ />
+ <menuitem
+ value="x-devanagari"
+ data-l10n-id="font-language-group-devanagari"
+ />
+ <menuitem
+ value="x-ethi"
+ data-l10n-id="font-language-group-ethiopic"
+ />
+ <menuitem
+ value="x-geor"
+ data-l10n-id="font-language-group-georgian"
+ />
+ <menuitem value="el" data-l10n-id="font-language-group-el" />
+ <menuitem
+ value="x-gujr"
+ data-l10n-id="font-language-group-gujarati"
+ />
+ <menuitem
+ value="x-guru"
+ data-l10n-id="font-language-group-gurmukhi"
+ />
+ <menuitem value="he" data-l10n-id="font-language-group-hebrew" />
+ <menuitem
+ value="ja"
+ data-l10n-id="font-language-group-japanese"
+ />
+ <menuitem
+ value="x-knda"
+ data-l10n-id="font-language-group-kannada"
+ />
+ <menuitem
+ value="x-khmr"
+ data-l10n-id="font-language-group-khmer"
+ />
+ <menuitem value="ko" data-l10n-id="font-language-group-korean" />
+ <menuitem
+ value="x-western"
+ data-l10n-id="font-language-group-latin"
+ />
+ <menuitem
+ value="x-mlym"
+ data-l10n-id="font-language-group-malayalam"
+ />
+ <menuitem
+ value="x-math"
+ data-l10n-id="font-language-group-math"
+ />
+ <menuitem
+ value="x-orya"
+ data-l10n-id="font-language-group-odia"
+ />
+ <menuitem
+ value="x-sinh"
+ data-l10n-id="font-language-group-sinhala"
+ />
+ <menuitem
+ value="x-tamil"
+ data-l10n-id="font-language-group-tamil"
+ />
+ <menuitem
+ value="x-telu"
+ data-l10n-id="font-language-group-telugu"
+ />
+ <menuitem value="th" data-l10n-id="font-language-group-thai" />
+ <menuitem
+ value="x-tibt"
+ data-l10n-id="font-language-group-tibetan"
+ />
+ <menuitem
+ value="x-cans"
+ data-l10n-id="font-language-group-canadian"
+ />
+ <menuitem
+ value="x-unicode"
+ data-l10n-id="font-language-group-other"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <separator />
+ <box id="font-chooser-group">
+ <!-- proportional row -->
+ <hbox align="center" pack="end">
+ <label data-l10n-id="fonts-proportional-label" />
+ </hbox>
+ <menulist id="defaultFontType">
+ <menupopup>
+ <menuitem value="serif" data-l10n-id="default-font-serif" />
+ <menuitem
+ value="sans-serif"
+ data-l10n-id="default-font-sans-serif"
+ />
+ </menupopup>
+ </menulist>
+ <hbox align="center" pack="end">
+ <label
+ control="sizeVar"
+ data-l10n-id="font-size-proportional-label"
+ class="startSpacing"
+ />
+ </hbox>
+ <menulist id="sizeVar" delayprefsave="true">
+ <menupopup>
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+
+ <!-- serif row -->
+ <hbox align="center" pack="end">
+ <label control="serif" data-l10n-id="font-serif-label" />
+ </hbox>
+ <menulist id="serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- sans-serif row -->
+ <hbox align="center" pack="end">
+ <label control="sans-serif" data-l10n-id="font-sans-serif-label" />
+ </hbox>
+ <menulist id="sans-serif" delayprefsave="true" />
+ <spacer />
+ <spacer />
+
+ <!-- monospace row -->
+ <hbox align="center" pack="end">
+ <label control="monospace" data-l10n-id="font-monospace-label" />
+ </hbox>
+ <menulist id="monospace" crop="end" delayprefsave="true" />
+ <hbox align="center" pack="end">
+ <label
+ control="sizeMono"
+ data-l10n-id="font-size-monospace-label"
+ class="startSpacing"
+ />
+ </hbox>
+ <menulist id="sizeMono" delayprefsave="true">
+ <menupopup>
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+ </box>
+
+ <separator class="thin" />
+
+ <hbox flex="1">
+ <spacer flex="1" />
+ <hbox align="center" pack="end">
+ <label data-l10n-id="font-min-size-label" control="minSize" />
+ <menulist id="minSize">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="min-size-none" />
+ <menuitem value="9" label="9" />
+ <menuitem value="10" label="10" />
+ <menuitem value="11" label="11" />
+ <menuitem value="12" label="12" />
+ <menuitem value="13" label="13" />
+ <menuitem value="14" label="14" />
+ <menuitem value="15" label="15" />
+ <menuitem value="16" label="16" />
+ <menuitem value="17" label="17" />
+ <menuitem value="18" label="18" />
+ <menuitem value="20" label="20" />
+ <menuitem value="22" label="22" />
+ <menuitem value="24" label="24" />
+ <menuitem value="26" label="26" />
+ <menuitem value="28" label="28" />
+ <menuitem value="30" label="30" />
+ <menuitem value="32" label="32" />
+ <menuitem value="34" label="34" />
+ <menuitem value="36" label="36" />
+ <menuitem value="40" label="40" />
+ <menuitem value="44" label="44" />
+ <menuitem value="48" label="48" />
+ <menuitem value="56" label="56" />
+ <menuitem value="64" label="64" />
+ <menuitem value="72" label="72" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+
+ <html:fieldset>
+ <html:legend data-l10n-id="font-control-legend"></html:legend>
+ <hbox>
+ <checkbox
+ id="useDocumentFonts"
+ data-l10n-id="use-document-fonts-checkbox"
+ preference="browser.display.use_document_fonts"
+ />
+ </hbox>
+ <hbox>
+ <checkbox
+ id="mailFixedWidthMessages"
+ data-l10n-id="use-fixed-width-plain-checkbox"
+ preference="mail.fixed_width_messages"
+ />
+ </hbox>
+ </html:fieldset>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mail/components/preferences/general.inc.xhtml b/comm/mail/components/preferences/general.inc.xhtml
new file mode 100644
index 0000000000..438624649b
--- /dev/null
+++ b/comm/mail/components/preferences/general.inc.xhtml
@@ -0,0 +1,1096 @@
+# 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/.
+ <script src="chrome://communicator/content/utilityOverlay.js" />
+ <script src="chrome://messenger/content/preferences/general.js"/>
+ <script src="chrome://mozapps/content/preferences/fontbuilder.js"/>
+
+ <commandset id="appPaneCommandSet">
+ <command id="cmd_delete"
+ oncommand="gGeneralPane.onDelete();"/>
+ </commandset>
+
+ <keyset id="appPaneKeyset">
+ <key keycode="VK_BACK" modifiers="any" command="cmd_delete"/>
+ <key keycode="VK_DELETE" modifiers="any" command="cmd_delete"/>
+ </keyset>
+
+ <keyset>
+ <key data-l10n-id="focus-search-shortcut" modifiers="accel"
+ oncommand="gGeneralPane.focusFilterBox();"/>
+ <key data-l10n-id="focus-search-shortcut-alt" modifiers="accel"
+ oncommand="gGeneralPane.focusFilterBox();"/>
+ </keyset>
+
+ <stringbundle id="bundlePreferences" src="chrome://messenger/locale/preferences/preferences.properties"/>
+#ifdef HAVE_SHELL_SERVICE
+ <stringbundle id="bundleBrand" src="chrome://branding/locale/brand.properties"/>
+#endif
+ <html:template id="paneGeneral">
+ <hbox id="generalCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="pane-general-title"/>
+ </hbox>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="general-legend"></html:legend>
+ <vbox>
+ <hbox align="start">
+ <checkbox id="mailnewsStartPageEnabled"
+ preference="mailnews.start_page.enabled"
+ data-l10n-id="start-page-label"/>
+ </hbox>
+ <hbox align="center" class="input-container">
+ <label data-l10n-id="location-label" control="mailnewsStartPageUrl"/>
+ <html:input id="mailnewsStartPageUrl"
+ type="url"
+ preference="mailnews.start_page.url"/>
+ <button is="highlightable-button" id="browseForStartPageUrl"
+ data-l10n-id="restore-default-label"
+ oncommand="gGeneralPane.restoreDefaultStartPage();">
+ </button>
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="default-search-engine"></html:legend>
+ <hbox align="center">
+ <hbox>
+ <menulist id="defaultWebSearch">
+ <menupopup id="defaultWebSearchPopup"/>
+ </menulist>
+ </hbox>
+ <button is="highlightable-button" id="addSearchEngine"
+ data-l10n-id="add-web-search-engine"
+ oncommand="gGeneralPane.addSearchEngine();"/>
+ <button is="highlightable-button" id="removeSearchEngine"
+ data-l10n-id="remove-search-engine"
+ oncommand="gGeneralPane.removeSearchEngine();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+#ifdef HAVE_SHELL_SERVICE
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="systemDefaultsGroup" data-category="paneGeneral">
+ <html:legend data-l10n-id="system-integration-legend"></html:legend>
+ <vbox>
+ <hbox id="checkDefaultBox" align="center">
+ <checkbox id="alwaysCheckDefault"
+ preference="mail.shell.checkDefaultClient"
+ data-l10n-id="always-check-default"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="checkDefaultButton"
+ data-l10n-id="check-default-button"
+ oncommand="gGeneralPane.checkDefaultNow();"
+ preference="pref.general.disable_button.default_mail"
+ search-l10n-ids="
+ system-integration-title.title,
+ system-integration-dialog.buttonlabelaccept,
+ system-integration-dialog.buttonlabelcancel,
+ system-integration-dialog.buttonlabelcancel2,
+ default-client-intro,
+ unset-default-tooltip,
+ checkbox-email-label.label,
+ checkbox-newsgroups-label.label,
+ checkbox-feeds-label.label,
+ system-search-integration-label.label,
+ check-on-startup-label.label"/>
+ </hbox>
+ </hbox>
+#ifdef XP_WIN
+ <hbox align="start">
+ <checkbox data-l10n-id="minimize-to-tray-label"
+ preference="mail.minimizeToTray"/>
+ </hbox>
+#endif
+ <hbox id="searchIntegrationContainer">
+ <checkbox id="searchIntegration"
+ preference="searchintegration.enable"
+ data-l10n-id="search-integration-label"/>
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+#endif
+
+ <hbox id="languageAndAppearanceCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-language-and-appearance-header"/>
+ </hbox>
+
+ <!-- Window layout -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="layoutGroup" data-category="paneGeneral">
+ <html:legend data-l10n-id="window-layout-legend"></html:legend>
+ <hbox>
+ <checkbox id="drawInTitlebar"
+ data-l10n-id="draw-in-titlebar-label"
+ preference="mail.tabs.drawInTitlebar"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <vbox>
+ <checkbox id="autoHideTabbar"
+ data-l10n-id="auto-hide-tabbar-label"
+ preference="mail.tabs.autoHide"/>
+ <description data-l10n-id="auto-hide-tabbar-description"
+ class="tip-caption indent"/>
+ </vbox>
+ <spacer flex="1"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Fonts and Colors -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="fontsGroup" data-category="paneGeneral">
+ <html:legend data-l10n-id="fonts-legend"></html:legend>
+
+ <hbox id="fontSettings" flex="1">
+ <vbox id="fontRow" flex="1">
+ <hbox align="center">
+ <label data-l10n-id="default-font-label" control="defaultFont"/>
+ <hbox flex="1">
+ <menulist id="defaultFont" flex="1" sizetopopup="pref" crop="center">
+ <menupopup crop="center"/>
+ </menulist>
+ </hbox>
+ <label data-l10n-id="default-size-label" control="defaultFontSize"/>
+ <hbox flex="1">
+ <menulist id="defaultFontSize" flex="1">
+ <menupopup crop="center">
+ <menuitem value="9" label="9"/>
+ <menuitem value="10" label="10"/>
+ <menuitem value="11" label="11"/>
+ <menuitem value="12" label="12"/>
+ <menuitem value="13" label="13"/>
+ <menuitem value="14" label="14"/>
+ <menuitem value="15" label="15"/>
+ <menuitem value="16" label="16"/>
+ <menuitem value="17" label="17"/>
+ <menuitem value="18" label="18"/>
+ <menuitem value="20" label="20"/>
+ <menuitem value="22" label="22"/>
+ <menuitem value="24" label="24"/>
+ <menuitem value="26" label="26"/>
+ <menuitem value="28" label="28"/>
+ <menuitem value="30" label="30"/>
+ <menuitem value="32" label="32"/>
+ <menuitem value="34" label="34"/>
+ <menuitem value="36" label="36"/>
+ <menuitem value="40" label="40"/>
+ <menuitem value="44" label="44"/>
+ <menuitem value="48" label="48"/>
+ <menuitem value="56" label="56"/>
+ <menuitem value="64" label="64"/>
+ <menuitem value="72" label="72"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ </vbox>
+ <vbox id="colorsRow">
+ <hbox flex="1">
+ <button is="highlightable-button" id="advancedFonts"
+ data-l10n-id="font-options-button"
+ oncommand="gGeneralPane.configureFonts();"
+ flex="1"
+ search-l10n-ids="
+ fonts-label-default-unnamed.label,
+ fonts-dialog-title,
+ fonts-language-legend.value,
+ fonts-proportional-label.value,
+ font-language-group-latin.label,
+ font-language-group-japanese.label,
+ font-language-group-trad-chinese.label,
+ font-language-group-simpl-chinese.label,
+ font-language-group-trad-chinese-hk.label,
+ font-language-group-korean.label,
+ font-language-group-cyrillic.label,
+ font-language-group-el.label,
+ font-language-group-other.label,
+ font-language-group-thai.label,
+ font-language-group-hebrew.label,
+ font-language-group-arabic.label,
+ font-language-group-devanagari.label,
+ font-language-group-tamil.label,
+ font-language-group-armenian.label,
+ font-language-group-bengali.label,
+ font-language-group-canadian.label,
+ font-language-group-ethiopic.label,
+ font-language-group-georgian.label,
+ font-language-group-gujarati.label,
+ font-language-group-gurmukhi.label,
+ font-language-group-khmer.label,
+ font-language-group-malayalam.label,
+ font-language-group-math.label,
+ font-language-group-odia.label,
+ font-language-group-telugu.label,
+ font-language-group-kannada.label,
+ font-language-group-sinhala.label,
+ font-language-group-tibetan.label,
+ default-font-serif.label,
+ default-font-sans-serif.label,
+ font-size-label.value,
+ font-size-monospace-label.value,
+ font-serif-label.value,
+ font-sans-serif-label.value,
+ font-monospace-label.value,
+ font-min-size-label.value,
+ min-size-none.label,
+ font-control-legend,
+ use-document-fonts-checkbox.label,
+ use-fixed-width-plain-checkbox.label,
+ text-encoding-legend,
+ text-encoding-description,
+ font-outgoing-email-label.value,
+ font-incoming-email-label.value,
+ default-font-reply-checkbox.label"/>
+ </hbox>
+ <hbox flex="1">
+ <button is="highlightable-button" id="colors"
+ data-l10n-id="color-options-button"
+ oncommand="gGeneralPane.configureColors();"
+ flex="1"
+ search-l10n-ids="
+ colors-dialog-window2.title,
+ colors-dialog-legend,
+ text-color-label.value,
+ background-color-label.value,
+ use-system-colors.label,
+ colors-link-legend,
+ link-color-label.value,
+ visited-link-color-label.value,
+ underline-link-checkbox.label,
+ override-color-label.value,
+ override-color-always.label,
+ override-color-auto.label,
+ override-color-never.label"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <hbox>
+ <html:legend data-l10n-id="display-width-legend"></html:legend>
+ </hbox>
+ <hbox>
+ <checkbox id="displayGlyph"
+ preference="mail.display_glyph"
+ data-l10n-id="convert-emoticons-label"/>
+ <spacer flex="1"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <label control="displayText" data-l10n-id="display-text-label"/>
+ <hbox id="displayText" class="indent" align="center" role="group">
+ <label data-l10n-id="style-label" control="mailQuotedStyle"/>
+ <hbox>
+ <menulist id="mailQuotedStyle" preference="mail.quoted_style">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="regular-style-item"/>
+ <menuitem value="1" data-l10n-id="bold-style-item"/>
+ <menuitem value="2" data-l10n-id="italic-style-item"/>
+ <menuitem value="3" data-l10n-id="bold-italic-style-item"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <label data-l10n-id="size-label" control="mailQuotedSize"/>
+ <hbox>
+ <menulist id="mailQuotedSize" preference="mail.quoted_size">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="regular-size-item"/>
+ <menuitem value="1" data-l10n-id="bigger-size-item"/>
+ <menuitem value="2" data-l10n-id="smaller-size-item"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <label data-l10n-id="quoted-text-color" control="citationmenu"/>
+ <html:input type="color" id="citationmenu" preference="mail.citation_color"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Date and time formatting -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="datetime-formatting-legend"></html:legend>
+ <radiogroup id="formatLocale" align="start"
+ preference="intl.regional_prefs.use_os_locales"
+ orient="vertical">
+ <radio id="appLocale"
+ value="false"/>
+ <!-- label and accesskey will be set dynamically -->
+ <radio id="rsLocale"
+ value="true"/>
+ <!-- label and accesskey will be set dynamically -->
+ </radiogroup>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="messengerLanguagesBox" data-category="paneGeneral" hidden="hidden">
+ <html:legend data-l10n-id="language-selector-legend"></html:legend>
+ <vbox align="start">
+ <description flex="1"
+ controls="chooseMessengerLanguage"
+ data-l10n-id="choose-messenger-language-description"/>
+ <hbox>
+ <hbox>
+ <menulist id="primaryMessengerLocale"
+ oncommand="gGeneralPane.onPrimaryMessengerLanguageMenuChange(event)">
+ <menupopup/>
+ </menulist>
+ </hbox>
+ <hbox>
+ <button is="highlightable-button" id="manageMessengerLanguagesButton"
+ class="accessory-button"
+ data-l10n-id="manage-messenger-languages-button"
+ oncommand="gGeneralPane.showMessengerLanguagesSubDialog({search: false})"
+ search-l10n-ids="
+ languages-customize-moveup.label,
+ languages-customize-movedown.label,
+ languages-customize-remove.label,
+ languages-customize-select-language.placeholder,
+ languages-customize-add.label,
+ messenger-languages-window2.title,
+ messenger-languages-description,
+ messenger-languages-search,
+ messenger-languages-searching.label,
+ messenger-languages-downloading.label,
+ messenger-languages-select-language.label,
+ messenger-languages-installed-label,
+ messenger-languages-available-label,
+ messenger-languages-error"/>
+ </hbox>
+ </hbox>
+ </vbox>
+ <hbox id="confirmMessengerLanguage"
+ class="message-bar"
+ align="center"
+ hidden="true">
+ <html:img class="message-bar-icon"
+ src="chrome://global/skin/icons/info.svg" alt="" />
+ <vbox class="message-bar-content-container" align="stretch" flex="1"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Scrolling -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="scrollingGroup" data-category="paneGeneral">
+ <html:legend data-l10n-id="scrolling-legend"></html:legend>
+ <hbox>
+ <checkbox id="useAutoScroll"
+ data-l10n-id="autoscroll-label"
+ preference="general.autoScroll"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <checkbox id="useSmoothScrolling"
+ data-l10n-id="smooth-scrolling-label"
+ preference="general.smoothScroll"/>
+ <spacer flex="1"/>
+ </hbox>
+#ifdef MOZ_WIDGET_GTK
+ <hbox>
+ <checkbox id="useOverlayScrollbars"
+ data-l10n-id="browsing-gtk-use-non-overlay-scrollbars"
+ preference="widget.gtk.overlay-scrollbars.enabled"/>
+ <spacer flex="1"/>
+ </hbox>
+#endif
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="incomingMailCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-incoming-mail-header"/>
+ </hbox>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="new-message-arrival"></html:legend>
+#if defined(XP_MACOSX) || defined(XP_WIN)
+ <hbox align="center">
+ <description flex="1" data-l10n-id="change-dock-icon"/>
+ <hbox>
+ <button is="highlightable-button" id="dockOptions"
+ oncommand="gGeneralPane.configureDockOptions();"
+ data-l10n-id="app-icon-options"
+ search-l10n-ids="
+ dock-options-window-dialog2.title,
+ bounce-system-dock-icon.label,
+ dock-icon-legend,
+ dock-icon-show-label.value,
+ count-unread-messages-radio.label,
+ count-new-messages-radio.label,
+ notification-settings-info2"/>
+ </hbox>
+ </hbox>
+#endif
+#ifdef XP_MACOSX
+ <description class="bold" data-l10n-id="notification-settings2"/>
+#else
+ <hbox align="center">
+ <checkbox id="newMailNotificationAlert"
+ data-l10n-id="animated-alert-label"
+ preference="mail.biff.show_alert"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="customizeMailAlert"
+ oncommand="gGeneralPane.customizeMailAlert();"
+ data-l10n-id="customize-alert-label"
+ search-l10n-ids="
+ notifications-dialog-window.title,
+ customize-alert-description,
+ preview-text-checkbox.label,
+ subject-checkbox.label,
+ sender-checkbox.label,
+ open-time-label-before.value,
+ open-time-label-after.value"/>
+ </hbox>
+ </hbox>
+ <hbox align="center" class="indent">
+ <checkbox id="useSystemNotificationAlert"
+ data-l10n-id="biff-use-system-alert"
+ preference="mail.biff.use_system_alert"/>
+ </hbox>
+#ifdef XP_WIN
+ <vbox>
+ <checkbox id="newMailNotificationTrayIcon"
+ preference="mail.biff.show_tray_icon"
+ data-l10n-id="tray-icon-unread-label"/>
+ <description class="indent tip-caption"
+ flex="1"
+ data-l10n-id="tray-icon-unread-description"/>
+ </vbox>
+#endif
+#endif
+
+ <hbox align="center">
+ <checkbox id="newMailNotification"
+ preference="mail.biff.play_sound"
+ data-l10n-id="mail-play-sound-label"
+ oncommand="gGeneralPane.updatePlaySound();"/>
+ <spacer flex="1"/>
+ <button is="highlightable-button" id="playSound"
+ data-l10n-id="mail-play-button"
+ oncommand="gGeneralPane.previewSound();"/>
+ </hbox>
+
+#ifndef XP_MACOSX
+ <radiogroup id="soundType"
+ class="indent"
+ preference="mail.biff.play_sound.type"
+ orient="vertical"
+ oncommand="gGeneralPane.updatePlaySound();"
+ aria-labelledby="newMailNotification">
+ <hbox>
+ <radio id="system"
+ value="0"
+ data-l10n-id="mail-system-sound-label"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <radio id="custom"
+ value="1"
+ data-l10n-id="mail-custom-sound-label"/>
+ <spacer flex="1"/>
+ </hbox>
+ </radiogroup>
+#endif
+ <hbox align="center" class="input-container">
+ <html:input id="soundUrlLocation"
+ type="text"
+ class="input-filefield indent"
+ readonly="readonly"
+ preference="mail.biff.play_sound.url"
+ preference-editable="true"
+ aria-labelledby="custom"/>
+ <button is="highlightable-button" id="browseForSound"
+ data-l10n-id="mail-browse-sound-button"
+ oncommand="gGeneralPane.browseForSoundFile();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="filesAttachmentCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-files-and-attachment-header"/>
+ </hbox>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <search-textbox id="filter"
+ data-l10n-id="search-handler-table"
+ data-l10n-attrs="placeholder"
+ aria-controls="handlersTable"
+ oncommand="gGeneralPane._rebuildView();"/>
+ <separator class="thin"/>
+ <html:div id="handlersView"
+ preference="pref.downloads.disable_button.edit_actions">
+ <html:table id="handlersTable">
+ <html:thead>
+ <html:tr>
+ <html:th scope="col"
+ sort-type="type">
+ <html:button class="handlerHeaderButton">
+ <html:span class="handlerHeader"
+ data-l10n-id="type-column-header">
+ </html:span>
+ <html:img class="handlerSortHeaderIcon"
+ alt=""/>
+ </html:button>
+ </html:th>
+ <html:th scope="col"
+ sort-type="action">
+ <html:button class="handlerHeaderButton">
+ <html:span class="handlerHeader"
+ data-l10n-id="action-column-header">
+ </html:span>
+ <html:img class="handlerSortHeaderIcon"
+ alt=""/>
+ </html:button>
+ </html:th>
+ </html:tr>
+ </html:thead>
+ <html:tbody>
+ </html:tbody>
+ </html:table>
+ </html:div>
+
+ <separator class="thin"/>
+
+ <vbox align="start">
+ <radiogroup id="saveWhere" flex="1"
+ preference="browser.download.useDownloadDir">
+ <hbox id="saveToRow" align="center" class="input-container">
+ <radio id="saveTo" value="true"
+ data-l10n-id="save-to-label"
+ aria-labelledby="saveTo downloadFolder"/>
+ <html:input id="downloadFolder"
+ class="input-filefield"
+ type="text"
+ readonly="readonly"
+ aria-labelledby="saveTo"/>
+ <button is="highlightable-button" id="chooseFolder"
+ oncommand="gDownloadDirSection.chooseFolder();"
+ data-l10n-id="choose-folder-label"/>
+ </hbox>
+ <hbox>
+ <radio id="alwaysAsk"
+ value="false"
+ data-l10n-id="always-ask-label"/>
+ </hbox>
+ </radiogroup>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="tagsCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-tags-header"/>
+ </hbox>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <label control="tagList" data-l10n-id="display-tags-text"/>
+ <hbox>
+ <richlistbox id="tagList"
+ flex="1"
+ ondblclick="gGeneralPane.editTag();"
+ onselect="gGeneralPane.onSelectTag();"/>
+ <vbox id="tagButtons">
+ <hbox>
+ <button is="highlightable-button" id="newTagButton"
+ data-l10n-id="new-tag-button"
+ oncommand="gGeneralPane.addTag();"
+ search-l10n-ids="
+ tag-dialog-window.title,
+ tag-name-label.value"/>
+ </hbox>
+ <hbox>
+ <button is="highlightable-button" id="editTagButton"
+ disabled="true"
+ data-l10n-id="edit-tag-button"
+ oncommand="gGeneralPane.editTag();"
+ search-l10n-ids="
+ tag-dialog-window.title,
+ tag-name-label.value"/>
+ </hbox>
+ <button is="highlightable-button" id="removeTagButton"
+ disabled="true"
+ data-l10n-id="delete-tag-button"
+ oncommand="gGeneralPane.removeTag();"/>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="readingAndDisplayCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-reading-and-display-header"/>
+ </hbox>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <vbox>
+ <hbox>
+ <checkbox id="viewAttachmentsInline"
+ data-l10n-id="view-attachments-inline"
+ preference="mail.inline_attachments"/>
+ </hbox>
+
+ <hbox>
+ <checkbox id="automaticallyMarkAsRead"
+ preference="mailnews.mark_message_read.auto"
+ data-l10n-id="auto-mark-as-read"/>
+ </hbox>
+
+ <radiogroup id="markAsReadAutoPreferences" orient="vertical"
+ class="indent"
+ align="start"
+ preference="mailnews.mark_message_read.delay">
+ <radio id="mark_read_immediately"
+ data-l10n-id="mark-read-no-delay"
+ value="false"/>
+ <hbox align="center">
+ <radio id="markAsReadAfterDelay" value="true"
+ data-l10n-id="mark-read-delay"/>
+ <html:input id="markAsReadDelay" type="number" class="size3"
+ min="1" max="2147483"
+ preference="mailnews.mark_message_read.delay.interval"
+ aria-labelledby="markAsReadAfterDelay markAsReadDelay secondsLabel"/>
+ <label id="secondsLabel" data-l10n-id="seconds-label"/>
+ </hbox>
+ </radiogroup>
+ </vbox>
+
+ <separator/>
+
+ <vbox>
+ <hbox>
+ <label data-l10n-id="open-msg-label"
+ control="mailOpenMessageBehavior"/>
+ </hbox>
+ <hbox>
+ <radiogroup id="mailOpenMessageBehavior" class="indent"
+ preference="mail.openMessageBehavior"
+ orient="horizontal">
+ <radio id="newTab" value="2" data-l10n-id="open-msg-tab"/>
+ <radio id="newWindow" value="0" data-l10n-id="open-msg-window"/>
+ <radio id="existingWindow" value="1"
+ data-l10n-id="open-msg-ex-window"/>
+ </radiogroup>
+ </hbox>
+ <hbox>
+ <checkbox id="closeMsgOnMoveOrDelete"
+ data-l10n-id="close-move-delete"
+ preference="mail.close_message_window.on_delete"/>
+ </hbox>
+ </vbox>
+
+ <separator/>
+
+ <hbox>
+ <label data-l10n-id="display-name-label"/>
+ </hbox>
+ <hbox>
+ <checkbox id="showCondensedAddresses"
+ data-l10n-id="condensed-addresses-label"
+ preference="mail.showCondensedAddresses"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <description flex="1" data-l10n-id="return-receipts-description"/>
+ <hbox>
+ <button is="highlightable-button" id="showReturnReceipts"
+ data-l10n-id="return-receipts-button"
+ oncommand="gGeneralPane.showReturnReceipts();"
+ search-l10n-ids="
+ receipts-dialog-window.title,
+ return-receipt-checkbox-control.label,
+ receipt-arrive-label,
+ receipt-leave-radio-control.label,
+ receipt-move-radio-control.label,
+ receipt-request-label,
+ receipt-return-never-radio-control.label,
+ receipt-return-some-radio-control.label,
+ receipt-not-to-cc-label.value,
+ receipt-send-never-label.label,
+ receipt-send-always-label.label,
+ receipt-send-ask-label.label,
+ sender-outside-domain-label.value,
+ other-cases-text-label.value"/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+#ifdef MOZ_UPDATER
+ <hbox id="updatesCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-updates-header"/>
+ </hbox>
+
+ <!-- Update -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset id="updateApp" data-category="paneGeneral">
+ <html:legend data-l10n-id="update-app-legend"></html:legend>
+ <hbox align="center">
+ <vbox>
+ <description>
+ <label id="version"/>
+ <label is="text-link" id="releasenotes" hidden="true" data-l10n-id="release-notes-link"></label>
+ </description>
+ <description id="distribution" class="text-blurb" hidden="true"/>
+ <description id="distributionId" class="text-blurb" hidden="true"/>
+ </vbox>
+ <spacer flex="1"/>
+ <vbox>
+ <hbox>
+ <button is="highlightable-button" id="showUpdateHistory"
+ data-l10n-id="update-history-button"
+ preference="app.update.disable_button.showUpdateHistory"
+ oncommand="gGeneralPane.showUpdates();"
+ search-l10n-ids="
+ history-title,
+ history-intro,
+ close-button-label.buttonlabelcancel,
+ close-button-label.title,
+ no-updates-label,
+ name-header,
+ date-header,
+ type-header,
+ state-header"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <hbox id="updateBox">
+ <deck id="updateDeck" orient="vertical" flex="1">
+ <html:div id="checkForUpdates" class="update-deck-container">
+ <html:button id="checkForUpdatesButton"
+ data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ <html:div id="downloadAndInstall" class="update-deck-container">
+ <html:button id="downloadAndInstallButton"
+ onclick="gAppUpdater.startDownload();">
+ </html:button>
+ </html:div>
+ <html:div id="apply" class="update-deck-container">
+ <html:button id="updateButton"
+ data-l10n-id="update-update-button"
+ onclick="gAppUpdater.buttonRestartAfterDownload();">
+ </html:button>
+ </html:div>
+ <html:div id="checkingForUpdates" class="update-deck-container">
+ <html:img class="update-throbber" alt="" />
+ <html:span data-l10n-id="update-checking-for-updates"></html:span>
+ <!-- Button is only used for presentation in reference to the actual
+ - button that triggered this action, which would now be
+ - invisible -->
+ <html:button data-l10n-id="update-check-for-updates-button"
+ disabled="true">
+ </html:button>
+ </html:div>
+ <html:div id="downloading" class="update-deck-container"
+ data-l10n-id="update-downloading">
+ <html:img class="update-throbber" data-l10n-name="icon"></html:img>
+ <!-- Group within a single span to center align with icon. -->
+ <html:span id="downloadStatus" data-l10n-name="download-status"></html:span>
+ </html:div>
+ <html:div id="applying" class="update-deck-container">
+ <html:img class="update-throbber"/>
+ <html:span data-l10n-id="update-applying"></html:span>
+ </html:div>
+ <html:div id="downloadFailed" class="update-deck-container"
+ data-l10n-id="update-failed">
+ <html:a id="failedLink" class="text-link download-link"
+ data-l10n-name="failed-link"></html:a>
+ <html:button data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ <html:div id="policyDisabled" class="update-deck-container">
+ <html:span data-l10n-id="update-admin-disabled"></html:span>
+ </html:div>
+ <html:div id="noUpdatesFound" class="update-deck-container">
+ <html:span data-l10n-id="update-no-updates-found"></html:span>
+ <html:button data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ <html:div id="checkingFailed" class="update-deck-container">
+ <html:span data-l10n-id="aboutdialog-update-checking-failed"></html:span>
+ <html:button data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ <html:div id="otherInstanceHandlingUpdates"
+ class="update-deck-container">
+ <html:span data-l10n-id="update-other-instance-handling-updates"></html:span>
+ </html:div>
+ <html:div id="manualUpdate" class="update-deck-container"
+ data-l10n-id="update-manual">
+ <html:a id="manualLink" class="manualLink text-link download-link"
+ data-l10n-name="manual-link"></html:a>
+ <html:button data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ <html:div id="unsupportedSystem" class="update-deck-container" data-l10n-id="update-unsupported">
+ <html:a id="unsupportedLink" class="text-link download-link"
+ data-l10n-name="unsupported-link"></html:a>
+ </html:div>
+ <html:div id="restarting" class="update-deck-container">
+ <html:span class="update-throbber"></html:span>
+ <html:span data-l10n-id="update-restarting"></html:span>
+ </html:div>
+ <html:div id="internalError" class="update-deck-container"
+ data-l10n-id="update-internal-error">
+ <html:a id="internalErrorLink" class="manualLink text-link download-link"
+ data-l10n-name="manual-link"></html:a>
+ <html:button data-l10n-id="update-check-for-updates-button"
+ onclick="gAppUpdater.checkForUpdates();">
+ </html:button>
+ </html:div>
+ </deck>
+ </hbox>
+ <separator/>
+ <description id="updateAllowDescription" data-l10n-id="allow-description"/>
+ <vbox id="updateSettingsContainer">
+ <radiogroup id="updateRadioGroup"
+ align="start">
+ <radio id="autoDesktop"
+ value="true"
+ data-l10n-id="automatic-updates-label"/>
+ <radio id="manualDesktop"
+ value="false"
+ data-l10n-id="check-updates-label"/>
+ </radiogroup>
+ <description id="updateSettingCrossUserWarning"
+ data-l10n-id="cross-user-udpate-warning"
+ hidden="true"/>
+ </vbox>
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+ <separator class="thin"/>
+ <checkbox id="useService"
+ data-l10n-id="use-service"
+ preference="app.update.service.enabled"/>
+#endif
+ </html:fieldset>
+ </html:div>
+#endif
+
+ <hbox id="networkAndDiskspaceCategory"
+ class="subcategory"
+ data-category="paneGeneral">
+ <html:h1 data-l10n-id="general-network-and-diskspace-header"/>
+ </hbox>
+
+ <!-- Networking & Disk Space -->
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="networking-legend"></html:legend>
+ <hbox align="center">
+ <description control="catProxiesButton"
+ data-l10n-id="proxy-config-description"
+ flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="catProxiesButton"
+ data-l10n-id="network-settings-button"
+ oncommand="gGeneralPane.showConnections();"
+ search-l10n-ids="
+ connection-dns-over-https-url-resolver,
+ connection-dns-over-https-url-custom.label,
+ connection-dns-over-https-custom-label,
+ connection-proxy-legend,
+ proxy-type-no.label,
+ proxy-type-wpad.label,
+ proxy-type-system.label,
+ proxy-type-manual.label,
+ proxy-http-label.value,
+ http-port-label.value,
+ proxy-http-sharing.label,
+ proxy-https-label.value,
+ ssl-port-label.value,
+ proxy-socks-label.value,
+ socks-port-label.value,
+ proxy-socks4-label.label,
+ proxy-socks5-label.label,
+ proxy-type-auto.label,
+ proxy-reload-label.label,
+ no-proxy-label.value,
+ no-proxy-example,
+ connection-proxy-noproxy-localhost-desc-2,
+ proxy-password-prompt.label,
+ proxy-remote-dns.label,
+ proxy-enable-doh.label"/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="offline-legend"></html:legend>
+ <hbox align="center">
+ <description data-l10n-id="offline-settings"
+ control="offlineSettingsButton"
+ flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="offlineSettingsButton"
+ data-l10n-id="offline-settings-button"
+ oncommand="gGeneralPane.showOffline();"
+ search-l10n-ids="
+ offline-dialog-window.title,
+ autodetect-online-label.label,
+ startup-label,
+ status-radio-remember.label,
+ status-radio-ask.label,
+ status-radio-always-online.label,
+ status-radio-always-offline.label,
+ going-online-label,
+ going-online-auto.label,
+ going-online-not.label,
+ going-online-ask.label,
+ going-offline-label,
+ going-offline-auto.label,
+ going-offline-not.label,
+ going-offline-ask.label"/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="diskspace-legend"></html:legend>
+ <hbox align="center">
+ <label id="actualDiskCacheSize" flex="1"/>
+ <button is="highlightable-button" id="clearCacheButton"
+ data-l10n-id="clear-cache-button"
+ oncommand="gGeneralPane.clearCache();"/>
+ </hbox>
+ <hbox>
+ <checkbox id="clearCacheOnShutdown"
+ preference="privacy.clearOnShutdown.cache"
+ data-l10n-id="clear-cache-shutdown-label"/>
+ </hbox>
+ <hbox>
+ <checkbox id="allowSmartSize"
+ preference="browser.cache.disk.smart_size.enabled"
+ data-l10n-id="smart-cache-label"/>
+ </hbox>
+ <hbox align="center" class="indent">
+ <label id="useCacheBefore" control="cacheSize"
+ data-l10n-id="use-cache-before"/>
+ <html:input id="cacheSize" type="number" class="size4" max="1024"
+ preference="browser.cache.disk.capacity"
+ aria-labelledby="useCacheBefore cacheSize useCacheAfter"/>
+ <label id="useCacheAfter" data-l10n-id="use-cache-after" flex="1"/>
+ </hbox>
+ <hbox align="center">
+ <checkbox id="offlineCompactFolder"
+ data-l10n-id="offline-compact-folder"
+ aria-labelledby="offlineCompactFolder offlineCompactFolderMin compactFolderMB"
+ preference="mail.prompt_purge_threshhold"
+ oncommand="gGeneralPane.updateCompactOptions();"/>
+ <html:input id="offlineCompactFolderMin" type="number" class="size4"
+ min="1" max="2048" value="200"
+ preference="mail.purge_threshhold_mb"
+ aria-labelledby="offlineCompactFolder offlineCompactFolderMin compactFolderMB"/>
+ <label id="compactFolderMB" data-l10n-id="compact-folder-size" value=""/>
+ </hbox>
+ <hbox align="center" class="indent">
+ <checkbox id="offlineCompactFolderAutomatically"
+ data-l10n-id="offline-compact-folder-automatically"
+ preference="mail.purge.ask"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <html:legend data-l10n-id="general-indexing-label"></html:legend>
+ <vbox>
+ <hbox>
+ <checkbox id="enableGloda"
+ preference="mailnews.database.global.indexer.enabled"
+ data-l10n-id="enable-gloda-search-label"/>
+ </hbox>
+ <hbox align="center">
+ <label control="storeTypeMenulist" data-l10n-id="store-type-label"/>
+ <hbox>
+ <menulist id="storeTypeMenulist"
+ oncommand="gGeneralPane.updateDefaultStore(this.selectedItem.value)">
+ <menupopup id="storeTypeMenupopup">
+ <menuitem id="mboxStore"
+ data-l10n-id="mbox-store-label"
+ value="@mozilla.org/msgstore/berkeleystore;1"/>
+ <menuitem id="maildirStore"
+ data-l10n-id="maildir-store-label"
+ value="@mozilla.org/msgstore/maildirstore;1"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ <hbox>
+ <checkbox id="allowHWAccel"
+ preference="layers.acceleration.disabled"
+ data-l10n-id="allow-hw-accel"/>
+ </hbox>
+ </vbox>
+ <vbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" data-category="paneGeneral"/>
+
+ <html:div data-category="paneGeneral">
+ <html:fieldset data-category="paneGeneral">
+ <hbox pack="end">
+ <hbox>
+ <button is="highlightable-button" id="configEditor"
+ data-l10n-id="config-editor-button"
+ oncommand="gGeneralPane.showConfigEdit();"
+ searchkeywords="about:config"
+ search-l10n-ids="
+ about-config-page-title,
+ about-config-search-input1.placeholder,
+ about-config-show-all,
+ about-config-pref-add-button.title,
+ about-config-pref-toggle-button.title,
+ about-config-pref-edit-button.title,
+ about-config-pref-save-button.title,
+ about-config-pref-reset-button.title,
+ about-config-pref-delete-button.title,
+ about-config-pref-add-type-boolean,
+ about-config-pref-add-type-number,
+ about-config-pref-add-type-string
+ "/>
+ </hbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ </html:template>
diff --git a/comm/mail/components/preferences/general.js b/comm/mail/components/preferences/general.js
new file mode 100644
index 0000000000..96e8aaad2d
--- /dev/null
+++ b/comm/mail/components/preferences/general.js
@@ -0,0 +1,2962 @@
+/* -*- 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/. */
+
+"use strict";
+
+/* import-globals-from ../../base/content/aboutDialog-appUpdater.js */
+/* import-globals-from ../../../../toolkit/mozapps/preferences/fontbuilder.js */
+/* import-globals-from preferences.js */
+
+// ------------------------------
+// Constants & Enumeration Values
+
+var { DownloadUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/DownloadUtils.sys.mjs"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+var { TagUtils } = ChromeUtils.import("resource:///modules/TagUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetters(this, {
+ gHandlerService: [
+ "@mozilla.org/uriloader/handler-service;1",
+ "nsIHandlerService",
+ ],
+ gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"],
+});
+
+XPCOMUtils.defineLazyGetter(this, "gIsPackagedApp", () => {
+ return Services.sysinfo.getProperty("isPackagedApp");
+});
+
+const TYPE_PDF = "application/pdf";
+
+const PREF_PDFJS_DISABLED = "pdfjs.disabled";
+
+const AUTO_UPDATE_CHANGED_TOPIC = "auto-update-config-change";
+
+Preferences.addAll([
+ { id: "mail.pane_config.dynamic", type: "int" },
+ { id: "mailnews.reuse_message_window", type: "bool" },
+ { id: "mailnews.start_page.enabled", type: "bool" },
+ { id: "mailnews.start_page.url", type: "string" },
+ { id: "mail.biff.show_tray_icon", type: "bool" },
+ { id: "mail.biff.play_sound", type: "bool" },
+ { id: "mail.biff.play_sound.type", type: "int" },
+ { id: "mail.biff.play_sound.url", type: "string" },
+ { id: "mail.biff.use_system_alert", type: "bool" },
+ { id: "general.autoScroll", type: "bool" },
+ { id: "general.smoothScroll", type: "bool" },
+ { id: "widget.gtk.overlay-scrollbars.enabled", type: "bool", inverted: true },
+ { id: "mail.fixed_width_messages", type: "bool" },
+ { id: "mail.inline_attachments", type: "bool" },
+ { id: "mail.quoted_style", type: "int" },
+ { id: "mail.quoted_size", type: "int" },
+ { id: "mail.citation_color", type: "string" },
+ { id: "mail.display_glyph", type: "bool" },
+ { id: "font.language.group", type: "wstring" },
+ { id: "intl.regional_prefs.use_os_locales", type: "bool" },
+ { id: "mailnews.database.global.indexer.enabled", type: "bool" },
+ { id: "mailnews.labels.description.1", type: "wstring" },
+ { id: "mailnews.labels.color.1", type: "string" },
+ { id: "mailnews.labels.description.2", type: "wstring" },
+ { id: "mailnews.labels.color.2", type: "string" },
+ { id: "mailnews.labels.description.3", type: "wstring" },
+ { id: "mailnews.labels.color.3", type: "string" },
+ { id: "mailnews.labels.description.4", type: "wstring" },
+ { id: "mailnews.labels.color.4", type: "string" },
+ { id: "mailnews.labels.description.5", type: "wstring" },
+ { id: "mailnews.labels.color.5", type: "string" },
+ { id: "mail.showCondensedAddresses", type: "bool" },
+ { id: "mailnews.mark_message_read.auto", type: "bool" },
+ { id: "mailnews.mark_message_read.delay", type: "bool" },
+ { id: "mailnews.mark_message_read.delay.interval", type: "int" },
+ { id: "mail.openMessageBehavior", type: "int" },
+ { id: "mail.close_message_window.on_delete", type: "bool" },
+ { id: "mail.prompt_purge_threshhold", type: "bool" },
+ { id: "mail.purge.ask", type: "bool" },
+ { id: "mail.purge_threshhold_mb", type: "int" },
+ { id: "browser.cache.disk.capacity", type: "int" },
+ { id: "browser.cache.disk.smart_size.enabled", inverted: true, type: "bool" },
+ { id: "privacy.clearOnShutdown.cache", type: "bool" },
+ { id: "layers.acceleration.disabled", type: "bool", inverted: true },
+ { id: "searchintegration.enable", type: "bool" },
+ { id: "mail.tabs.drawInTitlebar", type: "bool" },
+ { id: "mail.tabs.autoHide", type: "bool" },
+]);
+if (AppConstants.platform == "win") {
+ Preferences.add({ id: "mail.minimizeToTray", type: "bool" });
+}
+if (AppConstants.platform != "macosx") {
+ Preferences.add({ id: "mail.biff.show_alert", type: "bool" });
+}
+
+var ICON_URL_APP = "";
+
+if (AppConstants.MOZ_WIDGET_GTK) {
+ ICON_URL_APP = "moz-icon://dummy.exe?size=16";
+} else {
+ ICON_URL_APP = "chrome://messenger/skin/preferences/application.png";
+}
+
+if (AppConstants.HAVE_SHELL_SERVICE) {
+ Preferences.addAll([
+ { id: "mail.shell.checkDefaultClient", type: "bool" },
+ { id: "pref.general.disable_button.default_mail", type: "bool" },
+ ]);
+}
+
+if (AppConstants.MOZ_UPDATER) {
+ Preferences.add({
+ id: "app.update.disable_button.showUpdateHistory",
+ type: "bool",
+ });
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ Preferences.add({ id: "app.update.service.enabled", type: "bool" });
+ }
+}
+
+var gGeneralPane = {
+ // The set of types the app knows how to handle. A map of HandlerInfoWrapper
+ // objects, indexed by type.
+ _handledTypes: new Map(),
+ // Map from a handlerInfoWrapper to the corresponding table HandlerRow.
+ _handlerRows: new Map(),
+ _handlerMenuId: 0,
+
+ // The list of types we can show, sorted by the sort column/direction.
+ // An array of HandlerInfoWrapper objects. We build this list when we first
+ // load the data and then rebuild it when users change a pref that affects
+ // what types we can show or change the sort column/direction.
+ // Note: this isn't necessarily the list of types we *will* show; if the user
+ // provides a filter string, we'll only show the subset of types in this list
+ // that match that string.
+ _visibleTypes: [],
+
+ // Map whose keys are string descriptions and values are references to the
+ // first visible HandlerInfoWrapper that has this description. We use this
+ // to determine whether or not to annotate descriptions with their types to
+ // distinguish duplicate descriptions from each other.
+ _visibleDescriptions: new Map(),
+
+ // -----------------------------------
+ // Convenience & Performance Shortcuts
+
+ // These get defined by init().
+ _brandShortName: null,
+ _handlerTbody: null,
+ _filter: null,
+ _prefsBundle: null,
+ mPane: null,
+ mStartPageUrl: "",
+ mShellServiceWorking: false,
+ mTagListBox: null,
+ requestingLocales: null,
+
+ async init() {
+ function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gGeneralPane));
+ }
+
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("saveWhere"),
+ () => gDownloadDirSection.onReadUseDownloadDir()
+ );
+
+ this.mPane = document.getElementById("paneGeneral");
+ this._prefsBundle = document.getElementById("bundlePreferences");
+ this._brandShortName = document
+ .getElementById("bundleBrand")
+ .getString("brandShortName");
+ this._handlerTbody = document.querySelector("#handlersTable > tbody");
+ this._filter = document.getElementById("filter");
+
+ this._handlerSort = { type: "type", descending: false };
+ this._handlerSortHeaders = document.querySelectorAll(
+ "#handlersTable > thead th[sort-type]"
+ );
+ for (let header of this._handlerSortHeaders) {
+ let button = header.querySelector("button");
+ button.addEventListener(
+ "click",
+ this.sort.bind(this, header.getAttribute("sort-type"))
+ );
+ }
+
+ this.updateStartPage();
+ this.updatePlaySound(
+ !Preferences.get("mail.biff.play_sound").value,
+ Preferences.get("mail.biff.play_sound.url").value,
+ Preferences.get("mail.biff.play_sound.type").value
+ );
+ if (AppConstants.platform != "macosx") {
+ this.updateShowAlert();
+ }
+ this.updateWebSearch();
+
+ // Search integration -- check whether we should hide or disable integration
+ let hideSearchUI = false;
+ let disableSearchUI = false;
+ const { SearchIntegration } = ChromeUtils.import(
+ "resource:///modules/SearchIntegration.jsm"
+ );
+ if (SearchIntegration) {
+ if (SearchIntegration.osVersionTooLow) {
+ hideSearchUI = true;
+ } else if (SearchIntegration.osComponentsNotRunning) {
+ disableSearchUI = true;
+ }
+ } else {
+ hideSearchUI = true;
+ }
+
+ if (hideSearchUI) {
+ document.getElementById("searchIntegrationContainer").hidden = true;
+ } else if (disableSearchUI) {
+ let searchCheckbox = document.getElementById("searchIntegration");
+ searchCheckbox.checked = false;
+ Preferences.get("searchintegration.enable").disabled = true;
+ }
+
+ // If the shell service is not working, disable the "Check now" button
+ // and "perform check at startup" checkbox.
+ try {
+ Cc["@mozilla.org/mail/shell-service;1"].getService(Ci.nsIShellService);
+ this.mShellServiceWorking = true;
+ } catch (ex) {
+ // The elements may not exist if HAVE_SHELL_SERVICE is off.
+ if (document.getElementById("alwaysCheckDefault")) {
+ document.getElementById("alwaysCheckDefault").disabled = true;
+ document.getElementById("alwaysCheckDefault").checked = false;
+ }
+ if (document.getElementById("checkDefaultButton")) {
+ document.getElementById("checkDefaultButton").disabled = true;
+ }
+ this.mShellServiceWorking = false;
+ }
+ this._rebuildFonts();
+
+ var menulist = document.getElementById("defaultFont");
+ if (menulist.selectedIndex == -1) {
+ // Prepend menuitem with empty name and value.
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", "");
+ item.setAttribute("value", "");
+ menulist.menupopup.insertBefore(
+ item,
+ menulist.menupopup.firstElementChild
+ );
+ menulist.selectedIndex = 0;
+ }
+
+ this.formatLocaleSetLabels();
+
+ if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
+ this.initPrimaryMessengerLanguageUI();
+ }
+
+ this.mTagListBox = document.getElementById("tagList");
+ this.buildTagList();
+ this.updateMarkAsReadOptions();
+
+ document.getElementById("citationmenu").value = Preferences.get(
+ "mail.citation_color"
+ ).value;
+
+ // By doing this in a timeout, we let the preferences dialog resize itself
+ // to an appropriate size before we add a bunch of items to the list.
+ // Otherwise, if there are many items, and the Applications prefpane
+ // is the one that gets displayed when the user first opens the dialog,
+ // the dialog might stretch too much in an attempt to fit them all in.
+ // XXX Shouldn't we perhaps just set a max-height on the richlistbox?
+ var _delayedPaneLoad = function (self) {
+ self._loadAppHandlerData();
+ self._rebuildVisibleTypes();
+ self._sortVisibleTypes();
+ self._rebuildView();
+
+ // Notify observers that the UI is now ready
+ Services.obs.notifyObservers(window, "app-handler-pane-loaded");
+ };
+ this.updateActualCacheSize();
+ this.updateCompactOptions();
+
+ // Default store type initialization.
+ let storeTypeElement = document.getElementById("storeTypeMenulist");
+ // set the menuitem to match the account
+ let defaultStoreID = Services.prefs.getCharPref(
+ "mail.serverDefaultStoreContractID"
+ );
+ let targetItem = storeTypeElement.getElementsByAttribute(
+ "value",
+ defaultStoreID
+ );
+ storeTypeElement.selectedItem = targetItem[0];
+ setTimeout(_delayedPaneLoad, 0, this);
+
+ if (AppConstants.MOZ_UPDATER) {
+ this.updateReadPrefs();
+ gAppUpdater = new appUpdater(); // eslint-disable-line no-global-assign
+ let updateDisabled =
+ Services.policies && !Services.policies.isAllowed("appUpdate");
+
+ if (gIsPackagedApp) {
+ // When we're running inside an app package, there's no point in
+ // displaying any update content here, and it would get confusing if we
+ // did, because our updater is not enabled.
+ // We can't rely on the hidden attribute for the toplevel elements,
+ // because of the pane hiding/showing code interfering.
+ document
+ .getElementById("updatesCategory")
+ .setAttribute("style", "display: none !important");
+ document
+ .getElementById("updateApp")
+ .setAttribute("style", "display: none !important");
+ } else if (updateDisabled || UpdateUtils.appUpdateAutoSettingIsLocked()) {
+ document.getElementById("updateAllowDescription").hidden = true;
+ document.getElementById("updateSettingsContainer").hidden = true;
+ if (updateDisabled && AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ document.getElementById("useService").hidden = true;
+ }
+ } else {
+ // Start with no option selected since we are still reading the value
+ document.getElementById("autoDesktop").removeAttribute("selected");
+ document.getElementById("manualDesktop").removeAttribute("selected");
+ // Start reading the correct value from the disk
+ this.updateReadPrefs();
+ setEventListener(
+ "updateRadioGroup",
+ "command",
+ gGeneralPane.updateWritePrefs
+ );
+ }
+
+ let defaults = Services.prefs.getDefaultBranch(null);
+ let distroId = defaults.getCharPref("distribution.id", "");
+ if (distroId) {
+ let distroVersion = defaults.getCharPref("distribution.version", "");
+
+ let distroIdField = document.getElementById("distributionId");
+ distroIdField.value = distroId + " - " + distroVersion;
+ distroIdField.style.display = "block";
+
+ let distroAbout = defaults.getStringPref("distribution.about", "");
+ if (distroAbout) {
+ let distroField = document.getElementById("distribution");
+ distroField.value = distroAbout;
+ distroField.style.display = "block";
+ }
+ }
+
+ if (AppConstants.platform == "win") {
+ // On Windows, the Application Update setting is an installation-
+ // specific preference, not a profile-specific one. Show a warning to
+ // inform users of this.
+ let updateContainer = document.getElementById(
+ "updateSettingsContainer"
+ );
+ updateContainer.classList.add("updateSettingCrossUserWarningContainer");
+ document.getElementById("updateSettingCrossUserWarning").hidden = false;
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ // Check to see if the maintenance service is installed.
+ // If it isn't installed, don't show the preference at all.
+ let installed;
+ try {
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ wrk.open(
+ wrk.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\MaintenanceService",
+ wrk.ACCESS_READ | wrk.WOW64_64
+ );
+ installed = wrk.readIntValue("Installed");
+ wrk.close();
+ } catch (e) {}
+ if (installed != 1) {
+ document.getElementById("useService").hidden = true;
+ }
+ }
+
+ let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
+
+ // Include the build ID and display warning if this is an "a#" (nightly) build
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let year = buildID.slice(0, 4);
+ let month = buildID.slice(4, 6);
+ let day = buildID.slice(6, 8);
+ version += ` (${year}-${month}-${day})`;
+ }
+
+ // Append "(32-bit)" or "(64-bit)" build architecture to the version number:
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let archResource = Services.appinfo.is64Bit
+ ? "aboutDialog.architecture.sixtyFourBit"
+ : "aboutDialog.architecture.thirtyTwoBit";
+ let arch = bundle.GetStringFromName(archResource);
+ version += ` (${arch})`;
+
+ document.l10n.setAttributes(
+ document.getElementById("version"),
+ "update-app-version",
+ { version }
+ );
+
+ if (!AppConstants.NIGHTLY_BUILD) {
+ // Show a release notes link if we have a URL.
+ let relNotesLink = document.getElementById("releasenotes");
+ let relNotesPrefType = Services.prefs.getPrefType(
+ "app.releaseNotesURL"
+ );
+ if (relNotesPrefType != Services.prefs.PREF_INVALID) {
+ let relNotesURL = Services.urlFormatter.formatURLPref(
+ "app.releaseNotesURL"
+ );
+ if (relNotesURL != "about:blank") {
+ relNotesLink.href = relNotesURL;
+ relNotesLink.hidden = false;
+ }
+ }
+ }
+ // Initialize Application section.
+
+ // Listen for window unload so we can remove our preference observers.
+ window.addEventListener("unload", this);
+
+ Services.obs.addObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.prefs.addObserver("mailnews.tags.", this);
+ }
+
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("allowSmartSize"),
+ () => this.readSmartSizeEnabled()
+ );
+
+ let element = document.getElementById("cacheSize");
+ Preferences.addSyncFromPrefListener(element, () => this.readCacheSize());
+ Preferences.addSyncToPrefListener(element, () => this.writeCacheSize());
+ Preferences.addSyncFromPrefListener(menulist, () =>
+ this.readFontSelection()
+ );
+ Preferences.addSyncFromPrefListener(
+ document.getElementById("soundUrlLocation"),
+ () => this.readSoundLocation()
+ );
+
+ if (!Services.policies.isAllowed("about:config")) {
+ document.getElementById("configEditor").disabled = true;
+ }
+ },
+
+ /**
+ * Restores the default start page as the user's start page
+ */
+ restoreDefaultStartPage() {
+ var startPage = Preferences.get("mailnews.start_page.url");
+ startPage.value = startPage.defaultValue;
+ },
+
+ /**
+ * Returns a formatted url corresponding to the value of mailnews.start_page.url
+ * Stores the original value of mailnews.start_page.url
+ */
+ readStartPageUrl() {
+ var pref = Preferences.get("mailnews.start_page.url");
+ this.mStartPageUrl = pref.value;
+ return Services.urlFormatter.formatURL(this.mStartPageUrl);
+ },
+
+ /**
+ * Returns the value of the mailnews start page url represented by the UI.
+ * If the url matches the formatted version of our stored value, then
+ * return the unformatted url.
+ */
+ writeStartPageUrl() {
+ var startPage = document.getElementById("mailnewsStartPageUrl");
+ return Services.urlFormatter.formatURL(this.mStartPageUrl) ==
+ startPage.value
+ ? this.mStartPageUrl
+ : startPage.value;
+ },
+
+ customizeMailAlert() {
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/notifications.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ configureDockOptions() {
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/dockoptions.xhtml",
+ { features: "resizable=no" }
+ );
+ },
+
+ convertURLToLocalFile(aFileURL) {
+ // convert the file url into a nsIFile
+ if (aFileURL) {
+ return Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getFileFromURLSpec(aFileURL);
+ }
+ return null;
+ },
+
+ readSoundLocation() {
+ var soundUrlLocation = document.getElementById("soundUrlLocation");
+ soundUrlLocation.value = Preferences.get("mail.biff.play_sound.url").value;
+ if (soundUrlLocation.value) {
+ soundUrlLocation.label = this.convertURLToLocalFile(
+ soundUrlLocation.value
+ ).leafName;
+ soundUrlLocation.style.backgroundImage =
+ "url(moz-icon://" + soundUrlLocation.label + "?size=16)";
+ }
+ },
+
+ previewSound() {
+ let sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
+
+ let soundLocation;
+ // soundType radio-group isn't used for macOS so it is not in the XUL file
+ // for the platform.
+ soundLocation =
+ AppConstants.platform == "macosx" ||
+ document.getElementById("soundType").value == 1
+ ? document.getElementById("soundUrlLocation").value
+ : "";
+
+ if (!soundLocation.includes("file://")) {
+ // User has not set any custom sound file to be played
+ sound.playEventSound(Ci.nsISound.EVENT_NEW_MAIL_RECEIVED);
+ } else {
+ // User has set a custom audio file to be played along the alert.
+ sound.play(Services.io.newURI(soundLocation));
+ }
+ },
+
+ browseForSoundFile() {
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+
+ // if we already have a sound file, then use the path for that sound file
+ // as the initial path in the dialog.
+ var localFile = this.convertURLToLocalFile(
+ document.getElementById("soundUrlLocation").value
+ );
+ if (localFile) {
+ fp.displayDirectory = localFile.parent;
+ }
+
+ // XXX todo, persist the last sound directory and pass it in
+ fp.init(
+ window,
+ document
+ .getElementById("bundlePreferences")
+ .getString("soundFilePickerTitle"),
+ Ci.nsIFilePicker.modeOpen
+ );
+ fp.appendFilters(Ci.nsIFilePicker.filterAudio);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ // convert the nsIFile into a nsIFile url
+ Preferences.get("mail.biff.play_sound.url").value = fp.fileURL.spec;
+ this.readSoundLocation(); // XXX We shouldn't have to be doing this by hand
+ this.updatePlaySound();
+ });
+ },
+
+ updatePlaySound(soundsDisabled, soundUrlLocation, soundType) {
+ // Update the sound type radio buttons based on the state of the
+ // play sound checkbox.
+ if (soundsDisabled === undefined) {
+ soundsDisabled = !document.getElementById("newMailNotification").checked;
+ soundUrlLocation = document.getElementById("soundUrlLocation").value;
+ }
+
+ // The UI is different on OS X as the user can only choose between letting
+ // the system play a default sound or setting a custom one. Therefore,
+ // "soundTypeEl" does not exist on OS X.
+ if (AppConstants.platform != "macosx") {
+ var soundTypeEl = document.getElementById("soundType");
+ if (soundType === undefined) {
+ soundType = soundTypeEl.value;
+ }
+
+ soundTypeEl.disabled = soundsDisabled;
+ document.getElementById("soundUrlLocation").disabled =
+ soundsDisabled || soundType != 1;
+ document.getElementById("browseForSound").disabled =
+ soundsDisabled || soundType != 1;
+ document.getElementById("playSound").disabled =
+ soundsDisabled || (!soundUrlLocation && soundType != 0);
+ } else {
+ // On OS X, if there is no selected custom sound then default one will
+ // be played. We keep consistency by disabling the "Play sound" checkbox
+ // if the user hasn't selected a custom sound file yet.
+ document.getElementById("newMailNotification").disabled =
+ !soundUrlLocation;
+ document.getElementById("playSound").disabled = !soundUrlLocation;
+ // The sound type radiogroup is hidden, but we have to keep the
+ // play_sound.type pref set appropriately.
+ Preferences.get("mail.biff.play_sound.type").value =
+ !soundsDisabled && soundUrlLocation ? 1 : 0;
+ }
+ },
+
+ updateStartPage() {
+ document.getElementById("mailnewsStartPageUrl").disabled = !Preferences.get(
+ "mailnews.start_page.enabled"
+ ).value;
+ document.getElementById("browseForStartPageUrl").disabled =
+ !Preferences.get("mailnews.start_page.enabled").value;
+ },
+
+ updateShowAlert() {
+ // The button does not exist on all platforms.
+ let customizeAlertButton = document.getElementById("customizeMailAlert");
+ if (customizeAlertButton) {
+ customizeAlertButton.disabled = !Preferences.get("mail.biff.show_alert")
+ .value;
+ }
+ // The checkmark does not exist on all platforms.
+ let systemNotification = document.getElementById(
+ "useSystemNotificationAlert"
+ );
+ if (systemNotification) {
+ systemNotification.disabled = !Preferences.get("mail.biff.show_alert")
+ .value;
+ }
+ },
+
+ updateWebSearch() {
+ let self = this;
+ Services.search.init().then(async () => {
+ let defaultEngine = await Services.search.getDefault();
+ let engineList = document.getElementById("defaultWebSearch");
+ for (let engine of await Services.search.getVisibleEngines()) {
+ let item = engineList.appendItem(engine.name);
+ item.engine = engine;
+ item.className = "menuitem-iconic";
+ item.setAttribute(
+ "image",
+ engine.iconURI
+ ? engine.iconURI.spec
+ : "resource://gre-resources/broken-image.png"
+ );
+ if (engine == defaultEngine) {
+ engineList.selectedItem = item;
+ }
+ }
+ self.defaultEngines = await Services.search.getAppProvidedEngines();
+ self.updateRemoveButton();
+
+ engineList.addEventListener("command", async () => {
+ await Services.search.setDefault(
+ engineList.selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ self.updateRemoveButton();
+ });
+ });
+ },
+
+ // Caches the default engines so we only retrieve them once.
+ defaultEngines: null,
+
+ async updateRemoveButton() {
+ let engineList = document.getElementById("defaultWebSearch");
+ let removeButton = document.getElementById("removeSearchEngine");
+ if (this.defaultEngines.includes(await Services.search.getDefault())) {
+ // Don't allow deletion of a default engine (saves us having a 'restore' button).
+ removeButton.disabled = true;
+ } else {
+ // Don't allow removal of last engine. This shouldn't happen since there should
+ // always be default engines.
+ removeButton.disabled = engineList.itemCount <= 1;
+ }
+ },
+
+ /**
+ * Look up OpenSearch Description URL.
+ *
+ * @param url - the url to use as basis for discovery
+ */
+ async lookupOpenSearch(url) {
+ let response = await fetch(url);
+ if (!response.ok) {
+ throw new Error(`Bad response for url=${url}`);
+ }
+ let contentType = response.headers.get("Content-Type")?.toLowerCase();
+ if (
+ contentType == "application/opensearchdescription+xml" ||
+ contentType == "application/xml" ||
+ contentType == "text/xml"
+ ) {
+ return url;
+ }
+ let doc = new DOMParser().parseFromString(
+ await response.text(),
+ "text/html"
+ );
+ let auto = doc.querySelector(
+ "link[rel='search'][type='application/opensearchdescription+xml']"
+ );
+ if (!auto) {
+ throw new Error(`No provider discovered for url=${url}`);
+ }
+ return /^https?:/.test(auto.href)
+ ? auto.href
+ : new URL(url).origin + auto.href;
+ },
+
+ async addSearchEngine() {
+ let input = { value: "https://" };
+ let [title, text] = await document.l10n.formatValues([
+ "add-opensearch-provider-title",
+ "add-opensearch-provider-text",
+ ]);
+ let result = Services.prompt.prompt(window, title, text, input, null, {
+ value: false,
+ });
+ input.value = input.value.trim();
+ if (!result || !input.value || input.value == "https://") {
+ return;
+ }
+ let url = input.value;
+ let engine;
+ try {
+ url = await this.lookupOpenSearch(url);
+ engine = await Services.search.addOpenSearchEngine(url, null);
+ } catch (reason) {
+ let [title, text] = await document.l10n.formatValues([
+ { id: "adding-opensearch-provider-failed-title" },
+ { id: "adding-opensearch-provider-failed-text", args: { url } },
+ ]);
+ Services.prompt.alert(window, title, text);
+ return;
+ }
+ // Wait a bit, so the engine iconURI has time to be fetched.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+
+ // Add new engine to the list, make the added engine the default.
+ let engineList = document.getElementById("defaultWebSearch");
+ let item = engineList.appendItem(engine.name);
+ item.engine = engine;
+ item.className = "menuitem-iconic";
+ item.setAttribute(
+ "image",
+ engine.iconURI
+ ? engine.iconURI.spec
+ : "resource://gre-resources/broken-image.png"
+ );
+ engineList.selectedIndex =
+ engineList.firstElementChild.childElementCount - 1;
+ await Services.search.setDefault(
+ engineList.selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ this.updateRemoveButton();
+ },
+
+ async removeSearchEngine() {
+ // Deletes the current engine. Firefox does a better job since it
+ // shows all the engines in the list. But better than nothing.
+ let defaultEngine = await Services.search.getDefault();
+ let engineList = document.getElementById("defaultWebSearch");
+ for (let i = 0; i < engineList.itemCount; i++) {
+ let item = engineList.getItemAtIndex(i);
+ if (item.engine == defaultEngine) {
+ await Services.search.removeEngine(item.engine);
+ item.remove();
+ engineList.selectedIndex = 0;
+ await Services.search.setDefault(
+ engineList.selectedItem.engine,
+ Ci.nsISearchService.CHANGE_REASON_USER
+ );
+ this.updateRemoveButton();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Checks whether Thunderbird is currently registered with the operating
+ * system as the default app for mail, rss and news. If Thunderbird is not
+ * currently the default app, the user is given the option of making it the
+ * default for each type; otherwise, the user is informed that Thunderbird is
+ * already the default.
+ */
+ checkDefaultNow(aAppType) {
+ if (!this.mShellServiceWorking) {
+ return;
+ }
+
+ // otherwise, bring up the default client dialog
+ gSubDialog.open(
+ "chrome://messenger/content/systemIntegrationDialog.xhtml",
+ { features: "resizable=no" },
+ "calledFromPrefs"
+ );
+ },
+
+ // FONTS
+
+ /**
+ * Populates the default font list in UI.
+ */
+ _rebuildFonts() {
+ var langGroupPref = Preferences.get("font.language.group");
+ var isSerif =
+ gGeneralPane._readDefaultFontTypeForLanguage(langGroupPref.value) ==
+ "serif";
+ gGeneralPane._selectDefaultLanguageGroup(langGroupPref.value, isSerif);
+ },
+
+ /**
+ * Select the default language group.
+ */
+ _selectDefaultLanguageGroupPromise: Promise.resolve(),
+
+ _selectDefaultLanguageGroup(aLanguageGroup, aIsSerif) {
+ this._selectDefaultLanguageGroupPromise = (async () => {
+ // Avoid overlapping language group selections by awaiting the resolution
+ // of the previous one. We do this because this function is re-entrant,
+ // as inserting <preference> elements into the DOM sometimes triggers a call
+ // back into this function. And since this function is also asynchronous,
+ // that call can enter this function before the previous run has completed,
+ // which would corrupt the font menulists. Awaiting the previous call's
+ // resolution avoids that fate.
+ await this._selectDefaultLanguageGroupPromise;
+
+ const kFontNameFmtSerif = "font.name.serif.%LANG%";
+ const kFontNameFmtSansSerif = "font.name.sans-serif.%LANG%";
+ const kFontNameListFmtSerif = "font.name-list.serif.%LANG%";
+ const kFontNameListFmtSansSerif = "font.name-list.sans-serif.%LANG%";
+ const kFontSizeFmtVariable = "font.size.variable.%LANG%";
+
+ // Make sure font.name-list is created before font.name so that it's
+ // available at the time readFontSelection below is called.
+ var prefs = [
+ {
+ format: aIsSerif ? kFontNameListFmtSerif : kFontNameListFmtSansSerif,
+ type: "unichar",
+ element: null,
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: aIsSerif ? kFontNameFmtSerif : kFontNameFmtSansSerif,
+ type: "fontname",
+ element: "defaultFont",
+ fonttype: aIsSerif ? "serif" : "sans-serif",
+ },
+ {
+ format: kFontSizeFmtVariable,
+ type: "int",
+ element: "defaultFontSize",
+ fonttype: null,
+ },
+ ];
+
+ for (var i = 0; i < prefs.length; ++i) {
+ var preference = Preferences.get(
+ prefs[i].format.replace(/%LANG%/, aLanguageGroup)
+ );
+ if (!preference) {
+ preference = Preferences.add({
+ id: prefs[i].format.replace(/%LANG%/, aLanguageGroup),
+ type: prefs[i].type,
+ });
+ }
+
+ if (!prefs[i].element) {
+ continue;
+ }
+
+ var element = document.getElementById(prefs[i].element);
+ if (element) {
+ if (prefs[i].fonttype) {
+ await FontBuilder.buildFontList(
+ aLanguageGroup,
+ prefs[i].fonttype,
+ element
+ );
+ }
+
+ element.setAttribute("preference", preference.id);
+
+ preference.setElementValue(element);
+ }
+ }
+ })().catch(console.error);
+ },
+
+ /**
+ * Displays the fonts dialog, where web page font names and sizes can be
+ * configured.
+ */
+ configureFonts() {
+ gSubDialog.open("chrome://messenger/content/preferences/fonts.xhtml", {
+ features: "resizable=no",
+ });
+ },
+
+ /**
+ * Displays the colors dialog, where default web page/link/etc. colors can be
+ * configured.
+ */
+ configureColors() {
+ gSubDialog.open("chrome://messenger/content/preferences/colors.xhtml", {
+ features: "resizable=no",
+ });
+ },
+
+ /**
+ * Returns the type of the current default font for the language denoted by
+ * aLanguageGroup.
+ */
+ _readDefaultFontTypeForLanguage(aLanguageGroup) {
+ const kDefaultFontType = "font.default.%LANG%";
+ var defaultFontTypePref = kDefaultFontType.replace(
+ /%LANG%/,
+ aLanguageGroup
+ );
+ var preference = Preferences.get(defaultFontTypePref);
+ if (!preference) {
+ Preferences.add({
+ id: defaultFontTypePref,
+ type: "string",
+ name: defaultFontTypePref,
+ }).on("change", gGeneralPane._rebuildFonts);
+ }
+
+ // We should return preference.value here, but we can't wait for the binding to load,
+ // or things get really messy. Fortunately this will give the same answer.
+ return Services.prefs.getCharPref(defaultFontTypePref);
+ },
+
+ /**
+ * Determine the appropriate value to select for defaultFont, for the
+ * following cases:
+ * - there is no setting
+ * - the font selected by the user is no longer present (e.g. deleted from
+ * fonts folder)
+ */
+ readFontSelection() {
+ let element = document.getElementById("defaultFont");
+ let preference = Preferences.get(element.getAttribute("preference"));
+ if (preference.value) {
+ let fontItem = element.querySelector(
+ '[value="' + preference.value + '"]'
+ );
+
+ // There is a setting that actually is in the list. Respect it.
+ if (fontItem) {
+ return undefined;
+ }
+ }
+
+ let defaultValue =
+ element.firstElementChild.firstElementChild.getAttribute("value");
+ let languagePref = Preferences.get("font.language.group");
+ let defaultType = this._readDefaultFontTypeForLanguage(languagePref.value);
+ let listPref = Preferences.get(
+ "font.name-list." + defaultType + "." + languagePref.value
+ );
+ if (!listPref) {
+ return defaultValue;
+ }
+
+ let fontNames = listPref.value.split(",");
+
+ for (let fontName of fontNames) {
+ let fontItem = element.querySelector('[value="' + fontName.trim() + '"]');
+ if (fontItem) {
+ return fontItem.getAttribute("value");
+ }
+ }
+ return defaultValue;
+ },
+
+ async formatLocaleSetLabels() {
+ // HACK: calling getLocaleDisplayNames may fail the first time due to
+ // synchronous loading of the .ftl files. If we load the files and wait
+ // for a known value asynchronously, no such failure will happen.
+ await new Localization([
+ "toolkit/intl/languageNames.ftl",
+ "toolkit/intl/regionNames.ftl",
+ ]).formatValue("language-name-en");
+
+ const osprefs = Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ );
+ let appLocale = Services.locale.appLocalesAsBCP47[0];
+ let rsLocale = osprefs.regionalPrefsLocales[0];
+ let names = Services.intl.getLocaleDisplayNames(undefined, [
+ appLocale,
+ rsLocale,
+ ]);
+ let appLocaleRadio = document.getElementById("appLocale");
+ let rsLocaleRadio = document.getElementById("rsLocale");
+ let appLocaleLabel = this._prefsBundle.getFormattedString(
+ "appLocale.label",
+ [names[0]]
+ );
+ let rsLocaleLabel = this._prefsBundle.getFormattedString("rsLocale.label", [
+ names[1],
+ ]);
+ appLocaleRadio.setAttribute("label", appLocaleLabel);
+ rsLocaleRadio.setAttribute("label", rsLocaleLabel);
+ appLocaleRadio.accessKey = this._prefsBundle.getString(
+ "appLocale.accesskey"
+ );
+ rsLocaleRadio.accessKey = this._prefsBundle.getString("rsLocale.accesskey");
+ },
+
+ // Load the preferences string bundle for other locales with fallbacks.
+ getBundleForLocales(newLocales) {
+ let locales = Array.from(
+ new Set([
+ ...newLocales,
+ ...Services.locale.requestedLocales,
+ Services.locale.lastFallbackLocale,
+ ])
+ );
+ return new Localization(
+ ["messenger/preferences/preferences.ftl", "branding/brand.ftl"],
+ false,
+ undefined,
+ locales
+ );
+ },
+
+ initPrimaryMessengerLanguageUI() {
+ gGeneralPane.updatePrimaryMessengerLanguageUI(
+ Services.locale.requestedLocale
+ );
+ },
+
+ /**
+ * Update the available list of locales and select the locale that the user
+ * is "selecting". This could be the currently requested locale or a locale
+ * that the user would like to switch to after confirmation.
+ *
+ * @param {string} selected - The selected BCP 47 locale.
+ */
+ async updatePrimaryMessengerLanguageUI(selected) {
+ // HACK: calling getLocaleDisplayNames may fail the first time due to
+ // synchronous loading of the .ftl files. If we load the files and wait
+ // for a known value asynchronously, no such failure will happen.
+ await new Localization([
+ "toolkit/intl/languageNames.ftl",
+ "toolkit/intl/regionNames.ftl",
+ ]).formatValue("language-name-en");
+
+ let available = await getAvailableLocales();
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ available,
+ { preferNative: true }
+ );
+ let locales = available.map((code, i) => ({ code, name: localeNames[i] }));
+ locales.sort((a, b) => a.name > b.name);
+
+ let fragment = document.createDocumentFragment();
+ for (let { code, name } of locales) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("value", code);
+ menuitem.setAttribute("label", name);
+ fragment.appendChild(menuitem);
+ }
+
+ // Add an option to search for more languages if downloading is supported.
+ if (Services.prefs.getBoolPref("intl.multilingual.downloadEnabled")) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.id = "primaryMessengerLocaleSearch";
+ menuitem.setAttribute(
+ "label",
+ await document.l10n.formatValue("messenger-languages-search")
+ );
+ menuitem.setAttribute("value", "search");
+ menuitem.addEventListener("command", () => {
+ gGeneralPane.showMessengerLanguagesSubDialog({ search: true });
+ });
+ fragment.appendChild(menuitem);
+ }
+
+ let menulist = document.getElementById("primaryMessengerLocale");
+ let menupopup = menulist.querySelector("menupopup");
+ menupopup.textContent = "";
+ menupopup.appendChild(fragment);
+ menulist.value = selected;
+
+ document.getElementById("messengerLanguagesBox").hidden = false;
+ },
+
+ /**
+ * Open the messenger languages sub dialog in either the normal mode, or search mode.
+ * The search mode is only available from the menu to change the primary browser
+ * language.
+ *
+ * @param {{ search: boolean }}
+ */
+ showMessengerLanguagesSubDialog({ search }) {
+ let opts = {
+ selectedLocalesForRestart: gGeneralPane.selectedLocalesForRestart,
+ search,
+ };
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/messengerLanguages.xhtml",
+ { closingCallback: this.messengerLanguagesClosed },
+ opts
+ );
+ },
+
+ /**
+ * Returns the assumed script directionality for known Firefox locales. This is
+ * somewhat crude, but should work until Bug 1750781 lands.
+ *
+ * TODO (Bug 1750781) - This should use Intl.LocaleInfo once it is standardized (see
+ * Bug 1693576), rather than maintaining a hardcoded list of RTL locales.
+ *
+ * @param {string} locale
+ * @returns {"ltr" | "rtl"}
+ */
+ getLocaleDirection(locale) {
+ if (
+ locale == "ar" ||
+ locale == "ckb" ||
+ locale == "fa" ||
+ locale == "he" ||
+ locale == "ur"
+ ) {
+ return "rtl";
+ }
+ return "ltr";
+ },
+
+ /**
+ * Determine the transition strategy for switching the locale based on prefs
+ * and the switched locales.
+ *
+ * @param {Array<string>} newLocales - List of BCP 47 locale identifiers.
+ * @returns {"locales-match" | "requires-restart" | "live-reload"}
+ */
+ getLanguageSwitchTransitionType(newLocales) {
+ const { appLocalesAsBCP47 } = Services.locale;
+ if (appLocalesAsBCP47.join(",") === newLocales.join(",")) {
+ // The selected locales match, the order matters.
+ return "locales-match";
+ }
+
+ if (Services.prefs.getBoolPref("intl.multilingual.liveReload")) {
+ if (
+ gGeneralPane.getLocaleDirection(newLocales[0]) !==
+ gGeneralPane.getLocaleDirection(appLocalesAsBCP47[0]) &&
+ !Services.prefs.getBoolPref("intl.multilingual.liveReloadBidirectional")
+ ) {
+ // Bug 1750852: The directionality of the text changed, which requires a restart
+ // until the quality of the switch can be improved.
+ return "requires-restart";
+ }
+
+ return "live-reload";
+ }
+
+ return "requires-restart";
+ },
+
+ /* Show or hide the confirm change message bar based on the updated ordering. */
+ messengerLanguagesClosed() {
+ // When the subdialog is closed, settings are stored on gMessengerLanguagesDialog.
+ // The next time the dialog is opened, a new gMessengerLanguagesDialog is created.
+ let { selected } = this.gMessengerLanguagesDialog;
+
+ if (!selected) {
+ // No locales were selected. Cancel the operation.
+ return;
+ }
+
+ switch (gGeneralPane.getLanguageSwitchTransitionType(selected)) {
+ case "requires-restart":
+ gGeneralPane.showConfirmLanguageChangeMessageBar(selected);
+ gGeneralPane.updatePrimaryMessengerLanguageUI(selected[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = selected;
+
+ gGeneralPane.updatePrimaryMessengerLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gGeneralPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gGeneralPane.updatePrimaryMessengerLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gGeneralPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ /* Show the confirmation message bar to allow a restart into the new locales. */
+ async showConfirmLanguageChangeMessageBar(locales) {
+ let messageBar = document.getElementById("confirmMessengerLanguage");
+
+ // Get the bundle for the new locale.
+ let newBundle = this.getBundleForLocales(locales);
+
+ // Find the messages and labels.
+ let messages = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-messenger-language-change-description")
+ )
+ );
+ let buttonLabels = await Promise.all(
+ [newBundle, document.l10n].map(async bundle =>
+ bundle.formatValue("confirm-messenger-language-change-button")
+ )
+ );
+
+ // If both the message and label are the same, just include one row.
+ if (messages[0] == messages[1] && buttonLabels[0] == buttonLabels[1]) {
+ messages.pop();
+ buttonLabels.pop();
+ }
+
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+
+ for (let i = 0; i < messages.length; i++) {
+ let messageContainer = document.createXULElement("hbox");
+ messageContainer.classList.add("message-bar-content");
+ messageContainer.setAttribute("flex", "1");
+ messageContainer.setAttribute("align", "center");
+
+ let description = document.createXULElement("description");
+ description.classList.add("message-bar-description");
+
+ if (i == 0 && gGeneralPane.getLocaleDirection(locales[0]) === "rtl") {
+ description.classList.add("rtl-locale");
+ }
+
+ description.setAttribute("flex", "1");
+ description.textContent = messages[i];
+ messageContainer.appendChild(description);
+
+ let button = document.createXULElement("button");
+ button.addEventListener("command", gGeneralPane.confirmLanguageChange);
+ button.classList.add("message-bar-button");
+ button.setAttribute("locales", locales.join(","));
+ button.setAttribute("label", buttonLabels[i]);
+ messageContainer.appendChild(button);
+
+ contentContainer.appendChild(messageContainer);
+ }
+
+ messageBar.hidden = false;
+ this.selectedLocalesForRestart = locales;
+ },
+
+ hideConfirmLanguageChangeMessageBar() {
+ let messageBar = document.getElementById("confirmMessengerLanguage");
+ messageBar.hidden = true;
+ let contentContainer = messageBar.querySelector(
+ ".message-bar-content-container"
+ );
+ contentContainer.textContent = "";
+ this.requestingLocales = null;
+ },
+
+ /* Confirm the locale change and restart the Thunderbird in the new locale. */
+ confirmLanguageChange(event) {
+ let localesString = (event.target.getAttribute("locales") || "").trim();
+ if (!localesString || localesString.length == 0) {
+ return;
+ }
+ let locales = localesString.split(",");
+ Services.locale.requestedLocales = locales;
+
+ // Restart with the new locale.
+ let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(
+ cancelQuit,
+ "quit-application-requested",
+ "restart"
+ );
+ if (!cancelQuit.data) {
+ Services.startup.quit(
+ Services.startup.eAttemptQuit | Services.startup.eRestart
+ );
+ }
+ },
+
+ /* Show or hide the confirm change message bar based on the new locale. */
+ onPrimaryMessengerLanguageMenuChange(event) {
+ let locale = event.target.value;
+
+ if (locale == "search") {
+ return;
+ } else if (locale == Services.locale.appLocaleAsBCP47) {
+ this.hideConfirmLanguageChangeMessageBar();
+ return;
+ }
+
+ let newLocales = Array.from(
+ new Set([locale, ...Services.locale.requestedLocales]).values()
+ );
+
+ switch (gGeneralPane.getLanguageSwitchTransitionType(newLocales)) {
+ case "requires-restart":
+ // Prepare to change the locales, as they were different.
+ gGeneralPane.showConfirmLanguageChangeMessageBar(newLocales);
+ gGeneralPane.updatePrimaryMessengerLanguageUI(newLocales[0]);
+ break;
+ case "live-reload":
+ Services.locale.requestedLocales = newLocales;
+ gGeneralPane.updatePrimaryMessengerLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gGeneralPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ case "locales-match":
+ // They matched, so we can reset the UI.
+ gGeneralPane.updatePrimaryMessengerLanguageUI(
+ Services.locale.appLocaleAsBCP47
+ );
+ gGeneralPane.hideConfirmLanguageChangeMessageBar();
+ break;
+ default:
+ throw new Error("Unhandled transition type.");
+ }
+ },
+
+ // appends the tag to the tag list box
+ appendTagItem(aTagName, aKey, aColor) {
+ let item = this.mTagListBox.appendItem(aTagName, aKey);
+ item.style.color = aColor;
+ return item;
+ },
+
+ buildTagList() {
+ let tagArray = MailServices.tags.getAllTags();
+ for (let i = 0; i < tagArray.length; ++i) {
+ let taginfo = tagArray[i];
+ this.appendTagItem(taginfo.tag, taginfo.key, taginfo.color);
+ }
+ },
+
+ removeTag() {
+ var index = this.mTagListBox.selectedIndex;
+ if (index >= 0) {
+ var itemToRemove = this.mTagListBox.getItemAtIndex(index);
+ MailServices.tags.deleteKey(itemToRemove.getAttribute("value"));
+ }
+ },
+
+ /**
+ * Open the edit tag dialog
+ */
+ editTag() {
+ var index = this.mTagListBox.selectedIndex;
+ if (index >= 0) {
+ var tagElToEdit = this.mTagListBox.getItemAtIndex(index);
+ var args = {
+ result: "",
+ keyToEdit: tagElToEdit.getAttribute("value"),
+ };
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/tagDialog.xhtml",
+ { features: "resizable=no" },
+ args
+ );
+ }
+ },
+
+ addTag() {
+ var args = { result: "", okCallback: addTagCallback };
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/tagDialog.xhtml",
+ { features: "resizable=no" },
+ args
+ );
+ },
+
+ onSelectTag() {
+ let btnEdit = document.getElementById("editTagButton");
+ let listBox = document.getElementById("tagList");
+
+ if (listBox.selectedCount > 0) {
+ btnEdit.disabled = false;
+ } else {
+ btnEdit.disabled = true;
+ }
+
+ document.getElementById("removeTagButton").disabled = btnEdit.disabled;
+ },
+
+ /**
+ * Enable/disable the options of automatic marking as read depending on the
+ * state of the automatic marking feature.
+ */
+ updateMarkAsReadOptions() {
+ let enableRadioGroup = Preferences.get(
+ "mailnews.mark_message_read.auto"
+ ).value;
+ let autoMarkAsPref = Preferences.get("mailnews.mark_message_read.delay");
+ let autoMarkDisabled = !enableRadioGroup || autoMarkAsPref.locked;
+ document.getElementById("markAsReadAutoPreferences").disabled =
+ autoMarkDisabled;
+ document.getElementById("secondsLabel").disabled = autoMarkDisabled;
+ gGeneralPane.updateMarkAsReadTextbox();
+ },
+
+ /**
+ * Automatically enable/disable delay textbox depending on state of the
+ * Mark As Read On Delay feature.
+ */
+ updateMarkAsReadTextbox() {
+ let radioGroupEnabled = Preferences.get(
+ "mailnews.mark_message_read.auto"
+ ).value;
+ let textBoxEnabled = Preferences.get(
+ "mailnews.mark_message_read.delay"
+ ).value;
+ let intervalPref = Preferences.get(
+ "mailnews.mark_message_read.delay.interval"
+ );
+
+ let delayTextbox = document.getElementById("markAsReadDelay");
+ delayTextbox.disabled =
+ !radioGroupEnabled || !textBoxEnabled || intervalPref.locked;
+ if (document.activeElement.id == "markAsReadAutoPreferences") {
+ delayTextbox.focus();
+ }
+ },
+
+ /**
+ * Display the return receipts configuration dialog.
+ */
+ showReturnReceipts() {
+ gSubDialog.open("chrome://messenger/content/preferences/receipts.xhtml", {
+ features: "resizable=no",
+ });
+ },
+
+ /**
+ * Show the about:config page in a tab.
+ */
+ showConfigEdit() {
+ // If the about:config tab is already open, switch to the tab.
+ let mainWin = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mainWin.document.getElementById("tabmail");
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:config") {
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+ // Wasn't open already. Open in a new tab.
+ tabmail.openTab("contentTab", { url: "about:config" });
+ },
+
+ /**
+ * Display the the connection settings dialog.
+ */
+ showConnections() {
+ gSubDialog.open("chrome://messenger/content/preferences/connection.xhtml");
+ },
+
+ /**
+ * Display the the offline settings dialog.
+ */
+ showOffline() {
+ gSubDialog.open("chrome://messenger/content/preferences/offline.xhtml", {
+ features: "resizable=no",
+ });
+ },
+
+ /*
+ * browser.cache.disk.capacity
+ * - the size of the browser cache in KB
+ */
+
+ // Retrieves the amount of space currently used by disk cache
+ updateActualCacheSize() {
+ let actualSizeLabel = document.getElementById("actualDiskCacheSize");
+ let prefStrBundle = document.getElementById("bundlePreferences");
+
+ // Needs to root the observer since cache service keeps only a weak reference.
+ this.observer = {
+ onNetworkCacheDiskConsumption(consumption) {
+ let size = DownloadUtils.convertByteUnits(consumption);
+ // The XBL binding for the string bundle may have been destroyed if
+ // the page was closed before this callback was executed.
+ if (!prefStrBundle.getFormattedString) {
+ return;
+ }
+ actualSizeLabel.value = prefStrBundle.getFormattedString(
+ "actualDiskCacheSize",
+ size
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsICacheStorageConsumptionObserver",
+ "nsISupportsWeakReference",
+ ]),
+ };
+
+ actualSizeLabel.value = prefStrBundle.getString(
+ "actualDiskCacheSizeCalculated"
+ );
+
+ try {
+ Services.cache2.asyncGetDiskConsumption(this.observer);
+ } catch (e) {}
+ },
+
+ updateCacheSizeUI(smartSizeEnabled) {
+ document.getElementById("useCacheBefore").disabled = smartSizeEnabled;
+ document.getElementById("cacheSize").disabled = smartSizeEnabled;
+ document.getElementById("useCacheAfter").disabled = smartSizeEnabled;
+ },
+
+ readSmartSizeEnabled() {
+ // The smart_size.enabled preference element is inverted="true", so its
+ // value is the opposite of the actual pref value
+ var disabled = Preferences.get(
+ "browser.cache.disk.smart_size.enabled"
+ ).value;
+ this.updateCacheSizeUI(!disabled);
+ },
+
+ /**
+ * Converts the cache size from units of KB to units of MB and returns that
+ * value.
+ */
+ readCacheSize() {
+ var preference = Preferences.get("browser.cache.disk.capacity");
+ return preference.value / 1024;
+ },
+
+ /**
+ * Converts the cache size as specified in UI (in MB) to KB and returns that
+ * value.
+ */
+ writeCacheSize() {
+ var cacheSize = document.getElementById("cacheSize");
+ var intValue = parseInt(cacheSize.value, 10);
+ return isNaN(intValue) ? 0 : intValue * 1024;
+ },
+
+ /**
+ * Clears the cache.
+ */
+ clearCache() {
+ try {
+ Services.cache2.clear();
+ } catch (ex) {}
+ this.updateActualCacheSize();
+ },
+
+ updateCompactOptions() {
+ let disabled =
+ !Preferences.get("mail.prompt_purge_threshhold").value ||
+ Preferences.get("mail.purge_threshhold_mb").locked;
+
+ document.getElementById("offlineCompactFolderMin").disabled = disabled;
+ document.getElementById("offlineCompactFolderAutomatically").disabled =
+ disabled;
+ },
+
+ /**
+ * Set the default store contract ID.
+ */
+ updateDefaultStore(storeID) {
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+ },
+
+ /**
+ * When the user toggles the layers.acceleration.disabled pref,
+ * sync its new value to the gfx.direct2d.disabled pref too.
+ * Note that layers.acceleration.disabled is inverted.
+ */
+ updateHardwareAcceleration() {
+ if (AppConstants.platform == "win") {
+ let preference = Preferences.get("layers.acceleration.disabled");
+ Services.prefs.setBoolPref("gfx.direct2d.disabled", !preference.value);
+ }
+ },
+
+ /**
+ * Selects the correct item in the update radio group
+ */
+ async updateReadPrefs() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ radiogroup.disabled = true;
+ try {
+ let enabled = await UpdateUtils.getAppUpdateAutoEnabled();
+ radiogroup.value = enabled;
+ radiogroup.disabled = false;
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ },
+
+ /**
+ * Writes the value of the update radio group to the disk
+ */
+ async updateWritePrefs() {
+ if (
+ AppConstants.MOZ_UPDATER &&
+ (!Services.policies || Services.policies.isAllowed("appUpdate")) &&
+ !gIsPackagedApp
+ ) {
+ let radiogroup = document.getElementById("updateRadioGroup");
+ let updateAutoValue = radiogroup.value == "true";
+ radiogroup.disabled = true;
+ try {
+ await UpdateUtils.setAppUpdateAutoEnabled(updateAutoValue);
+ radiogroup.disabled = false;
+ } catch (error) {
+ console.error(error);
+ await this.updateReadPrefs();
+ await this.reportUpdatePrefWriteError();
+ return;
+ }
+
+ // If the value was changed to false the user should be given the option
+ // to discard an update if there is one.
+ if (!updateAutoValue) {
+ await this.checkUpdateInProgress();
+ }
+ }
+ },
+
+ async reportUpdatePrefWriteError() {
+ let [title, message] = await document.l10n.formatValues([
+ { id: "update-setting-write-failure-title" },
+ {
+ id: "update-setting-write-failure-message",
+ args: { path: UpdateUtils.configFilePath },
+ },
+ ]);
+
+ // Set up the Ok Button
+ let buttonFlags =
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK;
+ Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+ },
+
+ async checkUpdateInProgress() {
+ let um = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ );
+ if (!um.readyUpdate && !um.downloadingUpdate) {
+ return;
+ }
+
+ let [title, message, okButton, cancelButton] =
+ await document.l10n.formatValues([
+ { id: "update-in-progress-title" },
+ { id: "update-in-progress-message" },
+ { id: "update-in-progress-ok-button" },
+ { id: "update-in-progress-cancel-button" },
+ ]);
+
+ // Continue is the cancel button which is BUTTON_POS_1 and is set as the
+ // default so pressing escape or using a platform standard method of closing
+ // the UI will not discard the update.
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 +
+ Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1 +
+ Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+
+ let rv = Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ buttonFlags,
+ okButton,
+ cancelButton,
+ null,
+ null,
+ {}
+ );
+ if (rv != 1) {
+ let aus = Cc["@mozilla.org/updates/update-service;1"].getService(
+ Ci.nsIApplicationUpdateService
+ );
+ aus.stopDownload();
+ um.cleanupReadyUpdate();
+ um.cleanupDownloadingUpdate();
+ }
+ },
+
+ showUpdates() {
+ gSubDialog.open("chrome://mozapps/content/update/history.xhtml");
+ },
+
+ _loadAppHandlerData() {
+ this._loadInternalHandlers();
+ this._loadApplicationHandlers();
+ },
+
+ _loadInternalHandlers() {
+ const internalHandlers = [new PDFHandlerInfoWrapper()];
+ for (const internalHandler of internalHandlers) {
+ if (internalHandler.enabled) {
+ this._handledTypes.set(internalHandler.type, internalHandler);
+ }
+ }
+ },
+
+ /**
+ * Load the set of handlers defined by the application datastore.
+ */
+ _loadApplicationHandlers() {
+ for (let wrappedHandlerInfo of gHandlerService.enumerate()) {
+ let type = wrappedHandlerInfo.type;
+
+ let handlerInfoWrapper;
+ if (this._handledTypes.has(type)) {
+ handlerInfoWrapper = this._handledTypes.get(type);
+ } else {
+ handlerInfoWrapper = new HandlerInfoWrapper(type, wrappedHandlerInfo);
+ this._handledTypes.set(type, handlerInfoWrapper);
+ }
+ }
+ },
+
+ // -----------------
+ // View Construction
+
+ _rebuildVisibleTypes() {
+ // Reset the list of visible types and the visible type description.
+ this._visibleTypes.length = 0;
+ this._visibleDescriptions.clear();
+
+ for (let handlerInfo of this._handledTypes.values()) {
+ // We couldn't find any reason to exclude the type, so include it.
+ this._visibleTypes.push(handlerInfo);
+
+ let otherHandlerInfo = this._visibleDescriptions.get(
+ handlerInfo.description
+ );
+ if (!otherHandlerInfo) {
+ // This is the first type with this description that we encountered
+ // while rebuilding the _visibleTypes array this time. Make sure the
+ // flag is reset so we won't add the type to the description.
+ handlerInfo.disambiguateDescription = false;
+ this._visibleDescriptions.set(handlerInfo.description, handlerInfo);
+ } else {
+ // There is at least another type with this description. Make sure we
+ // add the type to the description on both HandlerInfoWrapper objects.
+ handlerInfo.disambiguateDescription = true;
+ otherHandlerInfo.disambiguateDescription = true;
+ }
+ }
+ },
+
+ _rebuildView() {
+ // Clear the list of entries.
+ let tbody = this._handlerTbody;
+ while (tbody.hasChildNodes()) {
+ // Rows kept alive by the _handlerRows map.
+ tbody.removeChild(tbody.lastChild);
+ }
+
+ let sort = this._handlerSort;
+ for (let header of this._handlerSortHeaders) {
+ let icon = header.querySelector("img");
+ if (sort.type === header.getAttribute("sort-type")) {
+ icon.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/new/nav-down-sm.svg"
+ );
+ if (sort.descending) {
+ /* Rotates the src image to point up. */
+ icon.setAttribute("descending", "");
+ header.setAttribute("aria-sort", "descending");
+ } else {
+ icon.removeAttribute("descending");
+ header.setAttribute("aria-sort", "ascending");
+ }
+ } else {
+ icon.removeAttribute("src");
+ header.setAttribute("aria-sort", "none");
+ }
+ }
+
+ let visibleTypes = this._visibleTypes;
+
+ // If the user is filtering the list, then only show matching types.
+ if (this._filter.value) {
+ visibleTypes = visibleTypes.filter(this._matchesFilter, this);
+ }
+
+ for (let handlerInfo of visibleTypes) {
+ let row = this._handlerRows.get(handlerInfo);
+ if (row) {
+ tbody.appendChild(row.node);
+ } else {
+ row = new HandlerRow(handlerInfo, this.onDelete.bind(this));
+ row.constructNodeAndAppend(tbody, this._handlerMenuId);
+ this._handlerMenuId++;
+ this._handlerRows.set(handlerInfo, row);
+ }
+ }
+ },
+
+ _matchesFilter(aType) {
+ var filterValue = this._filter.value.toLowerCase();
+ return (
+ aType.typeDescription.toLowerCase().includes(filterValue) ||
+ aType.actionDescription.toLowerCase().includes(filterValue)
+ );
+ },
+
+ /**
+ * Get the details for the type represented by the given handler info
+ * object.
+ *
+ * @param aHandlerInfo {nsIHandlerInfo} the type to get the extensions for.
+ * @returns {string} the extensions for the type
+ */
+ _typeDetails(aHandlerInfo) {
+ let exts = [];
+ if (aHandlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ for (let extName of aHandlerInfo.wrappedHandlerInfo.getFileExtensions()) {
+ let ext = "." + extName;
+ if (!exts.includes(ext)) {
+ exts.push(ext);
+ }
+ }
+ }
+ exts.sort();
+ exts = exts.join(", ");
+ if (this._visibleDescriptions.has(aHandlerInfo.description)) {
+ if (exts) {
+ return this._prefsBundle.getFormattedString(
+ "typeDetailsWithTypeAndExt",
+ [aHandlerInfo.type, exts]
+ );
+ }
+ return this._prefsBundle.getFormattedString("typeDetailsWithTypeOrExt", [
+ aHandlerInfo.type,
+ ]);
+ }
+ if (exts) {
+ return this._prefsBundle.getFormattedString("typeDetailsWithTypeOrExt", [
+ exts,
+ ]);
+ }
+ return exts;
+ },
+
+ /**
+ * Whether or not the given handler app is valid.
+ *
+ * @param aHandlerApp {nsIHandlerApp} the handler app in question
+ * @returns {boolean} whether or not it's valid
+ */
+ isValidHandlerApp(aHandlerApp) {
+ if (!aHandlerApp) {
+ return false;
+ }
+
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._isValidHandlerExecutable(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return aHandlerApp.uriTemplate;
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) {
+ return aHandlerApp.uri;
+ }
+
+ return false;
+ },
+
+ _isValidHandlerExecutable(aExecutable) {
+ let isExecutable =
+ aExecutable && aExecutable.exists() && aExecutable.isExecutable();
+ // XXXben - we need to compare this with the running instance executable
+ // just don't know how to do that via script...
+ // XXXmano TBD: can probably add this to nsIShellService
+ if (AppConstants.platform == "win") {
+ return (
+ isExecutable &&
+ aExecutable.leafName != AppConstants.MOZ_APP_NAME + ".exe"
+ );
+ }
+
+ if (AppConstants.platform == "macosx") {
+ return (
+ isExecutable && aExecutable.leafName != AppConstants.MOZ_MACBUNDLE_NAME
+ );
+ }
+
+ return (
+ isExecutable && aExecutable.leafName != AppConstants.MOZ_APP_NAME + "-bin"
+ );
+ },
+
+ // -------------------
+ // Sorting & Filtering
+
+ /**
+ * Sort the list when the user clicks on a column header. If sortType is
+ * different than the last sort, the sort direction is toggled. Otherwise, the
+ * sort is changed to the new sortType with ascending direction.
+ *
+ * @param {string} sortType - The sort type associated with the column header.
+ */
+ sort(sortType) {
+ let sort = this._handlerSort;
+ if (sort.type === sortType) {
+ sort.descending = !sort.descending;
+ } else {
+ sort.type = sortType;
+ sort.descending = false;
+ }
+ this._sortVisibleTypes();
+ this._rebuildView();
+ },
+
+ /**
+ * Sort the list of visible types by the current sort column/direction.
+ */
+ _sortVisibleTypes() {
+ function sortByType(a, b) {
+ return a.typeDescription
+ .toLowerCase()
+ .localeCompare(b.typeDescription.toLowerCase());
+ }
+
+ function sortByAction(a, b) {
+ return a.actionDescription
+ .toLowerCase()
+ .localeCompare(b.actionDescription.toLowerCase());
+ }
+
+ let sort = this._handlerSort;
+ if (sort.type === "action") {
+ this._visibleTypes.sort(sortByAction);
+ } else {
+ this._visibleTypes.sort(sortByType);
+ }
+ if (sort.descending) {
+ this._visibleTypes.reverse();
+ }
+ },
+
+ focusFilterBox() {
+ this._filter.focus();
+ this._filter.select();
+ },
+
+ onDelete(handlerRow) {
+ let handlerInfo = handlerRow.handlerInfoWrapper;
+ let index = this._visibleTypes.indexOf(handlerInfo);
+ if (index != -1) {
+ this._visibleTypes.splice(index, 1);
+ }
+
+ let tbody = this._handlerTbody;
+ if (handlerRow.node.parentNode === tbody) {
+ tbody.removeChild(handlerRow.node);
+ }
+
+ this._handledTypes.delete(handlerInfo.type);
+ this._handlerRows.delete(handlerInfo);
+
+ handlerInfo.remove();
+ },
+
+ _getIconURLForHandlerApp(aHandlerApp) {
+ if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+ return this._getIconURLForFile(aHandlerApp.executable);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebHandlerApp) {
+ return this._getIconURLForWebApp(aHandlerApp.uriTemplate);
+ }
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo) {
+ return this._getIconURLForWebApp(aHandlerApp.uri);
+ }
+
+ // We know nothing about other kinds of handler apps.
+ return "";
+ },
+
+ _getIconURLForFile(aFile) {
+ let urlSpec = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getURLSpecFromActualFile(aFile);
+
+ return "moz-icon://" + urlSpec + "?size=16";
+ },
+
+ _getIconURLForWebApp(aWebAppURITemplate) {
+ var uri = Services.io.newURI(aWebAppURITemplate);
+
+ // Unfortunately we can't use the favicon service to get the favicon,
+ // because the service looks in the annotations table for a record with
+ // the exact URL we give it, and users won't have such records for URLs
+ // they don't visit, and users won't visit the web app's URL template,
+ // they'll only visit URLs derived from that template (i.e. with %s
+ // in the template replaced by the URL of the content being handled).
+
+ if (/^https?/.test(uri.scheme)) {
+ return uri.prePath + "/favicon.ico";
+ }
+
+ return /^https?/.test(uri.scheme) ? uri.resolve("/favicon.ico") : "";
+ },
+
+ destroy() {
+ window.removeEventListener("unload", this);
+
+ Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
+ Services.prefs.removeObserver("mailnews.tags.", this);
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+
+ // nsIObserver
+
+ async observe(subject, topic, data) {
+ if (topic == AUTO_UPDATE_CHANGED_TOPIC) {
+ if (data != "true" && data != "false") {
+ throw new Error(`Invalid value for app.update.auto ${data}`);
+ }
+ document.getElementById("updateRadioGroup").value = data;
+ } else if (topic == "nsPref:changed" && data.startsWith("mailnews.tags.")) {
+ let selIndex = this.mTagListBox.selectedIndex;
+ this.mTagListBox.replaceChildren();
+ this.buildTagList();
+ let numItemsInListBox = this.mTagListBox.getRowCount();
+ this.mTagListBox.selectedIndex =
+ selIndex < numItemsInListBox ? selIndex : numItemsInListBox - 1;
+ if (data.endsWith(".color") && Services.prefs.prefHasUserValue(data)) {
+ let key = data.replace(/^mailnews\.tags\./, "").replace(/\.color$/, "");
+ let color = Services.prefs.getCharPref(`mailnews.tags.${key}.color`);
+ // Add to style sheet. We simply add the new color, the rule is added
+ // at the end and will overrule the previous rule.
+ TagUtils.addTagToAllDocumentSheets(key, color);
+ }
+ }
+ },
+
+ // EventListener
+
+ handleEvent(aEvent) {
+ if (aEvent.type == "unload") {
+ this.destroy();
+ if (AppConstants.MOZ_UPDATER) {
+ onUnload();
+ }
+ }
+ },
+};
+
+function getDisplayNameForFile(aFile) {
+ if (AppConstants.platform == "win") {
+ if (aFile instanceof Ci.nsILocalFileWin) {
+ try {
+ return aFile.getVersionInfoField("FileDescription");
+ } catch (ex) {
+ // fall through to the file name
+ }
+ }
+ } else if (AppConstants.platform == "macosx") {
+ if (aFile instanceof Ci.nsILocalFileMac) {
+ try {
+ return aFile.bundleDisplayName;
+ } catch (ex) {
+ // fall through to the file name
+ }
+ }
+ }
+
+ return aFile.leafName;
+}
+
+function getLocalHandlerApp(aFile) {
+ var localHandlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ localHandlerApp.name = getDisplayNameForFile(aFile);
+ localHandlerApp.executable = aFile;
+
+ return localHandlerApp;
+}
+
+// eslint-disable-next-line no-undef
+let gHandlerRowFragment = MozXULElement.parseXULToFragment(`
+ <html:tr>
+ <html:td class="typeCell">
+ <html:div class="typeLabel">
+ <html:img class="typeIcon" alt=""/>
+ <label class="typeDescription" crop="end"/>
+ </html:div>
+ </html:td>
+ <html:td class="actionCell">
+ <menulist class="actionsMenu" crop="end" selectedIndex="1">
+ <menupopup/>
+ </menulist>
+ </html:td>
+ </html:tr>
+`);
+
+/**
+ * This is associated to rows in the handlers table.
+ */
+class HandlerRow {
+ constructor(handlerInfoWrapper, onDeleteCallback) {
+ this.handlerInfoWrapper = handlerInfoWrapper;
+ this.previousSelectedItem = null;
+ this.deleteCallback = onDeleteCallback;
+ }
+
+ constructNodeAndAppend(tbody, id) {
+ tbody.appendChild(document.importNode(gHandlerRowFragment, true));
+ this.node = tbody.lastChild;
+
+ this.menu = this.node.querySelector(".actionsMenu");
+ id = `action-menu-${id}`;
+ this.menu.setAttribute("id", id);
+ this.menu.addEventListener("command", event =>
+ this.onSelectAction(event.originalTarget)
+ );
+
+ let typeDescription = this.node.querySelector(".typeDescription");
+ typeDescription.setAttribute(
+ "value",
+ this.handlerInfoWrapper.typeDescription
+ );
+ // NOTE: Control only works for a XUL <label>. Using a HTML <label> and the
+ // corresponding "for" attribute would not currently work with the XUL
+ // <menulist> because a XUL <menulist> is technically not a labelable
+ // element, as required for the html:label "for" attribute.
+ typeDescription.setAttribute("control", id);
+ // Spoof the HTML label "for" attribute focus behaviour on the whole cell.
+ this.node
+ .querySelector(".typeCell")
+ .addEventListener("click", () => this.menu.focus());
+
+ this.node
+ .querySelector(".typeIcon")
+ .setAttribute("src", this.handlerInfoWrapper.smallIcon);
+
+ this.rebuildActionsMenu();
+ }
+
+ rebuildActionsMenu() {
+ let menu = this.menu;
+ let menuPopup = menu.menupopup;
+ let handlerInfo = this.handlerInfoWrapper;
+
+ // Clear out existing items.
+ while (menuPopup.hasChildNodes()) {
+ menuPopup.removeChild(menuPopup.lastChild);
+ }
+
+ let internalMenuItem;
+ // Add the "Preview in Thunderbird" option for optional internal handlers.
+ if (handlerInfo instanceof InternalHandlerInfoWrapper) {
+ internalMenuItem = document.createXULElement("menuitem");
+ internalMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.handleInternally
+ );
+ let label = gGeneralPane._prefsBundle.getFormattedString("previewInApp", [
+ gGeneralPane._brandShortName,
+ ]);
+ internalMenuItem.setAttribute("label", label);
+ internalMenuItem.setAttribute("tooltiptext", label);
+ internalMenuItem.setAttribute(
+ "image",
+ "chrome://messenger/skin/preferences/alwaysAsk.png"
+ );
+ menuPopup.appendChild(internalMenuItem);
+ }
+
+ let askMenuItem = document.createXULElement("menuitem");
+ askMenuItem.setAttribute("alwaysAsk", "true");
+ {
+ let label = gGeneralPane._prefsBundle.getString("alwaysAsk");
+ askMenuItem.setAttribute("label", label);
+ askMenuItem.setAttribute("tooltiptext", label);
+ askMenuItem.setAttribute(
+ "image",
+ "chrome://messenger/skin/preferences/alwaysAsk.png"
+ );
+ menuPopup.appendChild(askMenuItem);
+ }
+
+ // Create a menu item for saving to disk.
+ // Note: this option isn't available to protocol types, since we don't know
+ // what it means to save a URL having a certain scheme to disk.
+ let saveMenuItem;
+ if (handlerInfo.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ saveMenuItem = document.createXULElement("menuitem");
+ saveMenuItem.setAttribute("action", Ci.nsIHandlerInfo.saveToDisk);
+ let label = gGeneralPane._prefsBundle.getString("saveFile");
+ saveMenuItem.setAttribute("label", label);
+ saveMenuItem.setAttribute("tooltiptext", label);
+ saveMenuItem.setAttribute(
+ "image",
+ "chrome://messenger/skin/preferences/saveFile.png"
+ );
+ menuPopup.appendChild(saveMenuItem);
+ }
+
+ // Add a separator to distinguish these items from the helper app items
+ // that follow them.
+ let menuItem = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+
+ // Create a menu item for the OS default application, if any.
+ let defaultMenuItem;
+ if (handlerInfo.hasDefaultHandler) {
+ defaultMenuItem = document.createXULElement("menuitem");
+ defaultMenuItem.setAttribute(
+ "action",
+ Ci.nsIHandlerInfo.useSystemDefault
+ );
+ let label = gGeneralPane._prefsBundle.getFormattedString("useDefault", [
+ handlerInfo.defaultDescription,
+ ]);
+ defaultMenuItem.setAttribute("label", label);
+ defaultMenuItem.setAttribute(
+ "tooltiptext",
+ handlerInfo.defaultDescription
+ );
+ defaultMenuItem.setAttribute(
+ "image",
+ handlerInfo.iconURLForSystemDefault
+ );
+
+ menuPopup.appendChild(defaultMenuItem);
+ }
+
+ // Create menu items for possible handlers.
+ let preferredApp = handlerInfo.preferredApplicationHandler;
+ let possibleAppMenuItems = [];
+ for (let possibleApp of handlerInfo.possibleApplicationHandlers.enumerate()) {
+ if (!gGeneralPane.isValidHandlerApp(possibleApp)) {
+ continue;
+ }
+
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.setAttribute("action", Ci.nsIHandlerInfo.useHelperApp);
+ let label;
+ if (possibleApp instanceof Ci.nsILocalHandlerApp) {
+ label = getDisplayNameForFile(possibleApp.executable);
+ } else {
+ label = possibleApp.name;
+ }
+ label = gGeneralPane._prefsBundle.getFormattedString("useApp", [label]);
+ menuItem.setAttribute("label", label);
+ menuItem.setAttribute("tooltiptext", label);
+ menuItem.setAttribute(
+ "image",
+ gGeneralPane._getIconURLForHandlerApp(possibleApp)
+ );
+
+ // Attach the handler app object to the menu item so we can use it
+ // to make changes to the datastore when the user selects the item.
+ menuItem.handlerApp = possibleApp;
+
+ menuPopup.appendChild(menuItem);
+ possibleAppMenuItems.push(menuItem);
+ }
+
+ // Create a menu item for selecting a local application.
+ let createItem = true;
+ if (AppConstants.platform == "win") {
+ // On Windows, selecting an application to open another application
+ // would be meaningless so we special case executables.
+ let executableType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromExtension("exe");
+ if (handlerInfo.type == executableType) {
+ createItem = false;
+ }
+ }
+
+ if (createItem) {
+ let menuItem = document.createXULElement("menuitem");
+ menuItem.addEventListener("command", this.chooseApp.bind(this));
+ let label = gGeneralPane._prefsBundle.getString("useOtherApp");
+ menuItem.setAttribute("label", label);
+ menuItem.setAttribute("tooltiptext", label);
+ menuPopup.appendChild(menuItem);
+ }
+
+ // Create a menu item for managing applications.
+ if (possibleAppMenuItems.length) {
+ let menuItem = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+ menuItem = document.createXULElement("menuitem");
+ menuItem.addEventListener("command", this.manageApp.bind(this));
+ menuItem.setAttribute(
+ "label",
+ gGeneralPane._prefsBundle.getString("manageApp")
+ );
+ menuPopup.appendChild(menuItem);
+ }
+
+ menuItem = document.createXULElement("menuseparator");
+ menuPopup.appendChild(menuItem);
+ menuItem = document.createXULElement("menuitem");
+ menuItem.addEventListener("command", this.confirmDelete.bind(this));
+ menuItem.setAttribute(
+ "label",
+ gGeneralPane._prefsBundle.getString("delete")
+ );
+ menuPopup.appendChild(menuItem);
+
+ // Select the item corresponding to the preferred action. If the always
+ // ask flag is set, it overrides the preferred action. Otherwise we pick
+ // the item identified by the preferred action (when the preferred action
+ // is to use a helper app, we have to pick the specific helper app item).
+ if (handlerInfo.alwaysAskBeforeHandling) {
+ menu.selectedItem = askMenuItem;
+ } else {
+ switch (handlerInfo.preferredAction) {
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (internalMenuItem) {
+ menu.selectedItem = internalMenuItem;
+ } else {
+ console.error("No menu item defined to set!");
+ }
+ break;
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ menu.selectedItem = defaultMenuItem;
+ break;
+ case Ci.nsIHandlerInfo.useHelperApp:
+ if (preferredApp) {
+ menu.selectedItem = possibleAppMenuItems.filter(v =>
+ v.handlerApp.equals(preferredApp)
+ )[0];
+ }
+ break;
+ case Ci.nsIHandlerInfo.saveToDisk:
+ menu.selectedItem = saveMenuItem;
+ break;
+ }
+ }
+ // menu.selectedItem may be null if the preferredAction is
+ // useSystemDefault, but handlerInfo.hasDefaultHandler returns false.
+ // For now, we'll just use the askMenuItem to avoid ugly exceptions.
+ this.previousSelectedItem = this.menu.selectedItem || askMenuItem;
+ }
+
+ manageApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerInfo = this.handlerInfoWrapper;
+
+ let onComplete = () => {
+ // Rebuild the actions menu so that we revert to the previous selection,
+ // or "Always ask" if the previous default application has been removed.
+ this.rebuildActionsMenu();
+ };
+
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/applicationManager.xhtml",
+ { features: "resizable=no", closingCallback: onComplete },
+ handlerInfo
+ );
+ }
+
+ chooseApp(aEvent) {
+ // Don't let the normal "on select action" handler get this event,
+ // as we handle it specially ourselves.
+ aEvent.stopPropagation();
+
+ var handlerApp;
+ let onSelectionDone = function () {
+ // Rebuild the actions menu whether the user picked an app or canceled.
+ // If they picked an app, we want to add the app to the menu and select it.
+ // If they canceled, we want to go back to their previous selection.
+ this.rebuildActionsMenu();
+
+ // If the user picked a new app from the menu, select it.
+ if (handlerApp) {
+ let menuItems = this.menu.menupopup.children;
+ for (let i = 0; i < menuItems.length; i++) {
+ let menuItem = menuItems[i];
+ if (menuItem.handlerApp && menuItem.handlerApp.equals(handlerApp)) {
+ this.menu.selectedIndex = i;
+ this.onSelectAction(menuItem);
+ break;
+ }
+ }
+ }
+ }.bind(this);
+
+ if (AppConstants.platform == "win") {
+ let params = {};
+ let handlerInfo = this.handlerInfoWrapper;
+
+ params.mimeInfo = handlerInfo.wrappedHandlerInfo;
+
+ params.title = gGeneralPane._prefsBundle.getString("fpTitleChooseApp");
+ params.description = handlerInfo.description;
+ params.filename = null;
+ params.handlerApp = null;
+
+ let onAppSelected = () => {
+ if (gGeneralPane.isValidHandlerApp(params.handlerApp)) {
+ handlerApp = params.handlerApp;
+
+ // Add the app to the type's list of possible handlers.
+ handlerInfo.addPossibleApplicationHandler(handlerApp);
+ }
+ onSelectionDone();
+ };
+
+ gSubDialog.open(
+ "chrome://global/content/appPicker.xhtml",
+ { features: "resizable=no", closingCallback: onAppSelected },
+ params
+ );
+ } else {
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let winTitle = gGeneralPane._prefsBundle.getString("fpTitleChooseApp");
+ fp.init(window, winTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+
+ // Prompt the user to pick an app. If they pick one, and it's a valid
+ // selection, then add it to the list of possible handlers.
+
+ fp.open(rv => {
+ if (
+ rv == Ci.nsIFilePicker.returnOK &&
+ fp.file &&
+ gGeneralPane._isValidHandlerExecutable(fp.file)
+ ) {
+ handlerApp = Cc[
+ "@mozilla.org/uriloader/local-handler-app;1"
+ ].createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.name = getDisplayNameForFile(fp.file);
+ handlerApp.executable = fp.file;
+
+ // Add the app to the type's list of possible handlers.
+ let handlerInfo = this.handlerInfoWrapper;
+ handlerInfo.addPossibleApplicationHandler(handlerApp);
+ }
+ onSelectionDone();
+ });
+ }
+ }
+
+ confirmDelete(aEvent) {
+ aEvent.stopPropagation();
+ if (
+ Services.prompt.confirm(
+ null,
+ gGeneralPane._prefsBundle.getString("confirmDeleteTitle"),
+ gGeneralPane._prefsBundle.getString("confirmDeleteText")
+ )
+ ) {
+ // Deletes self.
+ this.deleteCallback(this);
+ } else {
+ // They hit cancel, so return them to the previously selected item.
+ this.menu.selectedItem = this.previousSelectedItem;
+ }
+ }
+
+ onSelectAction(aActionItem) {
+ this.previousSelectedItem = aActionItem;
+ this._storeAction(aActionItem);
+ }
+
+ _storeAction(aActionItem) {
+ var handlerInfo = this.handlerInfoWrapper;
+
+ if (aActionItem.hasAttribute("alwaysAsk")) {
+ handlerInfo.alwaysAskBeforeHandling = true;
+ } else if (aActionItem.hasAttribute("action")) {
+ let action = parseInt(aActionItem.getAttribute("action"));
+
+ // Set the preferred application handler.
+ // We leave the existing preferred app in the list when we set
+ // the preferred action to something other than useHelperApp so that
+ // legacy datastores that don't have the preferred app in the list
+ // of possible apps still include the preferred app in the list of apps
+ // the user can choose to handle the type.
+ if (action == Ci.nsIHandlerInfo.useHelperApp) {
+ handlerInfo.preferredApplicationHandler = aActionItem.handlerApp;
+ }
+
+ // Set the "always ask" flag.
+ handlerInfo.alwaysAskBeforeHandling = false;
+
+ // Set the preferred action.
+ handlerInfo.preferredAction = action;
+ }
+
+ handlerInfo.store();
+ }
+}
+
+/**
+ * This object wraps nsIHandlerInfo with some additional functionality
+ * the Applications prefpane needs to display and allow modification of
+ * the list of handled types.
+ *
+ * We create an instance of this wrapper for each entry we might display
+ * in the prefpane, and we compose the instances from various sources,
+ * including the handler service.
+ *
+ * We don't implement all the original nsIHandlerInfo functionality,
+ * just the stuff that the prefpane needs.
+ */
+class HandlerInfoWrapper {
+ constructor(type, handlerInfo) {
+ this.type = type;
+ this.wrappedHandlerInfo = handlerInfo;
+ this.disambiguateDescription = false;
+ }
+
+ get description() {
+ if (this.wrappedHandlerInfo.description) {
+ return this.wrappedHandlerInfo.description;
+ }
+
+ if (this.primaryExtension) {
+ var extension = this.primaryExtension.toUpperCase();
+ return document
+ .getElementById("bundlePreferences")
+ .getFormattedString("fileEnding", [extension]);
+ }
+ return this.type;
+ }
+
+ /**
+ * Describe, in a human-readable fashion, the type represented by the given
+ * handler info object. Normally this is just the description, but if more
+ * than one object presents the same description, "disambiguateDescription"
+ * is set and we annotate the duplicate descriptions with the type itself
+ * to help users distinguish between those types.
+ */
+ get typeDescription() {
+ if (this.disambiguateDescription) {
+ return gGeneralPane._prefsBundle.getFormattedString(
+ "typeDetailsWithTypeAndExt",
+ [this.description, this.type]
+ );
+ }
+
+ return this.description;
+ }
+
+ /**
+ * Describe, in a human-readable fashion, the preferred action to take on
+ * the type represented by the given handler info object.
+ */
+ get actionDescription() {
+ // alwaysAskBeforeHandling overrides the preferred action, so if that flag
+ // is set, then describe that behavior instead. For most types, this is
+ // the "alwaysAsk" string, but for the feed type we show something special.
+ if (this.alwaysAskBeforeHandling) {
+ return gGeneralPane._prefsBundle.getString("alwaysAsk");
+ }
+
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ return gGeneralPane._prefsBundle.getString("saveFile");
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ var preferredApp = this.preferredApplicationHandler;
+ var name;
+ if (preferredApp instanceof Ci.nsILocalHandlerApp) {
+ name = getDisplayNameForFile(preferredApp.executable);
+ } else {
+ name = preferredApp.name;
+ }
+ return gGeneralPane._prefsBundle.getFormattedString("useApp", [name]);
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (this instanceof InternalHandlerInfoWrapper) {
+ return gGeneralPane._prefsBundle.getFormattedString("previewInApp", [
+ gGeneralPane._brandShortName,
+ ]);
+ }
+
+ // For other types, handleInternally looks like either useHelperApp
+ // or useSystemDefault depending on whether or not there's a preferred
+ // handler app.
+ if (gGeneralPane.isValidHandlerApp(this.preferredApplicationHandler)) {
+ return this.preferredApplicationHandler.name;
+ }
+
+ return this.defaultDescription;
+
+ // XXX Why don't we say the app will handle the type internally?
+ // Is it because the app can't actually do that? But if that's true,
+ // then why would a preferredAction ever get set to this value
+ // in the first place?
+
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return gGeneralPane._prefsBundle.getFormattedString("useDefault", [
+ this.defaultDescription,
+ ]);
+
+ default:
+ throw new Error(`Unexpected preferredAction: ${this.preferredAction}`);
+ }
+ }
+
+ get actionIconClass() {
+ if (this.alwaysAskBeforeHandling) {
+ return "ask";
+ }
+
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.saveToDisk:
+ return "save";
+
+ case Ci.nsIHandlerInfo.handleInternally:
+ if (this instanceof InternalHandlerInfoWrapper) {
+ return "ask";
+ }
+ }
+
+ return "";
+ }
+
+ get actionIcon() {
+ switch (this.preferredAction) {
+ case Ci.nsIHandlerInfo.useSystemDefault:
+ return this.iconURLForSystemDefault;
+
+ case Ci.nsIHandlerInfo.useHelperApp:
+ let preferredApp = this.preferredApplicationHandler;
+ if (gGeneralPane.isValidHandlerApp(preferredApp)) {
+ return gGeneralPane._getIconURLForHandlerApp(preferredApp);
+ }
+ // This should never happen, but if preferredAction is set to some weird
+ // value, then fall back to the generic application icon.
+
+ // Explicit fall-through
+ default:
+ return ICON_URL_APP;
+ }
+ }
+
+ get iconURLForSystemDefault() {
+ // Handler info objects for MIME types on some OSes implement a property bag
+ // interface from which we can get an icon for the default app, so if we're
+ // dealing with a MIME type on one of those OSes, then try to get the icon.
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo instanceof Ci.nsIPropertyBag
+ ) {
+ try {
+ let url = this.wrappedHandlerInfo.getProperty(
+ "defaultApplicationIconURL"
+ );
+ if (url) {
+ return url + "?size=16";
+ }
+ } catch (ex) {}
+ }
+
+ // If this isn't a MIME type object on an OS that supports retrieving
+ // the icon, or if we couldn't retrieve the icon for some other reason,
+ // then use a generic icon.
+ return ICON_URL_APP;
+ }
+
+ get preferredApplicationHandler() {
+ return this.wrappedHandlerInfo.preferredApplicationHandler;
+ }
+
+ set preferredApplicationHandler(aNewValue) {
+ this.wrappedHandlerInfo.preferredApplicationHandler = aNewValue;
+
+ // Make sure the preferred handler is in the set of possible handlers.
+ if (aNewValue) {
+ this.addPossibleApplicationHandler(aNewValue);
+ }
+ }
+
+ get possibleApplicationHandlers() {
+ return this.wrappedHandlerInfo.possibleApplicationHandlers;
+ }
+
+ addPossibleApplicationHandler(aNewHandler) {
+ for (let possibleApp of this.possibleApplicationHandlers.enumerate()) {
+ if (possibleApp.equals(aNewHandler)) {
+ return;
+ }
+ }
+ this.possibleApplicationHandlers.appendElement(aNewHandler);
+ }
+
+ removePossibleApplicationHandler(aHandler) {
+ var defaultApp = this.preferredApplicationHandler;
+ if (defaultApp && aHandler.equals(defaultApp)) {
+ // If the app we remove was the default app, we must make sure
+ // it won't be used anymore
+ this.alwaysAskBeforeHandling = true;
+ this.preferredApplicationHandler = null;
+ }
+
+ var handlers = this.possibleApplicationHandlers;
+ for (var i = 0; i < handlers.length; ++i) {
+ var handler = handlers.queryElementAt(i, Ci.nsIHandlerApp);
+ if (handler.equals(aHandler)) {
+ handlers.removeElementAt(i);
+ break;
+ }
+ }
+ }
+
+ get hasDefaultHandler() {
+ return this.wrappedHandlerInfo.hasDefaultHandler;
+ }
+
+ get defaultDescription() {
+ return this.wrappedHandlerInfo.defaultDescription;
+ }
+
+ // What to do with content of this type.
+ get preferredAction() {
+ // If the action is to use a helper app, but we don't have a preferred
+ // handler app, then switch to using the system default, if any; otherwise
+ // fall back to saving to disk, which is the default action in nsMIMEInfo.
+ // Note: "save to disk" is an invalid value for protocol info objects,
+ // but the alwaysAskBeforeHandling getter will detect that situation
+ // and always return true in that case to override this invalid value.
+ if (
+ this.wrappedHandlerInfo.preferredAction ==
+ Ci.nsIHandlerInfo.useHelperApp &&
+ !gGeneralPane.isValidHandlerApp(this.preferredApplicationHandler)
+ ) {
+ if (this.wrappedHandlerInfo.hasDefaultHandler) {
+ return Ci.nsIHandlerInfo.useSystemDefault;
+ }
+ return Ci.nsIHandlerInfo.saveToDisk;
+ }
+
+ return this.wrappedHandlerInfo.preferredAction;
+ }
+
+ set preferredAction(aNewValue) {
+ this.wrappedHandlerInfo.preferredAction = aNewValue;
+ }
+
+ get alwaysAskBeforeHandling() {
+ // If this is a protocol type and the preferred action is "save to disk",
+ // which is invalid for such types, then return true here to override that
+ // action. This could happen when the preferred action is to use a helper
+ // app, but the preferredApplicationHandler is invalid, and there isn't
+ // a default handler, so the preferredAction getter returns save to disk
+ // instead.
+ if (
+ !(this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) &&
+ this.preferredAction == Ci.nsIHandlerInfo.saveToDisk
+ ) {
+ return true;
+ }
+
+ return this.wrappedHandlerInfo.alwaysAskBeforeHandling;
+ }
+
+ set alwaysAskBeforeHandling(aNewValue) {
+ this.wrappedHandlerInfo.alwaysAskBeforeHandling = aNewValue;
+ }
+
+ // The primary file extension associated with this type, if any.
+ get primaryExtension() {
+ try {
+ if (
+ this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo &&
+ this.wrappedHandlerInfo.primaryExtension
+ ) {
+ return this.wrappedHandlerInfo.primaryExtension;
+ }
+ } catch (ex) {}
+
+ return null;
+ }
+
+ // -------
+ // Storage
+
+ store() {
+ gHandlerService.store(this.wrappedHandlerInfo);
+ }
+
+ remove() {
+ gHandlerService.remove(this.wrappedHandlerInfo);
+ }
+
+ // -----
+ // Icons
+
+ get smallIcon() {
+ return this._getIcon(16);
+ }
+
+ get largeIcon() {
+ return this._getIcon(32);
+ }
+
+ _getIcon(aSize) {
+ if (this.primaryExtension) {
+ return "moz-icon://goat." + this.primaryExtension + "?size=" + aSize;
+ }
+
+ if (this.wrappedHandlerInfo instanceof Ci.nsIMIMEInfo) {
+ return "moz-icon://goat?size=" + aSize + "&contentType=" + this.type;
+ }
+
+ // FIXME: consider returning some generic icon when we can't get a URL for
+ // one (for example in the case of protocol schemes). Filed as bug 395141.
+ return null;
+ }
+}
+
+/**
+ * InternalHandlerInfoWrapper provides a basic mechanism to create an internal
+ * mime type handler that can be enabled/disabled in the applications preference
+ * menu.
+ */
+class InternalHandlerInfoWrapper extends HandlerInfoWrapper {
+ constructor(mimeType) {
+ super(mimeType, gMIMEService.getFromTypeAndExtension(mimeType, null));
+ }
+
+ // Override store so we so we can notify any code listening for registration
+ // or unregistration of this handler.
+ store() {
+ super.store();
+ }
+
+ get enabled() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ get description() {
+ return gGeneralPane._prefsBundle.getString(this._appPrefLabel);
+ }
+}
+
+class PDFHandlerInfoWrapper extends InternalHandlerInfoWrapper {
+ constructor() {
+ super(TYPE_PDF);
+ }
+
+ get _appPrefLabel() {
+ return "applications-type-pdf";
+ }
+
+ get enabled() {
+ return !Services.prefs.getBoolPref(PREF_PDFJS_DISABLED);
+ }
+}
+
+function addTagCallback(aName, aColor) {
+ MailServices.tags.addTag(aName, aColor, "");
+
+ // Add to style sheet.
+ let key = MailServices.tags.getKeyForTag(aName);
+ let tagListBox = document.getElementById("tagList");
+ let item = tagListBox.querySelector(`richlistitem[value=${key}]`);
+ tagListBox.ensureElementIsVisible(item);
+ tagListBox.selectItem(item);
+ tagListBox.focus();
+ return true;
+}
+
+Preferences.get("mailnews.start_page.enabled").on(
+ "change",
+ gGeneralPane.updateStartPage
+);
+Preferences.get("font.language.group").on("change", gGeneralPane._rebuildFonts);
+Preferences.get("mailnews.mark_message_read.auto").on(
+ "change",
+ gGeneralPane.updateMarkAsReadOptions
+);
+Preferences.get("mailnews.mark_message_read.delay").on(
+ "change",
+ gGeneralPane.updateMarkAsReadTextbox
+);
+Preferences.get("mail.prompt_purge_threshhold").on(
+ "change",
+ gGeneralPane.updateCompactOptions
+);
+Preferences.get("layers.acceleration.disabled").on(
+ "change",
+ gGeneralPane.updateHardwareAcceleration
+);
+if (AppConstants.platform != "macosx") {
+ Preferences.get("mail.biff.show_alert").on(
+ "change",
+ gGeneralPane.updateShowAlert
+ );
+}
diff --git a/comm/mail/components/preferences/jar.mn b/comm/mail/components/preferences/jar.mn
new file mode 100644
index 0000000000..fc9b56184e
--- /dev/null
+++ b/comm/mail/components/preferences/jar.mn
@@ -0,0 +1,55 @@
+# 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/.
+
+messenger.jar:
+* content/messenger/preferences/preferences.xhtml
+ content/messenger/preferences/preferences.js
+ content/messenger/preferences/preferencesTab.js
+ content/messenger/preferences/general.js
+#if defined(XP_MACOSX) || defined(XP_WIN)
+ content/messenger/preferences/dockoptions.js
+* content/messenger/preferences/dockoptions.xhtml
+#endif
+ content/messenger/preferences/chat.js
+#ifdef NIGHTLY_BUILD
+ content/messenger/preferences/sync.js
+#endif
+ content/messenger/preferences/messagestyle.js
+ content/messenger/preferences/messengerLanguages.js
+ content/messenger/preferences/messengerLanguages.xhtml
+ content/messenger/preferences/colors.js
+* content/messenger/preferences/colors.xhtml
+ content/messenger/preferences/compose.js
+ content/messenger/preferences/extensionControlled.js
+ content/messenger/preferences/privacy.js
+ content/messenger/preferences/receipts.js
+ content/messenger/preferences/receipts.xhtml
+ content/messenger/preferences/connection.js
+ content/messenger/preferences/connection.xhtml
+ content/messenger/preferences/downloads.js
+ content/messenger/preferences/attachmentReminder.js
+ content/messenger/preferences/attachmentReminder.xhtml
+ content/messenger/preferences/applicationManager.xhtml
+ content/messenger/preferences/applicationManager.js
+ content/messenger/preferences/actionsshared.js
+ content/messenger/preferences/findInPage.js
+ content/messenger/preferences/fonts.js
+ content/messenger/preferences/fonts.xhtml
+#ifndef XP_MACOSX
+ content/messenger/preferences/notifications.js
+ content/messenger/preferences/notifications.xhtml
+#endif
+ content/messenger/preferences/offline.js
+ content/messenger/preferences/offline.xhtml
+ content/messenger/preferences/cookies.js
+* content/messenger/preferences/cookies.xhtml
+ content/messenger/preferences/passwordManager.js
+ content/messenger/preferences/passwordManager.xhtml
+ content/messenger/preferences/permissions.js
+ content/messenger/preferences/permissions.xhtml
+#ifdef NIGHTLY_BUILD
+ content/messenger/preferences/syncDialog.js
+ content/messenger/preferences/syncDialog.xhtml
+#endif
+* content/messenger/preferences/tagDialog.xhtml
diff --git a/comm/mail/components/preferences/messagestyle.js b/comm/mail/components/preferences/messagestyle.js
new file mode 100644
index 0000000000..7d10553296
--- /dev/null
+++ b/comm/mail/components/preferences/messagestyle.js
@@ -0,0 +1,259 @@
+/* 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 preferences.js */
+
+var { GenericConvIMPrototype, GenericMessagePrototype } =
+ ChromeUtils.importESModule("resource:///modules/jsProtoHelper.sys.mjs");
+var { getThemeByName, getThemeVariants } = ChromeUtils.importESModule(
+ "resource:///modules/imThemes.sys.mjs"
+);
+
+var { IMServices } = ChromeUtils.importESModule(
+ "resource:///modules/IMServices.sys.mjs"
+);
+
+function Conversation(aName) {
+ this._name = aName;
+ this._observers = [];
+ let now = new Date();
+ this._date =
+ new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 42, 22) *
+ 1000;
+}
+Conversation.prototype = {
+ __proto__: GenericConvIMPrototype,
+ account: {
+ protocol: { name: "Fake Protocol" },
+ alias: "",
+ name: "Fake Account",
+ get statusInfo() {
+ return IMServices.core.globalUserStatus;
+ },
+ },
+};
+
+function Message(aWho, aMessage, aObject, aConversation) {
+ this._init(aWho, aMessage, aObject, aConversation);
+}
+Message.prototype = {
+ __proto__: GenericMessagePrototype,
+ get displayMessage() {
+ return this.originalMessage;
+ },
+};
+
+// Message style tooltips use this.
+function getBrowser() {
+ return document.getElementById("previewbrowser");
+}
+
+var previewObserver = {
+ _loaded: false,
+ load() {
+ let makeDate = function (aDateString) {
+ let array = aDateString.split(":");
+ let now = new Date();
+ return (
+ new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ array[0],
+ array[1],
+ array[2]
+ ) / 1000
+ );
+ };
+ let bundle = document.getElementById("themesBundle");
+ let msg = {};
+ [
+ "nick1",
+ "buddy1",
+ "nick2",
+ "buddy2",
+ "message1",
+ "message2",
+ "message3",
+ ].forEach(function (aText) {
+ msg[aText] = bundle.getString(aText);
+ });
+ let conv = new Conversation(msg.nick2);
+ conv.messages = [
+ new Message(
+ msg.buddy1,
+ msg.message1,
+ {
+ outgoing: true,
+ _alias: msg.nick1,
+ time: makeDate("10:42:22"),
+ },
+ conv
+ ),
+ new Message(
+ msg.buddy1,
+ msg.message2,
+ {
+ outgoing: true,
+ _alias: msg.nick1,
+ time: makeDate("10:42:25"),
+ },
+ conv
+ ),
+ new Message(
+ msg.buddy2,
+ msg.message3,
+ {
+ incoming: true,
+ _alias: msg.nick2,
+ time: makeDate("10:43:01"),
+ },
+ conv
+ ),
+ ];
+ previewObserver.conv = conv;
+
+ let themeName = document.getElementById("messagestyle-themename");
+ previewObserver.browser = document.getElementById("previewbrowser");
+
+ // If the preferences tab is opened straight to the message styles,
+ // loading the preview fails. Pushing this to back of the event queue
+ // prevents that failure.
+ setTimeout(() => {
+ previewObserver.displayTheme(themeName.value);
+ this._loaded = true;
+ });
+ },
+
+ currentThemeChanged() {
+ if (!this._loaded) {
+ return;
+ }
+
+ let currentTheme = document.getElementById("messagestyle-themename").value;
+ if (!currentTheme) {
+ return;
+ }
+
+ this.displayTheme(currentTheme);
+ },
+
+ _ignoreVariantChange: false,
+ currentVariantChanged() {
+ if (!this._loaded || this._ignoreVariantChange) {
+ return;
+ }
+
+ let variant = document.getElementById("themevariant").value;
+ if (!variant) {
+ return;
+ }
+
+ this.theme.variant = variant;
+ this.reloadPreview();
+ },
+
+ displayTheme(aTheme) {
+ try {
+ this.theme = getThemeByName(aTheme);
+ } catch (e) {
+ let previewBoxBrowser = document
+ .getElementById("previewBox")
+ .querySelector("browser");
+ if (previewBoxBrowser) {
+ previewBoxBrowser.hidden = true;
+ }
+ document.getElementById("noPreviewScreen").hidden = false;
+ return;
+ }
+
+ let menulist = document.getElementById("themevariant");
+ if (menulist.menupopup) {
+ menulist.menupopup.remove();
+ }
+ let popup = menulist.appendChild(document.createXULElement("menupopup"));
+ let variants = getThemeVariants(this.theme);
+
+ let defaultVariant = "";
+ if (
+ "DefaultVariant" in this.theme.metadata &&
+ variants.includes(this.theme.metadata.DefaultVariant)
+ ) {
+ defaultVariant = this.theme.metadata.DefaultVariant.replace(/_/g, " ");
+ }
+
+ let defaultText = defaultVariant;
+ if (!defaultText && "DisplayNameForNoVariant" in this.theme.metadata) {
+ defaultText = this.theme.metadata.DisplayNameForNoVariant;
+ }
+ // if the name in the metadata is 'Default', use the localized version
+ if (!defaultText || defaultText.toLowerCase() == "default") {
+ defaultText = document
+ .getElementById("themesBundle")
+ .getString("default");
+ }
+
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", defaultText);
+ menuitem.setAttribute("value", "default");
+ popup.appendChild(menuitem);
+ popup.appendChild(document.createXULElement("menuseparator"));
+
+ variants.sort().forEach(function (aVariantName) {
+ let displayName = aVariantName.replace(/_/g, " ");
+ if (displayName != defaultVariant) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", displayName);
+ menuitem.setAttribute("value", aVariantName);
+ popup.appendChild(menuitem);
+ }
+ });
+ this._ignoreVariantChange = true;
+ if (!this._loaded) {
+ menulist.value = this.theme.variant = menulist.value;
+ } else {
+ menulist.value = this.theme.variant; // (reset to "default")
+ Preferences.userChangedValue(menulist);
+ }
+ this._ignoreVariantChange = false;
+
+ // disable the variant menulist if there's no variant, or only one
+ // which is the default
+ menulist.disabled =
+ variants.length == 0 || (variants.length == 1 && defaultVariant);
+
+ this.reloadPreview();
+ document.getElementById("noPreviewScreen").hidden = true;
+ },
+
+ reloadPreview() {
+ this.browser.init(this.conv);
+ this.browser._theme = this.theme;
+ Services.obs.addObserver(this, "conversation-loaded");
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "conversation-loaded" || aSubject != this.browser) {
+ return;
+ }
+
+ // We want to avoid the convbrowser trying to scroll to the last
+ // added message, as that causes the entire pref pane to jump up
+ // (bug 1179943). Therefore, we override the method convbrowser
+ // uses to determine if it should scroll, as well as its
+ // mirror in the contentWindow (that messagestyle JS can call).
+ this.browser.convScrollEnabled = () => false;
+ this.browser.contentWindow.convScrollEnabled = () => false;
+
+ // Display all queued messages. Use a timeout so that message text
+ // modifiers can be added with observers for this notification.
+ setTimeout(function () {
+ for (let message of previewObserver.conv.messages) {
+ aSubject.appendMessage(message, false);
+ }
+ }, 0);
+
+ Services.obs.removeObserver(this, "conversation-loaded");
+ },
+};
diff --git a/comm/mail/components/preferences/messengerLanguages.js b/comm/mail/components/preferences/messengerLanguages.js
new file mode 100644
index 0000000000..e66f5f7fd0
--- /dev/null
+++ b/comm/mail/components/preferences/messengerLanguages.js
@@ -0,0 +1,632 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This is exported by preferences.js but we can't import that in a subdialog.
+let { getAvailableLocales } = window.top;
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
+});
+
+/* This dialog provides an interface for managing what language the messenger is
+ * displayed in.
+ *
+ * There is a list of "requested" locales and a list of "available" locales. The
+ * requested locales must be installed and enabled. Available locales could be
+ * installed and enabled, or fetched from the AMO language tools API.
+ *
+ * If a langpack is disabled, there is no way to determine what locale it is for and
+ * it will only be listed as available if that locale is also available on AMO and
+ * the user has opted to search for more languages.
+ */
+
+class OrderedListBox {
+ constructor({ richlistbox, upButton, downButton, removeButton, onRemove }) {
+ this.richlistbox = richlistbox;
+ this.upButton = upButton;
+ this.downButton = downButton;
+ this.removeButton = removeButton;
+ this.onRemove = onRemove;
+
+ this.items = [];
+
+ this.richlistbox.addEventListener("select", () => this.setButtonState());
+ this.upButton.addEventListener("command", () => this.moveUp());
+ this.downButton.addEventListener("command", () => this.moveDown());
+ this.removeButton.addEventListener("command", () => this.removeItem());
+ }
+
+ get selectedItem() {
+ return this.items[this.richlistbox.selectedIndex];
+ }
+
+ setButtonState() {
+ let { upButton, downButton, removeButton } = this;
+ let { selectedIndex, itemCount } = this.richlistbox;
+ upButton.disabled = selectedIndex <= 0;
+ downButton.disabled = selectedIndex == itemCount - 1;
+ removeButton.disabled = itemCount <= 1 || !this.selectedItem.canRemove;
+ }
+
+ moveUp() {
+ let { selectedIndex } = this.richlistbox;
+ if (selectedIndex == 0) {
+ return;
+ }
+ let { items } = this;
+ let selectedItem = items[selectedIndex];
+ let prevItem = items[selectedIndex - 1];
+ items[selectedIndex - 1] = items[selectedIndex];
+ items[selectedIndex] = prevItem;
+ let prevEl = document.getElementById(prevItem.id);
+ let selectedEl = document.getElementById(selectedItem.id);
+ this.richlistbox.insertBefore(selectedEl, prevEl);
+ this.richlistbox.ensureElementIsVisible(selectedEl);
+ this.setButtonState();
+ }
+
+ moveDown() {
+ let { selectedIndex } = this.richlistbox;
+ if (selectedIndex == this.items.length - 1) {
+ return;
+ }
+ let { items } = this;
+ let selectedItem = items[selectedIndex];
+ let nextItem = items[selectedIndex + 1];
+ items[selectedIndex + 1] = items[selectedIndex];
+ items[selectedIndex] = nextItem;
+ let nextEl = document.getElementById(nextItem.id);
+ let selectedEl = document.getElementById(selectedItem.id);
+ this.richlistbox.insertBefore(nextEl, selectedEl);
+ this.richlistbox.ensureElementIsVisible(selectedEl);
+ this.setButtonState();
+ }
+
+ removeItem() {
+ let { selectedIndex } = this.richlistbox;
+
+ if (selectedIndex == -1) {
+ return;
+ }
+
+ let [item] = this.items.splice(selectedIndex, 1);
+ this.richlistbox.selectedItem.remove();
+ this.richlistbox.selectedIndex = Math.min(
+ selectedIndex,
+ this.richlistbox.itemCount - 1
+ );
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ this.onRemove(item);
+ }
+
+ setItems(items) {
+ this.items = items;
+ this.populate();
+ this.setButtonState();
+ }
+
+ /**
+ * Add an item to the top of the ordered list.
+ *
+ * @param {object} item The item to insert.
+ */
+ addItem(item) {
+ this.items.unshift(item);
+ this.richlistbox.insertBefore(
+ this.createItem(item),
+ this.richlistbox.firstElementChild
+ );
+ this.richlistbox.selectedIndex = 0;
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ }
+
+ populate() {
+ this.richlistbox.textContent = "";
+
+ let frag = document.createDocumentFragment();
+ for (let item of this.items) {
+ frag.appendChild(this.createItem(item));
+ }
+ this.richlistbox.appendChild(frag);
+
+ this.richlistbox.selectedIndex = 0;
+ this.richlistbox.ensureElementIsVisible(this.richlistbox.selectedItem);
+ }
+
+ createItem({ id, label, value }) {
+ let listitem = document.createXULElement("richlistitem");
+ listitem.id = id;
+ listitem.setAttribute("value", value);
+
+ let labelEl = document.createXULElement("label");
+ labelEl.textContent = label;
+ listitem.appendChild(labelEl);
+
+ return listitem;
+ }
+}
+
+/**
+ * The sorted select list of Locales available for the app.
+ */
+class SortedItemSelectList {
+ constructor({ menulist, button, onSelect, onChange, compareFn }) {
+ /** @type {XULElement} */
+ this.menulist = menulist;
+
+ /** @type {XULElement} */
+ this.popup = menulist.menupopup;
+
+ /** @type {XULElement} */
+ this.button = button;
+
+ /** @type {(a: LocaleDisplayInfo, b: LocaleDisplayInfo) => number} */
+ this.compareFn = compareFn;
+
+ /** @type {Array<LocaleDisplayInfo>} */
+ this.items = [];
+
+ menulist.addEventListener("command", () => {
+ button.disabled = !menulist.selectedItem;
+ if (menulist.selectedItem) {
+ onChange(this.items[menulist.selectedIndex]);
+ }
+ });
+ button.addEventListener("command", () => {
+ if (!menulist.selectedItem) {
+ return;
+ }
+
+ let [item] = this.items.splice(menulist.selectedIndex, 1);
+ menulist.selectedItem.remove();
+ menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+ button.disabled = true;
+ menulist.disabled = menulist.itemCount == 0;
+ menulist.selectedIndex = -1;
+
+ onSelect(item);
+ });
+ }
+
+ /**
+ * @param {Array<LocaleDisplayInfo>} items
+ */
+ setItems(items) {
+ this.items = items.sort(this.compareFn);
+ this.populate();
+ }
+
+ populate() {
+ let { button, items, menulist, popup } = this;
+ popup.textContent = "";
+
+ let frag = document.createDocumentFragment();
+ for (let item of items) {
+ frag.appendChild(this.createItem(item));
+ }
+ popup.appendChild(frag);
+
+ menulist.setAttribute("label", menulist.getAttribute("placeholder"));
+ menulist.disabled = menulist.itemCount == 0;
+ menulist.selectedIndex = -1;
+ button.disabled = true;
+ }
+
+ /**
+ * Add an item to the list sorted by the label.
+ *
+ * @param {object} item The item to insert.
+ */
+ addItem(item) {
+ let { compareFn, items, menulist, popup } = this;
+
+ // Find the index of the item to insert before.
+ let i = items.findIndex(el => compareFn(el, item) >= 0);
+ items.splice(i, 0, item);
+ popup.insertBefore(this.createItem(item), menulist.getItemAtIndex(i));
+ menulist.disabled = menulist.itemCount == 0;
+ }
+
+ createItem({ label, value, className, disabled }) {
+ let item = document.createXULElement("menuitem");
+ item.setAttribute("label", label);
+ if (value) {
+ item.value = value;
+ }
+ if (className) {
+ item.classList.add(className);
+ }
+ if (disabled) {
+ item.setAttribute("disabled", "true");
+ }
+ return item;
+ }
+
+ /**
+ * Disable the inputs and set a data-l10n-id on the menulist. This can be
+ * reverted with `enableWithMessageId()`.
+ */
+ disableWithMessageId(messageId) {
+ this.menulist.setAttribute("data-l10n-id", messageId);
+ this.menulist.setAttribute(
+ "image",
+ "chrome://global/skin/icons/loading.png"
+ );
+ this.menulist.disabled = true;
+ this.button.disabled = true;
+ }
+
+ /**
+ * Enable the inputs and set a data-l10n-id on the menulist. This can be
+ * reverted with `disableWithMessageId()`.
+ */
+ enableWithMessageId(messageId) {
+ this.menulist.setAttribute("data-l10n-id", messageId);
+ this.menulist.removeAttribute("image");
+ this.menulist.disabled = this.menulist.itemCount == 0;
+ this.button.disabled = !this.menulist.selectedItem;
+ }
+}
+
+/**
+ * @typedef LocaleDisplayInfo
+ * @type {object}
+ * @property {string} id - A unique ID.
+ * @property {string} label - The localized display name.
+ * @property {string} value - The BCP 47 locale identifier or the word "search".
+ * @property {boolean} canRemove - Locales that are part of the packaged locales cannot be
+ * removed.
+ * @property {boolean} installed - Whether or not the locale is installed.
+ */
+
+/**
+ * @param {Array<string>} localeCodes - List of BCP 47 locale identifiers.
+ * @returns {Array<LocaleDisplayInfo>}
+ */
+async function getLocaleDisplayInfo(localeCodes) {
+ let availableLocales = new Set(await getAvailableLocales());
+ let packagedLocales = new Set(Services.locale.packagedLocales);
+ let localeNames = Services.intl.getLocaleDisplayNames(
+ undefined,
+ localeCodes,
+ { preferNative: true }
+ );
+ return localeCodes.map((code, i) => {
+ return {
+ id: "locale-" + code,
+ label: localeNames[i],
+ value: code,
+ canRemove: !packagedLocales.has(code),
+ installed: availableLocales.has(code),
+ };
+ });
+}
+
+/**
+ * @param {LocaleDisplayInfo} a
+ * @param {LocaleDisplayInfo} b
+ * @returns {number}
+ */
+function compareItems(a, b) {
+ // Sort by installed.
+ if (a.installed != b.installed) {
+ return a.installed ? -1 : 1;
+
+ // The search label is always last.
+ } else if (a.value == "search") {
+ return 1;
+ } else if (b.value == "search") {
+ return -1;
+
+ // If both items are locales, sort by label.
+ } else if (a.value && b.value) {
+ return a.label.localeCompare(b.label);
+
+ // One of them is a label, put it first.
+ } else if (a.value) {
+ return 1;
+ }
+ return -1;
+}
+
+var gMessengerLanguagesDialog = {
+ /**
+ * The publicly readable list of selected locales. It is only set when the dialog is
+ * accepted, and can be retrieved elsewhere by directly reading the property
+ * on gMessengerLanguagesDialog.
+ *
+ * let { selected } = gMessengerLanguagesDialog;
+ *
+ * @type {null | Array<string>}
+ */
+ selected: null,
+
+ /**
+ * @type {SortedItemSelectList}
+ */
+ _availableLocalesUI: null,
+
+ /**
+ * @type {OrderedListBox}
+ */
+ _selectedLocalesUI: null,
+
+ get downloadEnabled() {
+ // Downloading langpacks isn't always supported, check the pref.
+ return Services.prefs.getBoolPref("intl.multilingual.downloadEnabled");
+ },
+
+ async onLoad() {
+ /**
+ * @typedef {object} Options - Options passed in to configure the subdialog.
+ * @property {Array<string>} [selectedLocalesForRestart] The optional list of
+ * previously selected locales for when a restart is required. This list is
+ * preserved between openings of the dialog.
+ * @property {boolean} search Whether the user opened this from "Search for more
+ * languages" option.
+ */
+
+ /** @type {Options} */
+ let { selectedLocalesForRestart, search } = window.arguments[0];
+
+ // This is a list of available locales that the user selected. It's more
+ // restricted than the Intl notion of `requested` as it only contains
+ // locale codes for which we have matching locales available.
+ // The first time this dialog is opened, populate with appLocalesAsBCP47.
+ let selectedLocales =
+ selectedLocalesForRestart || Services.locale.appLocalesAsBCP47;
+ let selectedLocaleSet = new Set(selectedLocales);
+ let available = await getAvailableLocales();
+ let availableSet = new Set(available);
+
+ // Filter selectedLocales since the user may select a locale when it is
+ // available and then disable it.
+ selectedLocales = selectedLocales.filter(locale =>
+ availableSet.has(locale)
+ );
+ // Nothing in available should be in selectedSet.
+ available = available.filter(locale => !selectedLocaleSet.has(locale));
+
+ await this.initSelectedLocales(selectedLocales);
+ await this.initAvailableLocales(available, search);
+
+ this.initialized = true;
+
+ // Now the component is initialized, it's safe to accept the results.
+ document
+ .getElementById("MessengerLanguagesDialog")
+ .addEventListener("beforeaccept", () => {
+ this.selected = this._selectedLocalesUI.items.map(item => item.value);
+ });
+ },
+
+ /**
+ * @param {string[]} selectedLocales - BCP 47 locale identifiers
+ */
+ async initSelectedLocales(selectedLocales) {
+ this._selectedLocalesUI = new OrderedListBox({
+ richlistbox: document.getElementById("selectedLocales"),
+ upButton: document.getElementById("up"),
+ downButton: document.getElementById("down"),
+ removeButton: document.getElementById("remove"),
+ onRemove: item => this.selectedLocaleRemoved(item),
+ });
+ this._selectedLocalesUI.setItems(
+ await getLocaleDisplayInfo(selectedLocales)
+ );
+ },
+
+ /**
+ * @param {Set<string>} available - The set of available BCP 47 locale identifiers.
+ * @param {boolean} search - Whether the user opened this from "Search for more
+ * languages" option.
+ */
+ async initAvailableLocales(available, search) {
+ this._availableLocalesUI = new SortedItemSelectList({
+ menulist: document.getElementById("availableLocales"),
+ button: document.getElementById("add"),
+ compareFn: compareItems,
+ onSelect: item => this.availableLanguageSelected(item),
+ onChange: item => {
+ this.hideError();
+ if (item.value == "search") {
+ this.loadLocalesFromAMO();
+ }
+ },
+ });
+
+ // Populate the list with the installed locales even if the user is
+ // searching in case the download fails.
+ await this.loadLocalesFromInstalled(available);
+
+ // If the user opened this from the "Search for more languages" option,
+ // search AMO for available locales.
+ if (search) {
+ return this.loadLocalesFromAMO();
+ }
+
+ return undefined;
+ },
+
+ async loadLocalesFromAMO() {
+ if (!this.downloadEnabled) {
+ return;
+ }
+
+ // Disable the dropdown while we hit the network.
+ this._availableLocalesUI.disableWithMessageId(
+ "messenger-languages-searching"
+ );
+
+ // Fetch the available langpacks from AMO.
+ let availableLangpacks;
+ try {
+ availableLangpacks = await AddonRepository.getAvailableLangpacks();
+ } catch (e) {
+ this.showError();
+ return;
+ }
+
+ // Store the available langpack info for later use.
+ this.availableLangpacks = new Map();
+ for (let { target_locale, url, hash } of availableLangpacks) {
+ this.availableLangpacks.set(target_locale, { url, hash });
+ }
+
+ // Remove the installed locales from the available ones.
+ let installedLocales = new Set(await getAvailableLocales());
+ let notInstalledLocales = availableLangpacks
+ .filter(({ target_locale }) => !installedLocales.has(target_locale))
+ .map(lang => lang.target_locale);
+
+ // Create the rows for the remote locales.
+ let availableItems = await getLocaleDisplayInfo(notInstalledLocales);
+ availableItems.push({
+ label: await document.l10n.formatValue(
+ "messenger-languages-available-label"
+ ),
+ className: "label-item",
+ disabled: true,
+ installed: false,
+ });
+
+ // Remove the search option and add the remote locales.
+ let items = this._availableLocalesUI.items;
+ items.pop();
+ items = items.concat(availableItems);
+
+ // Update the dropdown and enable it again.
+ this._availableLocalesUI.setItems(items);
+ this._availableLocalesUI.enableWithMessageId(
+ "messenger-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {Set<string>} available - The set of available (BCP 47) locales.
+ */
+ async loadLocalesFromInstalled(available) {
+ let items;
+ if (available.length > 0) {
+ items = await getLocaleDisplayInfo(available);
+ items.push(await this.createInstalledLabel());
+ } else {
+ items = [];
+ }
+ if (this.downloadEnabled) {
+ items.push({
+ label: await document.l10n.formatValue("messenger-languages-search"),
+ value: "search",
+ });
+ }
+ this._availableLocalesUI.setItems(items);
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async availableLanguageSelected(item) {
+ if ((await getAvailableLocales()).includes(item.value)) {
+ await this.requestLocalLanguage(item);
+ } else if (this.availableLangpacks.has(item.value)) {
+ await this.requestRemoteLanguage(item);
+ } else {
+ this.showError();
+ }
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async requestLocalLanguage(item) {
+ this._selectedLocalesUI.addItem(item);
+ let selectedCount = this._selectedLocalesUI.items.length;
+ let availableCount = (await getAvailableLocales()).length;
+ if (selectedCount == availableCount) {
+ // Remove the installed label, they're all installed.
+ this._availableLocalesUI.items.shift();
+ this._availableLocalesUI.setItems(this._availableLocalesUI.items);
+ }
+
+ // The label isn't always reset when the selected item is removed, so set it again.
+ this._availableLocalesUI.enableWithMessageId(
+ "messenger-languages-select-language"
+ );
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async requestRemoteLanguage(item) {
+ this._availableLocalesUI.disableWithMessageId(
+ "messenger-languages-downloading"
+ );
+
+ let { url, hash } = this.availableLangpacks.get(item.value);
+ let addon;
+
+ try {
+ addon = await AddonManager.getInstallForURL(url, { hash });
+ await addon.install();
+ } catch (e) {
+ this.showError();
+ return;
+ }
+
+ // If the add-on was previously installed, it might be disabled still.
+ if (addon.userDisabled) {
+ await addon.enable();
+ }
+
+ item.installed = true;
+ this._selectedLocalesUI.addItem(item);
+ this._availableLocalesUI.enableWithMessageId(
+ "messenger-languages-select-language"
+ );
+ },
+
+ showError() {
+ document.getElementById("warning-message").hidden = false;
+ this._availableLocalesUI.enableWithMessageId(
+ "messenger-languages-select-language"
+ );
+
+ // The height has likely changed, find our SubDialog and tell it to resize.
+ requestAnimationFrame(() => {
+ let dialogs = window.opener.gSubDialog._dialogs;
+ let index = dialogs.findIndex(d => d._frame.contentDocument == document);
+ if (index != -1) {
+ dialogs[index].resizeDialog();
+ }
+ });
+ },
+
+ hideError() {
+ document.getElementById("warning-message").hidden = true;
+ },
+
+ /**
+ * @param {LocaleDisplayInfo} item
+ */
+ async selectedLocaleRemoved(item) {
+ this._availableLocalesUI.addItem(item);
+
+ // If the item we added is at the top of the list, it needs the label.
+ if (this._availableLocalesUI.items[0] == item) {
+ this._availableLocalesUI.addItem(await this.createInstalledLabel());
+ }
+ },
+
+ async createInstalledLabel() {
+ return {
+ label: await document.l10n.formatValue(
+ "messenger-languages-installed-label"
+ ),
+ className: "label-item",
+ disabled: true,
+ installed: true,
+ };
+ },
+};
diff --git a/comm/mail/components/preferences/messengerLanguages.xhtml b/comm/mail/components/preferences/messengerLanguages.xhtml
new file mode 100644
index 0000000000..116ee4e5b2
--- /dev/null
+++ b/comm/mail/components/preferences/messengerLanguages.xhtml
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="messenger-languages-window2"
+ onload="gMessengerLanguagesDialog.onLoad();"
+>
+ <dialog id="MessengerLanguagesDialog" buttons="accept,cancel">
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/preferences/languages.ftl"
+ />
+ </linkset>
+
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/messengerLanguages.js" />
+
+ <vbox
+ id="messengerLanguagesDialogPane"
+ class="prefpane largeDialogContainer"
+ flex="1"
+ >
+ <description data-l10n-id="messenger-languages-description" />
+ <hbox flex="1">
+ <vbox flex="1">
+ <richlistbox id="selectedLocales" flex="1" />
+ <menulist
+ id="availableLocales"
+ class="available-locales-list"
+ data-l10n-id="messenger-languages-select-language"
+ data-l10n-attrs="placeholder,label"
+ >
+ <menupopup />
+ </menulist>
+ </vbox>
+ <vbox>
+ <button
+ id="up"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-moveup"
+ />
+ <button
+ id="down"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-movedown"
+ />
+ <button
+ id="remove"
+ class="action-button"
+ disabled="true"
+ data-l10n-id="languages-customize-remove"
+ />
+ <vbox flex="1" pack="end">
+ <button
+ id="add"
+ class="add-messenger-language action-button"
+ data-l10n-id="languages-customize-add"
+ disabled="true"
+ />
+ </vbox>
+ </vbox>
+ </hbox>
+ <hbox
+ id="warning-message"
+ class="message-bar message-bar-warning"
+ hidden="true"
+ >
+ <html:img
+ class="message-bar-icon"
+ src="chrome://global/skin/icons/warning.svg"
+ alt=""
+ />
+ <description
+ class="message-bar-description"
+ data-l10n-id="messenger-languages-error"
+ />
+ </hbox>
+ <separator class="thin" />
+ </vbox>
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/moz.build b/comm/mail/components/preferences/moz.build
new file mode 100644
index 0000000000..4eff028aeb
--- /dev/null
+++ b/comm/mail/components/preferences/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+DEFINES["MOZ_MACBUNDLE_NAME"] = CONFIG["MOZ_MACBUNDLE_NAME"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] in ("windows", "gtk", "cocoa"):
+ DEFINES["HAVE_SHELL_SERVICE"] = 1
+
+if CONFIG["MOZ_UPDATER"]:
+ DEFINES["MOZ_UPDATER"] = 1
+
+BROWSER_CHROME_MANIFESTS += [
+ "test/browser/browser.ini",
+]
diff --git a/comm/mail/components/preferences/notifications.js b/comm/mail/components/preferences/notifications.js
new file mode 100644
index 0000000000..0970a944bd
--- /dev/null
+++ b/comm/mail/components/preferences/notifications.js
@@ -0,0 +1,25 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "mail.biff.alert.show_preview", type: "bool" },
+ { id: "mail.biff.alert.show_subject", type: "bool" },
+ { id: "mail.biff.alert.show_sender", type: "bool" },
+ { id: "alerts.totalOpenTime", type: "int" },
+]);
+
+var gNotificationsDialog = {
+ init() {
+ let element = document.getElementById("totalOpenTime");
+ Preferences.addSyncFromPrefListener(
+ element,
+ () => Preferences.get("alerts.totalOpenTime").value / 1000
+ );
+ Preferences.addSyncToPrefListener(element, element => element.value * 1000);
+ },
+};
+
+window.addEventListener("load", () => gNotificationsDialog.init());
diff --git a/comm/mail/components/preferences/notifications.xhtml b/comm/mail/components/preferences/notifications.xhtml
new file mode 100644
index 0000000000..d9abb47e4e
--- /dev/null
+++ b/comm/mail/components/preferences/notifications.xhtml
@@ -0,0 +1,71 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="notifications-dialog-window"
+>
+ <dialog id="NotificationsDialog" dlgbuttons="accept,cancel">
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/preferences/notifications.ftl"
+ />
+ </linkset>
+ <description data-l10n-id="customize-alert-description" />
+
+ <checkbox
+ id="previewText"
+ class="indent"
+ data-l10n-id="preview-text-checkbox"
+ preference="mail.biff.alert.show_preview"
+ />
+ <checkbox
+ id="subject"
+ class="indent"
+ data-l10n-id="subject-checkbox"
+ preference="mail.biff.alert.show_subject"
+ />
+ <checkbox
+ id="sender"
+ class="indent"
+ data-l10n-id="sender-checkbox"
+ preference="mail.biff.alert.show_sender"
+ />
+
+ <separator />
+
+ <hbox align="center">
+ <label
+ id="totalOpenTimeBefore"
+ control="totalOpenTime"
+ data-l10n-id="open-time-label-before"
+ />
+ <html:input
+ id="totalOpenTime"
+ type="number"
+ class="size3"
+ min="1"
+ max="3600"
+ preference="alerts.totalOpenTime"
+ />
+ <label
+ id="totalOpenTimeEnd"
+ data-l10n-id="open-time-label-after"
+ class="startSpacing"
+ />
+ </hbox>
+ <separator />
+
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/notifications.js" />
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/offline.js b/comm/mail/components/preferences/offline.js
new file mode 100644
index 0000000000..ae33950cf1
--- /dev/null
+++ b/comm/mail/components/preferences/offline.js
@@ -0,0 +1,31 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "offline.autoDetect", type: "bool" },
+ { id: "offline.startup_state", type: "int" },
+ { id: "offline.send.unsent_messages", type: "int" },
+ { id: "offline.download.download_messages", type: "int" },
+]);
+
+var kAutomatic = 4;
+var kRememberLastState = 0;
+
+var gOfflineDialog = {
+ dialogSetup() {
+ let offlineAutoDetection = Preferences.get("offline.autoDetect");
+ let offlineStartupStatePref = Preferences.get("offline.startup_state");
+
+ offlineStartupStatePref.disabled = offlineAutoDetection.value;
+ if (offlineStartupStatePref.disabled) {
+ offlineStartupStatePref.value = kAutomatic;
+ } else if (offlineStartupStatePref.value == kAutomatic) {
+ offlineStartupStatePref.value = kRememberLastState;
+ }
+ },
+};
+
+Preferences.get("offline.autoDetect").on("change", gOfflineDialog.dialogSetup);
diff --git a/comm/mail/components/preferences/offline.xhtml b/comm/mail/components/preferences/offline.xhtml
new file mode 100644
index 0000000000..77a86cfa86
--- /dev/null
+++ b/comm/mail/components/preferences/offline.xhtml
@@ -0,0 +1,77 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gOfflineDialog.dialogSetup();"
+ data-l10n-id="offline-dialog-window"
+>
+ <dialog id="OfflineSettingsDialog" dlgbuttons="accept,cancel">
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/offline.js" />
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/offline.ftl" />
+ </linkset>
+
+ <checkbox
+ data-l10n-id="autodetect-online-label"
+ preference="offline.autoDetect"
+ />
+
+ <separator class="thin" />
+
+ <label
+ data-l10n-id="offline-preference-startup-label"
+ control="whenStartingUp"
+ />
+ <radiogroup
+ id="whenStartingUp"
+ class="indent"
+ preference="offline.startup_state"
+ >
+ <radio value="0" data-l10n-id="status-radio-remember" />
+ <radio value="1" data-l10n-id="status-radio-ask" />
+ <radio value="2" data-l10n-id="status-radio-always-online" />
+ <radio value="3" data-l10n-id="status-radio-always-offline" />
+ <radio value="4" hidden="true" />
+ </radiogroup>
+
+ <separator />
+
+ <label data-l10n-id="going-online-label" control="whengoingOnlinestate" />
+ <radiogroup
+ id="whengoingOnlinestate"
+ orient="horizontal"
+ class="indent"
+ preference="offline.send.unsent_messages"
+ >
+ <radio value="1" data-l10n-id="going-online-auto" />
+ <radio value="2" data-l10n-id="going-online-not" />
+ <radio value="0" data-l10n-id="going-online-ask" />
+ </radiogroup>
+
+ <separator class="thin" />
+
+ <label data-l10n-id="going-offline-label" control="whengoingOfflinestate" />
+ <radiogroup
+ id="whengoingOfflinestate"
+ orient="horizontal"
+ class="indent"
+ preference="offline.download.download_messages"
+ >
+ <radio value="1" data-l10n-id="going-offline-auto" />
+ <radio value="2" data-l10n-id="going-offline-not" />
+ <radio value="0" data-l10n-id="going-offline-ask" />
+ </radiogroup>
+ <separator />
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/passwordManager.js b/comm/mail/components/preferences/passwordManager.js
new file mode 100644
index 0000000000..67f08767d3
--- /dev/null
+++ b/comm/mail/components/preferences/passwordManager.js
@@ -0,0 +1,819 @@
+/* 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/. */
+
+/** * =================== SAVED SIGNONS CODE =================== */
+/* eslint-disable-next-line no-var */
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+/* eslint-disable-next-line no-var */
+/* eslint-disable-next-line no-var */
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+// Default value for signon table sorting
+let lastSignonSortColumn = "origin";
+let lastSignonSortAscending = true;
+
+let showingPasswords = false;
+
+// password-manager lists
+let signons = [];
+let deletedSignons = [];
+
+// Elements that would be used frequently
+let filterField;
+let togglePasswordsButton;
+let signonsIntro;
+let removeButton;
+let removeAllButton;
+let signonsTree;
+
+let signonReloadDisplay = {
+ observe(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ switch (data) {
+ case "addLogin":
+ case "modifyLogin":
+ case "removeLogin":
+ case "removeAllLogins":
+ if (!signonsTree) {
+ return;
+ }
+ signons.length = 0;
+ LoadSignons();
+ // apply the filter if needed
+ if (filterField && filterField.value != "") {
+ FilterPasswords();
+ }
+ signonsTree.ensureRowIsVisible(
+ signonsTree.view.selection.currentIndex
+ );
+ break;
+ }
+ Services.obs.notifyObservers(null, "passwordmgr-dialog-updated");
+ }
+ },
+};
+
+// Formatter for localization.
+let dateFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+});
+let dateAndTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "medium",
+ timeStyle: "short",
+});
+
+function Startup() {
+ // be prepared to reload the display if anything changes
+ Services.obs.addObserver(signonReloadDisplay, "passwordmgr-storage-changed");
+
+ signonsTree = document.getElementById("signonsTree");
+ filterField = document.getElementById("filter");
+ togglePasswordsButton = document.getElementById("togglePasswords");
+ signonsIntro = document.getElementById("signonsIntro");
+ removeButton = document.getElementById("removeSignon");
+ removeAllButton = document.getElementById("removeAllSignons");
+
+ document.l10n.setAttributes(togglePasswordsButton, "show-passwords");
+ document.l10n.setAttributes(signonsIntro, "logins-description-all");
+ document.l10n.setAttributes(removeAllButton, "remove-all");
+
+ document
+ .getElementsByTagName("treecols")[0]
+ .addEventListener("click", event => {
+ let { target, button } = event;
+ let sortField = target.getAttribute("data-field-name");
+
+ if (target.nodeName != "treecol" || button != 0 || !sortField) {
+ return;
+ }
+
+ SignonColumnSort(sortField);
+ });
+
+ LoadSignons();
+
+ // filter the table if requested by caller
+ if (
+ window.arguments &&
+ window.arguments[0] &&
+ window.arguments[0].filterString
+ ) {
+ setFilter(window.arguments[0].filterString);
+ }
+
+ FocusFilterBox();
+ document.l10n
+ .translateElements(document.querySelectorAll("[data-l10n-id]"))
+ .then(() => window.sizeToContent());
+}
+
+function Shutdown() {
+ Services.obs.removeObserver(
+ signonReloadDisplay,
+ "passwordmgr-storage-changed"
+ );
+}
+
+function setFilter(aFilterString) {
+ filterField.value = aFilterString;
+ FilterPasswords();
+}
+
+let signonsTreeView = {
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+ _filterSet: [],
+ _lastSelectedRanges: [],
+ selection: null,
+
+ rowCount: 0,
+ setTree(tree) {},
+ getImageSrc(row, column) {
+ if (column.element.getAttribute("id") !== "providerCol") {
+ return "";
+ }
+
+ const signon = GetVisibleLogins()[row];
+
+ return PlacesUtils.urlWithSizeRef(window, "page-icon:" + signon.origin, 16);
+ },
+ getCellValue(row, column) {},
+ getCellText(row, column) {
+ let time;
+ let signon = GetVisibleLogins()[row];
+ switch (column.id) {
+ case "providerCol":
+ return signon.httpRealm
+ ? signon.origin + " (" + signon.httpRealm + ")"
+ : signon.origin;
+ case "userCol":
+ return signon.username || "";
+ case "passwordCol":
+ return signon.password || "";
+ case "timeCreatedCol":
+ time = new Date(signon.timeCreated);
+ return dateFormatter.format(time);
+ case "timeLastUsedCol":
+ time = new Date(signon.timeLastUsed);
+ return dateAndTimeFormatter.format(time);
+ case "timePasswordChangedCol":
+ time = new Date(signon.timePasswordChanged);
+ return dateFormatter.format(time);
+ case "timesUsedCol":
+ return signon.timesUsed;
+ default:
+ return "";
+ }
+ },
+ isEditable(row, col) {
+ if (col.id == "userCol" || col.id == "passwordCol") {
+ return true;
+ }
+ return false;
+ },
+ isSeparator(index) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(index) {
+ return false;
+ },
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ if (column.element.getAttribute("id") == "providerCol") {
+ return "ltr";
+ }
+
+ return "";
+ },
+ setCellText(row, col, value) {
+ let table = GetVisibleLogins();
+ function _editLogin(field) {
+ if (value == table[row][field]) {
+ return;
+ }
+ let existingLogin = table[row].clone();
+ table[row][field] = value;
+ table[row].timePasswordChanged = Date.now();
+ Services.logins.modifyLogin(existingLogin, table[row]);
+ signonsTree.invalidateRow(row);
+ }
+
+ if (col.id == "userCol") {
+ _editLogin("username");
+ } else if (col.id == "passwordCol") {
+ if (!value) {
+ return;
+ }
+ _editLogin("password");
+ }
+ },
+};
+
+function SortTree(column, ascending) {
+ let table = GetVisibleLogins();
+ // remember which item was selected so we can restore it after the sort
+ let selections = GetTreeSelections();
+ let selectedNumber = selections.length ? table[selections[0]].number : -1;
+ function compareFunc(a, b) {
+ let valA, valB;
+ switch (column) {
+ case "origin":
+ let realmA = a.httpRealm;
+ let realmB = b.httpRealm;
+ realmA = realmA == null ? "" : realmA.toLowerCase();
+ realmB = realmB == null ? "" : realmB.toLowerCase();
+
+ valA = a[column].toLowerCase() + realmA;
+ valB = b[column].toLowerCase() + realmB;
+ break;
+ case "username":
+ case "password":
+ valA = a[column].toLowerCase();
+ valB = b[column].toLowerCase();
+ break;
+
+ default:
+ valA = a[column];
+ valB = b[column];
+ }
+
+ if (valA < valB) {
+ return -1;
+ }
+ if (valA > valB) {
+ return 1;
+ }
+ return 0;
+ }
+
+ // do the sort
+ table.sort(compareFunc);
+ if (!ascending) {
+ table.reverse();
+ }
+
+ // restore the selection
+ let selectedRow = -1;
+ if (selectedNumber >= 0 && false) {
+ for (let s = 0; s < table.length; s++) {
+ if (table[s].number == selectedNumber) {
+ // update selection
+ // note: we need to deselect before reselecting in order to trigger ...Selected()
+ signonsTree.view.selection.select(-1);
+ signonsTree.view.selection.select(s);
+ selectedRow = s;
+ break;
+ }
+ }
+ }
+
+ // display the results
+ signonsTree.invalidate();
+ if (selectedRow >= 0) {
+ signonsTree.ensureRowIsVisible(selectedRow);
+ }
+}
+
+function LoadSignons() {
+ // loads signons into table
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ signons.forEach(login => login.QueryInterface(Ci.nsILoginMetaInfo));
+ signonsTreeView.rowCount = signons.length;
+
+ // sort and display the table
+ signonsTree.view = signonsTreeView;
+ // The sort column didn't change. SortTree (called by
+ // SignonColumnSort) assumes we want to toggle the sort
+ // direction but here we don't so we have to trick it
+ lastSignonSortAscending = !lastSignonSortAscending;
+ SignonColumnSort(lastSignonSortColumn);
+
+ // disable "remove all signons" button if there are no signons
+ if (signons.length == 0) {
+ removeAllButton.setAttribute("disabled", "true");
+ togglePasswordsButton.setAttribute("disabled", "true");
+ } else {
+ removeAllButton.removeAttribute("disabled");
+ togglePasswordsButton.removeAttribute("disabled");
+ }
+
+ return true;
+}
+
+function GetVisibleLogins() {
+ return signonsTreeView._filterSet.length
+ ? signonsTreeView._filterSet
+ : signons;
+}
+
+function GetTreeSelections() {
+ let selections = [];
+ let select = signonsTree.view.selection;
+ if (select) {
+ let count = select.getRangeCount();
+ let min = {};
+ let max = {};
+ for (let i = 0; i < count; i++) {
+ select.getRangeAt(i, min, max);
+ for (let k = min.value; k <= max.value; k++) {
+ if (k != -1) {
+ selections[selections.length] = k;
+ }
+ }
+ }
+ }
+ return selections;
+}
+
+function SignonSelected() {
+ let selections = GetTreeSelections();
+ if (selections.length) {
+ removeButton.removeAttribute("disabled");
+ } else {
+ removeButton.setAttribute("disabled", true);
+ }
+}
+
+function DeleteSignon() {
+ let syncNeeded = signonsTreeView._filterSet.length != 0;
+ let tree = signonsTree;
+ let view = signonsTreeView;
+ let table = GetVisibleLogins();
+
+ // Turn off tree selection notifications during the deletion
+ tree.view.selection.selectEventsSuppressed = true;
+
+ // remove selected items from list (by setting them to null) and place in deleted list
+ let selections = GetTreeSelections();
+ for (let s = selections.length - 1; s >= 0; s--) {
+ let i = selections[s];
+ deletedSignons.push(table[i]);
+ table[i] = null;
+ }
+
+ // collapse list by removing all the null entries
+ for (let j = 0; j < table.length; j++) {
+ if (table[j] == null) {
+ let k = j;
+ while (k < table.length && table[k] == null) {
+ k++;
+ }
+ table.splice(j, k - j);
+ view.rowCount -= k - j;
+ tree.rowCountChanged(j, j - k);
+ }
+ }
+
+ // update selection and/or buttons
+ if (table.length) {
+ // update selection
+ let nextSelection =
+ selections[0] < table.length ? selections[0] : table.length - 1;
+ tree.view.selection.select(nextSelection);
+ } else {
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ }
+ tree.view.selection.selectEventsSuppressed = false;
+ FinalizeSignonDeletions(syncNeeded);
+}
+
+async function DeleteAllSignons() {
+ // Confirm the user wants to remove all passwords
+ let dummy = { value: false };
+ let [title, message] = await document.l10n.formatValues([
+ { id: "remove-all-passwords-title" },
+ { id: "remove-all-passwords-prompt" },
+ ]);
+ if (
+ Services.prompt.confirmEx(
+ window,
+ title,
+ message,
+ Services.prompt.STD_YES_NO_BUTTONS + Services.prompt.BUTTON_POS_1_DEFAULT,
+ null,
+ null,
+ null,
+ null,
+ dummy
+ ) == 1
+ ) {
+ // 1 == "No" button
+ return;
+ }
+
+ let syncNeeded = signonsTreeView._filterSet.length != 0;
+ let view = signonsTreeView;
+ let table = GetVisibleLogins();
+
+ // remove all items from table and place in deleted table
+ for (let i = 0; i < table.length; i++) {
+ deletedSignons.push(table[i]);
+ }
+ table.length = 0;
+
+ // clear out selections
+ view.selection.select(-1);
+
+ // update the tree view and notify the tree
+ view.rowCount = 0;
+
+ signonsTree.rowCountChanged(0, -deletedSignons.length);
+ signonsTree.invalidate();
+
+ // disable buttons
+ removeButton.setAttribute("disabled", "true");
+ removeAllButton.setAttribute("disabled", "true");
+ FinalizeSignonDeletions(syncNeeded);
+}
+
+async function TogglePasswordVisible() {
+ if (showingPasswords || (await masterPasswordLogin(AskUserShowPasswords))) {
+ showingPasswords = !showingPasswords;
+ document.l10n.setAttributes(
+ togglePasswordsButton,
+ showingPasswords ? "hide-passwords" : "show-passwords"
+ );
+ document.getElementById("passwordCol").hidden = !showingPasswords;
+ FilterPasswords();
+ }
+
+ // Notify observers that the password visibility toggling is
+ // completed. (Mostly useful for tests)
+ Services.obs.notifyObservers(null, "passwordmgr-password-toggle-complete");
+}
+
+async function AskUserShowPasswords() {
+ let dummy = { value: false };
+
+ // Confirm the user wants to display passwords
+ return (
+ Services.prompt.confirmEx(
+ window,
+ null,
+ await document.l10n.formatValue("no-master-password-prompt"),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ dummy
+ ) == 0
+ ); // 0=="Yes" button
+}
+
+function FinalizeSignonDeletions(syncNeeded) {
+ for (let s = 0; s < deletedSignons.length; s++) {
+ Services.logins.removeLogin(deletedSignons[s]);
+ }
+ // If the deletion has been performed in a filtered view, reflect the deletion in the unfiltered table.
+ // See bug 405389.
+ if (syncNeeded) {
+ try {
+ signons = Services.logins.getAllLogins();
+ } catch (e) {
+ signons = [];
+ }
+ }
+ deletedSignons.length = 0;
+}
+
+function HandleSignonKeyPress(e) {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ if (
+ e.keyCode == KeyboardEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ e.keyCode == KeyboardEvent.DOM_VK_BACK_SPACE)
+ ) {
+ DeleteSignon();
+ e.preventDefault();
+ }
+}
+
+function getColumnByName(column) {
+ switch (column) {
+ case "origin":
+ return document.getElementById("providerCol");
+ case "username":
+ return document.getElementById("userCol");
+ case "password":
+ return document.getElementById("passwordCol");
+ case "timeCreated":
+ return document.getElementById("timeCreatedCol");
+ case "timeLastUsed":
+ return document.getElementById("timeLastUsedCol");
+ case "timePasswordChanged":
+ return document.getElementById("timePasswordChangedCol");
+ case "timesUsed":
+ return document.getElementById("timesUsedCol");
+ }
+ return undefined;
+}
+
+function SignonColumnSort(column) {
+ let sortedCol = getColumnByName(column);
+ let lastSortedCol = getColumnByName(lastSignonSortColumn);
+
+ // clear out the sortDirection attribute on the old column
+ lastSortedCol.removeAttribute("sortDirection");
+
+ // determine if sort is to be ascending or descending
+ lastSignonSortAscending =
+ column == lastSignonSortColumn ? !lastSignonSortAscending : true;
+
+ // sort
+ lastSignonSortColumn = column;
+ SortTree(lastSignonSortColumn, lastSignonSortAscending);
+
+ // set the sortDirection attribute to get the styling going
+ // first we need to get the right element
+ sortedCol.setAttribute(
+ "sortDirection",
+ lastSignonSortAscending ? "ascending" : "descending"
+ );
+}
+
+function SignonClearFilter() {
+ let singleSelection = signonsTreeView.selection.count == 1;
+
+ // Clear the Tree Display
+ signonsTreeView.rowCount = 0;
+ signonsTree.rowCountChanged(0, -signonsTreeView._filterSet.length);
+ signonsTreeView._filterSet = [];
+
+ // Just reload the list to make sure deletions are respected
+ LoadSignons();
+
+ // Restore selection
+ if (singleSelection) {
+ signonsTreeView.selection.clearSelection();
+ for (let i = 0; i < signonsTreeView._lastSelectedRanges.length; ++i) {
+ let range = signonsTreeView._lastSelectedRanges[i];
+ signonsTreeView.selection.rangedSelect(range.min, range.max, true);
+ }
+ } else {
+ signonsTreeView.selection.select(0);
+ }
+ signonsTreeView._lastSelectedRanges = [];
+
+ document.l10n.setAttributes(signonsIntro, "logins-description-all");
+ document.l10n.setAttributes(removeAllButton, "remove-all");
+}
+
+function FocusFilterBox() {
+ if (filterField.getAttribute("focused") != "true") {
+ filterField.focus();
+ }
+}
+
+function SignonMatchesFilter(aSignon, aFilterValue) {
+ if (aSignon.origin.toLowerCase().includes(aFilterValue)) {
+ return true;
+ }
+ if (
+ aSignon.username &&
+ aSignon.username.toLowerCase().includes(aFilterValue)
+ ) {
+ return true;
+ }
+ if (
+ aSignon.httpRealm &&
+ aSignon.httpRealm.toLowerCase().includes(aFilterValue)
+ ) {
+ return true;
+ }
+ if (
+ showingPasswords &&
+ aSignon.password &&
+ aSignon.password.toLowerCase().includes(aFilterValue)
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+function _filterPasswords(aFilterValue, view) {
+ aFilterValue = aFilterValue.toLowerCase();
+ return signons.filter(s => SignonMatchesFilter(s, aFilterValue));
+}
+
+function SignonSaveState() {
+ // Save selection
+ let seln = signonsTreeView.selection;
+ signonsTreeView._lastSelectedRanges = [];
+ let rangeCount = seln.getRangeCount();
+ for (let i = 0; i < rangeCount; ++i) {
+ let min = {};
+ let max = {};
+ seln.getRangeAt(i, min, max);
+ signonsTreeView._lastSelectedRanges.push({
+ min: min.value,
+ max: max.value,
+ });
+ }
+}
+
+function FilterPasswords() {
+ if (filterField.value == "") {
+ SignonClearFilter();
+ return;
+ }
+
+ let newFilterSet = _filterPasswords(filterField.value, signonsTreeView);
+ if (!signonsTreeView._filterSet.length) {
+ // Save Display Info for the Non-Filtered mode when we first
+ // enter Filtered mode.
+ SignonSaveState();
+ }
+ signonsTreeView._filterSet = newFilterSet;
+
+ // Clear the display
+ let oldRowCount = signonsTreeView.rowCount;
+ signonsTreeView.rowCount = 0;
+ signonsTree.rowCountChanged(0, -oldRowCount);
+ // Set up the filtered display
+ signonsTreeView.rowCount = signonsTreeView._filterSet.length;
+ signonsTree.rowCountChanged(0, signonsTreeView.rowCount);
+
+ // if the view is not empty then select the first item
+ if (signonsTreeView.rowCount > 0) {
+ signonsTreeView.selection.select(0);
+ }
+
+ document.l10n.setAttributes(signonsIntro, "logins-description-filtered");
+ document.l10n.setAttributes(removeAllButton, "remove-all-shown");
+}
+
+function CopyProviderUrl() {
+ // Copy selected provider url to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ let row = signonsTree.currentIndex;
+ let url = signonsTreeView.getCellText(row, { id: "providerCol" });
+ clipboard.copyString(url);
+}
+
+async function CopyPassword() {
+ // Don't copy passwords if we aren't already showing the passwords & a master
+ // password hasn't been entered.
+ if (!showingPasswords && !(await masterPasswordLogin())) {
+ return;
+ }
+ // Copy selected signon's password to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ let row = signonsTree.currentIndex;
+ let password = signonsTreeView.getCellText(row, { id: "passwordCol" });
+ clipboard.copyString(password);
+}
+
+function CopyUsername() {
+ // Copy selected signon's username to clipboard
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(
+ Ci.nsIClipboardHelper
+ );
+ let row = signonsTree.currentIndex;
+ let username = signonsTreeView.getCellText(row, { id: "userCol" });
+ clipboard.copyString(username);
+}
+
+function EditCellInSelectedRow(columnName) {
+ let row = signonsTree.currentIndex;
+ let columnElement = getColumnByName(columnName);
+ signonsTree.startEditing(
+ row,
+ signonsTree.columns.getColumnFor(columnElement)
+ );
+}
+
+function UpdateContextMenu() {
+ let singleSelection = signonsTreeView.selection.count == 1;
+ let menuItems = new Map();
+ let menupopup = document.getElementById("signonsTreeContextMenu");
+ for (let menuItem of menupopup.querySelectorAll("menuitem")) {
+ menuItems.set(menuItem.id, menuItem);
+ }
+
+ if (!singleSelection) {
+ for (let menuItem of menuItems.values()) {
+ menuItem.setAttribute("disabled", "true");
+ }
+ return;
+ }
+
+ let selectedRow = signonsTree.currentIndex;
+
+ // Disable "Copy Username" if the username is empty.
+ if (signonsTreeView.getCellText(selectedRow, { id: "userCol" }) != "") {
+ menuItems.get("context-copyusername").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-copyusername").setAttribute("disabled", "true");
+ }
+
+ menuItems.get("context-copyproviderurl").removeAttribute("disabled");
+ menuItems.get("context-editusername").removeAttribute("disabled");
+ menuItems.get("context-copypassword").removeAttribute("disabled");
+
+ // Disable "Edit Password" if the password column isn't showing.
+ if (!document.getElementById("passwordCol").hidden) {
+ menuItems.get("context-editpassword").removeAttribute("disabled");
+ } else {
+ menuItems.get("context-editpassword").setAttribute("disabled", "true");
+ }
+}
+
+async function masterPasswordLogin(noPasswordCallback) {
+ // This doesn't harm if passwords are not encrypted
+ let tokendb = Cc["@mozilla.org/security/pk11tokendb;1"].createInstance(
+ Ci.nsIPK11TokenDB
+ );
+ let token = tokendb.getInternalKeyToken();
+
+ // If there is no primary password, still give the user a chance to opt-out of displaying passwords
+ if (token.checkPassword("")) {
+ // The OS re-authentication on Linux isn't working (Bug 1527745),
+ // still add the confirm dialog for Linux.
+ if (
+ Services.prefs.getBoolPref("signon.management.page.os-auth.enabled") &&
+ AppConstants.platform !== "linux"
+ ) {
+ // Require OS authentication before the user can show the passwords or copy them.
+ let messageId = "password-os-auth-dialog-message";
+ if (AppConstants.platform == "macosx") {
+ // MacOS requires a special format of this dialog string.
+ // See preferences.ftl for more information.
+ messageId += "-macosx";
+ }
+ let [messageText, captionText] = await document.l10n.formatMessages([
+ {
+ id: messageId,
+ },
+ {
+ id: "password-os-auth-dialog-caption",
+ },
+ ]);
+ let win = Services.wm.getMostRecentWindow("");
+ let loggedIn = await OSKeyStore.ensureLoggedIn(
+ messageText.value,
+ captionText.value,
+ win,
+ false
+ );
+ if (!loggedIn.authenticated) {
+ return false;
+ }
+ return true;
+ }
+ return noPasswordCallback ? noPasswordCallback() : true;
+ }
+
+ // So there's a primary password. But since checkPassword didn't succeed, we're logged out (per nsIPK11Token.idl).
+ try {
+ // Relogin and ask for the primary password.
+ token.login(true); // 'true' means always prompt for token password. User will be prompted until
+ // clicking 'Cancel' or entering the correct password.
+ } catch (e) {
+ // An exception will be thrown if the user cancels the login prompt dialog.
+ // User is also logged out of Software Security Device.
+ }
+
+ return token.isLoggedIn();
+}
+
+function escapeKeyHandler() {
+ // If editing is currently performed, don't do anything.
+ if (signonsTree.getAttribute("editing")) {
+ return;
+ }
+ window.close();
+}
diff --git a/comm/mail/components/preferences/passwordManager.xhtml b/comm/mail/components/preferences/passwordManager.xhtml
new file mode 100644
index 0000000000..594f2da8d8
--- /dev/null
+++ b/comm/mail/components/preferences/passwordManager.xhtml
@@ -0,0 +1,186 @@
+<?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/shared/preferences/passwordmgr.css"?>
+
+<window
+ id="SignonViewerDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="Startup();"
+ onunload="Shutdown();"
+ data-l10n-id="saved-logins"
+ persist="width height"
+>
+ <linkset>
+ <html:link rel="localization" href="branding/brand.ftl" />
+ <html:link
+ rel="localization"
+ href="messenger/preferences/passwordManager.ftl"
+ />
+ </linkset>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://messenger/content/preferences/passwordManager.js" />
+
+ <keyset>
+ <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();" />
+ <key
+ data-l10n-id="window-close"
+ modifiers="accel"
+ oncommand="escapeKeyHandler();"
+ />
+ <key
+ data-l10n-id="focus-search-primary-shortcut"
+ modifiers="accel"
+ oncommand="FocusFilterBox();"
+ />
+ <key
+ data-l10n-id="focus-search-alt-shortcut"
+ modifiers="accel"
+ oncommand="FocusFilterBox();"
+ />
+ </keyset>
+
+ <popupset id="signonsTreeContextSet">
+ <menupopup id="signonsTreeContextMenu" onpopupshowing="UpdateContextMenu()">
+ <menuitem
+ id="context-copyproviderurl"
+ data-l10n-id="copy-provider-url-cmd"
+ oncommand="CopyProviderUrl()"
+ />
+ <menuseparator />
+ <menuitem
+ id="context-copyusername"
+ data-l10n-id="copy-username-cmd"
+ oncommand="CopyUsername()"
+ />
+ <menuitem
+ id="context-editusername"
+ data-l10n-id="edit-username-cmd"
+ oncommand="EditCellInSelectedRow('username')"
+ />
+ <menuseparator />
+ <menuitem
+ id="context-copypassword"
+ data-l10n-id="copy-password-cmd"
+ oncommand="CopyPassword()"
+ />
+ <menuitem
+ id="context-editpassword"
+ data-l10n-id="edit-password-cmd"
+ oncommand="EditCellInSelectedRow('password')"
+ />
+ </menupopup>
+ </popupset>
+
+ <!-- saved signons -->
+ <vbox id="savedsignons" class="contentPane" flex="1">
+ <!-- filter -->
+ <hbox align="center">
+ <search-textbox
+ id="filter"
+ flex="1"
+ aria-controls="signonsTree"
+ oncommand="FilterPasswords();"
+ data-l10n-id="search-filter"
+ />
+ </hbox>
+
+ <label control="signonsTree" id="signonsIntro" />
+ <separator class="thin" />
+ <tree
+ id="signonsTree"
+ flex="1"
+ onkeypress="HandleSignonKeyPress(event)"
+ onselect="SignonSelected();"
+ editable="true"
+ context="signonsTreeContextMenu"
+ >
+ <treecols>
+ <treecol
+ id="providerCol"
+ data-l10n-id="column-heading-provider"
+ data-field-name="origin"
+ persist="width"
+ ignoreincolumnpicker="true"
+ sortDirection="ascending"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="userCol"
+ data-l10n-id="column-heading-username"
+ ignoreincolumnpicker="true"
+ data-field-name="username"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="passwordCol"
+ data-l10n-id="column-heading-password"
+ ignoreincolumnpicker="true"
+ data-field-name="password"
+ persist="width"
+ hidden="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="timeCreatedCol"
+ data-l10n-id="column-heading-time-created"
+ data-field-name="timeCreated"
+ persist="width hidden"
+ hidden="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="timeLastUsedCol"
+ data-l10n-id="column-heading-time-last-used"
+ data-field-name="timeLastUsed"
+ persist="width hidden"
+ hidden="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="timePasswordChangedCol"
+ data-l10n-id="column-heading-time-password-changed"
+ data-field-name="timePasswordChanged"
+ persist="width hidden"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="timesUsedCol"
+ data-l10n-id="column-heading-times-used"
+ data-field-name="timesUsed"
+ persist="width hidden"
+ hidden="true"
+ />
+ <splitter class="tree-splitter" />
+ </treecols>
+ <treechildren />
+ </tree>
+ <separator class="thin" />
+ <hbox id="SignonViewerButtons">
+ <button
+ id="removeSignon"
+ disabled="true"
+ data-l10n-id="remove"
+ oncommand="DeleteSignon();"
+ />
+ <button id="removeAllSignons" oncommand="DeleteAllSignons();" />
+ <spacer flex="1" />
+ <button id="togglePasswords" oncommand="TogglePasswordVisible();" />
+ </hbox>
+ </vbox>
+ <hbox align="end">
+ <hbox class="actionButtons" flex="1">
+ <spacer flex="1" />
+ <button
+ oncommand="window.close();"
+ data-l10n-id="password-close-button"
+ />
+ </hbox>
+ </hbox>
+</window>
diff --git a/comm/mail/components/preferences/permissions.js b/comm/mail/components/preferences/permissions.js
new file mode 100644
index 0000000000..3a75bc0ec6
--- /dev/null
+++ b/comm/mail/components/preferences/permissions.js
@@ -0,0 +1,501 @@
+/* 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/. */
+
+// toolkit/content/treeUtils.js
+/* globals gTreeUtils */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+var NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+
+/**
+ * Magic URI base used so the permission manager can store
+ * remote content permissions for a given email address.
+ */
+var MAILURI_BASE = "chrome://messenger/content/email=";
+
+function Permission(principal, type, capability) {
+ this.principal = principal;
+ this.origin = principal.origin;
+ this.type = type;
+ this.capability = capability;
+}
+
+var gPermissionManager = {
+ _type: "",
+ _permissions: [],
+ _permissionsToAdd: new Map(),
+ _permissionsToDelete: new Map(),
+ _tree: null,
+ _observerRemoved: false,
+
+ _view: {
+ QueryInterface: ChromeUtils.generateQI(["nsITreeView"]),
+ _rowCount: 0,
+ get rowCount() {
+ return this._rowCount;
+ },
+ getCellText(aRow, aColumn) {
+ if (aColumn.id == "siteCol") {
+ return gPermissionManager._permissions[aRow].origin.replace(
+ MAILURI_BASE,
+ ""
+ );
+ } else if (aColumn.id == "statusCol") {
+ return gPermissionManager._permissions[aRow].capability;
+ }
+ return "";
+ },
+
+ isSeparator(aIndex) {
+ return false;
+ },
+ isSorted() {
+ return false;
+ },
+ isContainer(aIndex) {
+ return false;
+ },
+ setTree(aTree) {},
+ getImageSrc(aRow, aColumn) {},
+ getProgressMode(aRow, aColumn) {},
+ getCellValue(aRow, aColumn) {},
+ cycleHeader(column) {},
+ getRowProperties(row) {
+ return "";
+ },
+ getColumnProperties(column) {
+ return "";
+ },
+ getCellProperties(row, column) {
+ if (column.element.getAttribute("id") == "siteCol") {
+ return "ltr";
+ }
+ return "";
+ },
+ },
+
+ async _getCapabilityString(aCapability) {
+ var stringKey = null;
+ switch (aCapability) {
+ case Ci.nsIPermissionManager.ALLOW_ACTION:
+ stringKey = "permission-can-label";
+ break;
+ case Ci.nsIPermissionManager.DENY_ACTION:
+ stringKey = "permission-cannot-label";
+ break;
+ case Ci.nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY:
+ stringKey = "permission-can-access-first-party-label";
+ break;
+ case Ci.nsICookiePermission.ACCESS_SESSION:
+ stringKey = "permission-can-session-label";
+ break;
+ }
+ let string = await document.l10n.formatValue(stringKey);
+ return string;
+ },
+
+ async addPermission(aCapability) {
+ var textbox = document.getElementById("url");
+ var input_url = textbox.value.trim();
+ let principal;
+ try {
+ // The origin accessor on the principal object will throw if the
+ // principal doesn't have a canonical origin representation. This will
+ // help catch cases where the URI parser parsed something like
+ // `localhost:8080` as having the scheme `localhost`, rather than being
+ // an invalid URI. A canonical origin representation is required by the
+ // permission manager for storage, so this won't prevent any valid
+ // permissions from being entered by the user.
+ let uri;
+ try {
+ uri = Services.io.newURI(input_url);
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ // If we have ended up with an unknown scheme, the following will throw.
+ principal.origin;
+ } catch (ex) {
+ let scheme =
+ this._type != "image" || !input_url.includes("@")
+ ? "http://"
+ : MAILURI_BASE;
+ uri = Services.io.newURI(scheme + input_url);
+ principal = Services.scriptSecurityManager.createContentPrincipal(
+ uri,
+ {}
+ );
+ // If we have ended up with an unknown scheme, the following will throw.
+ principal.origin;
+ }
+ } catch (ex) {
+ let [title, message] = await document.l10n.formatValues([
+ { id: "invalid-uri-title" },
+ { id: "invalid-uri-message" },
+ ]);
+ Services.prompt.alert(window, title, message);
+ return;
+ }
+
+ var capabilityString = await this._getCapabilityString(aCapability);
+
+ // check whether the permission already exists, if not, add it
+ let permissionExists = false;
+ let capabilityExists = false;
+ for (var i = 0; i < this._permissions.length; ++i) {
+ // Thunderbird compares origins, not principals here.
+ if (this._permissions[i].principal.origin == principal.origin) {
+ permissionExists = true;
+ capabilityExists = this._permissions[i].capability == capabilityString;
+ if (!capabilityExists) {
+ this._permissions[i].capability = capabilityString;
+ }
+ break;
+ }
+ }
+
+ let permissionParams = {
+ principal,
+ type: this._type,
+ capability: aCapability,
+ };
+ if (!permissionExists) {
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._addPermission(permissionParams);
+ } else if (!capabilityExists) {
+ this._permissionsToAdd.set(principal.origin, permissionParams);
+ this._handleCapabilityChange();
+ }
+
+ textbox.value = "";
+ textbox.focus();
+
+ // covers a case where the site exists already, so the buttons don't disable
+ this.onHostInput(textbox);
+
+ // enable "remove all" button as needed
+ document.getElementById("removeAllPermissions").disabled =
+ this._permissions.length == 0;
+ },
+
+ _removePermission(aPermission) {
+ this._removePermissionFromList(aPermission.principal);
+
+ // If this permission was added during this session, let's remove
+ // it from the pending adds list to prevent calls to the
+ // permission manager.
+ let isNewPermission = this._permissionsToAdd.delete(
+ aPermission.principal.origin
+ );
+
+ if (!isNewPermission) {
+ this._permissionsToDelete.set(aPermission.principal.origin, aPermission);
+ }
+ },
+
+ _handleCapabilityChange() {
+ // Re-do the sort, if the status changed from Block to Allow
+ // or vice versa, since if we're sorted on status, we may no
+ // longer be in order.
+ if (this._lastPermissionSortColumn == "statusCol") {
+ this._resortPermissions();
+ }
+ this._tree.invalidate();
+ },
+
+ _addPermission(aPermission) {
+ this._addPermissionToList(aPermission);
+ ++this._view._rowCount;
+ this._tree.rowCountChanged(this._view.rowCount - 1, 1);
+ // Re-do the sort, since we inserted this new item at the end.
+ this._resortPermissions();
+ },
+
+ _resortPermissions() {
+ gTreeUtils.sort(
+ this._tree,
+ this._view,
+ this._permissions,
+ this._lastPermissionSortColumn,
+ this._permissionsComparator,
+ this._lastPermissionSortColumn,
+ !this._lastPermissionSortAscending
+ ); // keep sort direction
+ },
+
+ onHostInput(aSiteField) {
+ document.getElementById("btnSession").disabled = !aSiteField.value;
+ document.getElementById("btnBlock").disabled = !aSiteField.value;
+ document.getElementById("btnAllow").disabled = !aSiteField.value;
+ },
+
+ onWindowKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+ window.close();
+ }
+ },
+
+ onHostKeyPress(aEvent) {
+ if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
+ document.getElementById("btnAllow").click();
+ }
+ },
+
+ onLoad() {
+ var params = window.arguments[0];
+ this.init(params);
+ },
+
+ init(aParams) {
+ if (this._type) {
+ // reusing an open dialog, clear the old observer
+ this.uninit();
+ }
+
+ this._type = aParams.permissionType;
+ this._manageCapability = aParams.manageCapability;
+
+ var permissionsText = document.getElementById("permissionsText");
+ while (permissionsText.hasChildNodes()) {
+ permissionsText.lastChild.remove();
+ }
+ permissionsText.appendChild(document.createTextNode(aParams.introText));
+
+ document.title = aParams.windowTitle;
+
+ document.getElementById("btnBlock").hidden = !aParams.blockVisible;
+ document.getElementById("btnSession").hidden = !aParams.sessionVisible;
+ document.getElementById("btnAllow").hidden = !aParams.allowVisible;
+
+ var urlFieldVisible =
+ aParams.blockVisible || aParams.sessionVisible || aParams.allowVisible;
+
+ var urlField = document.getElementById("url");
+ urlField.value = aParams.prefilledHost;
+ urlField.hidden = !urlFieldVisible;
+
+ this.onHostInput(urlField);
+
+ var urlLabel = document.getElementById("urlLabel");
+ urlLabel.hidden = !urlFieldVisible;
+
+ let treecols = document.getElementsByTagName("treecols")[0];
+ treecols.addEventListener("click", event => {
+ if (event.target.nodeName != "treecol" || event.button != 0) {
+ return;
+ }
+
+ let sortField = event.target.getAttribute("data-field-name");
+ if (!sortField) {
+ return;
+ }
+
+ gPermissionManager.onPermissionSort(sortField);
+ });
+
+ Services.obs.notifyObservers(
+ null,
+ NOTIFICATION_FLUSH_PERMISSIONS,
+ this._type
+ );
+ Services.obs.addObserver(this, "perm-changed");
+
+ this._loadPermissions().then(() => urlField.focus());
+ },
+
+ uninit() {
+ if (!this._observerRemoved) {
+ Services.obs.removeObserver(this, "perm-changed");
+
+ this._observerRemoved = true;
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "perm-changed") {
+ var permission = aSubject.QueryInterface(Ci.nsIPermission);
+
+ // Ignore unrelated permission types.
+ if (permission.type != this._type) {
+ return;
+ }
+
+ if (aData == "added") {
+ this._addPermission(permission);
+ } else if (aData == "changed") {
+ for (var i = 0; i < this._permissions.length; ++i) {
+ if (permission.matches(this._permissions[i].principal, true)) {
+ this._permissions[i].capability = this._getCapabilityString(
+ permission.capability
+ );
+ break;
+ }
+ }
+ this._handleCapabilityChange();
+ } else if (aData == "deleted") {
+ this._removePermissionFromList(permission);
+ }
+ }
+ },
+
+ onPermissionSelected() {
+ var hasSelection = this._tree.view.selection.count > 0;
+ var hasRows = this._tree.view.rowCount > 0;
+ document.getElementById("removePermission").disabled =
+ !hasRows || !hasSelection;
+ document.getElementById("removeAllPermissions").disabled = !hasRows;
+ },
+
+ onPermissionDeleted() {
+ if (!this._view.rowCount) {
+ return;
+ }
+ var removedPermissions = [];
+ gTreeUtils.deleteSelectedItems(
+ this._tree,
+ this._view,
+ this._permissions,
+ removedPermissions
+ );
+ for (var i = 0; i < removedPermissions.length; ++i) {
+ var p = removedPermissions[i];
+ this._removePermission(p);
+ }
+ document.getElementById("removePermission").disabled =
+ !this._permissions.length;
+ document.getElementById("removeAllPermissions").disabled =
+ !this._permissions.length;
+ },
+
+ onAllPermissionsDeleted() {
+ if (!this._view.rowCount) {
+ return;
+ }
+ var removedPermissions = [];
+ gTreeUtils.deleteAll(
+ this._tree,
+ this._view,
+ this._permissions,
+ removedPermissions
+ );
+ for (var i = 0; i < removedPermissions.length; ++i) {
+ var p = removedPermissions[i];
+ this._removePermission(p);
+ }
+ document.getElementById("removePermission").disabled = true;
+ document.getElementById("removeAllPermissions").disabled = true;
+ },
+
+ onPermissionKeyPress(aEvent) {
+ if (
+ aEvent.keyCode == KeyEvent.DOM_VK_DELETE ||
+ (AppConstants.platform == "macosx" &&
+ aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+ ) {
+ this.onPermissionDeleted();
+ }
+ },
+
+ _lastPermissionSortColumn: "",
+ _lastPermissionSortAscending: false,
+ _permissionsComparator(a, b) {
+ return a.toLowerCase().localeCompare(b.toLowerCase());
+ },
+
+ onPermissionSort(aColumn) {
+ this._lastPermissionSortAscending = gTreeUtils.sort(
+ this._tree,
+ this._view,
+ this._permissions,
+ aColumn,
+ this._permissionsComparator,
+ this._lastPermissionSortColumn,
+ this._lastPermissionSortAscending
+ );
+ this._lastPermissionSortColumn = aColumn;
+ },
+
+ onApplyChanges() {
+ // Stop observing permission changes since we are about
+ // to write out the pending adds/deletes and don't need
+ // to update the UI
+ this.uninit();
+
+ for (let permissionParams of this._permissionsToAdd.values()) {
+ Services.perms.addFromPrincipal(
+ permissionParams.principal,
+ permissionParams.type,
+ permissionParams.capability
+ );
+ }
+
+ for (let p of this._permissionsToDelete.values()) {
+ Services.perms.removeFromPrincipal(p.principal, p.type);
+ }
+
+ window.close();
+ },
+
+ async _loadPermissions() {
+ this._tree = document.getElementById("permissionsTree");
+ this._permissions = [];
+
+ for (let perm of Services.perms.all) {
+ await this._addPermissionToList(perm);
+ }
+
+ this._view._rowCount = this._permissions.length;
+
+ // sort and display the table
+ this._tree.view = this._view;
+ this.onPermissionSort("origin");
+
+ // disable "remove all" button if there are none
+ document.getElementById("removeAllPermissions").disabled =
+ this._permissions.length == 0;
+ },
+
+ async _addPermissionToList(aPermission) {
+ if (
+ aPermission.type == this._type &&
+ (!this._manageCapability ||
+ aPermission.capability == this._manageCapability)
+ ) {
+ var principal = aPermission.principal;
+ var capabilityString = await this._getCapabilityString(
+ aPermission.capability
+ );
+ var p = new Permission(principal, aPermission.type, capabilityString);
+ this._permissions.push(p);
+ }
+ },
+
+ _removePermissionFromList(aPrincipal) {
+ for (let i = 0; i < this._permissions.length; ++i) {
+ // Thunderbird compares origins, not principals here.
+ if (this._permissions[i].principal.origin == aPrincipal.origin) {
+ this._permissions.splice(i, 1);
+ this._view._rowCount--;
+ this._tree.rowCountChanged(this._view.rowCount - 1, -1);
+ this._tree.invalidate();
+ break;
+ }
+ }
+ },
+
+ setOrigin(aOrigin) {
+ document.getElementById("url").value = aOrigin;
+ },
+};
+
+function setOrigin(aOrigin) {
+ gPermissionManager.setOrigin(aOrigin);
+}
+
+function initWithParams(aParams) {
+ gPermissionManager.init(aParams);
+}
diff --git a/comm/mail/components/preferences/permissions.xhtml b/comm/mail/components/preferences/permissions.xhtml
new file mode 100644
index 0000000000..33340d3f63
--- /dev/null
+++ b/comm/mail/components/preferences/permissions.xhtml
@@ -0,0 +1,128 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+
+<!DOCTYPE dialog>
+
+<window
+ id="PermissionsDialog"
+ class="windowDialog"
+ data-l10n-id="permissions-reminder-window2"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="gPermissionManager.onLoad();"
+ onunload="gPermissionManager.uninit();"
+ persist="width height"
+ onkeypress="gPermissionManager.onWindowKeyPress(event);"
+>
+ <script src="chrome://messenger/content/globalOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script src="chrome://global/content/treeUtils.js" />
+ <script src="chrome://messenger/content/preferences/permissions.js" />
+
+ <linkset>
+ <html:link
+ rel="localization"
+ href="messenger/preferences/permissions.ftl"
+ />
+ </linkset>
+
+ <keyset>
+ <key
+ data-l10n-id="permission-preferences-close-window"
+ data-l10n-attrs="key"
+ modifiers="accel"
+ oncommand="window.close();"
+ />
+ </keyset>
+
+ <vbox class="contentPane largeDialogContainer" flex="1">
+ <description id="permissionsText" control="url" />
+ <separator class="thin" />
+ <label id="urlLabel" control="url" data-l10n-id="website-address-label" />
+ <hbox align="start" class="input-container">
+ <html:input
+ id="url"
+ type="text"
+ oninput="gPermissionManager.onHostInput(event.target);"
+ onkeypress="gPermissionManager.onHostKeyPress(event);"
+ />
+ </hbox>
+ <hbox pack="end">
+ <button
+ id="btnBlock"
+ disabled="true"
+ data-l10n-id="block-button"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.DENY_ACTION);"
+ />
+ <button
+ id="btnSession"
+ disabled="true"
+ data-l10n-id="allow-session-button"
+ oncommand="gPermissionManager.addPermission(Ci.nsICookiePermission.ACCESS_SESSION);"
+ />
+ <button
+ id="btnAllow"
+ disabled="true"
+ data-l10n-id="allow-button"
+ default="true"
+ oncommand="gPermissionManager.addPermission(Ci.nsIPermissionManager.ALLOW_ACTION);"
+ />
+ </hbox>
+ <separator class="thin" />
+ <tree
+ id="permissionsTree"
+ flex="1"
+ style="height: 18em"
+ hidecolumnpicker="true"
+ onkeypress="gPermissionManager.onPermissionKeyPress(event)"
+ onselect="gPermissionManager.onPermissionSelected();"
+ >
+ <treecols>
+ <treecol
+ id="siteCol"
+ data-l10n-id="treehead-sitename-label"
+ data-field-name="rawHost"
+ persist="width"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="statusCol"
+ data-l10n-id="treehead-status-label"
+ data-field-name="capability"
+ persist="width"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </vbox>
+ <vbox>
+ <hbox class="actionButtons" flex="1">
+ <button
+ id="removePermission"
+ disabled="true"
+ data-l10n-id="remove-site-button"
+ oncommand="gPermissionManager.onPermissionDeleted();"
+ />
+ <button
+ id="removeAllPermissions"
+ data-l10n-id="remove-all-site-button"
+ oncommand="gPermissionManager.onAllPermissionsDeleted();"
+ />
+ </hbox>
+ <spacer flex="1" />
+ <hbox class="actionButtons" pack="end" flex="1">
+ <button oncommand="window.close();" data-l10n-id="cancel-button" />
+ <button
+ id="btnApplyChanges"
+ oncommand="gPermissionManager.onApplyChanges();"
+ data-l10n-id="save-button"
+ />
+ </hbox>
+ </vbox>
+</window>
diff --git a/comm/mail/components/preferences/preferences.js b/comm/mail/components/preferences/preferences.js
new file mode 100644
index 0000000000..1a123527ca
--- /dev/null
+++ b/comm/mail/components/preferences/preferences.js
@@ -0,0 +1,453 @@
+/* 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 ../../../../toolkit/content/preferencesBindings.js */
+/* import-globals-from general.js */
+/* import-globals-from compose.js */
+/* import-globals-from downloads.js */
+/* import-globals-from privacy.js */
+/* import-globals-from chat.js */
+/* import-globals-from sync.js */
+/* import-globals-from findInPage.js */
+/* globals gCalendarPane */
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { ExtensionSupport } = ChromeUtils.import(
+ "resource:///modules/ExtensionSupport.jsm"
+);
+var { calendarDeactivator } = ChromeUtils.import(
+ "resource:///modules/calendar/calCalendarDeactivator.jsm"
+);
+var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm");
+var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm");
+
+var paneDeck = document.getElementById("paneDeck");
+var defaultPane = "paneGeneral";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () {
+ const { SubDialogManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/SubDialog.sys.mjs"
+ );
+ return new SubDialogManager({
+ dialogStack: document.getElementById("dialogStack"),
+ dialogTemplate: document.getElementById("dialogTemplate"),
+ dialogOptions: {
+ styleSheets: [
+ "chrome://messenger/skin/preferences/dialog.css",
+ "chrome://messenger/skin/preferences/preferences.css",
+ ],
+ resizeCallback: ({ title, frame }) => {
+ UIFontSize.registerWindow(frame.contentWindow);
+
+ // Search within main document and highlight matched keyword.
+ gSearchResultsPane.searchWithinNode(title, gSearchResultsPane.query);
+
+ // Search within sub-dialog document and highlight matched keyword.
+ gSearchResultsPane.searchWithinNode(
+ frame.contentDocument.firstElementChild,
+ gSearchResultsPane.query
+ );
+
+ // Creating tooltips for all the instances found
+ for (let node of gSearchResultsPane.listSearchTooltips) {
+ if (!node.tooltipNode) {
+ gSearchResultsPane.createSearchTooltip(
+ node,
+ gSearchResultsPane.query
+ );
+ }
+ }
+
+ // Resize the dialog to fit the content with edited font size.
+ requestAnimationFrame(() => {
+ let dialogs = frame.ownerGlobal.gSubDialog._dialogs;
+ let dialog = dialogs.find(
+ d => d._frame.contentDocument == frame.contentDocument
+ );
+ if (dialog) {
+ UIFontSize.resizeSubDialog(dialog);
+ }
+ });
+ },
+ },
+ });
+});
+
+document.addEventListener("DOMContentLoaded", init, { once: true });
+
+var gCategoryInits = new Map();
+var gLastCategory = { category: undefined, subcategory: undefined };
+
+function init_category_if_required(category) {
+ let categoryInfo = gCategoryInits.get(category);
+ if (!categoryInfo) {
+ throw new Error(
+ "Unknown in-content prefs category! Can't init " + category
+ );
+ }
+ if (categoryInfo.inited) {
+ return null;
+ }
+ return categoryInfo.init();
+}
+
+function register_module(categoryName, categoryObject) {
+ gCategoryInits.set(categoryName, {
+ inited: false,
+ async init() {
+ let template = document.getElementById(categoryName);
+ if (template) {
+ // Replace the template element with the nodes inside of it.
+ let frag = template.content;
+ await document.l10n.translateFragment(frag);
+
+ // Actually insert them into the DOM.
+ document.l10n.pauseObserving();
+ template.replaceWith(frag);
+ document.l10n.resumeObserving();
+
+ // Asks Preferences to update the attribute value of the entire
+ // document again (this can be simplified if we could separate the
+ // preferences of each pane.)
+ Preferences.queueUpdateOfAllElements();
+ }
+ categoryObject.init();
+ this.inited = true;
+ },
+ });
+}
+
+function init() {
+ register_module("paneGeneral", gGeneralPane);
+ register_module("paneCompose", gComposePane);
+ register_module("panePrivacy", gPrivacyPane);
+ register_module("paneCalendar", gCalendarPane);
+ if (AppConstants.NIGHTLY_BUILD) {
+ register_module("paneSync", gSyncPane);
+ }
+ register_module("paneSearchResults", gSearchResultsPane);
+ if (Services.prefs.getBoolPref("mail.chat.enabled")) {
+ register_module("paneChat", gChatPane);
+ } else {
+ // Remove the pane from the DOM so it doesn't get incorrectly included in
+ // the search results.
+ document.getElementById("paneChat").remove();
+ }
+
+ // If no calendar is currently enabled remove it from the DOM so it doesn't
+ // get incorrectly included in the search results.
+ if (!calendarDeactivator.isCalendarActivated) {
+ document.getElementById("paneCalendar").remove();
+ document.getElementById("category-calendar").remove();
+ }
+ gSearchResultsPane.init();
+
+ let categories = document.getElementById("categories");
+ categories.addEventListener("select", event => gotoPref(event.target.value));
+
+ document.documentElement.addEventListener("keydown", event => {
+ if (event.key == "Tab") {
+ categories.setAttribute("keyboard-navigation", "true");
+ } else if ((event.ctrlKey || event.metaKey) && event.key == "f") {
+ document.getElementById("searchInput").focus();
+ event.preventDefault();
+ }
+ });
+
+ categories.addEventListener("mousedown", function () {
+ this.removeAttribute("keyboard-navigation");
+ });
+
+ window.addEventListener("hashchange", onHashChange);
+ let lastSelected = Services.xulStore.getValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected"
+ );
+ gotoPref(lastSelected);
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+}
+
+function onHashChange() {
+ gotoPref();
+}
+
+async function gotoPref(aCategory) {
+ let categories = document.getElementById("categories");
+ const kDefaultCategoryInternalName = "paneGeneral";
+ const kDefaultCategory = "general";
+ let hash = document.location.hash;
+
+ let category = aCategory || hash.substr(1) || kDefaultCategoryInternalName;
+ let breakIndex = category.indexOf("-");
+ // Subcategories allow for selecting smaller sections of the preferences
+ // until proper search support is enabled (bug 1353954).
+ let subcategory = breakIndex != -1 && category.substring(breakIndex + 1);
+ if (subcategory) {
+ category = category.substring(0, breakIndex);
+ }
+ category = friendlyPrefCategoryNameToInternalName(category);
+ if (category != "paneSearchResults") {
+ gSearchResultsPane.query = null;
+ gSearchResultsPane.searchInput.value = "";
+ gSearchResultsPane.getFindSelection(window).removeAllRanges();
+ gSearchResultsPane.removeAllSearchTooltips();
+ gSearchResultsPane.removeAllSearchMenuitemIndicators();
+ } else if (!gSearchResultsPane.searchInput.value) {
+ // Something tried to send us to the search results pane without
+ // a query string. Default to the General pane instead.
+ category = kDefaultCategoryInternalName;
+ document.location.hash = kDefaultCategory;
+ gSearchResultsPane.query = null;
+ }
+
+ // Updating the hash (below) or changing the selected category
+ // will re-enter gotoPref.
+ if (gLastCategory.category == category && !subcategory) {
+ return;
+ }
+
+ let item;
+ if (category != "paneSearchResults") {
+ // Hide second level headers in normal view
+ for (let element of document.querySelectorAll(".search-header")) {
+ element.hidden = true;
+ }
+
+ item = categories.querySelector(".category[value=" + category + "]");
+ if (!item) {
+ category = kDefaultCategoryInternalName;
+ item = categories.querySelector(".category[value=" + category + "]");
+ }
+ }
+
+ if (
+ gLastCategory.category ||
+ category != kDefaultCategoryInternalName ||
+ subcategory
+ ) {
+ let friendlyName = internalPrefCategoryNameToFriendlyName(category);
+ document.location.hash = friendlyName;
+ }
+ // Need to set the gLastCategory before setting categories.selectedItem since
+ // the categories 'select' event will re-enter the gotoPref codepath.
+ gLastCategory.category = category;
+ gLastCategory.subcategory = subcategory;
+ if (item) {
+ categories.selectedItem = item;
+ } else {
+ categories.clearSelection();
+ }
+ window.history.replaceState(category, document.title);
+
+ try {
+ await init_category_if_required(category);
+ } catch (ex) {
+ console.error(
+ new Error(
+ "Error initializing preference category " + category + ": " + ex
+ )
+ );
+ throw ex;
+ }
+
+ // Bail out of this goToPref if the category
+ // or subcategory changed during async operation.
+ if (
+ gLastCategory.category !== category ||
+ gLastCategory.subcategory !== subcategory
+ ) {
+ return;
+ }
+
+ search(category, "data-category");
+
+ let mainContent = document.querySelector(".main-content");
+ mainContent.scrollTop = 0;
+
+ spotlight(subcategory, category);
+
+ document.dispatchEvent(new CustomEvent("paneSelected", { bubbles: true }));
+ document.getElementById("preferencesContainer").scrollTo(0, 0);
+ document.getElementById("paneDeck").setAttribute("lastSelected", category);
+ Services.xulStore.setValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected",
+ category
+ );
+}
+
+function friendlyPrefCategoryNameToInternalName(aName) {
+ if (aName.startsWith("pane")) {
+ return aName;
+ }
+ return "pane" + aName.substring(0, 1).toUpperCase() + aName.substr(1);
+}
+
+// This function is duplicated inside of utilityOverlay.js's openPreferences.
+function internalPrefCategoryNameToFriendlyName(aName) {
+ return (aName || "").replace(/^pane./, function (toReplace) {
+ return toReplace[4].toLowerCase();
+ });
+}
+
+function search(aQuery, aAttribute) {
+ let paneDeck = document.getElementById("paneDeck");
+ let elements = paneDeck.children;
+ for (let element of elements) {
+ // If the "data-hidden-from-search" is "true", the
+ // element will not get considered during search.
+ if (
+ element.getAttribute("data-hidden-from-search") != "true" ||
+ element.getAttribute("data-subpanel") == "true"
+ ) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.hidden = false;
+ } else {
+ element.hidden = true;
+ }
+ } else if (
+ element.getAttribute("data-hidden-from-search") == "true" &&
+ !element.hidden
+ ) {
+ element.hidden = true;
+ }
+ element.classList.remove("visually-hidden");
+ }
+
+ let keysets = paneDeck.getElementsByTagName("keyset");
+ for (let element of keysets) {
+ let attributeValue = element.getAttribute(aAttribute);
+ if (attributeValue == aQuery) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", true);
+ }
+ }
+}
+
+async function spotlight(subcategory, category) {
+ let highlightedElements = document.querySelectorAll(".spotlight");
+ if (highlightedElements.length) {
+ for (let element of highlightedElements) {
+ element.classList.remove("spotlight");
+ }
+ }
+ if (subcategory) {
+ scrollAndHighlight(subcategory, category);
+ }
+}
+
+async function scrollAndHighlight(subcategory, category) {
+ let element = document.querySelector(`[data-subcategory="${subcategory}"]`);
+ if (!element) {
+ return;
+ }
+ let header = getClosestDisplayedHeader(element);
+
+ scrollContentTo(header);
+ element.classList.add("spotlight");
+}
+
+/**
+ * If there is no visible second level header it will return first level header,
+ * otherwise return second level header.
+ *
+ * @returns {Element} The closest displayed header.
+ */
+function getClosestDisplayedHeader(element) {
+ let header = element.closest("groupbox");
+ let searchHeader = header.querySelector(".search-header");
+ if (
+ searchHeader &&
+ searchHeader.hidden &&
+ header.previousElementSibling.classList.contains("subcategory")
+ ) {
+ header = header.previousElementSibling;
+ }
+ return header;
+}
+
+function scrollContentTo(element) {
+ const STICKY_CONTAINER_HEIGHT =
+ document.querySelector(".sticky-container").clientHeight;
+ let mainContent = document.querySelector(".main-content");
+ let top = element.getBoundingClientRect().top - STICKY_CONTAINER_HEIGHT;
+ mainContent.scroll({
+ top,
+ behavior: "smooth",
+ });
+}
+
+/**
+ * Selects the specified preferences pane
+ *
+ * @param paneID ID of prefpane to select
+ * @param scrollPaneTo ID of the element to scroll into view
+ * @param otherArgs.subdialog ID of button to activate, opening a subdialog
+ */
+function selectPrefPane(paneID, scrollPaneTo, otherArgs) {
+ if (paneID) {
+ if (gLastCategory.category != paneID) {
+ gotoPref(paneID);
+ }
+ if (scrollPaneTo) {
+ showTab(scrollPaneTo, otherArgs ? otherArgs.subdialog : undefined);
+ }
+ }
+}
+
+/**
+ * Select the specified tab
+ *
+ * @param scrollPaneTo ID of the element to scroll into view
+ * @param subdialogID ID of button to activate, opening a subdialog
+ */
+function showTab(scrollPaneTo, subdialogID) {
+ setTimeout(function () {
+ let scrollTarget = document.getElementById(scrollPaneTo);
+ if (scrollTarget.closest("groupbox")) {
+ scrollTarget = scrollTarget.closest("groupbox");
+ }
+ scrollTarget.scrollIntoView();
+ if (subdialogID) {
+ document.getElementById(subdialogID).click();
+ }
+ });
+}
+
+/**
+ * Filter the lastFallbackLocale from availableLocales if it doesn't have all
+ * of the needed strings.
+ *
+ * When the lastFallbackLocale isn't the defaultLocale, then by default only
+ * fluent strings are included. To fully use that locale you need the langpack
+ * to be installed, so if it isn't installed remove it from availableLocales.
+ */
+async function getAvailableLocales() {
+ let { availableLocales, defaultLocale, lastFallbackLocale } = Services.locale;
+ // If defaultLocale isn't lastFallbackLocale, then we still need the langpack
+ // for lastFallbackLocale for it to be useful.
+ if (defaultLocale != lastFallbackLocale) {
+ let lastFallbackId = `langpack-${lastFallbackLocale}@thunderbird.mozilla.org`;
+ let lastFallbackInstalled = await AddonManager.getAddonByID(lastFallbackId);
+ if (!lastFallbackInstalled) {
+ return availableLocales.filter(locale => locale != lastFallbackLocale);
+ }
+ }
+ return availableLocales;
+}
diff --git a/comm/mail/components/preferences/preferences.xhtml b/comm/mail/components/preferences/preferences.xhtml
new file mode 100644
index 0000000000..8092f1af97
--- /dev/null
+++ b/comm/mail/components/preferences/preferences.xhtml
@@ -0,0 +1,256 @@
+<?xml version="1.0"?>
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://global/skin/popup.css"?>
+<?xml-stylesheet href="chrome://global/skin/autocomplete.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/preferences/applications.css"?>
+<?xml-stylesheet href="chrome://global/skin/in-content/common.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/preferences/preferences.css"?>
+<?xml-stylesheet href="chrome://calendar/skin/shared/calendar-preferences.css" type="text/css"?>
+<?xml-stylesheet href="chrome://calendar/skin/calendar.css"?>
+
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % editorOverlayDTD SYSTEM "chrome://messenger/locale/messengercompose/editorOverlay.dtd">
+%editorOverlayDTD;
+<!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+%lightningDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://calendar/locale/global.dtd">
+%globalDTD;
+<!ENTITY % eventDTD SYSTEM "chrome://calendar/locale/calendar-event-dialog.dtd">
+%eventDTD;
+]>
+
+<html id="MailPreferences" xmlns="http://www.w3.org/1999/xhtml"
+ role="document"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false">
+<head>
+ <title data-l10n-id="preferences-doc-title2"></title>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; connect-src *; script-src chrome: 'unsafe-inline'; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
+ <meta name="color-scheme" content="light dark" />
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/preferences/preferences.ftl" />
+ <link rel="localization" href="messenger/preferences/fonts.ftl" />
+ <link rel="localization" href="messenger/preferences/languages.ftl" />
+ <link rel="localization" href="messenger/aboutDialog.ftl"/>
+
+ <!-- Links below are only used for search-l10n-ids into subdialogs -->
+ <link rel="localization" href="messenger/preferences/receipts.ftl" />
+ <link rel="localization" href="messenger/preferences/permissions.ftl" />
+ <link rel="localization" href="messenger/preferences/cookies.ftl" />
+ <link rel="localization" href="messenger/preferences/system-integration.ftl" />
+ <link rel="localization" href="messenger/preferences/colors.ftl" />
+ <link rel="localization" href="messenger/preferences/dock-options.ftl" />
+ <link rel="localization" href="messenger/preferences/notifications.ftl" />
+ <link rel="localization" href="messenger/preferences/new-tag.ftl" />
+ <link rel="localization" href="toolkit/updates/history.ftl" />
+ <link rel="localization" href="messenger/preferences/connection.ftl" />
+ <link rel="localization" href="messenger/preferences/offline.ftl" />
+ <link rel="localization" href="toolkit/about/config.ftl" />
+ <link rel="localization" href="messenger/preferences/attachment-reminder.ftl" />
+ <link rel="localization" href="messenger/preferences/passwordManager.ftl" />
+ <link rel="localization" href="security/certificates/certManager.ftl" />
+ <link rel="localization" href="security/certificates/deviceManager.ftl" />
+#ifdef NIGHTLY_BUILD
+ <link rel="localization" href="messenger/preferences/sync-dialog.ftl" />
+ <link rel="localization" href="messenger/firefoxAccounts.ftl" />
+#endif
+
+ <script defer="defer" src="chrome://global/content/preferencesBindings.js"></script>
+#ifdef MOZ_UPDATER
+ <script defer="defer" src="chrome://messenger/content/aboutDialog-appUpdater.js"></script>
+#endif
+ <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/accountUtils.js"></script>
+
+ <script defer="defer" src="chrome://communicator/content/contentAreaClick.js"></script>
+ <script defer="defer" src="chrome://messenger/content/preferences/preferences.js"></script>
+ <script defer="defer" src="chrome://messenger/content/preferences/findInPage.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-ui-utils.js"></script>
+ <script defer="defer" src="chrome://calendar/content/calendar-dialog-utils.js"></script>
+ <script defer="defer" src="chrome://calendar/content/preferences/general.js"></script>
+ <script defer="defer" src="chrome://calendar/content/preferences/alarms.js"></script>
+ <script defer="defer" src="chrome://calendar/content/widgets/calendar-notifications-setting.js"/>
+ <script defer="defer" src="chrome://calendar/content/preferences/notifications.js"/>
+ <script defer="defer" src="chrome://calendar/content/preferences/categories.js"></script>
+ <script defer="defer" src="chrome://calendar/content/preferences/views.js"></script>
+ <script defer="defer" src="chrome://calendar/content/preferences/calendar-preferences.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <stack id="preferences-stack" flex="1">
+ <hbox id="prefBox" class="main-content" flex="1">
+
+ <vbox id="pref-category-box">
+
+ <!-- category list -->
+ <richlistbox id="categories"
+ data-l10n-id="category-list"
+ data-l10n-attrs="aria-label">
+ <richlistitem id="category-general"
+ class="category"
+ value="paneGeneral"
+ data-l10n-id="category-general"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/settings.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-general-title"/>
+ </richlistitem>
+
+ <richlistitem id="category-compose"
+ class="category"
+ value="paneCompose"
+ data-l10n-id="category-compose"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/pencil.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-compose-title"/>
+ </richlistitem>
+
+ <richlistitem id="category-privacy"
+ class="category"
+ value="panePrivacy"
+ data-l10n-id="category-privacy"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/lock.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-privacy-title"/>
+ </richlistitem>
+
+ <richlistitem id="category-chat"
+ class="category"
+ value="paneChat"
+ data-l10n-id="category-chat"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/chat.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-chat-title"/>
+ </richlistitem>
+
+ <richlistitem id="category-calendar"
+ class="category"
+ value="paneCalendar"
+ data-l10n-id="category-calendar"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/calendar.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-calendar-title"/>
+ </richlistitem>
+#ifdef NIGHTLY_BUILD
+ <richlistitem id="category-sync"
+ class="category"
+ value="paneSync"
+ data-l10n-id="category-sync"
+ align="center">
+ <html:img class="category-icon"
+ src="chrome://messenger/skin/icons/new/touch/sync.svg"
+ alt="" />
+ <label class="category-name" flex="1" data-l10n-id="pane-sync-title"/>
+ </richlistitem>
+#endif
+ </richlistbox>
+
+ <spacer flex="1"/>
+
+ <vbox class="sidebar-footer-list">
+ <html:a id="accountButton" class="sidebar-footer-link"
+ onclick="MsgAccountManager(null);">
+ <html:img class="sidebar-footer-icon account-icon"
+ src="chrome://messenger/skin/icons/new/compact/account-settings.svg"
+ alt="" />
+ <label data-l10n-id="account-button" class="sidebar-footer-label" flex="1"/>
+ </html:a>
+
+ <html:a id="addonsButton" class="sidebar-footer-link"
+ onclick="window.browsingContext.topChromeWindow.openAddonsMgr();">
+ <html:img class="sidebar-footer-icon"
+ src="chrome://messenger/skin/icons/new/compact/extension.svg"
+ alt="" />
+ <label class="sidebar-footer-label"
+ data-l10n-id="open-addons-sidebar-button"
+ flex="1"/>
+ </html:a>
+ </vbox>
+
+ </vbox>
+
+ <vbox id="preferencesContainer" flex="1" align="start">
+ <vbox class="paneDeckContainer">
+ <hbox class="sticky-container" pack="end" align="top" flex="1">
+ <search-textbox id="searchInput"
+ data-l10n-id="search-preferences-input2"
+ data-l10n-attrs="placeholder, style"
+ hidden="true"/>
+ </hbox>
+ <vbox id="paneDeck">
+#include searchResults.inc.xhtml
+#include general.inc.xhtml
+#include compose.inc.xhtml
+#include privacy.inc.xhtml
+#include chat.inc.xhtml
+#include ../../../calendar/base/content/preferences/calendar-preferences.inc.xhtml
+#ifdef NIGHTLY_BUILD
+#include sync.inc.xhtml
+#endif
+ </vbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ <stack id="dialogStack" hidden="true"/>
+ <vbox id="dialogTemplate"
+ class="dialogOverlay"
+ align="center"
+ pack="center"
+ topmost="true"
+ hidden="true">
+ <vbox class="dialogBox"
+ pack="end"
+ role="dialog"
+ aria-labelledby="dialogTitle">
+ <hbox class="dialogTitleBar" align="center">
+ <label class="dialogTitle" flex="1"/>
+ <button class="dialogClose close-icon" data-l10n-id="close-button"/>
+ </hbox>
+ <browser class="dialogFrame"
+ autoscroll="false"
+ disablehistory="true"/>
+ </vbox>
+ </vbox>
+ </stack>
+
+ <!-- Helpers for the FileLink options browser select and autocomplete. -->
+ <popupset>
+ <!-- For select dropdowns. The menupopup is what shows the list of options,
+ and the popuponly menulist makes things like the menuactive attributes
+ work correctly on the menupopup. ContentSelectDropdown expects the
+ popuponly menulist to be its immediate parent. -->
+ <menulist popuponly="true" id="ContentSelectDropdown" hidden="true">
+ <menupopup rolluponmousewheel="true"
+ activateontab="true"
+ position="after_start"
+ level="parent"
+#ifdef XP_WIN
+ consumeoutsideclicks="false"
+ ignorekeys="shortcuts"
+#endif
+ />
+ </menulist>
+ <panel is="autocomplete-richlistbox-popup" id="PopupAutoComplete"
+ type="autocomplete"
+ role="group"
+ noautofocus="true"/>
+ </popupset>
+</html:body>
+</html>
diff --git a/comm/mail/components/preferences/preferencesTab.js b/comm/mail/components/preferences/preferencesTab.js
new file mode 100644
index 0000000000..0a59fda382
--- /dev/null
+++ b/comm/mail/components/preferences/preferencesTab.js
@@ -0,0 +1,162 @@
+/* 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/. */
+
+// mail/base/content/specialTabs.js
+/* globals contentTabBaseType, DOMLinkHandler */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { ExtensionParent } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionParent.sys.mjs"
+);
+
+/**
+ * A tab to show Preferences.
+ */
+var preferencesTabType = {
+ __proto__: contentTabBaseType,
+ name: "preferencesTab",
+ perTabPanel: "vbox",
+ lastBrowserId: 0,
+ bundle: Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ ),
+ protoSvc: Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(
+ Ci.nsIExternalProtocolService
+ ),
+
+ get loadingTabString() {
+ delete this.loadingTabString;
+ return (this.loadingTabString = document
+ .getElementById("bundle_messenger")
+ .getString("loadingTab"));
+ },
+
+ modes: {
+ preferencesTab: {
+ type: "preferencesTab",
+ },
+ },
+
+ shouldSwitchTo(aArgs) {
+ if (!this.tab) {
+ return -1;
+ }
+ this.tab.browser.contentWindow.selectPrefPane(
+ aArgs.paneID,
+ aArgs.scrollPaneTo,
+ aArgs.otherArgs
+ );
+ return document.getElementById("tabmail").tabInfo.indexOf(this.tab);
+ },
+
+ closeTab(aTab) {
+ this.tab = null;
+ },
+
+ openTab(aTab, aArgs) {
+ aTab.tabNode.setIcon(
+ "chrome://messenger/skin/icons/new/compact/settings.svg"
+ );
+
+ // First clone the page and set up the basics.
+ let clone = document
+ .getElementById("preferencesTab")
+ .firstElementChild.cloneNode(true);
+
+ clone.setAttribute("id", "preferencesTab" + this.lastBrowserId);
+ clone.setAttribute("collapsed", false);
+
+ aTab.panel.setAttribute("id", "preferencesTabWrapper" + this.lastBrowserId);
+ aTab.panel.appendChild(clone);
+
+ // Start setting up the browser.
+ aTab.browser = aTab.panel.querySelector("browser");
+ aTab.browser.setAttribute(
+ "id",
+ "preferencesTabBrowser" + this.lastBrowserId
+ );
+ aTab.browser.setAttribute("autocompletepopup", "PopupAutoComplete");
+ aTab.browser.addEventListener("DOMLinkAdded", DOMLinkHandler);
+
+ aTab.findbar = document.createXULElement("findbar");
+ aTab.findbar.setAttribute(
+ "browserid",
+ "preferencesTabBrowser" + this.lastBrowserId
+ );
+ aTab.panel.appendChild(aTab.findbar);
+
+ // Default to reload being disabled.
+ aTab.reloadEnabled = false;
+
+ aTab.url = "about:preferences";
+ aTab.paneID = aArgs.paneID;
+ aTab.scrollPaneTo = aArgs.scrollPaneTo;
+ aTab.otherArgs = aArgs.otherArgs;
+
+ // Now set up the listeners.
+ this._setUpTitleListener(aTab);
+ this._setUpCloseWindowListener(aTab);
+
+ // Wait for full loading of the tab and the automatic selecting of last tab.
+ // Then run the given onload code.
+ aTab.browser.addEventListener(
+ "paneSelected",
+ function (event) {
+ aTab.pageLoading = false;
+ aTab.pageLoaded = true;
+
+ if ("onLoad" in aArgs) {
+ // Let selection of the initial pane complete before selecting another.
+ // Otherwise we can end up with two panes selected at once.
+ aTab.browser.contentWindow.setTimeout(() => {
+ // By now, the tab could already be closed. Check that it isn't.
+ if (aTab.panel) {
+ aArgs.onLoad(event, aTab.browser);
+ }
+ }, 0);
+ }
+ },
+ { once: true }
+ );
+
+ // Initialize our unit testing variables.
+ aTab.pageLoading = true;
+ aTab.pageLoaded = false;
+
+ // Now start loading the content.
+ aTab.title = this.loadingTabString;
+
+ ExtensionParent.apiManager.emit("extension-browser-inserted", aTab.browser);
+ let params = {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ postData: aArgs.postData || null,
+ };
+ aTab.browser.loadURI(Services.io.newURI("about:preferences"), params);
+
+ this.tab = aTab;
+ this.lastBrowserId++;
+ },
+
+ persistTab(aTab) {
+ if (aTab.browser.currentURI.spec == "about:blank") {
+ return null;
+ }
+
+ return {
+ paneID: aTab.paneID,
+ scrollPaneTo: aTab.scrollPaneTo,
+ otherArgs: aTab.otherArgs,
+ };
+ },
+
+ restoreTab(aTabmail, aPersistedState) {
+ aTabmail.openTab("preferencesTab", {
+ paneID: aPersistedState.paneID,
+ scrollPaneTo: aPersistedState.scrollPaneTo,
+ otherArgs: aPersistedState.otherArgs,
+ });
+ },
+};
diff --git a/comm/mail/components/preferences/privacy.inc.xhtml b/comm/mail/components/preferences/privacy.inc.xhtml
new file mode 100644
index 0000000000..6afa5bb840
--- /dev/null
+++ b/comm/mail/components/preferences/privacy.inc.xhtml
@@ -0,0 +1,597 @@
+# 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/.
+ <script src="chrome://messenger/content/preferences/privacy.js"/>
+
+ <stringbundle id="bundlePreferences" src="chrome://messenger/locale/preferences/preferences.properties"/>
+ <html:template id="panePrivacy">
+ <hbox id="privacyCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="privacy-main-header"></html:h1>
+ </hbox>
+
+ <!-- Mail Content -->
+ <html:div data-category="panePrivacy">
+ <html:fieldset id="mailContentGroup" data-category="panePrivacy">
+ <html:legend data-l10n-id="mail-content"></html:legend>
+ <hbox id="remoteContentBox">
+ <checkbox id="acceptRemoteContent"
+ preference="mailnews.message_display.disable_remote_image"
+ data-l10n-id="remote-content-label"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="remoteContentExceptions"
+ oncommand="gPrivacyPane.showRemoteContentExceptions();"
+ data-l10n-id="exceptions-button"
+ search-l10n-ids="
+ permissions-reminder-window2.title,
+ website-address-label.value,
+ block-button.label,
+ allow-session-button.label,
+ allow-button.label,
+ treehead-sitename-label.label,
+ treehead-status-label.label,
+ remove-site-button.label,
+ remove-all-site-button.label,
+ cancel-button.label,
+ save-button.label,
+ permission-can-label,
+ permission-can-access-first-party-label,
+ permission-can-session-label,
+ permission-cannot-label,
+ invalid-uri-message,
+ invalid-uri-title"/>
+ </hbox>
+ </hbox>
+ <hbox>
+ <label is="text-link" id="acceptRemoteContentInfo"
+ href="https://support.mozilla.org/kb/remote-content-in-messages"
+ data-l10n-id="remote-content-info"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Web Content -->
+ <html:div data-category="panePrivacy">
+ <html:fieldset id="webContentGroup" data-category="panePrivacy">
+ <html:legend data-l10n-id="web-content"></html:legend>
+ <checkbox id="keepHistory"
+ preference="places.history.enabled"
+ data-l10n-id="history-label"/>
+ <hbox id="cookiesBox">
+ <checkbox id="acceptCookies"
+ preference="network.cookie.cookieBehavior"
+ data-l10n-id="cookies-label"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="cookieExceptions"
+ oncommand="gPrivacyPane.showCookieExceptions();"
+ data-l10n-id="exceptions-button"
+ preference="pref.privacy.disable_button.cookie_exceptions"
+ search-l10n-ids="
+ permissions-reminder-window2.title,
+ website-address-label.value,
+ block-button.label,
+ allow-session-button.label,
+ allow-button.label,
+ treehead-sitename-label.label,
+ treehead-status-label.label,
+ remove-site-button.label,
+ remove-all-site-button.label,
+ cancel-button.label,
+ save-button.label,
+ permission-can-label,
+ permission-can-access-first-party-label,
+ permission-can-session-label,
+ permission-cannot-label,
+ invalid-uri-message,
+ invalid-uri-title"/>
+ </hbox>
+ </hbox>
+ <hbox id="acceptThirdPartyRow" class="indent">
+ <hbox id="acceptThirdPartyBox" align="center">
+ <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu"
+ data-l10n-id="third-party-label"/>
+ <hbox>
+ <menulist id="acceptThirdPartyMenu" preference="network.cookie.cookieBehavior">
+ <menupopup>
+ <menuitem data-l10n-id="third-party-always" value="always"/>
+ <menuitem data-l10n-id="third-party-visited" value="visited"/>
+ <menuitem data-l10n-id="third-party-never" value="never"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </hbox>
+ <hbox flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="showCookiesButton"
+ data-l10n-id="cookies-button"
+ oncommand="gPrivacyPane.showCookies();"
+ preference="pref.privacy.disable_button.view_cookies"
+ search-l10n-ids="
+ cookies-window-dialog2.title,
+ filter-search-label.value,
+ cookies-on-system-label,
+ treecol-site-header.label,
+ treecol-name-header.label,
+ props-name-label.value,
+ props-value-label.value,
+ props-domain-label.value,
+ props-path-label.value,
+ props-secure-label.value,
+ props-expires-label.value,
+ props-container-label.value,
+ remove-cookie-button.label,
+ remove-all-cookies-button.label,
+ cookie-close-button.label"/>
+ </hbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox id="privacyDoNotTrackCheckbox"
+ class="tail-with-learn-more"
+ data-l10n-id="do-not-track-label"
+ preference="privacy.donottrackheader.enabled"/>
+ <label is="text-link" id="doNotTrackInfo"
+ href="https://www.mozilla.org/dnt"
+ data-l10n-id="dnt-learn-more-button"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="privacyPasswordsCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="privacy-passwords-header"></html:h1>
+ </hbox>
+
+ <separator data-category="panePrivacy"/>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <hbox align="center">
+ <description data-l10n-id="passwords-description"/>
+ <spacer flex="1"/>
+ <hbox>
+ <button is="highlightable-button" id="showPasswords"
+ data-l10n-id="passwords-button"
+ oncommand="gPrivacyPane.showPasswords();"
+ preference="pref.privacy.disable_button.view_passwords"
+ search-l10n-ids="
+ saved-logins.title,
+ copy-provider-url-cmd.label,
+ copy-username-cmd.label,
+ edit-username-cmd.label,
+ copy-password-cmd.label,
+ edit-password-cmd.label,
+ search-filter.placeholder,
+ column-heading-provider.label,
+ column-heading-username.label,
+ column-heading-password.label,
+ column-heading-time-created.label,
+ column-heading-time-last-used.label,
+ column-heading-time-password-changed.label,
+ column-heading-times-used.label,
+ remove.label,
+ import.label,
+ show-passwords.label,
+ hide-passwords.label,
+ logins-description-all,
+ logins-description-filtered,
+ remove-all.label,
+ remove-all-shown.label,
+ remove-all-passwords-prompt,
+ remove-all-passwords-title,
+ no-master-password-prompt,
+ password-os-auth-dialog-message,
+ password-os-auth-dialog-message-macosx,
+ password-os-auth-dialog-caption"/>
+ </hbox>
+ </hbox>
+ <!-- XXX button to do a showExceptions()? -->
+
+ <separator class="thin"/>
+
+ <description data-l10n-id="primary-password-description"/>
+ <hbox>
+ <checkbox id="useMasterPassword"
+ data-l10n-id="primary-password-label"
+ oncommand="gPrivacyPane.updateMasterPasswordButton();"/>
+ <spacer flex="1"/>
+ <button is="highlightable-button" id="changeMasterPassword"
+ data-l10n-id="primary-password-button"
+ oncommand="gPrivacyPane.changeMasterPassword();"/>
+ </hbox>
+ <!--
+ Those two strings are meant to be invisible and will be used exclusively to provide
+ localization for an alert window.
+ -->
+ <label id="fips-title" hidden="true" data-l10n-id="forms-primary-pw-fips-title"></label>
+ <label id="fips-desc" hidden="true" data-l10n-id="forms-master-pw-fips-desc"></label>
+ </html:fieldset>
+ </html:div>
+
+ <hbox id="privacyJunkCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="privacy-junk-header"></html:h1>
+ </hbox>
+
+ <separator data-category="panePrivacy"/>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <description data-l10n-id="junk-description"/>
+ <separator class="thin"/>
+ <hbox>
+ <checkbox id="manualMark"
+ data-l10n-id="junk-label"
+ preference="mail.spam.manualMark"
+ oncommand="gPrivacyPane.updateManualMarkMode(this.checked);"/>
+ <spacer flex="1"/>
+ </hbox>
+ <radiogroup id="manualMarkMode"
+ class="indent"
+ preference="mail.spam.manualMarkMode"
+ aria-labelledby="manualMark">
+ <hbox>
+ <radio id="manualMarkMode0"
+ value="0"
+ data-l10n-id="junk-move-label"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox>
+ <radio id="manualMarkMode1"
+ value="1"
+ data-l10n-id="junk-delete-label"/>
+ <spacer flex="1"/>
+ </hbox>
+ </radiogroup>
+ <hbox>
+ <checkbox id="markAsReadOnSpam"
+ data-l10n-id="junk-read-label"
+ preference="mail.spam.markAsReadOnSpam"/>
+ <spacer flex="1"/>
+ </hbox>
+ <hbox align="start">
+ <checkbox id="enableJunkLogging" data-l10n-id="junk-log-label"
+ oncommand="gPrivacyPane.updateJunkLogButton(this.checked);"
+ preference="mail.spam.logging.enabled"/>
+ <spacer flex="1"/>
+ <button is="highlightable-button" id="openJunkLogButton"
+ data-l10n-id="junk-log-button"
+ oncommand="gPrivacyPane.openJunkLog();"/>
+ </hbox>
+ <hbox align="start">
+ <spacer flex="1"/>
+ <button is="highlightable-button"
+ data-l10n-id="reset-junk-button"
+ oncommand="gPrivacyPane.resetTrainingData()"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+#ifdef MOZ_DATA_REPORTING
+ <hbox id="privacyDataCollectionCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="collection-header"></html:h1>
+ </hbox>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <description>
+ <label class="tail-with-learn-more" data-l10n-id="collection-description"/>
+ <label id="dataCollectionPrivacyNotice"
+ class="learnMore" is="text-link"
+ data-l10n-id="collection-privacy-notice"/>
+ </description>
+ <description>
+ <html:div id="telemetry-container" hidden="hidden">
+ <html:img id="telemetryInfoIcon" alt=""
+ src="chrome://global/skin/icons/info.svg" />
+ <html:span id="telemetryDisabledDescription"
+ class="tail-with-learn-more"
+ data-l10n-id="collection-health-report-telemetry-disabled">
+ </html:span>
+ <button id="telemetryDataDeletionLearnMore"
+ class="learnMore" is="text-link"
+ data-l10n-id="collection-health-report-telemetry-disabled-link"/>
+ </html:div>
+ </description>
+ <vbox data-subcategory="reports">
+ <hbox align="center">
+ <checkbox id="submitHealthReportBox"
+ data-l10n-id="collection-health-report"
+ class="tail-with-learn-more"/>
+ <label id="FHRLearnMore"
+ class="learnMore" is="text-link"
+ data-l10n-id="collection-health-report-link"/>
+ </hbox>
+#ifndef MOZ_TELEMETRY_REPORTING
+ <description id="TelemetryDisabledDesc"
+ class="indent tip-caption" control="telemetryGroup"
+ data-l10n-id="collection-health-report-disabled"/>
+#endif
+
+#ifdef MOZ_CRASHREPORTER
+ <hbox align="center">
+ <checkbox id="automaticallySubmitCrashesBox"
+ class="tail-with-learn-more"
+ preference="browser.crashReports.unsubmittedCheck.autoSubmit2"
+ data-l10n-id="collection-backlogged-crash-reports"/>
+ <label id="crashReporterLearnMore"
+ class="learnMore" is="text-link" data-l10n-id="collection-backlogged-crash-reports-link"/>
+ </hbox>
+#endif
+ </vbox>
+ </html:fieldset>
+ </html:div>
+#endif
+
+ <hbox id="privacySecurityCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="privacy-security-header"></html:h1>
+ </hbox>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <html:legend data-l10n-id="privacy-scam-detection-title"></html:legend>
+ <description data-l10n-id="phishing-description"/>
+ <separator class="thin"/>
+ <hbox>
+ <checkbox id="enablePhishingDetector"
+ data-l10n-id="phishing-label"
+ preference="mail.phishing.detection.enabled"/>
+ <spacer flex="1"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Anti Virus -->
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <html:legend data-l10n-id="privacy-anti-virus-title"></html:legend>
+ <description data-l10n-id="antivirus-description"/>
+ <separator class="thin"/>
+ <hbox>
+ <checkbox id="enableAntiVirusQuarantine"
+ data-l10n-id="antivirus-label"
+ preference="mailnews.downloadToTempFile"/>
+ <spacer flex="1"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset data-category="panePrivacy">
+ <html:legend data-l10n-id="privacy-certificates-title"></html:legend>
+ <description id="CertSelectionDesc" control="certSelection"
+ data-l10n-id="certificate-description"/>
+
+ <!--
+ The values on these radio buttons may look like l12y issues, but
+ they're not - this preference uses *those strings* as its values.
+ I KID YOU NOT.
+ -->
+ <radiogroup id="certSelection" class="indent"
+ orient="horizontal" preftype="string"
+ preference="security.default_personal_cert"
+ aria-labelledby="CertGroupCaption CertSelectionDesc">
+ <radio id="certSelectionAuto"
+ data-l10n-id="certificate-auto"
+ value="Select Automatically"/>
+ <radio id="certSelectionAsk"
+ data-l10n-id="certificate-ask"
+ value="Ask Every Time"/>
+ </radiogroup>
+
+ <separator/>
+
+ <hbox align="start">
+ <checkbox id="enableOCSP"
+ data-l10n-id="ocsp-label"
+ preference="security.OCSP.enabled"
+ flex="1"/>
+ <spacer flex="1"/>
+ <vbox>
+ <hbox flex="1">
+ <button is="highlightable-button" id="manageCertificatesButton"
+ data-l10n-id="certificate-button"
+ flex="1"
+ oncommand="gPrivacyPane.showCertificates();"
+ preference="security.disable_button.openCertManager"
+ search-l10n-ids="
+ certmgr-title.title,
+ certmgr-tab-mine.label,
+ certmgr-tab-remembered.label,
+ certmgr-tab-people.label,
+ certmgr-tab-servers.label,
+ certmgr-tab-ca.label,
+ certmgr-mine,
+ certmgr-remembered,
+ certmgr-people,
+ certmgr-ca,
+ certmgr-server,
+ certmgr-edit-ca-cert2.title,
+ certmgr-edit-cert-edit-trust,
+ certmgr-edit-cert-trust-ssl.label,
+ certmgr-edit-cert-trust-email.label,
+ certmgr-delete-cert2.title,
+ certmgr-cert-host.label,
+ certmgr-cert-name.label,
+ certmgr-cert-server.label,
+ certmgr-token-name.label,
+ certmgr-begins-label.label,
+ certmgr-expires-label.label,
+ certmgr-email.label,
+ certmgr-serial.label,
+ certmgr-fingerprint-sha-256.label,
+ certmgr-view.label,
+ certmgr-edit.label,
+ certmgr-export.label,
+ certmgr-delete.label,
+ certmgr-delete-builtin.label,
+ certmgr-backup.label,
+ certmgr-backup-all.label,
+ certmgr-restore.label,
+ certmgr-add-exception.label,
+ exception-mgr.title,
+ exception-mgr-extra-button.label,
+ exception-mgr-supplemental-warning,
+ exception-mgr-cert-location-url.value,
+ exception-mgr-cert-location-download.label,
+ exception-mgr-cert-status-view-cert.label,
+ exception-mgr-permanent.label,
+ pk11-bad-password,
+ pkcs12-decode-err,
+ pkcs12-unknown-err-restore,
+ pkcs12-unknown-err-backup,
+ pkcs12-unknown-err,
+ pkcs12-info-no-smartcard-backup,
+ pkcs12-dup-data,
+ choose-p12-backup-file-dialog,
+ file-browse-pkcs12-spec,
+ choose-p12-restore-file-dialog,
+ file-browse-certificate-spec,
+ import-ca-certs-prompt,
+ import-email-cert-prompt,
+ delete-user-cert-title.title,
+ delete-user-cert-confirm,
+ delete-user-cert-impact,
+ delete-ca-cert-title.title,
+ delete-ca-cert-confirm,
+ delete-ca-cert-impact,
+ delete-ssl-override-title.title,
+ delete-ssl-override-confirm,
+ delete-ssl-override-impact,
+ delete-email-cert-title.title,
+ delete-email-cert-confirm,
+ delete-email-cert-impact,
+ send-no-client-certificate,
+ no-cert-stored-for-override,
+ permanent-override,
+ temporary-override,
+ add-exception-branded-warning,
+ add-exception-invalid-header,
+ add-exception-domain-mismatch-short,
+ add-exception-domain-mismatch-long,
+ add-exception-expired-short,
+ add-exception-expired-long,
+ add-exception-unverified-or-bad-signature-short,
+ add-exception-unverified-or-bad-signature-long,
+ add-exception-valid-short,
+ add-exception-valid-long,
+ add-exception-checking-short,
+ add-exception-checking-long,
+ add-exception-no-cert-short,
+ add-exception-no-cert-long,
+ save-cert-as,
+ cert-format-base64,
+ cert-format-base64-chain,
+ write-file-failure,
+ cert-format-der,
+ cert-format-pkcs7,
+ cert-format-pkcs7-chain"/>
+ </hbox>
+ <hbox flex="1">
+ <button is="highlightable-button" id="viewSecurityDevicesButton"
+ data-l10n-id="security-devices-button"
+ flex="1"
+ oncommand="gPrivacyPane.showSecurityDevices();"
+ preference="security.disable_button.openDeviceManager"
+ search-l10n-ids="
+ devmgr-window.title,
+ devmgr-devlist.label,
+ devmgr-header-details.label,
+ devmgr-header-value.label,
+ devmgr-button-login.label,
+ devmgr-button-logout.label,
+ devmgr-button-changepw.label,
+ devmgr-button-load.label,
+ devmgr-button-unload.label,
+ devmgr-button-enable-fips.label,
+ devmgr-button-disable-fips.label,
+ load-device.title,
+ load-device-info,
+ load-device-modname.value,
+ load-device-modname-default.value,
+ load-device-filename.value,
+ load-device-browse.label,
+ devinfo-status.label,
+ devinfo-status-disabled.label,
+ devinfo-status-not-present.label,
+ devinfo-status-uninitialized.label,
+ devinfo-status-not-logged-in.label,
+ devinfo-status-logged-in.label,
+ devinfo-status-ready.label,
+ devinfo-desc.label,
+ unable-to-toggle-fips,
+ load-pk11-module-file-picker-title,
+ fips-nonempty-primary-password-required,
+ load-module-help-root-certs-module-name.value,
+ add-module-failure,
+ del-module-warning,
+ del-module-error,
+ devinfo-man-id.label,
+ devinfo-hwversion.label,
+ load-module-help-empty-module-name.value,
+ devinfo-fwversion.label,
+ devinfo-modname.label,
+ devinfo-modpath.label,
+ login-failed,
+ devinfo-label.label,
+ devinfo-serialnum"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <!-- Email End-To-End Encryption -->
+ <hbox id="privacyCategory"
+ class="subcategory"
+ data-category="panePrivacy">
+ <html:h1 data-l10n-id="email-e2ee-header"></html:h1>
+ </hbox>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset id="emailE2eeGroupPreparation" data-category="panePrivacy">
+ <description data-l10n-id="email-e2ee-enable-info"/>
+ <html:button id="settingsButton"
+ type="button"
+ data-l10n-id="account-button"
+ class="button button-flat"
+ onclick="window.browsingContext.topChromeWindow.MsgAccountManager(null);">
+ </html:button>
+ </html:fieldset>
+ </html:div>
+
+ <html:div data-category="panePrivacy">
+ <html:fieldset id="emailE2eeGroupAutomatism" data-category="panePrivacy">
+ <html:legend data-l10n-id="email-e2ee-automatism"></html:legend>
+ <description data-l10n-id="email-e2ee-automatism-pre"/>
+ <separator class="thin"/>
+
+ <checkbox id="emailE2eeAutoEnable"
+ preference="mail.e2ee.auto_enable"
+ data-l10n-id="email-e2ee-auto-on"
+ oncommand="gPrivacyPane.updateE2eeCheckboxes();"/>
+ <checkbox id="emailE2eeAutoDisable"
+ preference="mail.e2ee.auto_disable"
+ data-l10n-id="email-e2ee-auto-off"
+ oncommand="gPrivacyPane.updateE2eeCheckboxes();"/>
+ <checkbox id="emailE2eeAutoDisableNotify"
+ preference="mail.e2ee.notify_on_auto_disable"
+ data-l10n-id="email-e2ee-auto-off-notify"
+ oncommand="gPrivacyPane.updateE2eeCheckboxes();"/>
+
+ <separator class="thin"/>
+ <description data-l10n-id="email-e2ee-automatism-post"/>
+ </html:fieldset>
+ </html:div>
+ </html:template>
diff --git a/comm/mail/components/preferences/privacy.js b/comm/mail/components/preferences/privacy.js
new file mode 100644
index 0000000000..2bb8cba6ad
--- /dev/null
+++ b/comm/mail/components/preferences/privacy.js
@@ -0,0 +1,562 @@
+/* -*- 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/. */
+
+"use strict";
+
+/* import-globals-from preferences.js */
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
+ OSKeyStore: "resource://gre/modules/OSKeyStore.sys.mjs",
+});
+
+const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
+
+Preferences.addAll([
+ { id: "mail.spam.manualMark", type: "bool" },
+ { id: "mail.spam.manualMarkMode", type: "int" },
+ { id: "mail.spam.markAsReadOnSpam", type: "bool" },
+ { id: "mail.spam.logging.enabled", type: "bool" },
+ { id: "mail.phishing.detection.enabled", type: "bool" },
+ { id: "browser.safebrowsing.enabled", type: "bool" },
+ { id: "mailnews.downloadToTempFile", type: "bool" },
+ { id: "pref.privacy.disable_button.view_passwords", type: "bool" },
+ { id: "pref.privacy.disable_button.cookie_exceptions", type: "bool" },
+ { id: "pref.privacy.disable_button.view_cookies", type: "bool" },
+ {
+ id: "mailnews.message_display.disable_remote_image",
+ type: "bool",
+ inverted: "true",
+ },
+ { id: "places.history.enabled", type: "bool" },
+ { id: "network.cookie.cookieBehavior", type: "int" },
+ { id: "network.cookie.blockFutureCookies", type: "bool" },
+ { id: "privacy.donottrackheader.enabled", type: "bool" },
+ { id: "security.default_personal_cert", type: "string" },
+ { id: "security.disable_button.openCertManager", type: "bool" },
+ { id: "security.disable_button.openDeviceManager", type: "bool" },
+ { id: "security.OCSP.enabled", type: "int" },
+ { id: "mail.e2ee.auto_enable", type: "bool" },
+ { id: "mail.e2ee.auto_disable", type: "bool" },
+ { id: "mail.e2ee.notify_on_auto_disable", type: "bool" },
+]);
+
+if (AppConstants.MOZ_DATA_REPORTING) {
+ Preferences.addAll([
+ // Preference instances for prefs that we need to monitor while the page is open.
+ { id: PREF_UPLOAD_ENABLED, type: "bool" },
+ ]);
+}
+
+// Data Choices tab
+if (AppConstants.MOZ_CRASHREPORTER) {
+ Preferences.add({
+ id: "browser.crashReports.unsubmittedCheck.autoSubmit2",
+ type: "bool",
+ });
+}
+
+function setEventListener(aId, aEventType, aCallback) {
+ document
+ .getElementById(aId)
+ .addEventListener(aEventType, aCallback.bind(gPrivacyPane));
+}
+
+var gPrivacyPane = {
+ init() {
+ this.updateManualMarkMode(Preferences.get("mail.spam.manualMark").value);
+ this.updateJunkLogButton(
+ Preferences.get("mail.spam.logging.enabled").value
+ );
+
+ this._initMasterPasswordUI();
+
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ this.initDataCollection();
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ this.initSubmitCrashes();
+ }
+ this.initSubmitHealthReport();
+ setEventListener(
+ "submitHealthReportBox",
+ "command",
+ gPrivacyPane.updateSubmitHealthReport
+ );
+ setEventListener(
+ "telemetryDataDeletionLearnMore",
+ "command",
+ gPrivacyPane.showDataDeletion
+ );
+ }
+
+ this.readAcceptCookies();
+ let element = document.getElementById("acceptCookies");
+ Preferences.addSyncFromPrefListener(element, () =>
+ this.readAcceptCookies()
+ );
+ Preferences.addSyncToPrefListener(element, () => this.writeAcceptCookies());
+
+ element = document.getElementById("acceptThirdPartyMenu");
+ Preferences.addSyncFromPrefListener(element, () =>
+ this.readAcceptThirdPartyCookies()
+ );
+ Preferences.addSyncToPrefListener(element, () =>
+ this.writeAcceptThirdPartyCookies()
+ );
+
+ element = document.getElementById("enableOCSP");
+ Preferences.addSyncFromPrefListener(element, () => this.readEnableOCSP());
+ Preferences.addSyncToPrefListener(element, () => this.writeEnableOCSP());
+
+ this.initE2eeCheckboxes();
+ },
+
+ /**
+ * Reload the current message after a preference affecting the view
+ * has been changed.
+ */
+ reloadMessageInOpener() {
+ if (window.opener && typeof window.opener.ReloadMessage == "function") {
+ window.opener.ReloadMessage();
+ }
+ },
+
+ /**
+ * Reads the network.cookie.cookieBehavior preference value and
+ * enables/disables the rest of the cookie UI accordingly, returning true
+ * if cookies are enabled.
+ */
+ readAcceptCookies() {
+ let pref = Preferences.get("network.cookie.cookieBehavior");
+ let exceptionsButton = document.getElementById("cookieExceptions");
+ let acceptThirdPartyLabel = document.getElementById(
+ "acceptThirdPartyLabel"
+ );
+ let acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu");
+ let showCookiesButton = document.getElementById("showCookiesButton");
+
+ // enable the rest of the UI for anything other than "disable all cookies"
+ let acceptCookies = pref.value != 2;
+ let cookieBehaviorLocked = Services.prefs.prefIsLocked(
+ "network.cookie.cookieBehavior"
+ );
+
+ exceptionsButton.disabled = cookieBehaviorLocked;
+ acceptThirdPartyLabel.disabled = acceptThirdPartyMenu.disabled =
+ !acceptCookies || cookieBehaviorLocked;
+ showCookiesButton.disabled = cookieBehaviorLocked;
+
+ return acceptCookies;
+ },
+
+ /**
+ * Enables/disables the "keep until" label and menulist in response to the
+ * "accept cookies" checkbox being checked or unchecked.
+ *
+ * @returns 0 if cookies are accepted, 2 if they are not;
+ * the value network.cookie.cookieBehavior should get
+ */
+ writeAcceptCookies() {
+ let accept = document.getElementById("acceptCookies");
+ let acceptThirdPartyMenu = document.getElementById("acceptThirdPartyMenu");
+ // if we're enabling cookies, automatically select 'accept third party always'
+ if (accept.checked) {
+ acceptThirdPartyMenu.selectedIndex = 0;
+ }
+
+ return accept.checked ? 0 : 2;
+ },
+
+ /**
+ * Displays fine-grained, per-site preferences for cookies.
+ */
+ showCookieExceptions() {
+ let bundle = document.getElementById("bundlePreferences");
+ let params = {
+ blockVisible: true,
+ sessionVisible: true,
+ allowVisible: true,
+ prefilledHost: "",
+ permissionType: "cookie",
+ windowTitle: bundle.getString("cookiepermissionstitle"),
+ introText: bundle.getString("cookiepermissionstext"),
+ };
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+
+ /**
+ * Displays all the user's cookies in a dialog.
+ */
+ showCookies(aCategory) {
+ gSubDialog.open("chrome://messenger/content/preferences/cookies.xhtml");
+ },
+
+ /**
+ * Converts between network.cookie.cookieBehavior and the third-party cookie UI
+ */
+ readAcceptThirdPartyCookies() {
+ let pref = Preferences.get("network.cookie.cookieBehavior");
+ switch (pref.value) {
+ case 0:
+ return "always";
+ case 1:
+ return "never";
+ case 2:
+ return "never";
+ case 3:
+ return "visited";
+ default:
+ return undefined;
+ }
+ },
+
+ writeAcceptThirdPartyCookies() {
+ let accept = document.getElementById("acceptThirdPartyMenu").selectedItem;
+ switch (accept.value) {
+ case "always":
+ return 0;
+ case "visited":
+ return 3;
+ case "never":
+ return 1;
+ default:
+ return undefined;
+ }
+ },
+
+ /**
+ * Displays fine-grained, per-site preferences for remote content.
+ * We use the "image" type for that, but it can also be stylesheets or
+ * iframes.
+ */
+ showRemoteContentExceptions() {
+ let bundle = document.getElementById("bundlePreferences");
+ let params = {
+ blockVisible: true,
+ sessionVisible: false,
+ allowVisible: true,
+ prefilledHost: "",
+ permissionType: "image",
+ windowTitle: bundle.getString("imagepermissionstitle"),
+ introText: bundle.getString("imagepermissionstext"),
+ };
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ undefined,
+ params
+ );
+ },
+ updateManualMarkMode(aEnableRadioGroup) {
+ document.getElementById("manualMarkMode").disabled = !aEnableRadioGroup;
+ },
+
+ updateJunkLogButton(aEnableButton) {
+ document.getElementById("openJunkLogButton").disabled = !aEnableButton;
+ },
+
+ openJunkLog() {
+ // The junk log dialog can't work as a sub-dialog, because that means
+ // loading it in a browser, and we can't load a chrome: page containing a
+ // file: page in a browser. Open it as a real dialog instead.
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/junkLog.xhtml"
+ );
+ },
+
+ resetTrainingData() {
+ // make sure the user really wants to do this
+ var bundle = document.getElementById("bundlePreferences");
+ var title = bundle.getString("confirmResetJunkTrainingTitle");
+ var text = bundle.getString("confirmResetJunkTrainingText");
+
+ // if the user says no, then just fall out
+ if (!Services.prompt.confirm(window, title, text)) {
+ return;
+ }
+
+ // otherwise go ahead and remove the training data
+ MailServices.junk.resetTrainingData();
+ },
+
+ /**
+ * Initializes primary password UI: the "use primary password" checkbox, selects
+ * the primary password button to show, and enables/disables it as necessary.
+ * The primary password is controlled by various bits of NSS functionality,
+ * so the UI for it can't be controlled by the normal preference bindings.
+ */
+ _initMasterPasswordUI() {
+ var noMP = !LoginHelper.isPrimaryPasswordSet();
+
+ var button = document.getElementById("changeMasterPassword");
+ button.disabled = noMP;
+
+ var checkbox = document.getElementById("useMasterPassword");
+ checkbox.checked = !noMP;
+ checkbox.disabled =
+ (noMP && !Services.policies.isAllowed("createMasterPassword")) ||
+ (!noMP && !Services.policies.isAllowed("removeMasterPassword"));
+ },
+
+ /**
+ * Enables/disables the primary password button depending on the state of the
+ * "use primary password" checkbox, and prompts for primary password removal
+ * if one is set.
+ */
+ async updateMasterPasswordButton() {
+ var checkbox = document.getElementById("useMasterPassword");
+ var button = document.getElementById("changeMasterPassword");
+ button.disabled = !checkbox.checked;
+
+ // unchecking the checkbox should try to immediately remove the master
+ // password, because it's impossible to non-destructively remove the master
+ // password used to encrypt all the passwords without providing it (by
+ // design), and it would be extremely odd to pop up that dialog when the
+ // user closes the prefwindow and saves his settings
+ if (!checkbox.checked) {
+ await this._removeMasterPassword();
+ } else {
+ await this.changeMasterPassword();
+ }
+
+ this._initMasterPasswordUI();
+ },
+
+ /**
+ * Displays the "remove primary password" dialog to allow the user to remove
+ * the current primary password. When the dialog is dismissed, primary password
+ * UI is automatically updated.
+ */
+ async _removeMasterPassword() {
+ var secmodDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(
+ Ci.nsIPKCS11ModuleDB
+ );
+ if (secmodDB.isFIPSEnabled) {
+ let title = document.getElementById("fips-title").textContent;
+ let desc = document.getElementById("fips-desc").textContent;
+ Services.prompt.alert(window, title, desc);
+ this._initMasterPasswordUI();
+ } else {
+ gSubDialog.open("chrome://mozapps/content/preferences/removemp.xhtml", {
+ closingCallback: this._initMasterPasswordUI.bind(this),
+ });
+ }
+ this._initMasterPasswordUI();
+ },
+
+ /**
+ * Displays a dialog in which the primary password may be changed.
+ */
+ async changeMasterPassword() {
+ // OS reauthenticate functionality is not available on Linux yet (bug 1527745)
+ if (
+ !LoginHelper.isPrimaryPasswordSet() &&
+ Services.prefs.getBoolPref("signon.management.page.os-auth.enabled") &&
+ AppConstants.platform != "linux"
+ ) {
+ let messageId =
+ "primary-password-os-auth-dialog-message-" + AppConstants.platform;
+ let [messageText, captionText] = await document.l10n.formatMessages([
+ {
+ id: messageId,
+ },
+ {
+ id: "master-password-os-auth-dialog-caption",
+ },
+ ]);
+ let win = Services.wm.getMostRecentWindow("");
+ let loggedIn = await OSKeyStore.ensureLoggedIn(
+ messageText.value,
+ captionText.value,
+ win,
+ false
+ );
+ if (!loggedIn.authenticated) {
+ return;
+ }
+ }
+
+ gSubDialog.open("chrome://mozapps/content/preferences/changemp.xhtml", {
+ closingCallback: this._initMasterPasswordUI.bind(this),
+ });
+ },
+
+ /**
+ * Shows the sites where the user has saved passwords and the associated
+ * login information.
+ */
+ showPasswords() {
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/passwordManager.xhtml"
+ );
+ },
+
+ updateDownloadedPhishingListState() {
+ document.getElementById("useDownloadedList").disabled =
+ !document.getElementById("enablePhishingDetector").checked;
+ },
+
+ /**
+ * Display the user's certificates and associated options.
+ */
+ showCertificates() {
+ gSubDialog.open("chrome://pippki/content/certManager.xhtml");
+ },
+
+ /**
+ * security.OCSP.enabled is an integer value for legacy reasons.
+ * A value of 1 means OCSP is enabled. Any other value means it is disabled.
+ */
+ readEnableOCSP() {
+ var preference = Preferences.get("security.OCSP.enabled");
+ // This is the case if the preference is the default value.
+ if (preference.value === undefined) {
+ return true;
+ }
+ return preference.value == 1;
+ },
+
+ /**
+ * See documentation for readEnableOCSP.
+ */
+ writeEnableOCSP() {
+ var checkbox = document.getElementById("enableOCSP");
+ return checkbox.checked ? 1 : 0;
+ },
+
+ /**
+ * Display a dialog from which the user can manage his security devices.
+ */
+ showSecurityDevices() {
+ gSubDialog.open("chrome://pippki/content/device_manager.xhtml");
+ },
+
+ /**
+ * Displays the learn more health report page when a user opts out of data collection.
+ */
+ showDataDeletion() {
+ let url =
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "telemetry-clientid";
+ window.open(url, "_blank");
+ },
+
+ initDataCollection() {
+ this._setupLearnMoreLink(
+ "toolkit.datacollection.infoURL",
+ "dataCollectionPrivacyNotice"
+ );
+ },
+
+ initSubmitCrashes() {
+ this._setupLearnMoreLink(
+ "toolkit.crashreporter.infoURL",
+ "crashReporterLearnMore"
+ );
+ },
+
+ /**
+ * Set up or hide the Learn More links for various data collection options
+ */
+ _setupLearnMoreLink(pref, element) {
+ // set up the Learn More link with the correct URL
+ let url = Services.urlFormatter.formatURLPref(pref);
+ let el = document.getElementById(element);
+
+ if (url) {
+ el.setAttribute("href", url);
+ } else {
+ el.setAttribute("hidden", "true");
+ }
+ },
+
+ /**
+ * Initialize the health report service reference and checkbox.
+ */
+ initSubmitHealthReport() {
+ this._setupLearnMoreLink(
+ "datareporting.healthreport.infoURL",
+ "FHRLearnMore"
+ );
+
+ let checkbox = document.getElementById("submitHealthReportBox");
+
+ // Telemetry is only sending data if MOZ_TELEMETRY_REPORTING is defined.
+ // We still want to display the preferences panel if that's not the case, but
+ // we want it to be disabled and unchecked.
+ if (
+ Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED) ||
+ !AppConstants.MOZ_TELEMETRY_REPORTING
+ ) {
+ checkbox.setAttribute("disabled", "true");
+ return;
+ }
+
+ checkbox.checked =
+ Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED) &&
+ AppConstants.MOZ_TELEMETRY_REPORTING;
+ },
+
+ /**
+ * Update the health report preference with state from checkbox.
+ */
+ updateSubmitHealthReport() {
+ let checkbox = document.getElementById("submitHealthReportBox");
+
+ Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked);
+
+ // If allow telemetry is checked, hide the box saying you're no longer
+ // allowing it.
+ document.getElementById("telemetry-container").hidden = checkbox.checked;
+ },
+
+ initE2eeCheckboxes() {
+ let on = document.getElementById("emailE2eeAutoEnable");
+ let off = document.getElementById("emailE2eeAutoDisable");
+ let notify = document.getElementById("emailE2eeAutoDisableNotify");
+
+ on.checked = Preferences.get("mail.e2ee.auto_enable").value;
+ off.checked = Preferences.get("mail.e2ee.auto_disable").value;
+ notify.checked = Preferences.get("mail.e2ee.notify_on_auto_disable").value;
+
+ if (!on.checked) {
+ off.disabled = true;
+ notify.disabled = true;
+ } else {
+ off.disabled = false;
+ notify.disabled = !off.checked;
+ }
+ },
+
+ updateE2eeCheckboxes() {
+ let on = document.getElementById("emailE2eeAutoEnable");
+ let off = document.getElementById("emailE2eeAutoDisable");
+ let notify = document.getElementById("emailE2eeAutoDisableNotify");
+
+ if (!on.checked) {
+ off.disabled = true;
+ notify.disabled = true;
+ } else {
+ off.disabled = false;
+ notify.disabled = !off.checked;
+ }
+ },
+};
+
+Preferences.get("mailnews.message_display.disable_remote_image").on(
+ "change",
+ gPrivacyPane.reloadMessageInOpener
+);
+Preferences.get("mail.phishing.detection.enabled").on(
+ "change",
+ gPrivacyPane.reloadMessageInOpener
+);
diff --git a/comm/mail/components/preferences/receipts.js b/comm/mail/components/preferences/receipts.js
new file mode 100644
index 0000000000..748a65d127
--- /dev/null
+++ b/comm/mail/components/preferences/receipts.js
@@ -0,0 +1,38 @@
+/* -*- 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/. */
+
+/* import-globals-from ../../../../toolkit/content/preferencesBindings.js */
+
+Preferences.addAll([
+ { id: "mail.receipt.request_return_receipt_on", type: "bool" },
+ { id: "mail.incorporate.return_receipt", type: "int" },
+ { id: "mail.mdn.report.enabled", type: "bool" },
+ { id: "mail.mdn.report.not_in_to_cc", type: "int" },
+ { id: "mail.mdn.report.outside_domain", type: "int" },
+ { id: "mail.mdn.report.other", type: "int" },
+]);
+
+/**
+ * Enables/disables the labels and menulists depending whether
+ * sending of return receipts is enabled.
+ */
+function enableDisableAllowedReceipts() {
+ let enable = document.getElementById("receiptSend").value === "true";
+ enableElement(document.getElementById("notInToCcLabel"), enable);
+ enableElement(document.getElementById("notInToCcPref"), enable);
+ enableElement(document.getElementById("outsideDomainLabel"), enable);
+ enableElement(document.getElementById("outsideDomainPref"), enable);
+ enableElement(document.getElementById("otherCasesLabel"), enable);
+ enableElement(document.getElementById("otherCasesPref"), enable);
+}
+
+/**
+ * Set disabled state of aElement, unless its associated pref is locked.
+ */
+function enableElement(aElement, aEnable) {
+ let pref = aElement.getAttribute("preference");
+ let prefIsLocked = pref ? Preferences.get(pref).locked : false;
+ aElement.disabled = !aEnable || prefIsLocked;
+}
diff --git a/comm/mail/components/preferences/receipts.xhtml b/comm/mail/components/preferences/receipts.xhtml
new file mode 100644
index 0000000000..32f8a7237d
--- /dev/null
+++ b/comm/mail/components/preferences/receipts.xhtml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+
+<!DOCTYPE window>
+
+<window
+ type="child"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="receipts-dialog-window"
+ onload="enableDisableAllowedReceipts();"
+>
+ <dialog id="ReturnReceiptsDialog" dlgbuttons="accept,cancel">
+ <script src="chrome://global/content/preferencesBindings.js" />
+ <script src="chrome://messenger/content/preferences/receipts.js" />
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/receipts.ftl" />
+ </linkset>
+
+ <vbox id="returnReceiptSettings" align="start">
+ <checkbox
+ id="alwaysRequest"
+ data-l10n-id="return-receipt-checkbox-control"
+ preference="mail.receipt.request_return_receipt_on"
+ />
+ </vbox>
+
+ <separator class="thin" />
+ <separator class="groove" />
+ <separator class="thin" />
+
+ <label control="receiptFolder" data-l10n-id="receipt-arrive-label" />
+ <radiogroup
+ id="receiptFolder"
+ class="indent"
+ preference="mail.incorporate.return_receipt"
+ >
+ <radio value="0" data-l10n-id="receipt-leave-radio-control" />
+ <radio value="1" data-l10n-id="receipt-move-radio-control" />
+ </radiogroup>
+
+ <separator class="thin" />
+ <separator class="groove" />
+ <separator class="thin" />
+
+ <label control="receiptSend" data-l10n-id="receipt-request-label" />
+ <radiogroup
+ id="receiptSend"
+ class="indent"
+ preference="mail.mdn.report.enabled"
+ oncommand="enableDisableAllowedReceipts();"
+ >
+ <radio value="false" data-l10n-id="receipt-return-never-radio-control" />
+ <radio value="true" data-l10n-id="receipt-return-some-radio-control" />
+
+ <vbox class="indent">
+ <hbox align="center">
+ <hbox flex="1">
+ <label
+ id="notInToCcLabel"
+ data-l10n-id="receipt-not-to-cc-label"
+ control="notInToCcPref"
+ />
+ </hbox>
+ <menulist
+ id="notInToCcPref"
+ preference="mail.mdn.report.not_in_to_cc"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="receipt-send-never-label" />
+ <menuitem value="1" data-l10n-id="receipt-send-always-label" />
+ <menuitem value="2" data-l10n-id="receipt-send-ask-label" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox align="center">
+ <hbox flex="1">
+ <label
+ id="outsideDomainLabel"
+ data-l10n-id="sender-outside-domain-label"
+ control="outsideDomainPref"
+ />
+ </hbox>
+ <menulist
+ id="outsideDomainPref"
+ preference="mail.mdn.report.outside_domain"
+ >
+ <menupopup>
+ <menuitem value="0" data-l10n-id="receipt-send-never-label" />
+ <menuitem value="1" data-l10n-id="receipt-send-always-label" />
+ <menuitem value="2" data-l10n-id="receipt-send-ask-label" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox align="center">
+ <hbox flex="1">
+ <label
+ id="otherCasesLabel"
+ data-l10n-id="other-cases-text-label"
+ control="otherCasesPref"
+ />
+ </hbox>
+ <menulist id="otherCasesPref" preference="mail.mdn.report.other">
+ <menupopup>
+ <menuitem value="0" data-l10n-id="receipt-send-never-label" />
+ <menuitem value="1" data-l10n-id="receipt-send-always-label" />
+ <menuitem value="2" data-l10n-id="receipt-send-ask-label" />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+ </radiogroup>
+ <separator />
+ </dialog>
+</window>
diff --git a/comm/mail/components/preferences/searchResults.inc.xhtml b/comm/mail/components/preferences/searchResults.inc.xhtml
new file mode 100644
index 0000000000..9fa66a27c5
--- /dev/null
+++ b/comm/mail/components/preferences/searchResults.inc.xhtml
@@ -0,0 +1,24 @@
+<!-- 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/. -->
+
+ <hbox id="header-searchResults"
+ class="subcategory"
+ hidden="true"
+ data-hidden-from-search="true"
+ data-category="paneSearchResults">
+ <html:h1 data-l10n-id="search-results-header"/>
+ </hbox>
+ <groupbox id="no-results-message"
+ data-hidden-from-search="true"
+ data-category="paneSearchResults"
+ hidden="true">
+ <vbox class="no-results-container">
+ <label id="sorry-message" data-l10n-id="search-results-empty-message2">
+ <html:span data-l10n-name="query" id="sorry-message-query"/>
+ </label>
+ <label id="need-help" data-l10n-id="search-results-help-link">
+ <html:a class="text-link" data-l10n-name="url" target="_blank"></html:a>
+ </label>
+ </vbox>
+ </groupbox>
diff --git a/comm/mail/components/preferences/sync.inc.xhtml b/comm/mail/components/preferences/sync.inc.xhtml
new file mode 100644
index 0000000000..a101d4359b
--- /dev/null
+++ b/comm/mail/components/preferences/sync.inc.xhtml
@@ -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/.
+<script src="chrome://messenger/content/preferences/sync.js"/>
+
+<html:template id="paneSync">
+ <html:div id="syncPaneCategory"
+ class="subcategory"
+ data-category="paneSync"
+ xmlns="http://www.w3.org/1999/xhtml">
+ <h1 data-l10n-id="sync-pane-header"></h1>
+
+ <section id="noFxaAccount" hidden="hidden">
+ <div id="noFxaInfo">
+ <h2 id="noFxaCaption" data-l10n-id="sync-signedout-caption"></h2>
+ <p id="noFxaDescription"
+ data-l10n-id="sync-signedout-description">
+ </p>
+ <button id="noFxaSignIn"
+ class="primary"
+ type="button"
+ data-l10n-id="sync-signedout-account-signin-btn">
+ </button>
+ </div>
+ <img id="noFxaSyncIllustration"
+ src="chrome://messenger/skin/illustrations/sync-devices.svg"
+ alt=""/>
+ </section>
+
+ <section id="hasFxaAccount" hidden="hidden">
+ <!-- Account NOT Verified -->
+ <section id="fxaLoginUnverified"
+ class="sync-account-section"
+ hidden="hidden">
+ <div id="photoDisplay">
+ <img class="contact-photo" src="" alt="" />
+ </div>
+ <div id="fxaLoginUnverifiedInfo">
+ <span id="fxaAccountMailNotVerified"
+ data-l10n-id="sync-pane-email-not-verified"
+ data-l10n-args='{"userEmail": ""}'>
+ </span>
+ <div id="fxaUnverifiedButtonOptions">
+ <button id="fxaResendVerification"
+ class="place-self-end primary"
+ data-l10n-id="sync-pane-resend-verification"
+ type="button">
+ </button>
+ <button id="fxaUnverifiedRemoveAccount"
+ class="place-self-end"
+ data-l10n-id="sync-pane-remove-account"
+ type="button">
+ </button>
+ </div>
+ </div>
+ </section>
+
+ <!-- Server Rejected Credientials -->
+ <section id="fxaLoginRejected"
+ class="sync-account-section"
+ hidden="hidden">
+ <div id="photoDisplay">
+ <img class="contact-photo" src="" alt="" />
+ </div>
+ <div id="fxaLoginRejectedInfo">
+ <span id="fxaAccountLoginRejected"
+ data-l10n-id="sync-signedin-login-failure"
+ data-l10n-args='{"userEmail": ""}'>
+ </span>
+ <div id="fxaRejectedButtonOptions">
+ <button id="fxaRejectedSignIn"
+ class="place-self-end primary"
+ data-l10n-id="sync-pane-sign-in"
+ type="button">
+ </button>
+ <button id="fxaRejectedRemoveAccount"
+ class="place-self-end"
+ data-l10n-id="sync-pane-remove-account"
+ type="button">
+ </button>
+ </div>
+ </div>
+ </section>
+
+ <!-- Account Verified -->
+ <section id="fxaLoginVerified"
+ class="sync-account-section"
+ hidden="hidden">
+ <div id="photoInput">
+ <button type="button" id="photoButton"
+ class="plain-button"
+ data-l10n-id="sync-pane-edit-photo">
+ <img id="fxaAvatar" class="contact-photo" src="" alt="" />
+ <div id="photoOverlay"></div>
+ </button>
+ </div>
+ <div id="fxaAccountInfo">
+ <span id="fxaDisplayName"></span>
+ <span id="fxaEmailAddress"></span>
+ <a id="verifiedManage" href="#" data-l10n-id="sync-pane-manage-account"></a>
+ </div>
+ <button id="fxaAccountSignOut"
+ class="place-self-end"
+ data-l10n-id="sync-pane-sign-out"
+ type="button">
+ </button>
+ </section>
+
+ <fieldset id="fxaDeviceInfo" hidden="hidden">
+ <legend data-l10n-id="sync-pane-device-name-title"></legend>
+ <input id="fxaDeviceNameInput" type="text" readonly="readonly"/>
+ <!-- Hidden by default, shown if #fxaDeviceNameChangeDeviceName is pressed -->
+ <button id="fxaDeviceNameCancel"
+ class="place-self-end"
+ data-l10n-id="sync-pane-cancel"
+ type="button"
+ hidden="hidden">
+ </button>
+ <button id="fxaDeviceNameSave"
+ class="place-self-end"
+ data-l10n-id="sync-pane-save"
+ type="button"
+ hidden="hidden">
+ </button>
+ <!-- Disappear once pressed to allow the previous two buttons to take
+ - its place, reappears once cancel or save is pressed -->
+ <button id="fxaDeviceNameChangeDeviceName"
+ class="place-self-end needs-account-ready"
+ data-l10n-id="sync-pane-change-device-name"
+ type="button">
+ </button>
+ </fieldset>
+
+ <div id="syncConnected" class="sync-section" hidden="hidden">
+ <div id="showSyncedHeader">
+ <h2 class="sync-header"
+ data-l10n-id="sync-pane-show-synced-header-on">
+ </h2>
+ <button id="syncShowSyncedSyncNow"
+ class="place-self-end needs-account-ready"
+ data-l10n-id="sync-pane-sync-now"
+ type="button">
+ </button>
+ </div>
+
+ <div class="sync-panel">
+ <div id="showSyncedListHeader">
+ <h3 id="showSyncedListHeading" data-l10n-id="show-synced-list-heading"></h3>
+ <a id="enginesLearnMore" href="#" data-l10n-id="show-synced-learn-more"></a>
+ </div>
+
+ <ul id="showSyncedList" class="synced-list">
+ <!-- For when we get per-account controls: -->
+ <!-- <li id="showSyncAccount">
+ <span id="showSyncAccountLabel"
+ class="synced-item"
+ data-l10n-id="show-synced-item-account">
+ </span>
+ <div id="syncedAccounts">
+
+ <div class="synced-account">
+ <h4 class="synced-account-name">nemo@thunderbird.net</h4>
+ <ul class="synced-list">
+ <li class="synced-item synced-account-server-config"
+ data-l10n-id="synced-acount-item-server-config">
+ </li>
+ <li class="synced-item synced-account-filters"
+ data-l10n-id="synced-acount-item-filters">
+ </li>
+ <li class="synced-item synced-account-keys"
+ data-l10n-id="synced-acount-item-keys">
+ </li>
+ </ul>
+ </div>
+
+ <div class="synced-account">
+ <h4 class="synced-account-name">example@example.com</h4>
+ <ul class="synced-list">
+ <li class="synced-item synced-account-server-config"
+ data-l10n-id="synced-acount-item-server-config">
+ </li>
+ <li class="synced-item synced-account-filters"
+ data-l10n-id="synced-acount-item-filters">
+ </li>
+ <li class="synced-item synced-account-keys"
+ data-l10n-id="synced-acount-item-keys">
+ </li>
+ </ul>
+ </div>
+
+ </div>
+ </li> -->
+ <li id="showSyncAccount"
+ class="synced-item"
+ data-l10n-id="show-synced-item-account">
+ </li>
+ <li id="showSyncIdentity"
+ class="synced-item"
+ data-l10n-id="show-synced-item-identity">
+ </li>
+ <li id="showSyncAddress"
+ class="synced-item"
+ data-l10n-id="show-synced-item-address">
+ </li>
+ <li id="showSyncCalendar"
+ class="synced-item"
+ data-l10n-id="show-synced-item-calendar">
+ </li>
+ <li id="showSyncPasswords"
+ class="synced-item"
+ data-l10n-id="show-synced-item-passwords">
+ </li>
+ </ul>
+
+ <button id="syncChangeOptions"
+ class="place-self-end primary"
+ data-l10n-id="show-synced-change"
+ type="button">
+ </button>
+ </div>
+ </div>
+
+ <div id="syncDisconnected" class="sync-section" hidden="hidden">
+ <h2 class="sync-header"
+ data-l10n-id="sync-pane-show-synced-header-off">
+ </h2>
+
+ <div class="sync-panel">
+ <p data-l10n-id="sync-disconnected-text"></p>
+ <button id="syncSetup"
+ class="place-self-start needs-account-ready"
+ data-l10n-id="sync-disconnected-turn-on-sync"
+ type="button">
+ </button>
+ </div>
+ </div>
+ </section>
+ </html:div>
+</html:template>
diff --git a/comm/mail/components/preferences/sync.js b/comm/mail/components/preferences/sync.js
new file mode 100644
index 0000000000..d8336c6e23
--- /dev/null
+++ b/comm/mail/components/preferences/sync.js
@@ -0,0 +1,377 @@
+/* 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 preferences.js */
+
+ChromeUtils.defineESModuleGetters(this, {
+ UIState: "resource://services-sync/UIState.sys.mjs",
+ Weave: "resource://services-sync/main.sys.mjs",
+});
+
+var { FxAccounts, getFxAccountsSingleton } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+var fxAccounts = getFxAccountsSingleton();
+
+var gSyncPane = {
+ init() {
+ this._setupEventListeners();
+ this.setupEnginesUI();
+
+ Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this);
+
+ window.addEventListener("unload", () => {
+ Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this);
+ });
+
+ let cachedComputerName = Services.prefs.getStringPref(
+ "identity.fxaccounts.account.device.name",
+ ""
+ );
+ if (cachedComputerName) {
+ this._populateComputerName(cachedComputerName);
+ }
+
+ this.updateWeavePrefs();
+ },
+
+ /**
+ * Update the UI based on the current state.
+ */
+ updateWeavePrefs() {
+ let state = UIState.get();
+
+ let noFxaAccount = document.getElementById("noFxaAccount");
+ let hasFxaAccount = document.getElementById("hasFxaAccount");
+ if (state.status == UIState.STATUS_NOT_CONFIGURED) {
+ noFxaAccount.hidden = false;
+ hasFxaAccount.hidden = true;
+ return;
+ }
+ noFxaAccount.hidden = true;
+ hasFxaAccount.hidden = false;
+
+ let syncReady = false; // Is sync able to actually sync?
+ let fxaLoginUnverified = document.getElementById("fxaLoginUnverified");
+ let fxaLoginRejected = document.getElementById("fxaLoginRejected");
+ let fxaLoginVerified = document.getElementById("fxaLoginVerified");
+ if (state.status == UIState.STATUS_LOGIN_FAILED) {
+ fxaLoginUnverified.hidden = true;
+ fxaLoginRejected.hidden = false;
+ fxaLoginVerified.hidden = true;
+ } else if (state.status == UIState.STATUS_NOT_VERIFIED) {
+ fxaLoginUnverified.hidden = false;
+ fxaLoginRejected.hidden = true;
+ fxaLoginVerified.hidden = true;
+ } else {
+ fxaLoginUnverified.hidden = true;
+ fxaLoginRejected.hidden = true;
+ fxaLoginVerified.hidden = false;
+ syncReady = true;
+ }
+
+ this._populateComputerName(Weave.Service.clientsEngine.localName);
+ for (let elt of document.querySelectorAll(".needs-account-ready")) {
+ elt.disabled = !syncReady;
+ }
+
+ let syncConnected = document.getElementById("syncConnected");
+ let syncDisconnected = document.getElementById("syncDisconnected");
+ syncConnected.hidden = !syncReady || !state.syncEnabled;
+ syncDisconnected.hidden = !syncReady || state.syncEnabled;
+
+ document.l10n.setAttributes(
+ document.getElementById("fxaAccountMailNotVerified"),
+ "sync-pane-email-not-verified",
+ { userEmail: state.email }
+ );
+ document.l10n.setAttributes(
+ document.getElementById("fxaAccountLoginRejected"),
+ "sync-signedin-login-failure",
+ { userEmail: state.email }
+ );
+
+ document.getElementById("fxaAvatar").src =
+ state.avatarURL && !state.avatarIsDefault ? state.avatarURL : "";
+ document.getElementById("fxaDisplayName").textContent = state.displayName;
+ document.getElementById("fxaEmailAddress").textContent = state.email;
+
+ this._updateSyncNow(state.syncing);
+ },
+
+ _toggleComputerNameControls(editMode) {
+ let textbox = document.getElementById("fxaDeviceNameInput");
+ textbox.readOnly = !editMode;
+ document.getElementById("fxaDeviceNameChangeDeviceName").hidden = editMode;
+ document.getElementById("fxaDeviceNameCancel").hidden = !editMode;
+ document.getElementById("fxaDeviceNameSave").hidden = !editMode;
+ },
+
+ _focusComputerNameTextbox() {
+ let textbox = document.getElementById("fxaDeviceNameInput");
+ let valLength = textbox.value.length;
+ textbox.focus();
+ textbox.setSelectionRange(valLength, valLength);
+ },
+
+ _blurComputerNameTextbox() {
+ document.getElementById("fxaDeviceNameInput").blur();
+ },
+
+ _focusAfterComputerNameTextbox() {
+ // Focus the most appropriate element that's *not* the "computer name" box.
+ Services.focus.moveFocus(
+ window,
+ document.getElementById("fxaDeviceNameInput"),
+ Services.focus.MOVEFOCUS_FORWARD,
+ 0
+ );
+ },
+
+ _updateComputerNameValue(save) {
+ if (save) {
+ let textbox = document.getElementById("fxaDeviceNameInput");
+ Weave.Service.clientsEngine.localName = textbox.value;
+ }
+ this._populateComputerName(Weave.Service.clientsEngine.localName);
+ },
+
+ _setupEventListeners() {
+ function setEventListener(id, eventType, callback) {
+ document
+ .getElementById(id)
+ .addEventListener(eventType, callback.bind(gSyncPane));
+ }
+
+ setEventListener("noFxaSignIn", "click", function () {
+ window.browsingContext.topChromeWindow.gSync.initFxA();
+ return false;
+ });
+ setEventListener(
+ "fxaResendVerification",
+ "click",
+ gSyncPane.verifyFirefoxAccount
+ );
+ setEventListener("fxaUnverifiedRemoveAccount", "click", function () {
+ /* No warning as account can't have previously synced. */
+ gSyncPane.unlinkFirefoxAccount(false);
+ });
+ setEventListener("fxaRejectedSignIn", "click", gSyncPane.reSignIn);
+ setEventListener("fxaRejectedRemoveAccount", "click", function () {
+ gSyncPane.unlinkFirefoxAccount(true);
+ });
+ setEventListener("photoButton", "click", function (event) {
+ window.browsingContext.topChromeWindow.gSync.openFxAAvatarPage(
+ "preferences"
+ );
+ });
+ setEventListener("verifiedManage", "click", function (event) {
+ window.browsingContext.topChromeWindow.gSync.openFxAManagePage(
+ "preferences"
+ );
+ event.preventDefault();
+ // Stop attempts to open this link in an external browser.
+ event.stopPropagation();
+ });
+ setEventListener("fxaAccountSignOut", "click", function () {
+ gSyncPane.unlinkFirefoxAccount(true);
+ });
+ setEventListener("fxaDeviceNameCancel", "click", function () {
+ // We explicitly blur the textbox because of bug 75324, then after
+ // changing the state of the buttons, force focus to whatever the focus
+ // manager thinks should be next (which on the mac, depends on an OSX
+ // keyboard access preference)
+ this._blurComputerNameTextbox();
+ this._toggleComputerNameControls(false);
+ this._updateComputerNameValue(false);
+ this._focusAfterComputerNameTextbox();
+ });
+ setEventListener("fxaDeviceNameSave", "click", function () {
+ // Work around bug 75324 - see above.
+ this._blurComputerNameTextbox();
+ this._toggleComputerNameControls(false);
+ this._updateComputerNameValue(true);
+ this._focusAfterComputerNameTextbox();
+ });
+ setEventListener("fxaDeviceNameChangeDeviceName", "click", function () {
+ this._toggleComputerNameControls(true);
+ this._focusComputerNameTextbox();
+ });
+ setEventListener("syncShowSyncedSyncNow", "click", function () {
+ // syncing can take a little time to send the "started" notification, so
+ // pretend we already got it.
+ this._updateSyncNow(true);
+ Weave.Service.sync({ why: "aboutprefs" });
+ });
+ setEventListener("enginesLearnMore", "click", function (event) {
+ // TODO: A real page.
+ window.browsingContext.topChromeWindow.openContentTab(
+ "https://example.org/?page=learnMore"
+ );
+ event.preventDefault();
+ // Stop attempts to open this link in an external browser.
+ event.stopPropagation();
+ });
+ setEventListener("syncChangeOptions", "click", function () {
+ gSyncPane._chooseWhatToSync(true);
+ });
+ setEventListener("syncSetup", "click", function () {
+ gSyncPane._chooseWhatToSync(false);
+ });
+ },
+
+ async _chooseWhatToSync(isAlreadySyncing) {
+ // Assuming another device is syncing and we're not, we update the engines
+ // selection so the correct checkboxes are pre-filled.
+ if (!isAlreadySyncing) {
+ try {
+ await Weave.Service.updateLocalEnginesState();
+ } catch (err) {
+ console.error("Error updating the local engines state", err);
+ }
+ }
+ let params = {};
+ if (isAlreadySyncing) {
+ // If we are already syncing then we also offer to disconnect.
+ params.disconnectFun = () => this.disconnectSync();
+ }
+ gSubDialog.open(
+ "chrome://messenger/content/preferences/syncDialog.xhtml",
+ {
+ features: "resizable=no",
+ closingCallback: event => {
+ if (!isAlreadySyncing && event.detail.button == "accept") {
+ // We weren't syncing but the user has accepted the dialog - so we
+ // want to start!
+ fxAccounts.telemetry
+ .recordConnection(["sync"], "ui")
+ .then(() => {
+ return Weave.Service.configure();
+ })
+ .catch(err => {
+ console.error("Failed to enable sync", err);
+ });
+ }
+ },
+ },
+ params
+ );
+ },
+
+ _updateSyncNow(syncing) {
+ let button = document.getElementById("syncShowSyncedSyncNow");
+ if (syncing) {
+ document.l10n.setAttributes(button, "sync-panel-sync-now-syncing");
+ button.disabled = true;
+ } else {
+ document.l10n.setAttributes(button, "sync-pane-sync-now");
+ button.disabled = false;
+ }
+ },
+
+ /**
+ * If connecting to Firefox Accounts failed, try again.
+ */
+ async reSignIn() {
+ // There's a bit of an edge-case here - we might be forcing reauth when we've
+ // lost the FxA account data - in which case we'll not get a URL as the re-auth
+ // URL embeds account info and the server endpoint complains if we don't
+ // supply it - so we just use the regular "sign in" URL in that case.
+ if (!(await FxAccounts.canConnectAccount())) {
+ return;
+ }
+
+ const url =
+ (await FxAccounts.config.promiseForceSigninURI("preferences")) ||
+ (await FxAccounts.config.promiseConnectAccountURI("preferences"));
+ window.browsingContext.topChromeWindow.openContentTab(url);
+ },
+
+ /**
+ * Send a confirmation email to the account's email address.
+ */
+ verifyFirefoxAccount() {
+ let onError = async () => {
+ let [title, body] = await document.l10n.formatValues([
+ "fxa-verification-not-sent-title",
+ "fxa-verification-not-sent-body",
+ ]);
+ new Notification(title, { body });
+ };
+
+ let onSuccess = async data => {
+ if (data) {
+ let [title, body] = await document.l10n.formatValues([
+ "fxa-verification-sent-title",
+ { id: "fxa-verification-sent-body", args: { userEmail: data.email } },
+ ]);
+ new Notification(title, { body });
+ } else {
+ onError();
+ }
+ };
+
+ fxAccounts
+ .resendVerificationEmail()
+ .then(() => fxAccounts.getSignedInUser(), onError)
+ .then(onSuccess, onError);
+ },
+
+ /**
+ * Disconnect the account, including everything linked.
+ *
+ * @param {boolean} confirm - If true, asks the user if they're sure.
+ */
+ unlinkFirefoxAccount(confirm) {
+ window.browsingContext.topChromeWindow.gSync.disconnect({ confirm });
+ },
+
+ /**
+ * Disconnect sync, leaving the FxA account connected.
+ */
+ disconnectSync() {
+ return window.browsingContext.topChromeWindow.gSync.disconnect({
+ confirm: true,
+ disconnectAccount: false,
+ });
+ },
+
+ _populateComputerName(value) {
+ let textbox = document.getElementById("fxaDeviceNameInput");
+ if (!textbox.hasAttribute("placeholder")) {
+ textbox.setAttribute(
+ "placeholder",
+ fxAccounts.device.getDefaultLocalName()
+ );
+ }
+ textbox.value = value;
+ },
+
+ /**
+ * Arranges to dynamically show or hide sync engine name elements based on
+ * the preferences used for the engines.
+ */
+ setupEnginesUI() {
+ let observe = (element, prefName) => {
+ element.hidden = !Services.prefs.getBoolPref(prefName, false);
+ };
+
+ let engineItems = {
+ showSyncAccount: "services.sync.engine.accounts",
+ showSyncIdentity: "services.sync.engine.identities",
+ showSyncAddress: "services.sync.engine.addressbooks",
+ showSyncCalendar: "services.sync.engine.calendars",
+ showSyncPasswords: "services.sync.engine.passwords",
+ };
+
+ for (let [id, prefName] of Object.entries(engineItems)) {
+ let obs = observe.bind(null, document.getElementById(id), prefName);
+ obs();
+ Services.prefs.addObserver(prefName, obs);
+ window.addEventListener("unload", () => {
+ Services.prefs.removeObserver(prefName, obs);
+ });
+ }
+ },
+};
diff --git a/comm/mail/components/preferences/syncDialog.js b/comm/mail/components/preferences/syncDialog.js
new file mode 100644
index 0000000000..6920a8aa59
--- /dev/null
+++ b/comm/mail/components/preferences/syncDialog.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+const engineItems = {
+ configSyncAccount: "services.sync.engine.accounts",
+ configSyncAddress: "services.sync.engine.addressbooks",
+ configSyncCalendar: "services.sync.engine.calendars",
+ configSyncIdentity: "services.sync.engine.identities",
+ configSyncPasswords: "services.sync.engine.passwords",
+};
+
+window.addEventListener("load", function () {
+ for (let [id, prefName] of Object.entries(engineItems)) {
+ let element = document.getElementById(id);
+ element.checked = Services.prefs.getBoolPref(prefName, false);
+ }
+
+ let options = window.arguments[0];
+ if (options.disconnectFun) {
+ window.addEventListener("dialogextra2", function () {
+ options.disconnectFun().then(disconnected => {
+ if (disconnected) {
+ window.close();
+ }
+ });
+ });
+ } else {
+ document.querySelector("dialog").getButton("extra2").hidden = true;
+ }
+});
+
+window.addEventListener("dialogaccept", function () {
+ for (let [id, prefName] of Object.entries(engineItems)) {
+ let element = document.getElementById(id);
+ Services.prefs.setBoolPref(prefName, element.checked);
+ }
+});
diff --git a/comm/mail/components/preferences/syncDialog.xhtml b/comm/mail/components/preferences/syncDialog.xhtml
new file mode 100644
index 0000000000..ce035245ea
--- /dev/null
+++ b/comm/mail/components/preferences/syncDialog.xhtml
@@ -0,0 +1,210 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+>
+ <head>
+ <title data-l10n-id="config-sync-dailog-title"></title>
+
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <link
+ rel="stylesheet"
+ href="chrome://messenger/skin/preferences/preferences.css"
+ />
+
+ <link rel="localization" href="messenger/preferences/preferences.ftl" />
+ <link rel="localization" href="messenger/preferences/sync-dialog.ftl" />
+
+ <script src="chrome://messenger/content/preferences/syncDialog.js"></script>
+ </head>
+ <body>
+ <xul:dialog
+ id="configSyncDialog"
+ buttons="accept,cancel,extra2"
+ style="min-width: 49em"
+ data-l10n-attrs="buttonlabelaccept, buttonaccesskeyaccept, buttonlabelextra2, buttonaccesskeyextra2"
+ data-l10n-id="sync-dialog"
+ >
+ <div id="configSyncDialogContent">
+ <ul id="configSyncList" class="config-list">
+ <li id="configAccountsContainer">
+ <div class="config-item">
+ <input
+ type="checkbox"
+ id="configSyncAccount"
+ name="configSynced"
+ />
+ <label
+ for="configSyncAccount"
+ id="configSyncAccountLabel"
+ data-l10n-id="show-synced-item-account"
+ >
+ </label>
+ </div>
+ <!-- For when we get per-account controls: -->
+ <!-- <div id="configAccounts">
+
+ <div class="synced-account">
+ <div class="config-item synced-account-name">
+ <input type="checkbox"
+ id="configSyncAccount_1"
+ name="configSyncedAccounts"/>
+ <label for="configSyncAccount_1">
+ nemo@thunderbird.net
+ </label>
+ </div>
+ <ul class="config-list">
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncServer_1"
+ name="configSyncedAccount_1"/>
+ <label for="configSyncServer_1"
+ class="synced-account-server-config"
+ data-l10n-id="synced-acount-item-server-config">
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncFilters_1"
+ name="configSyncedAccount_1"/>
+ <label for="configSyncFilters_1"
+ class="synced-account-filters"
+ data-l10n-id="synced-acount-item-filters">
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncKeys_1"
+ name="configSyncedAccount_1"/>
+ <label for="configSyncKeys_1"
+ class="synced-account-keys"
+ data-l10n-id="synced-acount-item-keys">
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+
+ <div class="synced-account">
+ <div class="config-item synced-account-name">
+ <input type="checkbox"
+ id="configSyncAccount_2"
+ name="configSyncedAccounts"/>
+ <label for="configSyncAccount_2">
+ example@example.com
+ </label>
+ </div>
+ <ul class="config-list">
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncServer_2"
+ name="configSyncedAccount_2"/>
+ <label for="configSyncServer_2"
+ class="synced-account-server-config"
+ data-l10n-id="synced-acount-item-server-config">
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncFilters_2"
+ name="configSyncedAccount_2"/>
+ <label for="configSyncFilters_2"
+ class="synced-account-filters"
+ data-l10n-id="synced-acount-item-filters">
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input type="checkbox"
+ id="configSyncKeys_2"
+ name="configSyncedAccount_2"/>
+ <label for="configSyncKeys_2"
+ class="synced-account-keys"
+ data-l10n-id="synced-acount-item-keys">
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+
+ </div> -->
+ </li>
+ <li>
+ <div class="config-item">
+ <input
+ type="checkbox"
+ id="configSyncIdentity"
+ name="configSynced"
+ />
+ <label
+ id="configSyncIdentityLabel"
+ for="configSyncIdentity"
+ data-l10n-id="show-synced-item-identity"
+ >
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input
+ type="checkbox"
+ id="configSyncAddress"
+ name="configSynced"
+ />
+ <label
+ id="configSyncAddressLabel"
+ for="configSyncAddress"
+ data-l10n-id="show-synced-item-address"
+ >
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input
+ type="checkbox"
+ id="configSyncCalendar"
+ name="configSynced"
+ />
+ <label
+ id="configSyncCalendarLabel"
+ for="configSyncCalendar"
+ data-l10n-id="show-synced-item-calendar"
+ >
+ </label>
+ </div>
+ </li>
+ <li>
+ <div class="config-item">
+ <input
+ type="checkbox"
+ id="configSyncPasswords"
+ name="configSynced"
+ />
+ <label
+ id="configSyncPasswordsLabel"
+ for="configSyncPasswords"
+ data-l10n-id="show-synced-item-passwords"
+ >
+ </label>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </xul:dialog>
+ </body>
+</html>
diff --git a/comm/mail/components/preferences/tagDialog.xhtml b/comm/mail/components/preferences/tagDialog.xhtml
new file mode 100644
index 0000000000..32dd72268d
--- /dev/null
+++ b/comm/mail/components/preferences/tagDialog.xhtml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+
+<!DOCTYPE window>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ data-l10n-id="tag-dialog-window"
+ style="min-width: 25em;"
+ onload="onLoad();">
+<dialog>
+
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+
+ <script src="chrome://messenger/content/globalOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+ <script src="chrome://messenger/content/newTagDialog.js"/>
+#include ../../base/content/tagDialog.inc.xhtml
+</dialog>
+</window>
diff --git a/comm/mail/components/preferences/test/browser/browser.ini b/comm/mail/components/preferences/test/browser/browser.ini
new file mode 100644
index 0000000000..44b7d97a31
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = head.js
+prefs =
+ mail.provider.suppress_dialog_on_startup=true
+ mail.spotlight.firstRunDone=true
+ mail.winsearch.firstRunDone=true
+ mailnews.start_page.override_url=about:blank
+ mailnews.start_page.url=about:blank
+subsuite = thunderbird
+
+[browser_chat.js]
+[browser_cloudfile.js]
+support-files = files/icon.svg files/management.html
+[browser_compose.js]
+[browser_general.js]
+[browser_openPreferences.js]
+[browser_privacy.js]
+[browser_sync.js]
+skip-if = !nightly_build
+support-files = files/avatar.png
diff --git a/comm/mail/components/preferences/test/browser/browser_chat.js b/comm/mail/components/preferences/test/browser/browser_chat.js
new file mode 100644
index 0000000000..009f2a9211
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_chat.js
@@ -0,0 +1,74 @@
+/* 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/. */
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneChat",
+ "chatPaneCategory",
+ {
+ checkboxID: "reportIdle",
+ pref: "messenger.status.reportIdle",
+ enabledElements: ["#autoAway", "#timeBeforeAway"],
+ },
+ {
+ checkboxID: "sendTyping",
+ pref: "purple.conversations.im.send_typing",
+ },
+ {
+ checkboxID: "desktopChatNotifications",
+ pref: "mail.chat.show_desktop_notifications",
+ },
+ {
+ checkboxID: "getAttention",
+ pref: "messenger.options.getAttentionOnNewMessages",
+ },
+ {
+ checkboxID: "chatNotification",
+ pref: "mail.chat.play_sound",
+ enabledElements: ["#chatSoundType radio"],
+ }
+ );
+
+ Services.prefs.setBoolPref("messenger.status.reportIdle", true);
+ await testCheckboxes("paneChat", "chatPaneCategory", {
+ checkboxID: "autoAway",
+ pref: "messenger.status.awayWhenIdle",
+ enabledElements: ["#defaultIdleAwayMessage"],
+ });
+
+ Services.prefs.setBoolPref("mail.chat.play_sound", true);
+ await testRadioButtons("paneChat", "chatPaneCategory", {
+ pref: "mail.chat.play_sound.type",
+ states: [
+ {
+ id: "chatSoundSystemSound",
+ prefValue: 0,
+ },
+ {
+ id: "chatSoundCustom",
+ prefValue: 1,
+ enabledElements: ["#chatSoundUrlLocation", "#browseForChatSound"],
+ },
+ ],
+ });
+});
+
+add_task(async function testMessageStylePreview() {
+ await openNewPrefsTab("paneChat", "chatPaneCategory");
+ const conversationLoad = TestUtils.topicObserved("conversation-loaded");
+ const [subject] = await conversationLoad;
+ do {
+ await BrowserTestUtils.waitForEvent(subject, "MessagesDisplayed");
+ } while (subject.getPendingMessagesCount() > 0);
+ const messageParent = subject.contentChatNode;
+ let message = messageParent.firstElementChild;
+ const messages = new Set();
+ while (message) {
+ ok(message._originalMsg);
+ messages.add(message._originalMsg);
+ message = message.nextElementSibling;
+ }
+ is(messages.size, 3, "All 3 messages displayed");
+ await closePrefsTab();
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_cloudfile.js b/comm/mail/components/preferences/test/browser/browser_cloudfile.js
new file mode 100644
index 0000000000..9f395f9ab8
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_cloudfile.js
@@ -0,0 +1,796 @@
+/* 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/. */
+
+/* eslint-env webextensions */
+
+let { cloudFileAccounts } = ChromeUtils.import(
+ "resource:///modules/cloudFileAccounts.jsm"
+);
+let { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+function ManagementScript() {
+ browser.test.onMessage.addListener((message, assertMessage, browserStyle) => {
+ if (message !== "check-style") {
+ return;
+ }
+ function verifyButton(buttonElement, expected) {
+ let buttonStyle = window.getComputedStyle(buttonElement);
+ let buttonBackgroundColor = buttonStyle.backgroundColor;
+ if (browserStyle && expected.hasBrowserStyleClass) {
+ browser.test.assertEq(
+ "rgb(9, 150, 248)",
+ buttonBackgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ buttonBackgroundColor !== "rgb(9, 150, 248)",
+ assertMessage
+ );
+ }
+ }
+
+ function verifyCheckboxOrRadio(element, expected) {
+ let style = window.getComputedStyle(element);
+ let styledBackground = element.checked
+ ? "rgb(9, 150, 248)"
+ : "rgb(255, 255, 255)";
+ if (browserStyle && expected.hasBrowserStyleClass) {
+ browser.test.assertEq(
+ styledBackground,
+ style.backgroundColor,
+ assertMessage
+ );
+ } else {
+ browser.test.assertTrue(
+ style.backgroundColor != styledBackground,
+ assertMessage
+ );
+ }
+ }
+
+ let normalButton = document.getElementById("normalButton");
+ let browserStyleButton = document.getElementById("browserStyleButton");
+ verifyButton(normalButton, { hasBrowserStyleClass: false });
+ verifyButton(browserStyleButton, { hasBrowserStyleClass: true });
+
+ let normalCheckbox1 = document.getElementById("normalCheckbox1");
+ let normalCheckbox2 = document.getElementById("normalCheckbox2");
+ let browserStyleCheckbox = document.getElementById("browserStyleCheckbox");
+ verifyCheckboxOrRadio(normalCheckbox1, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(normalCheckbox2, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(browserStyleCheckbox, {
+ hasBrowserStyleClass: true,
+ });
+
+ let normalRadio1 = document.getElementById("normalRadio1");
+ let normalRadio2 = document.getElementById("normalRadio2");
+ let browserStyleRadio = document.getElementById("browserStyleRadio");
+ verifyCheckboxOrRadio(normalRadio1, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(normalRadio2, { hasBrowserStyleClass: false });
+ verifyCheckboxOrRadio(browserStyleRadio, { hasBrowserStyleClass: true });
+
+ browser.test.notifyPass("management-ui-browser_style");
+ });
+ browser.test.sendMessage("management-ui-ready");
+}
+
+let extension;
+async function startExtension(browser_style) {
+ let cloud_file = {
+ name: "Mochitest",
+ management_url: "management.html",
+ };
+
+ switch (browser_style) {
+ case "true":
+ cloud_file.browser_style = true;
+ break;
+ case "false":
+ cloud_file.browser_style = false;
+ break;
+ }
+
+ extension = ExtensionTestUtils.loadExtension({
+ async background() {
+ browser.test.onMessage.addListener(async message => {
+ if (message != "set-configured") {
+ return;
+ }
+ let accounts = await browser.cloudFile.getAllAccounts();
+ for (let account of accounts) {
+ await browser.cloudFile.updateAccount(account.id, {
+ configured: true,
+ });
+ }
+ browser.test.sendMessage("ready");
+ });
+ },
+ files: {
+ "management.html": `<html>
+ <body>
+ <a id="a" href="https://www.example.com/">Click me!</a>
+ <button id="normalButton" name="button" class="default">Default</button>
+ <button id="browserStyleButton" name="button" class="browser-style default">Default</button>
+
+ <input id="normalCheckbox1" type="checkbox"/>
+ <input id="normalCheckbox2" type="checkbox"/><label>Checkbox</label>
+ <div class="browser-style">
+ <input id="browserStyleCheckbox" type="checkbox"><label for="browserStyleCheckbox">Checkbox</label>
+ </div>
+
+ <input id="normalRadio1" type="radio"/>
+ <input id="normalRadio2" type="radio"/><label>Radio</label>
+ <div class="browser-style">
+ <input id="browserStyleRadio" checked="" type="radio"><label for="browserStyleRadio">Radio</label>
+ </div>
+ </body>
+ <script src="management.js" type="text/javascript"></script>
+ </html>`,
+ "management.js": ManagementScript,
+ },
+ manifest: {
+ cloud_file,
+ applications: { gecko: { id: "cloudfile@mochitest" } },
+ },
+ });
+
+ info("Starting extension");
+ await extension.startup();
+
+ if (accountIsConfigured) {
+ extension.sendMessage("set-configured");
+ await extension.awaitMessage("ready");
+ }
+}
+
+add_task(async () => {
+ // Register a fake provider representing a built-in provider. We don't
+ // currently ship any built-in providers, but if we did, we should check
+ // if they are present before doing this. Built-in providers can be
+ // problematic for artifact builds.
+ cloudFileAccounts.registerProvider("Fake-Test", {
+ displayName: "XYZ Fake",
+ type: "ext-fake@extensions.thunderbird.net",
+ });
+ registerCleanupFunction(() => {
+ cloudFileAccounts.unregisterProvider("Fake-Test");
+ });
+});
+
+let accountIsConfigured = false;
+
+// Mock the prompt service. We're going to be asked if we're sure
+// we want to remove an account, so let's say yes.
+
+/** @implements {nsIPromptService} */
+let mockPromptService = {
+ confirmCount: 0,
+ confirm() {
+ this.confirmCount++;
+ return true;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIPromptService"]),
+};
+/** @implements {nsIExternalProtocolService} */
+let mockExternalProtocolService = {
+ _loadedURLs: [],
+ externalProtocolHandlerExists(aProtocolScheme) {},
+ getApplicationDescription(aScheme) {},
+ getProtocolHandlerInfo(aProtocolScheme) {},
+ getProtocolHandlerInfoFromOS(aProtocolScheme, aFound) {},
+ isExposedProtocol(aProtocolScheme) {},
+ loadURI(aURI, aWindowContext) {
+ this._loadedURLs.push(aURI.spec);
+ },
+ setProtocolHandlerDefaults(aHandlerInfo, aOSHandlerExists) {},
+ urlLoaded(aURL) {
+ return this._loadedURLs.includes(aURL);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+};
+
+let originalPromptService = Services.prompt;
+Services.prompt = mockPromptService;
+
+let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+);
+
+registerCleanupFunction(() => {
+ Services.prompt = originalPromptService;
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+});
+
+add_task(async function addRemoveAccounts() {
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Load the preferences tab.
+
+ let { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ "paneCompose",
+ "compositionAttachmentsCategory"
+ );
+
+ // Check everything is as it should be.
+
+ let accountList = prefsDocument.getElementById("cloudFileView");
+ is(accountList.itemCount, 0);
+
+ let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons");
+ ok(!buttonList.hidden);
+ is(buttonList.childElementCount, 1);
+ is(
+ buttonList.children[0].getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+
+ let menuButton = prefsDocument.getElementById("addCloudFileAccount");
+ ok(menuButton.hidden);
+ is(menuButton.itemCount, 1);
+ is(
+ menuButton.getItemAtIndex(0).getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+
+ let removeButton = prefsDocument.getElementById("removeCloudFileAccount");
+ ok(removeButton.disabled);
+
+ let cloudFileDefaultPanel = prefsDocument.getElementById(
+ "cloudFileDefaultPanel"
+ );
+ ok(!cloudFileDefaultPanel.hidden);
+
+ let browserWrapper = prefsDocument.getElementById("cloudFileSettingsWrapper");
+ is(browserWrapper.childElementCount, 0);
+
+ // Register our test provider.
+
+ await startExtension();
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(buttonList.childElementCount, 2);
+ is(
+ buttonList.children[0].getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest");
+ is(
+ buttonList.children[1].style.listStyleImage,
+ `url("chrome://messenger/content/extension.svg")`
+ );
+
+ is(menuButton.itemCount, 2);
+ is(
+ menuButton.getItemAtIndex(0).getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(
+ menuButton.getItemAtIndex(1).getAttribute("value"),
+ "ext-cloudfile@mochitest"
+ );
+ is(
+ menuButton.getItemAtIndex(1).getAttribute("image"),
+ "chrome://messenger/content/extension.svg"
+ );
+
+ // Create a new account.
+
+ EventUtils.synthesizeMouseAtCenter(
+ buttonList.children[1],
+ { clickCount: 1 },
+ prefsWindow
+ );
+ is(cloudFileAccounts.accounts.length, 1);
+ is(cloudFileAccounts.configuredAccounts.length, 0);
+
+ let account = cloudFileAccounts.accounts[0];
+ let accountKey = account.accountKey;
+ is(cloudFileAccounts.accounts[0].type, "ext-cloudfile@mochitest");
+
+ // Check prefs were updated.
+
+ is(
+ Services.prefs.getCharPref(
+ `mail.cloud_files.accounts.${accountKey}.displayName`
+ ),
+ "Mochitest"
+ );
+ is(
+ Services.prefs.getCharPref(`mail.cloud_files.accounts.${accountKey}.type`),
+ "ext-cloudfile@mochitest"
+ );
+
+ // Check UI was updated.
+
+ is(accountList.itemCount, 1);
+ is(accountList.selectedIndex, 0);
+ ok(!removeButton.disabled);
+
+ let accountListItem = accountList.selectedItem;
+ is(accountListItem.getAttribute("value"), accountKey);
+ is(
+ accountListItem.querySelector(".typeIcon:not(.configuredWarning)").src,
+ "chrome://messenger/content/extension.svg"
+ );
+ is(accountListItem.querySelector("label").value, "Mochitest");
+ is(accountListItem.querySelector(".configuredWarning").hidden, false);
+
+ ok(cloudFileDefaultPanel.hidden);
+ is(browserWrapper.childElementCount, 1);
+
+ let browser = browserWrapper.firstElementChild;
+ if (
+ browser.webProgress?.isLoadingDocument ||
+ browser.currentURI?.spec == "about:blank"
+ ) {
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ is(
+ browser.currentURI.pathQueryRef,
+ `/management.html?accountId=${accountKey}`
+ );
+ await extension.awaitMessage("management-ui-ready");
+
+ let tabmail = document.getElementById("tabmail");
+ let tabCount = tabmail.tabInfo.length;
+ BrowserTestUtils.synthesizeMouseAtCenter("a", {}, browser);
+ // It might take a moment to get to the external protocol service.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 500));
+ ok(
+ mockExternalProtocolService.urlLoaded("https://www.example.com/"),
+ "Link click sent to external protocol service."
+ );
+ is(tabmail.tabInfo.length, tabCount, "No new tab opened");
+
+ // Rename the account.
+
+ EventUtils.synthesizeMouseAtCenter(
+ accountListItem,
+ { clickCount: 1 },
+ prefsWindow
+ );
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(
+ prefsDocument.activeElement.closest("input"),
+ accountListItem.querySelector("input")
+ );
+ ok(accountListItem.querySelector("label").hidden);
+ ok(!accountListItem.querySelector("input").hidden);
+ is(accountListItem.querySelector("input").value, "Mochitest");
+ EventUtils.synthesizeKey("VK_RIGHT", undefined, prefsWindow);
+ EventUtils.synthesizeKey("!", undefined, prefsWindow);
+ EventUtils.synthesizeKey("VK_RETURN", undefined, prefsWindow);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(prefsDocument.activeElement, accountList);
+ ok(!accountListItem.querySelector("label").hidden);
+ is(accountListItem.querySelector("label").value, "Mochitest!");
+ ok(accountListItem.querySelector("input").hidden);
+ is(
+ Services.prefs.getCharPref(
+ `mail.cloud_files.accounts.${accountKey}.displayName`
+ ),
+ "Mochitest!"
+ );
+
+ // Start to rename the account, but bail out.
+
+ EventUtils.synthesizeMouseAtCenter(
+ accountListItem,
+ { clickCount: 1 },
+ prefsWindow
+ );
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(
+ prefsDocument.activeElement.closest("input"),
+ accountListItem.querySelector("input")
+ );
+ EventUtils.synthesizeKey("O", undefined, prefsWindow);
+ EventUtils.synthesizeKey("o", undefined, prefsWindow);
+ EventUtils.synthesizeKey("p", undefined, prefsWindow);
+ EventUtils.synthesizeKey("s", undefined, prefsWindow);
+ EventUtils.synthesizeKey("VK_ESCAPE", undefined, prefsWindow);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(prefsDocument.activeElement, accountList);
+ ok(!accountListItem.querySelector("label").hidden);
+ is(accountListItem.querySelector("label").value, "Mochitest!");
+ ok(accountListItem.querySelector("input").hidden);
+ is(
+ Services.prefs.getCharPref(
+ `mail.cloud_files.accounts.${accountKey}.displayName`
+ ),
+ "Mochitest!"
+ );
+
+ // Configure the account.
+
+ account.configured = true;
+ accountIsConfigured = true;
+ cloudFileAccounts.emit("accountConfigured", account);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(accountListItem.querySelector(".configuredWarning").hidden, true);
+ is(cloudFileAccounts.accounts.length, 1);
+ is(cloudFileAccounts.configuredAccounts.length, 1);
+
+ // Remove the test provider. The list item, button, and browser should disappear.
+
+ info("Stopping extension");
+ await extension.unload();
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(buttonList.childElementCount, 1);
+ is(
+ buttonList.children[0].getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(menuButton.itemCount, 1);
+ is(
+ menuButton.getItemAtIndex(0).getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(accountList.itemCount, 0);
+ ok(!cloudFileDefaultPanel.hidden);
+ is(browserWrapper.childElementCount, 0);
+
+ // Re-add the test provider.
+
+ await startExtension();
+
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 1);
+ is(cloudFileAccounts.configuredAccounts.length, 1);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(buttonList.childElementCount, 2);
+ is(
+ buttonList.children[0].getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest");
+
+ is(menuButton.itemCount, 2);
+ is(
+ menuButton.getItemAtIndex(0).getAttribute("value"),
+ "ext-fake@extensions.thunderbird.net"
+ );
+ is(
+ menuButton.getItemAtIndex(1).getAttribute("value"),
+ "ext-cloudfile@mochitest"
+ );
+
+ is(accountList.itemCount, 1);
+ is(accountList.selectedIndex, -1);
+ ok(removeButton.disabled);
+
+ accountListItem = accountList.getItemAtIndex(0);
+ is(
+ Services.prefs.getCharPref(
+ `mail.cloud_files.accounts.${accountKey}.displayName`
+ ),
+ "Mochitest!"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ accountList.getItemAtIndex(0),
+ { clickCount: 1 },
+ prefsWindow
+ );
+ ok(!removeButton.disabled);
+ EventUtils.synthesizeMouseAtCenter(
+ removeButton,
+ { clickCount: 1 },
+ prefsWindow
+ );
+ is(mockPromptService.confirmCount, 1);
+
+ ok(
+ !Services.prefs.prefHasUserValue(
+ `mail.cloud_files.accounts.${accountKey}.displayName`
+ )
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(
+ `mail.cloud_files.accounts.${accountKey}.type`
+ )
+ );
+
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ info("Stopping extension");
+ await extension.unload();
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Close the preferences tab.
+
+ await closePrefsTab();
+});
+
+async function subtestBrowserStyle(assertMessage, expected) {
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Load the preferences tab.
+
+ let { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ "paneCompose",
+ "compositionAttachmentsCategory"
+ );
+
+ // Minimal check everything is as it should be.
+
+ let accountList = prefsDocument.getElementById("cloudFileView");
+ is(accountList.itemCount, 0);
+
+ let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons");
+ ok(!buttonList.hidden);
+
+ let browserWrapper = prefsDocument.getElementById("cloudFileSettingsWrapper");
+ is(browserWrapper.childElementCount, 0);
+
+ // Register our test provider.
+
+ await startExtension(expected.browser_style);
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ await new Promise(resolve => prefsWindow.requestAnimationFrame(resolve));
+
+ is(buttonList.childElementCount, 2);
+ is(buttonList.children[1].getAttribute("value"), "ext-cloudfile@mochitest");
+
+ // Create a new account.
+
+ EventUtils.synthesizeMouseAtCenter(
+ buttonList.children[1],
+ { clickCount: 1 },
+ prefsWindow
+ );
+ is(cloudFileAccounts.accounts.length, 1);
+ is(cloudFileAccounts.configuredAccounts.length, 0);
+
+ let account = cloudFileAccounts.accounts[0];
+ let accountKey = account.accountKey;
+ is(cloudFileAccounts.accounts[0].type, "ext-cloudfile@mochitest");
+
+ // Minimal check UI was updated.
+
+ is(accountList.itemCount, 1);
+ is(accountList.selectedIndex, 0);
+
+ let accountListItem = accountList.selectedItem;
+ is(accountListItem.getAttribute("value"), accountKey);
+
+ is(browserWrapper.childElementCount, 1);
+ let browser = browserWrapper.firstElementChild;
+ if (
+ browser.webProgress?.isLoadingDocument ||
+ browser.currentURI?.spec == "about:blank"
+ ) {
+ await BrowserTestUtils.browserLoaded(browser);
+ }
+ is(
+ browser.currentURI.pathQueryRef,
+ `/management.html?accountId=${accountKey}`
+ );
+ await extension.awaitMessage("management-ui-ready");
+
+ // Test browser_style
+
+ extension.sendMessage(
+ "check-style",
+ assertMessage,
+ expected.browser_style == "true"
+ );
+ await extension.awaitFinish("management-ui-browser_style");
+
+ // Remove the account
+
+ accountListItem = accountList.getItemAtIndex(0);
+ EventUtils.synthesizeMouseAtCenter(
+ accountList.getItemAtIndex(0),
+ { clickCount: 1 },
+ prefsWindow
+ );
+
+ let removeButton = prefsDocument.getElementById("removeCloudFileAccount");
+ ok(!removeButton.disabled);
+ EventUtils.synthesizeMouseAtCenter(
+ removeButton,
+ { clickCount: 1 },
+ prefsWindow
+ );
+ is(mockPromptService.confirmCount, expected.confirmCount);
+
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ info("Stopping extension");
+ await extension.unload();
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Close the preferences tab.
+
+ await closePrefsTab();
+}
+
+add_task(async function test_without_setting_browser_style() {
+ await subtestBrowserStyle(
+ "Expected correct style when browser_style is excluded",
+ {
+ confirmCount: 2,
+ browser_style: "default",
+ }
+ );
+});
+
+add_task(async function test_with_browser_style_set_to_true() {
+ await subtestBrowserStyle(
+ "Expected correct style when browser_style is set to `true`",
+ {
+ confirmCount: 3,
+ browser_style: "true",
+ }
+ );
+});
+
+add_task(async function test_with_browser_style_set_to_false() {
+ await subtestBrowserStyle(
+ "Expected no style when browser_style is set to `false`",
+ {
+ confirmCount: 4,
+ browser_style: "false",
+ }
+ );
+});
+
+add_task(async function accountListOverflow() {
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Register our test provider.
+
+ await startExtension();
+
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ // Load the preferences tab.
+
+ let { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ "paneCompose",
+ "compositionAttachmentsCategory"
+ );
+
+ let accountList = prefsDocument.getElementById("cloudFileView");
+ is(accountList.itemCount, 0);
+
+ let buttonList = prefsDocument.getElementById("addCloudFileAccountButtons");
+ ok(!buttonList.hidden);
+ is(buttonList.childElementCount, 2);
+ is(buttonList.children[0].getAttribute("value"), "ext-cloudfile@mochitest");
+
+ let menuButton = prefsDocument.getElementById("addCloudFileAccount");
+ ok(menuButton.hidden);
+
+ // Add new accounts until the list overflows. The list of buttons should be hidden
+ // and the button with the drop-down should appear.
+
+ let count = 0;
+ do {
+ let readyPromise = extension.awaitMessage("management-ui-ready");
+ EventUtils.synthesizeMouseAtCenter(
+ buttonList.children[0],
+ { clickCount: 1 },
+ prefsWindow
+ );
+ await readyPromise;
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ if (buttonList.hidden) {
+ break;
+ }
+ } while (++count < 25);
+
+ ok(count < 24); // If count reaches 25, we have a problem.
+ ok(!menuButton.hidden);
+
+ // Remove the added accounts. The list of buttons should not reappear and the
+ // button with the drop-down should remain.
+
+ let removeButton = prefsDocument.getElementById("removeCloudFileAccount");
+ do {
+ EventUtils.synthesizeMouseAtCenter(
+ accountList.getItemAtIndex(0),
+ { clickCount: 1 },
+ prefsWindow
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ removeButton,
+ { clickCount: 1 },
+ prefsWindow
+ );
+ await new Promise(resolve => setTimeout(resolve));
+ } while (--count > 0);
+
+ ok(buttonList.hidden);
+ ok(!menuButton.hidden);
+
+ // Close the preferences tab.
+
+ await closePrefsTab();
+ info("Stopping extension");
+ await extension.unload();
+ Services.prefs.deleteBranch("mail.cloud_files.accounts");
+});
+
+add_task(async function accountListOrder() {
+ is(cloudFileAccounts.providers.length, 1);
+ is(cloudFileAccounts.accounts.length, 0);
+
+ for (let [key, displayName] of [
+ ["someKey1", "carl's Account"],
+ ["someKey2", "Amber's Account"],
+ ["someKey3", "alice's Account"],
+ ["someKey4", "Bob's Account"],
+ ]) {
+ Services.prefs.setCharPref(
+ `mail.cloud_files.accounts.${key}.type`,
+ "ext-cloudfile@mochitest"
+ );
+ Services.prefs.setCharPref(
+ `mail.cloud_files.accounts.${key}.displayName`,
+ displayName
+ );
+ }
+
+ // Register our test provider.
+
+ await startExtension();
+
+ is(cloudFileAccounts.providers.length, 2);
+ is(cloudFileAccounts.accounts.length, 4);
+
+ let { prefsDocument } = await openNewPrefsTab(
+ "paneCompose",
+ "compositionAttachmentsCategory"
+ );
+
+ let accountList = prefsDocument.getElementById("cloudFileView");
+ is(accountList.itemCount, 4);
+
+ is(accountList.getItemAtIndex(0).value, "someKey3");
+ is(accountList.getItemAtIndex(1).value, "someKey2");
+ is(accountList.getItemAtIndex(2).value, "someKey4");
+ is(accountList.getItemAtIndex(3).value, "someKey1");
+
+ await closePrefsTab();
+ info("Stopping extension");
+ await extension.unload();
+ Services.prefs.deleteBranch("mail.cloud_files.accounts");
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_compose.js b/comm/mail/components/preferences/test/browser/browser_compose.js
new file mode 100644
index 0000000000..ea253cb555
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_compose.js
@@ -0,0 +1,87 @@
+/* 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/. */
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneCompose",
+ "compositionMainCategory",
+ {
+ checkboxID: "addExtension",
+ pref: "mail.forward_add_extension",
+ },
+ {
+ checkboxID: "autoSave",
+ pref: "mail.compose.autosave",
+ enabledElements: ["#autoSaveInterval"],
+ },
+ {
+ checkboxID: "mailWarnOnSendAccelKey",
+ pref: "mail.warn_on_send_accel_key",
+ },
+ {
+ checkboxID: "spellCheckBeforeSend",
+ pref: "mail.SpellCheckBeforeSend",
+ },
+ {
+ checkboxID: "inlineSpellCheck",
+ pref: "mail.spellcheck.inline",
+ }
+ );
+
+ await testCheckboxes(
+ "paneCompose",
+ "FontSelect",
+ {
+ checkboxID: "useReaderDefaults",
+ pref: "msgcompose.default_colors",
+ enabledInverted: true,
+ enabledElements: [
+ "#textColorLabel",
+ "#textColorButton",
+ "#backgroundColorLabel",
+ "#backgroundColorButton",
+ ],
+ },
+ {
+ checkboxID: "defaultToParagraph",
+ pref: "mail.compose.default_to_paragraph",
+ }
+ );
+
+ await testCheckboxes(
+ "paneCompose",
+ "compositionAddressingCategory",
+ {
+ checkboxID: "addressingAutocomplete",
+ pref: "mail.enable_autocomplete",
+ },
+ {
+ checkboxID: "autocompleteLDAP",
+ pref: "ldap_2.autoComplete.useDirectory",
+ enabledElements: ["#directoriesList", "#editButton"],
+ },
+ {
+ checkboxID: "emailCollectionOutgoing",
+ pref: "mail.collect_email_address_outgoing",
+ enabledElements: ["#localDirectoriesList"],
+ }
+ );
+});
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneCompose",
+ "compositionAttachmentsCategory",
+ {
+ checkboxID: "attachment_reminder_label",
+ pref: "mail.compose.attachment_reminder",
+ enabledElements: ["#attachment_reminder_button"],
+ },
+ {
+ checkboxID: "enableThreshold",
+ pref: "mail.compose.big_attachments.notify",
+ enabledElements: ["#cloudFileThreshold"],
+ }
+ );
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_general.js b/comm/mail/components/preferences/test/browser/browser_general.js
new file mode 100644
index 0000000000..f011be1e38
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_general.js
@@ -0,0 +1,380 @@
+/* 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"
+);
+
+add_task(async () => {
+ requestLongerTimeout(2);
+
+ // Temporarily disable `Once` StaticPrefs check for this test so that we
+ // can change layers.acceleration.disabled without debug builds failing.
+ await SpecialPowers.pushPrefEnv({
+ set: [["preferences.force-disable.check.once.policy", true]],
+ });
+});
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneGeneral",
+ "generalCategory",
+ {
+ checkboxID: "mailnewsStartPageEnabled",
+ pref: "mailnews.start_page.enabled",
+ enabledElements: [
+ "#mailnewsStartPageUrl",
+ "#mailnewsStartPageUrl + button",
+ ],
+ },
+ {
+ checkboxID: "alwaysCheckDefault",
+ pref: "mail.shell.checkDefaultClient",
+ }
+ );
+});
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneGeneral",
+ "scrollingGroup",
+ {
+ checkboxID: "useAutoScroll",
+ pref: "general.autoScroll",
+ },
+ {
+ checkboxID: "useSmoothScrolling",
+ pref: "general.smoothScroll",
+ }
+ );
+});
+
+add_task(async () => {
+ await testCheckboxes(
+ "paneGeneral",
+ "enableGloda",
+ {
+ checkboxID: "enableGloda",
+ pref: "mailnews.database.global.indexer.enabled",
+ },
+ {
+ checkboxID: "allowHWAccel",
+ pref: "layers.acceleration.disabled",
+ prefValues: [true, false],
+ }
+ );
+});
+
+add_task(async () => {
+ if (AppConstants.platform != "macosx") {
+ await testCheckboxes(
+ "paneGeneral",
+ "incomingMailCategory",
+ {
+ checkboxID: "newMailNotification",
+ pref: "mail.biff.play_sound",
+ enabledElements: ["#soundType radio"],
+ },
+ {
+ checkboxID: "newMailNotificationAlert",
+ pref: "mail.biff.show_alert",
+ enabledElements: ["#customizeMailAlert"],
+ }
+ );
+ }
+});
+
+add_task(async () => {
+ if (AppConstants.platform == "macosx") {
+ return;
+ }
+
+ Services.prefs.setBoolPref("mail.biff.play_sound", true);
+
+ await testRadioButtons("paneGeneral", "incomingMailCategory", {
+ pref: "mail.biff.play_sound.type",
+ states: [
+ {
+ id: "system",
+ prefValue: 0,
+ },
+ {
+ id: "custom",
+ prefValue: 1,
+ enabledElements: ["#soundUrlLocation", "#browseForSound"],
+ },
+ ],
+ });
+});
+
+add_task(async () => {
+ await testCheckboxes("paneGeneral", "fontsGroup", {
+ checkboxID: "displayGlyph",
+ pref: "mail.display_glyph",
+ });
+
+ await testCheckboxes(
+ "paneGeneral",
+ "readingAndDisplayCategory",
+ {
+ checkboxID: "automaticallyMarkAsRead",
+ pref: "mailnews.mark_message_read.auto",
+ enabledElements: ["#markAsReadAutoPreferences radio"],
+ },
+ {
+ checkboxID: "closeMsgOnMoveOrDelete",
+ pref: "mail.close_message_window.on_delete",
+ },
+ {
+ checkboxID: "showCondensedAddresses",
+ pref: "mail.showCondensedAddresses",
+ }
+ );
+});
+
+add_task(async () => {
+ Services.prefs.setBoolPref("mailnews.mark_message_read.auto", true);
+
+ await testRadioButtons(
+ "paneGeneral",
+ "mark_read_immediately",
+ {
+ pref: "mailnews.mark_message_read.delay",
+ states: [
+ {
+ id: "mark_read_immediately",
+ prefValue: false,
+ },
+ {
+ id: "markAsReadAfterDelay",
+ prefValue: true,
+ enabledElements: ["#markAsReadDelay"],
+ },
+ ],
+ },
+ {
+ pref: "mail.openMessageBehavior",
+ states: [
+ {
+ id: "newTab",
+ prefValue: 2,
+ },
+ {
+ id: "newWindow",
+ prefValue: 0,
+ },
+ {
+ id: "existingWindow",
+ prefValue: 1,
+ },
+ ],
+ }
+ );
+});
+
+add_task(async () => {
+ // We don't want to wake up the platform search for this test.
+ // if (AppConstants.platform == "macosx") {
+ // tests.push({
+ // checkboxID: "searchIntegration",
+ // pref: "mail.spotlight.enable",
+ // });
+ // } else if (AppConstants.platform == "win") {
+ // tests.push({
+ // checkboxID: "searchIntegration",
+ // pref: "mail.winsearch.enable",
+ // });
+ // }
+
+ await testCheckboxes(
+ "paneGeneral",
+ "allowSmartSize",
+ {
+ checkboxID: "allowSmartSize",
+ pref: "browser.cache.disk.smart_size.enabled",
+ prefValues: [true, false],
+ enabledElements: ["#cacheSize"],
+ },
+ {
+ checkboxID: "offlineCompactFolder",
+ pref: "mail.prompt_purge_threshhold",
+ enabledElements: [
+ "#offlineCompactFolderMin",
+ "#offlineCompactFolderAutomatically",
+ ],
+ }
+ );
+});
+
+add_task(async () => {
+ await testRadioButtons("paneGeneral", "formatLocale", {
+ pref: "intl.regional_prefs.use_os_locales",
+ states: [
+ {
+ id: "appLocale",
+ prefValue: false,
+ },
+ {
+ id: "rsLocale",
+ prefValue: true,
+ },
+ ],
+ });
+});
+
+add_task(async () => {
+ await testRadioButtons("paneGeneral", "filesAttachmentCategory", {
+ pref: "browser.download.useDownloadDir",
+ states: [
+ {
+ id: "saveTo",
+ prefValue: true,
+ enabledElements: ["#downloadFolder", "#chooseFolder"],
+ },
+ {
+ id: "alwaysAsk",
+ prefValue: false,
+ },
+ ],
+ });
+});
+
+add_task(async function testTagDialog() {
+ const { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ "paneGeneral",
+ "tagsCategory"
+ );
+
+ let newTagDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/tagDialog.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+
+ EventUtils.sendString("tbird", dialogWindow);
+ // "#000080" == rgb(0, 0, 128);
+ dialogDocument.getElementById("tagColorPicker").value = "#000080";
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.querySelector("dialog").getButton("accept"),
+ {},
+ dialogWindow
+ );
+ await new Promise(r => setTimeout(r));
+ },
+ }
+ );
+
+ let newTagButton = prefsDocument.getElementById("newTagButton");
+ EventUtils.synthesizeMouseAtCenter(newTagButton, {}, prefsWindow);
+ await newTagDialogPromise;
+
+ let tagList = prefsDocument.getElementById("tagList");
+
+ Assert.ok(
+ tagList.querySelector('richlistitem[value="tbird"]'),
+ "new tbird tag should be in the list"
+ );
+ Assert.equal(
+ tagList.querySelector('richlistitem[value="tbird"]').style.color,
+ "rgb(0, 0, 128)",
+ "tbird tag color should be correct"
+ );
+ Assert.equal(
+ tagList.querySelectorAll('richlistitem[value="tbird"]').length,
+ 1,
+ "new tbird tag should be in the list exactly once"
+ );
+
+ Assert.equal(
+ tagList.querySelector('richlistitem[value="tbird"]'),
+ tagList.selectedItem,
+ "tbird tag should be selected"
+ );
+
+ // Now edit the tag. The key should stay the same, name and color will change.
+
+ let editTagDialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/tagDialog.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+
+ Assert.equal(
+ dialogDocument.getElementById("name").value,
+ "tbird",
+ "should have existing tbird tag name prefilled"
+ );
+ Assert.equal(
+ dialogDocument.getElementById("tagColorPicker").value,
+ "#000080",
+ "should have existing tbird tag color prefilled"
+ );
+
+ EventUtils.sendString("-xx", dialogWindow); // => tbird-xx
+ // "#FFD700" == rgb(255, 215, 0);
+ dialogDocument.getElementById("tagColorPicker").value = "#FFD700";
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.querySelector("dialog").getButton("accept"),
+ {},
+ dialogWindow
+ );
+ await new Promise(r => setTimeout(r));
+ },
+ }
+ );
+
+ let editTagButton = prefsDocument.getElementById("editTagButton");
+ EventUtils.synthesizeMouseAtCenter(editTagButton, {}, prefsWindow);
+ await editTagDialogPromise;
+
+ Assert.ok(
+ tagList.querySelector(
+ 'richlistitem[value="tbird"] > label[value="tbird-xx"]'
+ ),
+ "tbird-xx tag should be in the list"
+ );
+ Assert.equal(
+ tagList.querySelector('richlistitem[value="tbird"]').style.color,
+ "rgb(255, 215, 0)",
+ "tbird-xx tag color should be correct"
+ );
+ Assert.equal(
+ tagList.querySelectorAll('richlistitem[value="tbird"]').length,
+ 1,
+ "tbird-xx tag should be in the list exactly once"
+ );
+
+ // And remove it.
+
+ EventUtils.synthesizeMouseAtCenter(
+ prefsDocument.getElementById("removeTagButton"),
+ {},
+ prefsWindow
+ );
+ await new Promise(r => setTimeout(r));
+
+ Assert.equal(
+ tagList.querySelector('richlistitem[value="tbird"]'),
+ null,
+ "tbird-xx (with key tbird) tag should have been removed from the list"
+ );
+
+ await closePrefsTab();
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_openPreferences.js b/comm/mail/components/preferences/test/browser/browser_openPreferences.js
new file mode 100644
index 0000000000..01aceb085a
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_openPreferences.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function getStoredLastSelected() {
+ return Services.xulStore.getValue(
+ "about:preferences",
+ "paneDeck",
+ "lastSelected"
+ );
+}
+
+add_task(async () => {
+ // Check that openPreferencesTab with no arguments and no stored value opens the first pane.
+ Services.xulStore.removeDocument("about:preferences");
+
+ let { prefsWindow } = await openNewPrefsTab();
+ is(prefsWindow.gLastCategory.category, "paneGeneral");
+
+ await closePrefsTab();
+});
+
+add_task(async () => {
+ // Check that openPreferencesTab with one argument opens the right pane…
+ Services.xulStore.removeDocument("about:preferences");
+
+ await openNewPrefsTab("panePrivacy");
+ is(getStoredLastSelected(), "panePrivacy");
+
+ await closePrefsTab();
+
+ // … even with a value in the XULStore.
+ await openNewPrefsTab("paneCompose");
+ is(getStoredLastSelected(), "paneCompose");
+
+ await closePrefsTab();
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_privacy.js b/comm/mail/components/preferences/test/browser/browser_privacy.js
new file mode 100644
index 0000000000..1b91ec35e8
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_privacy.js
@@ -0,0 +1,454 @@
+/* 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/. */
+
+add_task(async () => {
+ await testCheckboxes(
+ "panePrivacy",
+ "privacyCategory",
+ {
+ checkboxID: "acceptRemoteContent",
+ pref: "mailnews.message_display.disable_remote_image",
+ prefValues: [true, false],
+ },
+ {
+ checkboxID: "keepHistory",
+ pref: "places.history.enabled",
+ },
+ {
+ checkboxID: "acceptCookies",
+ pref: "network.cookie.cookieBehavior",
+ prefValues: [2, 0],
+ enabledElements: ["#acceptThirdPartyMenu"],
+ unaffectedElements: ["#cookieExceptions"],
+ },
+ {
+ checkboxID: "privacyDoNotTrackCheckbox",
+ pref: "privacy.donottrackheader.enabled",
+ }
+ );
+});
+
+add_task(async () => {
+ await testCheckboxes(
+ "panePrivacy",
+ "privacyJunkCategory",
+ {
+ checkboxID: "manualMark",
+ pref: "mail.spam.manualMark",
+ enabledElements: ["#manualMarkMode radio"],
+ },
+ {
+ checkboxID: "markAsReadOnSpam",
+ pref: "mail.spam.markAsReadOnSpam",
+ },
+ {
+ checkboxID: "enableJunkLogging",
+ pref: "mail.spam.logging.enabled",
+ enabledElements: ["#openJunkLogButton"],
+ }
+ );
+
+ await testCheckboxes("panePrivacy", "privacySecurityCategory", {
+ checkboxID: "enablePhishingDetector",
+ pref: "mail.phishing.detection.enabled",
+ });
+
+ await testCheckboxes("panePrivacy", "enableAntiVirusQuarantine", {
+ checkboxID: "enableAntiVirusQuarantine",
+ pref: "mailnews.downloadToTempFile",
+ });
+});
+
+add_task(async () => {
+ Services.prefs.setBoolPref("mail.spam.manualMark", true);
+
+ await testRadioButtons("panePrivacy", "privacyJunkCategory", {
+ pref: "mail.spam.manualMarkMode",
+ states: [
+ {
+ id: "manualMarkMode0",
+ prefValue: 0,
+ },
+ {
+ id: "manualMarkMode1",
+ prefValue: 1,
+ },
+ ],
+ });
+});
+
+add_task(async () => {
+ // Telemetry pref is locked.
+ // await testCheckboxes("paneAdvanced", undefined, {
+ // checkboxID: "submitTelemetryBox",
+ // pref: "toolkit.telemetry.enabled",
+ // });
+
+ await testCheckboxes("panePrivacy", "enableOCSP", {
+ checkboxID: "enableOCSP",
+ pref: "security.OCSP.enabled",
+ prefValues: [0, 1],
+ });
+});
+
+// Here we'd test the update choices, but I don't want to go near that.
+add_task(async () => {
+ await testRadioButtons("panePrivacy", "enableOCSP", {
+ pref: "security.default_personal_cert",
+ states: [
+ {
+ id: "certSelectionAuto",
+ prefValue: "Select Automatically",
+ },
+ {
+ id: "certSelectionAsk",
+ prefValue: "Ask Every Time",
+ },
+ ],
+ });
+});
+
+add_task(async function testRemoteContentDialog() {
+ const { prefsDocument, prefsWindow } = await openNewPrefsTab("panePrivacy");
+
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+ const url = dialogDocument.getElementById("url");
+ const permissionsTree =
+ dialogDocument.getElementById("permissionsTree");
+
+ EventUtils.sendString("accept.invalid", dialogWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnAllow"),
+ {},
+ dialogWindow
+ );
+ await new Promise(f => setTimeout(f));
+ Assert.equal(url.value, "", "url input should be cleared");
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 1,
+ "new entry should be added to list"
+ );
+
+ Assert.ok(
+ BrowserTestUtils.is_hidden(
+ dialogDocument.getElementById("btnSession")
+ ),
+ "session button should be hidden"
+ );
+
+ EventUtils.sendString("block.invalid", dialogWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnBlock"),
+ {},
+ dialogWindow
+ );
+ await new Promise(f => setTimeout(f));
+ Assert.equal(url.value, "", "url input should be cleared");
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 2,
+ "new entry should be added to list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnApplyChanges"),
+ {},
+ dialogWindow
+ );
+ },
+ }
+ );
+ let remoteContentExceptions = prefsDocument.getElementById(
+ "remoteContentExceptions"
+ );
+ EventUtils.synthesizeMouseAtCenter(remoteContentExceptions, {}, prefsWindow);
+ await dialogPromise;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let acceptURI = Services.io.newURI("http://accept.invalid/");
+ let acceptPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ acceptURI,
+ {}
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(acceptPrincipal, "image"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "accept permission should exist for accept.invalid"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let blockURI = Services.io.newURI("http://block.invalid/");
+ let blockPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ blockURI,
+ {}
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(blockPrincipal, "image"),
+ Ci.nsIPermissionManager.DENY_ACTION,
+ "block permission should exist for block.invalid"
+ );
+
+ dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+ const permissionsTree =
+ dialogDocument.getElementById("permissionsTree");
+
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 2,
+ "list should be populated"
+ );
+
+ permissionsTree.view.selection.select(0);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("removePermission"),
+ {},
+ dialogWindow
+ );
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 1,
+ "row should be removed from list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("removeAllPermissions"),
+ {},
+ dialogWindow
+ );
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 0,
+ "row should be removed from list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnApplyChanges"),
+ {},
+ dialogWindow
+ );
+ },
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(remoteContentExceptions, {}, prefsWindow);
+ await dialogPromise;
+
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(acceptPrincipal, "image"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "permission should be removed for accept.invalid"
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(blockPrincipal, "image"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "permission should be removed for block.invalid"
+ );
+
+ await closePrefsTab();
+});
+
+add_task(async function testCookiesDialog() {
+ const { prefsDocument, prefsWindow } = await openNewPrefsTab("panePrivacy");
+
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+ const url = dialogDocument.getElementById("url");
+ const permissionsTree =
+ dialogDocument.getElementById("permissionsTree");
+
+ EventUtils.sendString("accept.invalid", dialogWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnAllow"),
+ {},
+ dialogWindow
+ );
+ await new Promise(f => setTimeout(f));
+ Assert.equal(url.value, "", "url input should be cleared");
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 1,
+ "new entry should be added to list"
+ );
+
+ EventUtils.sendString("session.invalid", dialogWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnSession"),
+ {},
+ dialogWindow
+ );
+ await new Promise(f => setTimeout(f));
+ Assert.equal(url.value, "", "url input should be cleared");
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 2,
+ "new entry should be added to list"
+ );
+
+ EventUtils.sendString("block.invalid", dialogWindow);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnBlock"),
+ {},
+ dialogWindow
+ );
+ await new Promise(f => setTimeout(f));
+ Assert.equal(url.value, "", "url input should be cleared");
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 3,
+ "new entry should be added to list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnApplyChanges"),
+ {},
+ dialogWindow
+ );
+ },
+ }
+ );
+ let cookieExceptions = prefsDocument.getElementById("cookieExceptions");
+ EventUtils.synthesizeMouseAtCenter(cookieExceptions, {}, prefsWindow);
+ await dialogPromise;
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let acceptURI = Services.io.newURI("http://accept.invalid/");
+ let acceptPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ acceptURI,
+ {}
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(acceptPrincipal, "cookie"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "accept permission should exist for accept.invalid"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let sessionURI = Services.io.newURI("http://session.invalid/");
+ let sessionPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ sessionURI,
+ {}
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(sessionPrincipal, "cookie"),
+ Ci.nsICookiePermission.ACCESS_SESSION,
+ "session permission should exist for session.invalid"
+ );
+
+ // eslint-disable-next-line @microsoft/sdl/no-insecure-url
+ let blockURI = Services.io.newURI("http://block.invalid/");
+ let blockPrincipal = Services.scriptSecurityManager.createContentPrincipal(
+ blockURI,
+ {}
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(blockPrincipal, "cookie"),
+ Ci.nsIPermissionManager.DENY_ACTION,
+ "block permission should exist for block.invalid"
+ );
+
+ dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/permissions.xhtml",
+ {
+ isSubDialog: true,
+ async callback(dialogWindow) {
+ await TestUtils.waitForCondition(
+ () => Services.focus.focusedWindow == dialogWindow,
+ "waiting for subdialog to be focused"
+ );
+
+ const dialogDocument = dialogWindow.document;
+ const permissionsTree =
+ dialogDocument.getElementById("permissionsTree");
+
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 3,
+ "list should be populated"
+ );
+
+ permissionsTree.view.selection.select(0);
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("removePermission"),
+ {},
+ dialogWindow
+ );
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 2,
+ "row should be removed from list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("removeAllPermissions"),
+ {},
+ dialogWindow
+ );
+ Assert.equal(
+ permissionsTree.view.rowCount,
+ 0,
+ "row should be removed from list"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.getElementById("btnApplyChanges"),
+ {},
+ dialogWindow
+ );
+ },
+ }
+ );
+ EventUtils.synthesizeMouseAtCenter(cookieExceptions, {}, prefsWindow);
+ await dialogPromise;
+
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(acceptPrincipal, "cookie"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "permission should be removed for accept.invalid"
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(sessionPrincipal, "cookie"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "permission should be removed for session.invalid"
+ );
+ Assert.equal(
+ Services.perms.testPermissionFromPrincipal(blockPrincipal, "cookie"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "permission should be removed for block.invalid"
+ );
+
+ await closePrefsTab();
+});
diff --git a/comm/mail/components/preferences/test/browser/browser_sync.js b/comm/mail/components/preferences/test/browser/browser_sync.js
new file mode 100644
index 0000000000..108695ebb5
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/browser_sync.js
@@ -0,0 +1,419 @@
+/* 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/. */
+
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const { FxAccounts } = ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+);
+FxAccounts.config.promiseConnectAccountURI = entryPoint =>
+ `https://example.org/?page=connect&entryPoint=${entryPoint}`;
+FxAccounts.config.promiseManageURI = entryPoint =>
+ `https://example.org/?page=manage&entryPoint=${entryPoint}`;
+FxAccounts.config.promiseChangeAvatarURI = entryPoint =>
+ `https://example.org/?page=avatar&entryPoint=${entryPoint}`;
+
+const ALL_ENGINES = [
+ "accounts",
+ "identities",
+ "addressbooks",
+ "calendars",
+ "passwords",
+];
+const PREF_PREFIX = "services.sync.engine";
+
+let prefsWindow, prefsDocument, tabmail;
+
+add_setup(async function () {
+ for (let engine of ALL_ENGINES) {
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.${engine}`, true);
+ }
+
+ ({ prefsWindow, prefsDocument } = await openNewPrefsTab("paneSync"));
+ tabmail = document.getElementById("tabmail");
+
+ /** @implements {nsIExternalProtocolService} */
+ let mockExternalProtocolService = {
+ QueryInterface: ChromeUtils.generateQI(["nsIExternalProtocolService"]),
+ externalProtocolHandlerExists(protocolScheme) {},
+ isExposedProtocol(protocolScheme) {},
+ loadURI(uri, windowContext) {
+ Assert.report(
+ true,
+ undefined,
+ undefined,
+ `should not be opening ${uri.spec} in an external browser`
+ );
+ },
+ };
+
+ let mockExternalProtocolServiceCID = MockRegistrar.register(
+ "@mozilla.org/uriloader/external-protocol-service;1",
+ mockExternalProtocolService
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(mockExternalProtocolServiceCID);
+ });
+});
+
+add_task(async function testSectionStates() {
+ let noFxaAccount = prefsDocument.getElementById("noFxaAccount");
+ let hasFxaAccount = prefsDocument.getElementById("hasFxaAccount");
+ let accountStates = [noFxaAccount, hasFxaAccount];
+
+ let fxaLoginUnverified = prefsDocument.getElementById("fxaLoginUnverified");
+ let fxaLoginRejected = prefsDocument.getElementById("fxaLoginRejected");
+ let fxaLoginVerified = prefsDocument.getElementById("fxaLoginVerified");
+ let loginStates = [fxaLoginUnverified, fxaLoginRejected, fxaLoginVerified];
+
+ let fxaDeviceInfo = prefsDocument.getElementById("fxaDeviceInfo");
+ let syncConnected = prefsDocument.getElementById("syncConnected");
+ let syncDisconnected = prefsDocument.getElementById("syncDisconnected");
+ let syncStates = [syncConnected, syncDisconnected];
+
+ function assertStateVisible(states, visibleState) {
+ for (let state of states) {
+ let visible = BrowserTestUtils.is_visible(state);
+ Assert.equal(
+ visible,
+ state == visibleState,
+ `${state.id} should be ${state == visibleState ? "visible" : "hidden"}`
+ );
+ }
+ }
+
+ function checkStates({
+ accountState,
+ loginState = null,
+ deviceInfoVisible = false,
+ syncState = null,
+ }) {
+ prefsWindow.gSyncPane.updateWeavePrefs();
+ assertStateVisible(accountStates, accountState);
+ assertStateVisible(loginStates, loginState);
+ Assert.equal(
+ BrowserTestUtils.is_visible(fxaDeviceInfo),
+ deviceInfoVisible,
+ `fxaDeviceInfo should be ${deviceInfoVisible ? "visible" : "hidden"}`
+ );
+ assertStateVisible(syncStates, syncState);
+ }
+
+ async function assertTabOpens(target, expectedURL) {
+ if (typeof target == "string") {
+ target = prefsDocument.getElementById(target);
+ }
+
+ let tabPromise = BrowserTestUtils.waitForEvent(window, "TabOpen");
+ EventUtils.synthesizeMouseAtCenter(target, {}, prefsWindow);
+ await tabPromise;
+ let tab = tabmail.currentTabInfo;
+ await BrowserTestUtils.browserLoaded(tab.browser);
+ Assert.equal(
+ tab.browser.currentURI.spec,
+ `https://example.org/${expectedURL}`,
+ "a tab opened to the correct URL"
+ );
+ tabmail.closeTab(tab);
+ }
+
+ info("No account");
+ Assert.equal(prefsWindow.UIState.get().status, "not_configured");
+ checkStates({ accountState: noFxaAccount });
+
+ // Check clicking the Sign In button opens the connect page in a tab.
+ await assertTabOpens("noFxaSignIn", "?page=connect&entryPoint=");
+
+ // Override the window's UIState object with mock values.
+ let baseState = {
+ email: "test@invalid",
+ displayName: "Testy McTest",
+ avatarURL:
+ "https://example.org/browser/comm/mail/components/preferences/test/browser/files/avatar.png",
+ avatarIsDefault: false,
+ };
+ let mockState;
+ prefsWindow.UIState = {
+ ON_UPDATE: "sync-ui-state:update",
+ STATUS_LOGIN_FAILED: "login_failed",
+ STATUS_NOT_CONFIGURED: "not_configured",
+ STATUS_NOT_VERIFIED: "not_verified",
+ STATUS_SIGNED_IN: "signed_in",
+ get() {
+ return mockState;
+ },
+ };
+
+ info("Login not verified");
+ mockState = { ...baseState, status: "not_verified" };
+ checkStates({
+ accountState: hasFxaAccount,
+ loginState: fxaLoginUnverified,
+ deviceInfoVisible: true,
+ });
+ Assert.deepEqual(
+ await prefsDocument.l10n.getAttributes(
+ prefsDocument.getElementById("fxaAccountMailNotVerified")
+ ),
+ {
+ id: "sync-pane-email-not-verified",
+ args: { userEmail: "test@invalid" },
+ },
+ "email address set correctly"
+ );
+
+ // Untested: Resend and remove account buttons.
+
+ info("Login rejected");
+ mockState = { ...baseState, status: "login_failed" };
+ checkStates({
+ accountState: hasFxaAccount,
+ loginState: fxaLoginRejected,
+ deviceInfoVisible: true,
+ });
+ Assert.deepEqual(
+ await prefsDocument.l10n.getAttributes(
+ prefsDocument.getElementById("fxaAccountLoginRejected")
+ ),
+ {
+ id: "sync-signedin-login-failure",
+ args: { userEmail: "test@invalid" },
+ },
+ "email address set correctly"
+ );
+
+ // Untested: Sign in and remove account buttons.
+
+ info("Logged in, sync disabled");
+ mockState = { ...baseState, status: "verified", syncEnabled: false };
+ checkStates({
+ accountState: hasFxaAccount,
+ loginState: fxaLoginVerified,
+ deviceInfoVisible: true,
+ syncState: syncDisconnected,
+ });
+ let photo = fxaLoginVerified.querySelector(".contact-photo");
+ Assert.equal(
+ photo.src,
+ "https://example.org/browser/comm/mail/components/preferences/test/browser/files/avatar.png",
+ "avatar image set correctly"
+ );
+
+ // Check clicking the avatar image opens the avatar page in a tab.
+ await assertTabOpens(photo, "?page=avatar&entryPoint=preferences");
+
+ Assert.equal(
+ prefsDocument.getElementById("fxaDisplayName").textContent,
+ "Testy McTest",
+ "display name set correctly"
+ );
+ Assert.equal(
+ prefsDocument.getElementById("fxaEmailAddress").textContent,
+ "test@invalid",
+ "email address set correctly"
+ );
+
+ // Check clicking the management link opens the management page in a tab.
+ await assertTabOpens("verifiedManage", "?page=manage&entryPoint=preferences");
+
+ // Untested: Sign out button.
+
+ info("Device name section");
+ let deviceNameInput = prefsDocument.getElementById("fxaDeviceNameInput");
+ let deviceNameCancel = prefsDocument.getElementById("fxaDeviceNameCancel");
+ let deviceNameSave = prefsDocument.getElementById("fxaDeviceNameSave");
+ let deviceNameChange = prefsDocument.getElementById(
+ "fxaDeviceNameChangeDeviceName"
+ );
+ Assert.ok(deviceNameInput.readOnly, "input is read-only");
+ Assert.ok(
+ BrowserTestUtils.is_hidden(deviceNameCancel),
+ "cancel button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(deviceNameSave),
+ "save button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(deviceNameChange),
+ "change button is visible"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(deviceNameChange, {}, prefsWindow);
+ Assert.ok(!deviceNameInput.readOnly, "input is writeable");
+ Assert.equal(prefsDocument.activeElement, deviceNameInput, "input is active");
+ Assert.ok(
+ BrowserTestUtils.is_visible(deviceNameCancel),
+ "cancel button is visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(deviceNameSave),
+ "save button is visible"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(deviceNameChange),
+ "change button is hidden"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(deviceNameCancel, {}, prefsWindow);
+ Assert.ok(deviceNameInput.readOnly, "input is read-only");
+ Assert.ok(
+ BrowserTestUtils.is_hidden(deviceNameCancel),
+ "cancel button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_hidden(deviceNameSave),
+ "save button is hidden"
+ );
+ Assert.ok(
+ BrowserTestUtils.is_visible(deviceNameChange),
+ "change button is visible"
+ );
+
+ // Check the turn on sync button works.
+ await openEngineDialog({ expectEngines: ALL_ENGINES, button: "syncSetup" });
+
+ info("Logged in, sync enabled");
+ mockState = { ...baseState, status: "verified", syncEnabled: true };
+ checkStates({
+ accountState: hasFxaAccount,
+ loginState: fxaLoginVerified,
+ deviceInfoVisible: true,
+ syncState: syncConnected,
+ });
+
+ // Untested: Sync now button.
+
+ // Check the learn more link opens a tab.
+ await assertTabOpens("enginesLearnMore", "?page=learnMore");
+
+ // Untested: Disconnect button.
+});
+
+add_task(async function testEngines() {
+ function assertEnginesEnabled(...expectedEnabled) {
+ for (let engine of ALL_ENGINES) {
+ let enabled = Services.prefs.getBoolPref(`${PREF_PREFIX}.${engine}`);
+ Assert.equal(
+ enabled,
+ expectedEnabled.includes(engine),
+ `${engine} should be ${
+ expectedEnabled.includes(engine) ? "enabled" : "disabled"
+ }`
+ );
+ }
+ }
+
+ function assertEnginesShown(...expectEngines) {
+ let ENGINES_TO_ITEMS = {
+ accounts: "showSyncAccount",
+ identities: "showSyncIdentity",
+ addressbooks: "showSyncAddress",
+ calendars: "showSyncCalendar",
+ passwords: "showSyncPasswords",
+ };
+ let expectItems = expectEngines.map(engine => ENGINES_TO_ITEMS[engine]);
+ let items = Array.from(
+ prefsDocument.querySelectorAll("#showSyncedList > li:not([hidden])"),
+ li => li.id
+ );
+ Assert.deepEqual(items, expectItems, "enabled engines shown correctly");
+ }
+
+ assertEnginesShown(...ALL_ENGINES);
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.accounts`, false);
+ assertEnginesShown("identities", "addressbooks", "calendars", "passwords");
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.identities`, false);
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.addressbooks`, false);
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.calendars`, false);
+ assertEnginesShown("passwords");
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.passwords`, false);
+ assertEnginesShown();
+
+ info("Checking the engine selection dialog");
+ await openEngineDialog({
+ toggleEngines: ["accounts", "identities", "passwords"],
+ });
+
+ assertEnginesEnabled("accounts", "identities", "passwords");
+ assertEnginesShown("accounts", "identities", "passwords");
+
+ await openEngineDialog({
+ expectEngines: ["accounts", "identities", "passwords"],
+ toggleEngines: ["calendars", "passwords"],
+ action: "cancel",
+ });
+
+ assertEnginesEnabled("accounts", "identities", "passwords");
+ assertEnginesShown("accounts", "identities", "passwords");
+
+ await openEngineDialog({
+ expectEngines: ["accounts", "identities", "passwords"],
+ toggleEngines: ["calendars", "passwords"],
+ action: "accept",
+ });
+
+ assertEnginesEnabled("accounts", "identities", "calendars");
+ assertEnginesShown("accounts", "identities", "calendars");
+
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.addressbooks`, true);
+ Services.prefs.setBoolPref(`${PREF_PREFIX}.passwords`, true);
+ assertEnginesShown(...ALL_ENGINES);
+});
+
+async function openEngineDialog({
+ expectEngines = [],
+ toggleEngines = [],
+ action = "accept",
+ button = "syncChangeOptions",
+}) {
+ const ENGINES_TO_CHECKBOXES = {
+ accounts: "configSyncAccount",
+ identities: "configSyncIdentity",
+ addressbooks: "configSyncAddress",
+ calendars: "configSyncCalendar",
+ passwords: "configSyncPasswords",
+ };
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ undefined,
+ "chrome://messenger/content/preferences/syncDialog.xhtml",
+ { isSubDialog: true }
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ prefsDocument.getElementById(button),
+ {},
+ prefsWindow
+ );
+ let dialogWindow = await dialogPromise;
+ let dialogDocument = dialogWindow.document;
+ await new Promise(resolve => dialogWindow.setTimeout(resolve));
+
+ let expectItems = expectEngines.map(engine => ENGINES_TO_CHECKBOXES[engine]);
+
+ let checkedItems = Array.from(
+ dialogDocument.querySelectorAll(`input[type="checkbox"]`)
+ )
+ .filter(cb => cb.checked)
+ .map(cb => cb.id);
+ Assert.deepEqual(
+ checkedItems,
+ expectItems,
+ "enabled engines checked correctly"
+ );
+
+ for (let toggleItem of toggleEngines) {
+ let checkbox = dialogDocument.getElementById(
+ ENGINES_TO_CHECKBOXES[toggleItem]
+ );
+ checkbox.checked = !checkbox.checked;
+ }
+
+ EventUtils.synthesizeMouseAtCenter(
+ dialogDocument.querySelector("dialog").getButton(action),
+ {},
+ dialogWindow
+ );
+}
diff --git a/comm/mail/components/preferences/test/browser/files/avatar.png b/comm/mail/components/preferences/test/browser/files/avatar.png
new file mode 100644
index 0000000000..ca0894316a
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/files/avatar.png
Binary files differ
diff --git a/comm/mail/components/preferences/test/browser/files/icon.svg b/comm/mail/components/preferences/test/browser/files/icon.svg
new file mode 100644
index 0000000000..6c1a552445
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/files/icon.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <circle cx="8" cy="8" r="7.5" fill="#ffffff" stroke="#00aa00" stroke-width="1.5"/>
+ <circle cx="5" cy="6" r="1.5" fill="#00aa00"/>
+ <circle cx="11" cy="6" r="1.5" fill="#00aa00"/>
+ <path d="M 12.83,9.30 C 12.24,11.48 10.26,13 8,13 5.75,13 3.74,11.48 3.17,9.29" fill="none" stroke="#00aa00" stroke-width="1.5"/>
+</svg>
diff --git a/comm/mail/components/preferences/test/browser/files/management.html b/comm/mail/components/preferences/test/browser/files/management.html
new file mode 100644
index 0000000000..7e3561d823
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/files/management.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8"/>
+ <title></title>
+</head>
+<body>
+ <a id="a" href="https://www.example.com/">Click me!</a>
+</body>
+</html>
diff --git a/comm/mail/components/preferences/test/browser/head.js b/comm/mail/components/preferences/test/browser/head.js
new file mode 100644
index 0000000000..12cbdb17f1
--- /dev/null
+++ b/comm/mail/components/preferences/test/browser/head.js
@@ -0,0 +1,314 @@
+/* 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 ../../../../base/content/utilityOverlay.js */
+
+async function openNewPrefsTab(paneID, scrollPaneTo, otherArgs) {
+ let tabmail = document.getElementById("tabmail");
+ let prefsTabMode = tabmail.tabModes.preferencesTab;
+
+ is(prefsTabMode.tabs.length, 0, "Prefs tab is not open");
+
+ let prefsDocument = await new Promise(resolve => {
+ Services.obs.addObserver(function documentLoaded(subject) {
+ if (subject.URL.startsWith("about:preferences")) {
+ Services.obs.removeObserver(documentLoaded, "chrome-document-loaded");
+ subject.ownerGlobal.setTimeout(() => resolve(subject));
+ }
+ }, "chrome-document-loaded");
+ openPreferencesTab(paneID, scrollPaneTo, otherArgs);
+ });
+ ok(prefsDocument.URL.startsWith("about:preferences"), "Prefs tab is open");
+
+ prefsDocument = prefsTabMode.tabs[0].browser.contentDocument;
+ let prefsWindow = prefsDocument.ownerGlobal;
+ prefsWindow.resizeTo(screen.availWidth, screen.availHeight);
+
+ if (paneID) {
+ await new Promise(resolve => prefsWindow.setTimeout(resolve));
+ is(
+ prefsWindow.gLastCategory.category,
+ paneID,
+ `Selected pane is ${paneID}`
+ );
+ } else {
+ // If we don't wait here for other scripts to run, they
+ // could be in a bad state if our test closes the tab.
+ await new Promise(resolve => prefsWindow.setTimeout(resolve));
+ }
+
+ registerCleanupOnce();
+
+ await new Promise(resolve => prefsWindow.setTimeout(resolve));
+ let container = prefsDocument.getElementById("preferencesContainer");
+ if (scrollPaneTo && container.scrollHeight > container.clientHeight) {
+ Assert.greater(
+ container.scrollTop,
+ 0,
+ "Prefs page did scroll when it was supposed to"
+ );
+ }
+ return { prefsDocument, prefsWindow };
+}
+
+async function openExistingPrefsTab(paneID, scrollPaneTo, otherArgs) {
+ let tabmail = document.getElementById("tabmail");
+ let prefsTabMode = tabmail.tabModes.preferencesTab;
+
+ is(prefsTabMode.tabs.length, 1, "Prefs tab is open");
+
+ let prefsDocument = prefsTabMode.tabs[0].browser.contentDocument;
+ let prefsWindow = prefsDocument.ownerGlobal;
+ prefsWindow.resizeTo(screen.availWidth, screen.availHeight);
+
+ if (paneID && prefsWindow.gLastCategory.category != paneID) {
+ openPreferencesTab(paneID, scrollPaneTo, otherArgs);
+ }
+
+ await new Promise(resolve => prefsWindow.setTimeout(resolve));
+ is(prefsWindow.gLastCategory.category, paneID, `Selected pane is ${paneID}`);
+
+ if (scrollPaneTo) {
+ Assert.greater(
+ prefsDocument.getElementById("preferencesContainer").scrollTop,
+ 0,
+ "Prefs page did scroll when it was supposed to"
+ );
+ }
+
+ registerCleanupOnce();
+ return { prefsDocument, prefsWindow };
+}
+
+function registerCleanupOnce() {
+ if (registerCleanupOnce.alreadyRegistered) {
+ return;
+ }
+ registerCleanupFunction(closePrefsTab);
+ registerCleanupOnce.alreadyRegistered = true;
+}
+
+async function closePrefsTab() {
+ info("Closing prefs tab");
+ let tabmail = document.getElementById("tabmail");
+ let prefsTab = tabmail.tabModes.preferencesTab.tabs[0];
+ if (prefsTab) {
+ tabmail.closeTab(prefsTab);
+ }
+}
+
+/**
+ * Tests a checkbox sets the preference is set in the right state when the preferences tab opens,
+ * that the preference it relates to is set properly, and any UI elements that should be disabled
+ * by it are disabled.
+ *
+ * Each of the tests arguments is an object describing a test, containing:
+ * checkboxID - the ID of the checkbox to test
+ * pref - the name of a preference,
+ * prefValues - an array of two values: pref value when not checked, pref value when checked
+ * (optional, defaults to [false, true])
+ * enabledElements - an array of CSS selectors (optional)
+ * enabledInverted - if the elements should be disabled when the checkbox is checked (optional)
+ * unaffectedElements - array of CSS selectors that should not be affected by
+ * the toggling of the checkbox.
+ */
+async function testCheckboxes(paneID, scrollPaneTo, ...tests) {
+ for (let initiallyChecked of [true, false]) {
+ info(`Opening ${paneID} with prefs set to ${initiallyChecked}`);
+
+ for (let test of tests) {
+ let wantedValue = initiallyChecked;
+ if (test.prefValues) {
+ wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0];
+ }
+ if (typeof wantedValue == "number") {
+ Services.prefs.setIntPref(test.pref, wantedValue);
+ } else {
+ Services.prefs.setBoolPref(test.pref, wantedValue);
+ }
+ }
+
+ let { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ paneID,
+ scrollPaneTo
+ );
+
+ let testUIState = function (test, checked) {
+ let wantedValue = checked;
+ if (test.prefValues) {
+ wantedValue = wantedValue ? test.prefValues[1] : test.prefValues[0];
+ }
+ let checkbox = prefsDocument.getElementById(test.checkboxID);
+ is(
+ checkbox.checked,
+ checked,
+ wantedValue,
+ "Checkbox " + (checked ? "is" : "isn't") + " checked"
+ );
+ if (typeof wantedValue == "number") {
+ is(
+ Services.prefs.getIntPref(test.pref, -999),
+ wantedValue,
+ `Pref is ${wantedValue}`
+ );
+ } else {
+ is(
+ Services.prefs.getBoolPref(test.pref),
+ wantedValue,
+ `Pref is ${wantedValue}`
+ );
+ }
+
+ if (test.enabledElements) {
+ let disabled = checked;
+ if (test.enabledInverted) {
+ disabled = !disabled;
+ }
+ for (let selector of test.enabledElements) {
+ let elements = prefsDocument.querySelectorAll(selector);
+ ok(
+ elements.length >= 1,
+ `At least one element matched '${selector}'`
+ );
+ for (let element of elements) {
+ is(
+ element.disabled,
+ !disabled,
+ "Element " + (disabled ? "isn't" : "is") + " disabled"
+ );
+ }
+ }
+ }
+ };
+
+ let testUnaffected = function (ids, states) {
+ ids.forEach((sel, index) => {
+ let isOk = prefsDocument.querySelector(sel).disabled === states[index];
+ is(isOk, true, `Element "${sel}" is unaffected`);
+ });
+ };
+
+ for (let test of tests) {
+ info(`Checking ${test.checkboxID}`);
+
+ let unaffectedSelectors = test.unaffectedElements || [];
+ let unaffectedStates = unaffectedSelectors.map(
+ sel => prefsDocument.querySelector(sel).disabled
+ );
+
+ let checkbox = prefsDocument.getElementById(test.checkboxID);
+ checkbox.scrollIntoView(false);
+ testUIState(test, initiallyChecked);
+
+ EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow);
+ testUIState(test, !initiallyChecked);
+ testUnaffected(unaffectedSelectors, unaffectedStates);
+
+ EventUtils.synthesizeMouseAtCenter(checkbox, {}, prefsWindow);
+ testUIState(test, initiallyChecked);
+ testUnaffected(unaffectedSelectors, unaffectedStates);
+ }
+
+ await closePrefsTab();
+ }
+}
+
+/**
+ * Tests a set of radio buttons is in the right state when the preferences tab opens, and when
+ * the selected button changes that the preference it relates to is set properly, and any related
+ * UI elements that should be disabled are disabled.
+ *
+ * Each of the tests arguments is an object describing a test, containing:
+ * pref - the name of an integer preference,
+ * states - an array with each element describing a radio button:
+ * id - the ID of the button to test,
+ * prefValue - the value the pref should be set to
+ * enabledElements - an array of CSS selectors to elements that should be enabled when this
+ * radio button is selected (optional)
+ */
+async function testRadioButtons(paneID, scrollPaneTo, ...tests) {
+ for (let { pref, states } of tests) {
+ for (let initialState of states) {
+ info(`Opening ${paneID} with ${pref} set to ${initialState.prefValue}`);
+
+ if (typeof initialState.prefValue == "number") {
+ Services.prefs.setIntPref(pref, initialState.prefValue);
+ } else if (typeof initialState.prefValue == "boolean") {
+ Services.prefs.setBoolPref(pref, initialState.prefValue);
+ } else {
+ Services.prefs.setCharPref(pref, initialState.prefValue);
+ }
+
+ let { prefsDocument, prefsWindow } = await openNewPrefsTab(
+ paneID,
+ scrollPaneTo
+ );
+
+ let testUIState = function (currentState) {
+ info(`Testing with ${pref} set to ${currentState.prefValue}`);
+ for (let state of states) {
+ let isCurrentState = state == currentState;
+ let radio = prefsDocument.getElementById(state.id);
+ is(radio.selected, isCurrentState, `${state.id}.selected`);
+
+ if (state.enabledElements) {
+ for (let selector of state.enabledElements) {
+ let elements = prefsDocument.querySelectorAll(selector);
+ ok(
+ elements.length >= 1,
+ `At least one element matched '${selector}'`
+ );
+ for (let element of elements) {
+ is(
+ element.disabled,
+ !isCurrentState,
+ "Element " + (isCurrentState ? "isn't" : "is") + " disabled"
+ );
+ }
+ }
+ }
+ }
+ if (typeof initialState.prefValue == "number") {
+ is(
+ Services.prefs.getIntPref(pref, -999),
+ currentState.prefValue,
+ `Pref is ${currentState.prefValue}`
+ );
+ } else if (typeof initialState.prefValue == "boolean") {
+ is(
+ Services.prefs.getBoolPref(pref),
+ currentState.prefValue,
+ `Pref is ${currentState.prefValue}`
+ );
+ } else {
+ is(
+ Services.prefs.getCharPref(pref, "FAKE VALUE"),
+ currentState.prefValue,
+ `Pref is ${currentState.prefValue}`
+ );
+ }
+ };
+
+ // Check the initial setup is correct.
+ testUIState(initialState);
+ // Cycle through possible values, checking each one.
+ for (let state of states) {
+ if (state == initialState) {
+ continue;
+ }
+ let radio = prefsDocument.getElementById(state.id);
+ radio.scrollIntoView(false);
+ EventUtils.synthesizeMouseAtCenter(radio, {}, prefsWindow);
+ testUIState(state);
+ }
+ // Go back to the initial value.
+ let initialRadio = prefsDocument.getElementById(initialState.id);
+ initialRadio.scrollIntoView(false);
+ EventUtils.synthesizeMouseAtCenter(initialRadio, {}, prefsWindow);
+ testUIState(initialState);
+
+ await closePrefsTab();
+ }
+ }
+}