diff --git a/README.md b/README.md
index 8ba8f06..b093ee7 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# Quoting Email Headers in Thunderbird and Postbox
- [![Version](https://img.shields.io/badge/version-2.1.0-blue.svg)](https://github.com/jeevatkm/ReplyWithHeaderMozilla/releases/latest) [![License](https://img.shields.io/github/license/jeevatkm/ReplyWithHeaderMozilla.svg)](LICENSE)
+# Quoting Email Headers in Thunderbird
+ [![Version](https://img.shields.io/badge/version-2.3.0-blue.svg)](https://github.com/jeevatkm/ReplyWithHeaderMozilla/releases/latest) [![License](https://img.shields.io/github/license/jeevatkm/ReplyWithHeaderMozilla.svg)](LICENSE)
-ReplyWithHeaderMozilla aka [RWH Mozilla] is an add-on for Thunderbird and Postbox mail client that enables email header.
-Brings Outlook header capabilities into Thunderbird and Postbox.
+ReplyWithHeaderMozilla aka [RWH Thunderbird] is an add-on for Thunderbird mail client that enables email header.
+Brings Outlook header capabilities into Thunderbird.
**| [Home page](http://myjeeva.com/replywithheader-mozilla) | [Download RWH Mozilla](https://addons.mozilla.org/en-US/thunderbird/addon/replywithheader/) | [Report Issues](#report-issues) | [FAQ](http://myjeeva.com/replywithheader-mozilla#faq) |**
@@ -22,7 +22,7 @@ ReplyWithHeaderMozilla uses [GitHub’s integrated issue tracking system](https:
# Creator
-Jeevanandam M. (jeeva@myjeeva.com)
+[Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com)
# Contributors
diff --git a/chrome.manifest b/chrome.manifest
deleted file mode 100644
index 4daf96c..0000000
--- a/chrome.manifest
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# Copyright (c) Jeevanandam M. (jeeva@myjeeva.com)
-#
-# 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://github.com/jeevatkm/ReplyWithHeaderMozilla/blob/master/LICENSE.
-#
-
-content replywithheader chrome/content/
-resource replywithheader chrome/resource/
-
-skin replywithheader default/1.0 skin/
-
-# messengercompose.xul [MailCompose Window]
-overlay chrome://messenger/content/messengercompose/messengercompose.xul chrome://replywithheader/content/messengerComposeOverlay.xul
\ No newline at end of file
diff --git a/chrome/content/core.js b/chrome/content/core.js
index a01ee45..506e4fe 100755
--- a/chrome/content/core.js
+++ b/chrome/content/core.js
@@ -13,8 +13,8 @@ var EXPORTED_SYMBOLS = ['ReplyWithHeader'];
var { XPCOMUtils } = ChromeUtils.import('resource://gre/modules/XPCOMUtils.jsm');
var { AddonManager } = ChromeUtils.import('resource://gre/modules/AddonManager.jsm');
-var { rwhlog } = ChromeUtils.import('resource://replywithheader/log.jsm');
-var { rwhhost } = ChromeUtils.import('resource://replywithheader/host.jsm');
+var { rwhlog } = ChromeUtils.import('chrome://replywithheader/content/log.jsm');
+var { rwhhost } = ChromeUtils.import('chrome://replywithheader/content/host.jsm');
// ReplyWithHeader Add-On ID
const ReplyWithHeaderAddOnID = 'replywithheader@myjeeva.com';
@@ -357,7 +357,7 @@ var ReplyWithHeader = {
// for HTML emails
if (this.isHtmlMail) {
- let fontFace = this.Prefs.headerFontFace;
+ let fontFace = this.Prefs.headerFontFace;
let fontSize = this.Prefs.headerFontSize;
let fontSizeUnit = this.Prefs.headerFontSizeUnit;
let fontColor = this.Prefs.headerFontColor;
@@ -850,7 +850,7 @@ var ReplyWithHeader = {
try {
ReplyWithHeader.handleMailCompose();
} catch(ex) {
- rwhlog.errorWithException('An error occurred, please report an issue to add-on author here '
+ rwhlog.errorWithException('An error occurred, please report an issue to add-on author here '
+ '- https://github.com/jeevatkm/ReplyWithHeaderMozilla/issues', ex);
}
},
@@ -861,17 +861,17 @@ var ReplyWithHeader = {
handleMailCompose: function() {
let prefs = this.Prefs;
rwhlog.enableDebug = prefs.isDebugEnabled;
-
- rwhlog.debug('Initializing ' + ReplyWithHeader.addOnName + ' v' + ReplyWithHeader.addOnVersion
- + ' (' + rwhhost.app + ' ' + rwhhost.version
+
+ rwhlog.debug('Initializing ' + ReplyWithHeader.addOnName + ' v' + ReplyWithHeader.addOnVersion
+ + ' (' + rwhhost.app + ' ' + rwhhost.version
+ ', ' + rwhhost.OS + ', ' + rwhhost.buildID + ')');
-
+
/*
* ReplyWithHeader has to be enabled; extensions.replywithheader.enable=true and
* ReplyWithHeader.isOkayToMoveOn must return true
* Add-On comes into play :)
*/
-
+
if (prefs.isEnabled && this.isOkayToMoveOn) {
this.hdrCnt = 4; // From, To, Subject, Date
@@ -918,7 +918,7 @@ var ReplyWithHeader = {
showAlert: function(str) {
if (str) {
try {
- this.alerts.showAlertNotification('chrome://replywithheader/skin/icon-64.png',
+ this.alerts.showAlertNotification('resource://replywithheader/icon-64.png',
'ReplyWithHeader', str, false, '', null, '');
} catch (ex) {
rwhlog.errorWithException('Unable to show RWH notify alert.', ex);
diff --git a/chrome/resource/host.jsm b/chrome/content/host.jsm
similarity index 100%
rename from chrome/resource/host.jsm
rename to chrome/content/host.jsm
diff --git a/chrome/resource/log.jsm b/chrome/content/log.jsm
similarity index 100%
rename from chrome/resource/log.jsm
rename to chrome/content/log.jsm
diff --git a/chrome/content/messengerComposeOverlay.xul b/chrome/content/messengerComposeOverlay.xul
deleted file mode 100755
index caf9abb..0000000
--- a/chrome/content/messengerComposeOverlay.xul
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/chrome/content/prefs.js b/chrome/content/prefs.js
index 0e69a9a..b5e747e 100755
--- a/chrome/content/prefs.js
+++ b/chrome/content/prefs.js
@@ -12,8 +12,8 @@
/* globals ReplyWithHeader */
var { XPCOMUtils } = ChromeUtils.import('resource://gre/modules/XPCOMUtils.jsm');
var { Services } = ChromeUtils.import('resource://gre/modules/Services.jsm');
-var { rwhlog } = ChromeUtils.import('resource://replywithheader/log.jsm');
-var { rwhhost } = ChromeUtils.import('resource://replywithheader/host.jsm');
+var { rwhlog } = ChromeUtils.import('chrome://replywithheader/content/log.jsm');
+var { rwhhost } = ChromeUtils.import('chrome://replywithheader/content/host.jsm');
ReplyWithHeader.Prefs = {
prefService: Services.prefs,
@@ -174,7 +174,7 @@ ReplyWithHeader.Prefs = {
},
createMenuItem: function(v, l) {
- var menuItem = document.createElement('menuitem');
+ var menuItem = document.createXULElement('menuitem');
menuItem.setAttribute('value', v);
menuItem.setAttribute('label', l);
return menuItem;
@@ -185,7 +185,7 @@ ReplyWithHeader.Prefs = {
.createInstance(Ci.nsIFontEnumerator).EnumerateAllFonts({});
let hdrFontface = this.headerFontFace;
- let menuPopup = document.createElement('menupopup');
+ let menuPopup = document.createXULElement('menupopup');
let selectedIdx = 0;
for (let fontCount = allFonts.length, i = 0; i < fontCount; i++) {
@@ -201,7 +201,7 @@ ReplyWithHeader.Prefs = {
},
loadFontSizes: function() {
- let menuPopup = document.createElement('menupopup');
+ let menuPopup = document.createXULElement('menupopup');
let selectedIdx = 0;
for (let i = 7, j = 0; i < 35; i++, j++) {
diff --git a/chrome/content/rwh-prefs.xul b/chrome/content/rwh-prefs.xhtml
similarity index 92%
rename from chrome/content/rwh-prefs.xul
rename to chrome/content/rwh-prefs.xhtml
index 9ac3fd6..415e923 100755
--- a/chrome/content/rwh-prefs.xul
+++ b/chrome/content/rwh-prefs.xhtml
@@ -5,7 +5,7 @@
radio { font-size: 12px; margin-top:-2px; }
radio:first-child { margin-top:0px; }
radiogroup { margin-left:20px; }
- radiogroup.horizontal { display:inline-block; }
+ radiogroup.horizontal { display:inline-block; display: flex; }
.horizontal radio { margin-right:20px; }
button { cursor: pointer }
" type="text/css"?>
@@ -23,22 +23,22 @@
buttons="accept"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
- title="RWH Preferences" style="width:525px"
+ title="RWH Preferences" style="width:625px"
onload="ReplyWithHeader.Prefs.init();">
-
+
-
+
-
-
+
-
@@ -69,7 +69,7 @@
-
@@ -79,7 +79,7 @@
-
@@ -91,7 +91,7 @@
-
@@ -99,7 +99,7 @@
-
@@ -107,8 +107,8 @@
-
@@ -116,14 +116,14 @@
-
-
-
@@ -141,7 +141,7 @@
-
@@ -154,7 +154,7 @@
-
@@ -168,7 +168,7 @@
-
@@ -183,7 +183,7 @@
-
@@ -205,41 +205,41 @@
-
-
-
-
-
-
-
+
@@ -250,9 +250,9 @@
-
+
-
@@ -261,14 +261,14 @@
-
-
diff --git a/chrome/content/scripts/messengercompose.js b/chrome/content/scripts/messengercompose.js
new file mode 100644
index 0000000..faad9ee
--- /dev/null
+++ b/chrome/content/scripts/messengercompose.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) Jeevanandam M. (jeeva@myjeeva.com)
+ *
+ * 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://github.com/jeevatkm/ReplyWithHeaderMozilla/blob/master/LICENSE
+ */
+
+// Import any needed modules.
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Load an additional JavaScript file.
+Services.scriptloader.loadSubScript('chrome://replywithheader/content/core.js', window, 'UTF-8');
+Services.scriptloader.loadSubScript('chrome://replywithheader/content/prefs.js', window, 'UTF-8');
+Services.scriptloader.loadSubScript('chrome://replywithheader/content/i18n.js', window, 'UTF-8');
+// Services.scriptloader.loadSubScript('chrome://replywithheader/content/rwh-prefs.js', window, 'UTF-8');
+
+function onLoad(activatedWhileWindowOpen) {
+ // Registering RWH into compose window of Thunderbird - msgcomposeWindow
+ window.addEventListener('compose-window-init', function() {
+ window.ReplyWithHeader.Prefs.fixCursorBlink();
+ window.ReplyWithHeader.init()
+ }, true);
+}
+
+function onUnload(deactivatedWhileWindowOpen) {
+ /* unused */
+}
\ No newline at end of file
diff --git a/experiment/api/WindowListener/implementation.js b/experiment/api/WindowListener/implementation.js
new file mode 100644
index 0000000..6da0faa
--- /dev/null
+++ b/experiment/api/WindowListener/implementation.js
@@ -0,0 +1,664 @@
+/*
+ * This file is provided by the addon-developer-support repository at
+ * https://github.com/thundernest/addon-developer-support
+ *
+ * Version: 1.22
+ * - to reduce confusions, only check built-in URLs as add-on URLs cannot
+ * be resolved if a temp installed add-on has bin zipped
+ *
+ * Version: 1.21
+ * - print debug messages only if add-ons are installed temporarily from
+ * the add-on debug page
+ * - add checks to registered windows and scripts, if they actually exists
+ *
+ * Version: 1.20
+ * - fix long delay before customize window opens
+ * - fix non working removal of palette items
+ *
+ * Version: 1.19
+ * - add support for ToolbarPalette
+ *
+ * Version: 1.18
+ * - execute shutdown script also during global app shutdown (fixed)
+ *
+ * Version: 1.17
+ * - execute shutdown script also during global app shutdown
+ *
+ * Version: 1.16
+ * - support for persist
+ *
+ * Version: 1.15
+ * - make (undocumented) startup() async
+ *
+ * Version: 1.14
+ * - support resource urls
+ *
+ * Version: 1.12
+ * - no longer allow to enforce custom "namespace"
+ * - no longer call it namespace but uniqueRandomID / scopeName
+ * - expose special objects as the global WL object
+ * - autoremove injected elements after onUnload has ben executed
+ *
+ * Version: 1.9
+ * - automatically remove all entries added by injectElements
+ *
+ * Version: 1.8
+ * - add injectElements
+ *
+ * Version: 1.7
+ * - add injectCSS
+ * - add optional enforced namespace
+ *
+ * Version: 1.6
+ * - added mutation observer to be able to inject into browser elements
+ * - use larger icons as fallback
+ *
+ * Author: John Bieling (john@thunderbird.net)
+ *
+ * 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 some things we need.
+var { ExtensionCommon } = ChromeUtils.import("resource://gre/modules/ExtensionCommon.jsm");
+var { ExtensionSupport } = ChromeUtils.import("resource:///modules/ExtensionSupport.jsm");
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var WindowListener = class extends ExtensionCommon.ExtensionAPI {
+ log(msg) {
+ if (this.debug) console.log("WindowListener API: " + msg);
+ }
+
+ error(msg) {
+ if (this.debug) console.error("WindowListener API: " + msg);
+ }
+
+ getAPI(context) {
+ // track if this is the background/main context
+ this.isBackgroundContext = (context.viewType == "background");
+
+ this.uniqueRandomID = "AddOnNS" + context.extension.instanceId;
+ this.menu_addonsManager_id ="addonsManager";
+ this.menu_addonsManager_prefs_id = "addonsManager_prefs_revived";
+ this.menu_addonPrefs_id = "addonPrefs_revived";
+
+ this.registeredWindows = {};
+ this.pathToStartupScript = null;
+ this.pathToShutdownScript = null;
+ this.pathToOptionsPage = null;
+ this.chromeHandle = null;
+ this.chromeData = null;
+ this.resourceData = null;
+ this.openWindows = [];
+ this.debug = context.extension.addonData.temporarilyInstalled;
+
+ const aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"].getService(Ci.amIAddonManagerStartup);
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+
+ let self = this;
+
+ return {
+ WindowListener: {
+
+ aDocumentExistsAt(uriString) {
+ self.log("Checking if document at <" + uriString + "> used in registration actually exists.");
+ try {
+ let uriObject = Services.io.newURI(uriString);
+ let content = Cu.readUTF8URI(uriObject);
+ } catch (e) {
+ Components.utils.reportError(e);
+ return false;
+ }
+ return true;
+ },
+
+ registerOptionsPage(optionsUrl) {
+ self.pathToOptionsPage = optionsUrl.startsWith("chrome://")
+ ? optionsUrl
+ : context.extension.rootURI.resolve(optionsUrl);
+ },
+
+ registerDefaultPrefs(defaultUrl) {
+ let url = context.extension.rootURI.resolve(defaultUrl);
+
+ let prefsObj = {};
+ prefsObj.Services = ChromeUtils.import("resource://gre/modules/Services.jsm").Services;
+ prefsObj.pref = function(aName, aDefault) {
+ let defaults = Services.prefs.getDefaultBranch("");
+ switch (typeof aDefault) {
+ case "string":
+ return defaults.setCharPref(aName, aDefault);
+
+ case "number":
+ return defaults.setIntPref(aName, aDefault);
+
+ case "boolean":
+ return defaults.setBoolPref(aName, aDefault);
+
+ default:
+ throw new Error("Preference <" + aName + "> has an unsupported type <" + typeof aDefault + ">. Allowed are string, number and boolean.");
+ }
+ }
+ Services.scriptloader.loadSubScript(url, prefsObj, "UTF-8");
+ },
+
+ registerChromeUrl(data) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ let chromeData = [];
+ let resourceData = [];
+ for (let entry of data) {
+ if (entry[0] == "resource") resourceData.push(entry);
+ else chromeData.push(entry)
+ }
+
+ if (chromeData.length > 0) {
+ const manifestURI = Services.io.newURI(
+ "manifest.json",
+ null,
+ context.extension.rootURI
+ );
+ self.chromeHandle = aomStartup.registerChrome(manifestURI, chromeData);
+ }
+
+ for (let res of resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ let uri = Services.io.newURI(
+ res[2],
+ null,
+ context.extension.rootURI
+ );
+ resProto.setSubstitutionWithFlags(
+ res[1],
+ uri,
+ resProto.ALLOW_CONTENT_ACCESS
+ );
+ }
+
+ self.chromeData = chromeData;
+ self.resourceData = resourceData;
+ },
+
+ registerWindow(windowHref, jsFile) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ if (self.debug && !this.aDocumentExistsAt(windowHref)) {
+ self.error("Attempt to register an injector script for non-existent window: " + windowHref);
+ return;
+ }
+
+ if (!self.registeredWindows.hasOwnProperty(windowHref)) {
+ // path to JS file can either be chrome:// URL or a relative URL
+ let path = jsFile.startsWith("chrome://")
+ ? jsFile
+ : context.extension.rootURI.resolve(jsFile)
+
+ self.registeredWindows[windowHref] = path;
+ } else {
+ self.error("Window <" +windowHref + "> has already been registered");
+ }
+ },
+
+ registerStartupScript(aPath) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ self.pathToStartupScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+ },
+
+ registerShutdownScript(aPath) {
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ self.pathToShutdownScript = aPath.startsWith("chrome://")
+ ? aPath
+ : context.extension.rootURI.resolve(aPath);
+ },
+
+ async startListening() {
+ // async sleep function using Promise
+ async function sleep(delay) {
+ let timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ return new Promise(function(resolve, reject) {
+ let event = {
+ notify: function(timer) {
+ resolve();
+ }
+ }
+ timer.initWithCallback(event, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+ });
+ };
+
+ if (!self.isBackgroundContext)
+ throw new Error("The WindowListener API may only be called from the background page.");
+
+ // load the registered startup script, if one has been registered
+ // (mail3:pane may not have been fully loaded yet)
+ if (self.pathToStartupScript) {
+ let startupJS = {};
+ startupJS.WL = {}
+ startupJS.WL.extension = self.extension;
+ startupJS.WL.messenger = Array.from(self.extension.views).find(
+ view => view.viewType === "background").xulBrowser.contentWindow
+ .wrappedJSObject.browser;
+ try {
+ if (self.pathToStartupScript) {
+ Services.scriptloader.loadSubScript(self.pathToStartupScript, startupJS, "UTF-8");
+ // delay startup until startup has been finished
+ self.log("Waiting for async startup() in <" + self.pathToStartupScript + "> to finish.");
+ if (startupJS.startup) {
+ await startupJS.startup();
+ self.log("startup() in <" + self.pathToStartupScript + "> finished");
+ } else {
+ self.log("No startup() in <" + self.pathToStartupScript + "> found.");
+ }
+ }
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+
+ let urls = Object.keys(self.registeredWindows);
+ if (urls.length > 0) {
+ // Before registering the window listener, check which windows are already open
+ self.openWindows = [];
+ for (let window of Services.wm.getEnumerator(null)) {
+ self.openWindows.push(window);
+ }
+
+ // Register window listener for all pre-registered windows
+ ExtensionSupport.registerWindowListener("injectListener_" + self.uniqueRandomID, {
+ // React on all windows and manually reduce to the registered
+ // windows, so we can do special actions when the main
+ // messenger window is opened.
+ //chromeURLs: Object.keys(self.registeredWindows),
+ async onLoadWindow(window) {
+ // Create add-on scope
+ window[self.uniqueRandomID] = {};
+
+ // Special action #1: If this is the main messenger window
+ if (window.location.href == "chrome://messenger/content/messenger.xul" ||
+ window.location.href == "chrome://messenger/content/messenger.xhtml") {
+
+ if (self.pathToOptionsPage) {
+ try {
+ // add the add-on options menu if needed
+ if (!window.document.getElementById(self.menu_addonsManager_prefs_id)) {
+ let addonprefs = window.MozXULElement.parseXULToFragment(`
+
+ `, ["chrome://messenger/locale/messenger.dtd"]);
+
+ let element_addonsManager = window.document.getElementById(self.menu_addonsManager_id);
+ element_addonsManager.parentNode.insertBefore(addonprefs, element_addonsManager.nextSibling);
+ }
+
+ // add the options entry
+ let element_addonPrefs = window.document.getElementById(self.menu_addonPrefs_id);
+ let id = self.menu_addonPrefs_id + "_" + self.uniqueRandomID;
+
+ // Get the best size of the icon (16px or bigger)
+ let iconSizes = Object.keys(self.extension.manifest.icons);
+ iconSizes.sort((a,b)=>a-b);
+ let bestSize = iconSizes.filter(e => parseInt(e) >= 16).shift();
+ let icon = bestSize ? self.extension.manifest.icons[bestSize] : "";
+
+ let name = self.extension.manifest.name;
+ let entry = window.MozXULElement.parseXULToFragment(
+ ``);
+ element_addonPrefs.appendChild(entry);
+ window.document.getElementById(id).addEventListener("command", function() {window.openDialog(self.pathToOptionsPage, "AddonOptions")});
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+
+ // Special action #2: If this page contains browser elements
+ let browserElements = window.document.getElementsByTagName("browser");
+ if (browserElements.length > 0) {
+ //register a MutationObserver
+ window[self.uniqueRandomID]._mObserver = new window.MutationObserver(function(mutations) {
+ mutations.forEach(async function(mutation) {
+ if (mutation.attributeName == "src" && self.registeredWindows.hasOwnProperty(mutation.target.getAttribute("src"))) {
+ // When the MutationObserver callsback, the window is still showing "about:black" and it is going
+ // to unload and then load the new page. Any eventListener attached to the window will be removed
+ // so we cannot listen for the load event. We have to poll manually to learn when loading has finished.
+ // On my system it takes 70ms.
+ let loaded = false;
+ for (let i=0; i < 100 && !loaded; i++) {
+ await sleep(100);
+ let targetWindow = mutation.target.contentWindow.wrappedJSObject;
+ if (targetWindow && targetWindow.location.href == mutation.target.getAttribute("src") && targetWindow.document.readyState == "complete") {
+ loaded = true;
+ break;
+ }
+ }
+ if (loaded) {
+ let targetWindow = mutation.target.contentWindow.wrappedJSObject;
+ // Create add-on scope
+ targetWindow[self.uniqueRandomID] = {};
+ // Inject with isAddonActivation = false
+ self._loadIntoWindow(targetWindow, false);
+ }
+ }
+ });
+ });
+
+ for (let element of browserElements) {
+ if (self.registeredWindows.hasOwnProperty(element.getAttribute("src"))) {
+ let targetWindow = element.contentWindow.wrappedJSObject;
+ // Create add-on scope
+ targetWindow[self.uniqueRandomID] = {};
+ // Inject with isAddonActivation = true
+ self._loadIntoWindow(targetWindow, true);
+ } else {
+ // Window/Browser is not yet fully loaded, postpone injection via MutationObserver
+ window[self.uniqueRandomID]._mObserver.observe(element, { attributes: true, childList: false, characterData: false });
+ }
+ }
+ }
+
+ // Load JS into window
+ self._loadIntoWindow(window, self.openWindows.includes(window));
+ },
+
+ onUnloadWindow(window) {
+ // Remove JS from window, window is being closed, addon is not shut down
+ self._unloadFromWindow(window, false);
+ }
+ });
+ } else {
+ self.error("Failed to start listening, no windows registered");
+ }
+ },
+
+ }
+ };
+ }
+
+ _loadIntoWindow(window, isAddonActivation) {
+ if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
+ try {
+ let uniqueRandomID = this.uniqueRandomID;
+
+ // Add reference to window to add-on scope
+ window[this.uniqueRandomID].window = window;
+ window[this.uniqueRandomID].document = window.document;
+
+ // Keep track of toolbarpalettes we are injecting into
+ window[this.uniqueRandomID]._toolbarpalettes = {};
+
+ //Create WLDATA object
+ window[this.uniqueRandomID].WL = {};
+ window[this.uniqueRandomID].WL.scopeName = this.uniqueRandomID;
+
+ // Add helper function to inject CSS to WLDATA object
+ window[this.uniqueRandomID].WL.injectCSS = function (cssFile) {
+ let element;
+ let v = parseInt(Services.appinfo.version.split(".").shift());
+
+ // using createElementNS in TB78 delays the insert process and hides any security violation errors
+ if (v > 68) {
+ element = window.document.createElement("link");
+ } else {
+ let ns = window.document.documentElement.lookupNamespaceURI("html");
+ element = window.document.createElementNS(ns, "link");
+ }
+
+ element.setAttribute("wlapi_autoinjected", uniqueRandomID);
+ element.setAttribute("rel", "stylesheet");
+ element.setAttribute("href", cssFile);
+ return window.document.documentElement.appendChild(element);
+ }
+
+ // Add helper function to inject XUL to WLDATA object
+ window[this.uniqueRandomID].WL.injectElements = function (xulString, dtdFiles = [], debug = false) {
+ let toolbarsToResolve = [];
+
+ function checkElements(stringOfIDs) {
+ let arrayOfIDs = stringOfIDs.split(",").map(e => e.trim());
+ for (let id of arrayOfIDs) {
+ let element = window.document.getElementById(id);
+ if (element) {
+ return element;
+ }
+ }
+ return null;
+ }
+
+
+ function injectChildren(elements, container) {
+ if (debug) console.log(elements);
+
+ for (let i = 0; i < elements.length; i++) {
+ // take care of persists
+ const uri = window.document.documentURI;
+ for (const persistentNode of elements[i].querySelectorAll("[persist]")) {
+ for (const persistentAttribute of persistentNode.getAttribute("persist").trim().split(" ")) {
+ if (Services.xulStore.hasValue(uri, persistentNode.id, persistentAttribute)) {
+ persistentNode.setAttribute(
+ persistentAttribute,
+ Services.xulStore.getValue(uri, persistentNode.id, persistentAttribute)
+ );
+ }
+ }
+ }
+
+ if (elements[i].hasAttribute("insertafter") && checkElements(elements[i].getAttribute("insertafter"))) {
+ let insertAfterElement = checkElements(elements[i].getAttribute("insertafter"));
+
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertafter " + insertAfterElement.id);
+ if (debug && elements[i].id && window.document.getElementById(elements[i].id)) {
+ console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
+ }
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ insertAfterElement.parentNode.insertBefore(elements[i], insertAfterElement.nextSibling);
+
+ } else if (elements[i].hasAttribute("insertbefore") && checkElements(elements[i].getAttribute("insertbefore"))) {
+ let insertBeforeElement = checkElements(elements[i].getAttribute("insertbefore"));
+
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": insertbefore " + insertBeforeElement.id);
+ if (debug && elements[i].id && window.document.getElementById(elements[i].id)) {
+ console.error("The id <" + elements[i].id + "> of the injected element already exists in the document!");
+ }
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ insertBeforeElement.parentNode.insertBefore(elements[i], insertBeforeElement);
+
+ } else if (elements[i].id && window.document.getElementById(elements[i].id)) {
+ // existing container match, dive into recursivly
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + " is an existing container, injecting into " + elements[i].id);
+ injectChildren(Array.from(elements[i].children), window.document.getElementById(elements[i].id));
+
+ } else if (elements[i].localName === "toolbarpalette") {
+ // These vanish from the document but still exist via the palette property
+ if (debug) console.log(elements[i].id + " is a toolbarpalette");
+ let boxes = [...window.document.getElementsByTagName("toolbox")];
+ let box = boxes.find(box => box.palette && box.palette.id === elements[i].id);
+ let palette = box ? box.palette : null;
+
+ if (!palette) {
+ if (debug) console.log(`The palette for ${elements[i].id} could not be found, deferring to later`);
+ continue;
+ }
+
+ if (debug) console.log(`The toolbox for ${elements[i].id} is ${box.id}`);
+
+ toolbarsToResolve.push(...box.querySelectorAll("toolbar"));
+ toolbarsToResolve.push(...window.document.querySelectorAll(`toolbar[toolboxid="${box.id}"]`));
+ for (let child of elements[i].children) {
+ child.setAttribute("wlapi_autoinjected", uniqueRandomID);
+ }
+ window[uniqueRandomID]._toolbarpalettes[palette.id] = palette;
+ injectChildren(Array.from(elements[i].children), palette);
+ } else {
+ // append element to the current container
+ if (debug) console.log(elements[i].tagName + "#" + elements[i].id + ": append to " + container.id);
+ elements[i].setAttribute("wlapi_autoinjected", uniqueRandomID);
+ container.appendChild(elements[i]);
+ }
+ }
+ }
+
+ if (debug) console.log ("Injecting into root document:");
+ injectChildren(Array.from(window.MozXULElement.parseXULToFragment(xulString, dtdFiles).children), window.document.documentElement);
+
+ for (let bar of toolbarsToResolve) {
+ let currentset = Services.xulStore.getValue(
+ window.location,
+ bar.id,
+ "currentset"
+ );
+ if (currentset) {
+ bar.currentSet = currentset;
+ } else if (bar.getAttribute("defaultset")) {
+ bar.currentSet = bar.getAttribute("defaultset");
+ }
+ }
+ }
+
+ // Add extension object to WLDATA object
+ window[this.uniqueRandomID].WL.extension = this.extension;
+ // Add messenger object to WLDATA object
+ window[this.uniqueRandomID].WL.messenger = Array.from(this.extension.views).find(
+ view => view.viewType === "background").xulBrowser.contentWindow
+ .wrappedJSObject.browser;
+ // Load script into add-on scope
+ Services.scriptloader.loadSubScript(this.registeredWindows[window.location.href], window[this.uniqueRandomID], "UTF-8");
+ window[this.uniqueRandomID].onLoad(isAddonActivation);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+ }
+
+ _unloadFromWindow(window, isAddonDeactivation) {
+ // unload any contained browser elements
+ if (window.hasOwnProperty(this.uniqueRandomID) && window[this.uniqueRandomID].hasOwnProperty("_mObserver")) {
+ window[this.uniqueRandomID]._mObserver.disconnect();
+ let browserElements = window.document.getElementsByTagName("browser");
+ for (let element of browserElements) {
+ this._unloadFromWindow(element.contentWindow.wrappedJSObject, isAddonDeactivation);
+ }
+ }
+
+ if (window.hasOwnProperty(this.uniqueRandomID) && this.registeredWindows.hasOwnProperty(window.location.href)) {
+ // Remove this window from the list of open windows
+ this.openWindows = this.openWindows.filter(e => (e != window));
+
+ if (window[this.uniqueRandomID].onUnload) {
+ try {
+ // Call onUnload()
+ window[this.uniqueRandomID].onUnload(isAddonDeactivation);
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+ }
+
+ // Remove all auto injected objects
+ let elements = Array.from(window.document.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
+ for (let element of elements) {
+ element.remove();
+ }
+
+ // Remove all autoinjected toolbarpalette items
+ for (const palette of Object.values(window[this.uniqueRandomID]._toolbarpalettes)) {
+ let elements = Array.from(palette.querySelectorAll('[wlapi_autoinjected="' + this.uniqueRandomID + '"]'));
+ for (let element of elements) {
+ element.remove();
+ }
+ }
+
+ }
+
+ // Remove add-on scope, if it exists
+ if (window.hasOwnProperty(this.uniqueRandomID)) {
+ delete window[this.uniqueRandomID];
+ }
+ }
+
+ onShutdown(isAppShutdown) {
+ // Unload from all still open windows
+ let urls = Object.keys(this.registeredWindows);
+ if (urls.length > 0) {
+ for (let window of Services.wm.getEnumerator(null)) {
+
+ //remove our entry in the add-on options menu
+ if (
+ this.pathToOptionsPage &&
+ (window.location.href == "chrome://messenger/content/messenger.xul" ||
+ window.location.href == "chrome://messenger/content/messenger.xhtml")) {
+ let id = this.menu_addonPrefs_id + "_" + this.uniqueRandomID;
+ window.document.getElementById(id).remove();
+
+ //do we have to remove the entire add-on options menu?
+ let element_addonPrefs = window.document.getElementById(this.menu_addonPrefs_id);
+ if (element_addonPrefs.children.length == 0) {
+ window.document.getElementById(this.menu_addonsManager_prefs_id).remove();
+ }
+ }
+
+ // if it is app shutdown, it is not just an add-on deactivation
+ this._unloadFromWindow(window, !isAppShutdown);
+ }
+ // Stop listening for new windows.
+ ExtensionSupport.unregisterWindowListener("injectListener_" + this.uniqueRandomID);
+ }
+
+ // Load registered shutdown script
+ let shutdownJS = {};
+ shutdownJS.extension = this.extension;
+ try {
+ if (this.pathToShutdownScript) Services.scriptloader.loadSubScript(this.pathToShutdownScript, shutdownJS, "UTF-8");
+ } catch (e) {
+ Components.utils.reportError(e)
+ }
+
+ // Extract all registered chrome content urls
+ let chromeUrls = [];
+ if (this.chromeData) {
+ for (let chromeEntry of this.chromeData) {
+ if (chromeEntry[0].toLowerCase().trim() == "content") {
+ chromeUrls.push("chrome://" + chromeEntry[1] + "/");
+ }
+ }
+ }
+
+ // Unload JSMs of this add-on
+ const rootURI = this.extension.rootURI.spec;
+ for (let module of Cu.loadedModules) {
+ if (module.startsWith(rootURI) || (module.startsWith("chrome://") && chromeUrls.find(s => module.startsWith(s)))) {
+ this.log("Unloading: " + module);
+ Cu.unload(module);
+ }
+ }
+
+ // Flush all caches
+ Services.obs.notifyObservers(null, "startupcache-invalidate");
+ this.registeredWindows = {};
+
+ if (this.resourceData) {
+ const resProto = Cc["@mozilla.org/network/protocol;1?name=resource"].getService(Ci.nsISubstitutingProtocolHandler);
+ for (let res of this.resourceData) {
+ // [ "resource", "shortname" , "path" ]
+ resProto.setSubstitution(
+ res[1],
+ null,
+ );
+ }
+ }
+
+ if (this.chromeHandle) {
+ this.chromeHandle.destruct();
+ this.chromeHandle = null;
+ }
+ }
+};
\ No newline at end of file
diff --git a/experiment/api/WindowListener/schema.json b/experiment/api/WindowListener/schema.json
new file mode 100644
index 0000000..72fd3ee
--- /dev/null
+++ b/experiment/api/WindowListener/schema.json
@@ -0,0 +1,91 @@
+[
+ {
+ "namespace": "WindowListener",
+ "functions": [
+ {
+ "name": "registerDefaultPrefs",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Relative path to the default file."
+ }
+ ]
+ },
+ {
+ "name": "registerOptionsPage",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to the options page, which should be made accessible in the (legacy) Add-On Options menu."
+ }
+ ]
+ },
+ {
+ "name": "registerChromeUrl",
+ "type": "function",
+ "description": "Register folders which should be available as chrome:// urls (as defined in the legacy chrome.manifest)",
+ "parameters": [
+ {
+ "name": "data",
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items" : {
+ "type": "string"
+ }
+ },
+ "description": "Array of manifest url definitions (content, locale, resource)"
+ }
+ ]
+ },
+ {
+ "name": "startListening",
+ "type": "function",
+ "async": true,
+ "parameters": []
+ },
+ {
+ "name": "registerWindow",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "windowHref",
+ "type": "string",
+ "description": "Url of the window, which should be listen for."
+ },
+ {
+ "name": "jsFile",
+ "type": "string",
+ "description": "Path to the JavaScript file, which should be loaded into the window."
+ }
+ ]
+ },
+ {
+ "name": "registerStartupScript",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to a JavaScript file, which should be executed on add-on startup. The script will be executed after the main application window has been sucessfully loaded."
+ }
+ ]
+ },
+ {
+ "name": "registerShutdownScript",
+ "type": "function",
+ "parameters": [
+ {
+ "name": "aPath",
+ "type": "string",
+ "description": "Path to a JavaScript file, which should be executed on add-on shutdown."
+ }
+ ]
+ }
+ ]
+ }
+ ]
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index 7c0b2b9..6e637ae 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,23 +1,40 @@
{
- "manifest_version": 2,
"name": "ReplyWithHeader",
+ "short_name": "RWH",
"description": "Outlook style headers and few goodies for Thunderbird",
- "version": "2.2.1",
+ "version_name": "2.3.0",
+ "version": "2.3.0",
"author": "Jeevanandam M.",
"homepage_url": "https://myjeeva.com/replywithheader-mozilla",
+
"applications": {
"gecko": {
"id": "replywithheader@myjeeva.com",
- "strict_min_version": "68.0"
+ "strict_min_version": "78.0"
}
},
+
"icons": {
"64": "skin/icon-64.png",
"128": "icon.png"
},
- "legacy": {
- "options": {
- "page": "chrome://replywithheader/content/rwh-prefs.xul"
+
+ "background": {
+ "scripts": [
+ "scripts/background.js"
+ ]
+ },
+
+ "experiment_apis": {
+ "WindowListener": {
+ "schema": "experiment/api/WindowListener/schema.json",
+ "parent": {
+ "scopes": ["addon_parent"],
+ "paths": [["WindowListener"]],
+ "script": "experiment/api/WindowListener/implementation.js"
+ }
}
- }
+ },
+
+ "manifest_version": 2
}
diff --git a/scripts/background.js b/scripts/background.js
new file mode 100644
index 0000000..7a2b0b8
--- /dev/null
+++ b/scripts/background.js
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) Jeevanandam M. (jeeva@myjeeva.com)
+ *
+ * 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://github.com/jeevatkm/ReplyWithHeaderMozilla/blob/master/LICENSE
+ */
+
+(async () => {
+
+ messenger.WindowListener.registerDefaultPrefs("defaults/preferences/rwh-defaults.js");
+
+ messenger.WindowListener.registerChromeUrl([
+ ["content", "replywithheader", "chrome/content/"],
+ ["resource", "replywithheader", "skin/"]
+ ]);
+
+ messenger.WindowListener.registerOptionsPage("chrome://replywithheader/content/rwh-prefs.xhtml");
+
+ messenger.WindowListener.registerWindow(
+ "chrome://messenger/content/messengercompose/messengercompose.xhtml",
+ "chrome://replywithheader/content/scripts/messengercompose.js"
+ );
+
+ messenger.WindowListener.startListening();
+
+})()
\ No newline at end of file