Bug 1448808 - Implement legacy overlay loader for WebExtensions. r=darktrojan a=jorgk
authorPhilipp Kewisch <mozilla@kewis.ch>
Fri, 20 Apr 2018 22:29:47 +0200
changeset 32302 74bfdeb9f25f245f78dbec5d9299b230a4103d1a
parent 32301 2098a38fa6b5bdf865c6716ea3e10f1ef04d94f5
child 32303 eccd4a2309f5b3492f94fe500806e4997ea514b7
push id385
push userclokep@gmail.com
push dateTue, 04 Sep 2018 23:26:14 +0000
reviewersdarktrojan, jorgk
bugs1448808
Bug 1448808 - Implement legacy overlay loader for WebExtensions. r=darktrojan a=jorgk MozReview-Commit-ID: 5Ke6q3bZK8Z
common/.eslintrc.js
common/moz.build
common/public/moz.build
common/public/nsCommonBaseCID.h
common/public/nsIComponentManagerExtra.idl
common/src/ChromeManifest.jsm
common/src/Overlays.jsm
common/src/extensionSupport.jsm
common/src/moz.build
common/src/nsCommonModule.cpp
common/src/nsComponentManagerExtra.cpp
common/src/nsComponentManagerExtra.h
mail/base/content/extensions.xml
mail/base/content/extensionsOverlay.css
mail/base/jar.mn
mail/components/extensions/ext-mail.json
mail/components/extensions/jar.mn
mail/components/extensions/parent/ext-legacy.js
mail/components/extensions/schemas/legacy.json
mail/locales/en-US/chrome/messenger/extensionsOverlay.properties
mail/moz.build
--- a/common/.eslintrc.js
+++ b/common/.eslintrc.js
@@ -1,10 +1,15 @@
 "use strict";
 
 module.exports = {
   "rules": {
     // Don't Disallow Undeclared Variables (for now).
     // The linter does not see many globals from imported files
     // and .xul linked .js files, so there are too many false positives.
     "no-undef": "off",
+
+    // Require spaces around operators, except for a|0.
+    // Disabled for now given eslint doesn't support default args yet
+    // "space-infix-ops": [2, { "int32Hint": true }],
+    "space-infix-ops": 0,
   },
 };
new file mode 100644
--- /dev/null
+++ b/common/moz.build
@@ -0,0 +1,9 @@
+# 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/.
+
+DIRS += [
+    'public',
+    'src'
+]
new file mode 100644
--- /dev/null
+++ b/common/public/moz.build
@@ -0,0 +1,14 @@
+# 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/.
+
+XPIDL_SOURCES += [
+    'nsIComponentManagerExtra.idl'
+]
+
+XPIDL_MODULE = 'msgcommonbase'
+
+EXPORTS += [
+    'nsCommonBaseCID.h'
+]
new file mode 100644
--- /dev/null
+++ b/common/public/nsCommonBaseCID.h
@@ -0,0 +1,21 @@
+/* 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/. */
+
+#ifndef nsgCommonBaseCID_h__
+#define nsgCommonBaseCID_h__
+
+#include "nsISupports.h"
+#include "nsIFactory.h"
+#include "nsIComponentManager.h"
+
+// nsComponentManagerExtra
+#define NS_COMPONENTMANAGEREXTRA_CONTRACTID \
+  "@mozilla.org/component-manager-extra;1"
+
+#define NS_COMPONENTMANAGEREXTRA_CID \
+{ /* b4359b53-3060-46ff-ad42-e67eea6ccf59 */ \
+ 0xb4359b53, 0x3060, 0x46ff, \
+ {0xad, 0x42, 0xe6, 0x7e, 0xea, 0x6c, 0xcf, 0x59}}
+
+#endif // nsCommonBaseCID_h__
new file mode 100644
--- /dev/null
+++ b/common/public/nsIComponentManagerExtra.idl
@@ -0,0 +1,22 @@
+/* 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+/**
+ * Some additional methods in the same style as nsIComponentManager
+ */
+[scriptable, uuid(fe5948c1-458a-464f-9251-310f8535a34c)]
+interface nsIComponentManagerExtra : nsISupports
+{
+    /**
+     * Register an extension manifest. This is either the path to the unpacked extension folder, or
+     * the path to the xpi file.
+     *
+     * @param {nsIFile} aLocation       The file pointing to the extension root
+     */
+    void addLegacyExtensionManifestLocation(in nsIFile aLocation);
+};
new file mode 100644
--- /dev/null
+++ b/common/src/ChromeManifest.jsm
@@ -0,0 +1,326 @@
+/* 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.EXPORTED_SYMBOLS = ["ChromeManifest"];
+
+/**
+ * A parser for chrome.manifest files. Implements a subset of
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Chrome_Registration
+ */
+class ChromeManifest {
+
+  /**
+   * Constucts the chrome.manifest parser
+   *
+   * @param {Function} loader           An asynchronous function that will load further files, e.g.
+   *                                      those included via the |manifest| instruction. The
+   *                                      function will take the file as an argument and should
+   *                                      resolve with the string contents of that file
+   * @param {Object} options            Object describing the current system. The keys are manifest
+   *                                      instructions
+   */
+  constructor(loader, options) {
+    this.loader = loader;
+    this.options = options;
+
+    this.overlay = new DefaultMap(() => []);
+    this.locales = new DefaultMap(() => new Map());
+    this.style = new DefaultMap(() => new Set());
+    this.category = new DefaultMap(() => new Map());
+
+    this.component = new Map();
+    this.contract = new Map();
+
+    this.content = new Map();
+    this.skin = new Map();
+    this.resource = new Map();
+    this.override = new Map();
+
+  }
+
+
+  /**
+   * Parse the given file.
+   *
+   * @param {String} filename           The filename to load
+   * @param {String} base               The relative directory this file is expected to be in.
+   * @return {Promise}                  Resolved when loading completes
+   */
+  async parse(filename="chrome.manifest", base="") {
+    await this.parseString(await this.loader(filename), base);
+  }
+
+  /**
+   * Parse the given string.
+   *
+   * @param {String} data               The file data to load
+   * @param {String} base               The relative directory this file is expected to be in.
+   * @return {Promise}                  Resolved when loading completes
+   */
+  async parseString(data, base="") {
+    let lines = data.split("\n");
+    let extraManifests = [];
+    for (let line of lines) {
+      let parts = line.split(/\s+/);
+      let directive = parts.shift();
+      switch (directive) {
+        case "manifest":
+          extraManifests.push(this._parseManifest(base, ...parts));
+          break;
+        case "component": this._parseComponent(...parts); break;
+        case "contract": this._parseContract(...parts); break;
+
+        case "category": this._parseCategory(...parts); break;
+        case "content": this._parseContent(...parts); break;
+        case "locale": this._parseLocale(...parts); break;
+        case "skin": this._parseSkin(...parts); break;
+        case "resource": this._parseResource(...parts); break;
+
+        case "overlay": this._parseOverlay(...parts); break;
+        case "style": this._parseStyle(...parts); break;
+        case "override": this._parseOverride(...parts); break;
+      }
+    }
+
+    await Promise.all(extraManifests);
+  }
+
+  /**
+   * Ensure the flags provided for the instruction match our options
+   *
+   * @param {String[]} flags        An array of raw flag values in the form key=value.
+   * @return {Boolean}              True, if the flags match the options provided in the constructor
+   */
+  _parseFlags(flags) {
+    let matchString = (a, sign, b) => {
+      if (sign != "=") {
+        console.warn(`Invalid sign ${sign} in ${a}${sign}${b}, dropping manifest instruction`);
+        return false;
+      }
+      return a == b;
+    };
+
+    let matchVersion = (a, sign, b) => {
+      switch (sign) {
+        case "=": return Services.vc.compare(a, b) == 0;
+        case ">": return Services.vc.compare(a, b) > 0;
+        case "<": return Services.vc.compare(a, b) < 0;
+        case ">=": return Services.vc.compare(a, b) >= 0;
+        case "<=": return Services.vc.compare(a, b) <= 0;
+        default:
+          console.warn(`Invalid sign ${sign} in ${a}${sign}${b}, dropping manifest instruction`);
+          return false;
+      }
+    };
+
+    let flagMatches = (key, typeMatch) => {
+      return !flagdata.has(key) || flagdata.get(key).some(val => typeMatch(this.options[key], ...val));
+    };
+
+    let flagdata = new DefaultMap(() => []);
+
+    for (let flag of flags) {
+      let match = flag.match(/(\w+)(>=|<=|<|>|=)(.*)/);
+      if (match) {
+        flagdata.get(match[1]).push([match[2], match[3]]);
+      } else {
+        console.warn(`Invalid flag ${flag}, dropping manifest instruction`);
+      }
+    }
+
+    return flagMatches("application", matchString) &&
+           flagMatches("appversion", matchVersion) &&
+           flagMatches("platformversion", matchVersion) &&
+           flagMatches("os", matchString) &&
+           flagMatches("osversion", matchVersion) &&
+           flagMatches("abi", matchString);
+  }
+
+  /**
+   * Parse the manifest instruction, to load other files
+   *
+   * @param {String} base       The base directory the manifest file is in
+   * @param {String} filename   The file and path to load
+   * @param {...String} flags   The flags for this instruction
+   * @return {Promise}          Promise resolved when the manifest is loaded
+   */
+  async _parseManifest(base, filename, ...flags) {
+    if (this._parseFlags(flags)) {
+      let dirparts = filename.split("/");
+      dirparts.pop();
+
+      try {
+        await this.parse(filename, base + "/" + dirparts.join("/"));
+      } catch (e) {
+        console.log(`Could not read manifest '${base}/${filename}'.`);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Parse the component instruction, to load xpcom components
+   *
+   * @param {String} classid        The xpcom class id to load
+   * @param {String} loction        The file location of this component
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseComponent(classid, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.component.set(classid, location);
+    }
+  }
+
+  /**
+   * Parse the contract instruction, to load xpcom contract ids
+   *
+   * @param {String} contractid     The xpcom contract id to load
+   * @param {String} location       The file location of this component
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseContract(contractid, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.contract.set(contractid, location);
+    }
+  }
+
+  /**
+   * Parse the category instruction, to set up xpcom categories
+   *
+   * @param {String} category       The name of the category
+   * @param {String} entryName      The category entry name
+   * @param {String} value          The category entry value
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseCategory(category, entryName, value, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.category.get(category).set(entryName, value);
+    }
+  }
+
+  /**
+   * Parse the content instruction, to set chrome content locations
+   *
+   * @param {String} shortname      The content short name, e.g. chrome://shortname/content/
+   * @param {String} location       The location for this content registration
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseContent(shortname, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.content.set(shortname, location);
+    }
+  }
+
+  /**
+   * Parse the locale instruction, to set chrome locale locations
+   *
+   * @param {String} shortname      The locale short name, e.g. chrome://shortname/locale/
+   * @param {String} location       The location for this locale registration
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseLocale(shortname, locale, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.locales.get(shortname).set(locale, location);
+    }
+  }
+
+  /**
+   * Parse the skin instruction, to set chrome skin locations
+   *
+   * @param {String} shortname      The skin short name, e.g. chrome://shortname/skin/
+   * @param {String} location       The location for this skin registration
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseSkin(packagename, skinname, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.skin.set(packagename, location);
+    }
+  }
+
+  /**
+   * Parse the resource instruction, to set up resource uri subtitutions
+   *
+   * @param {String} packagename    The resource package name, e.g. resource://packagename/
+   * @param {String} url            The location for this content registration
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseResource(packagename, location, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.resource.set(packagename, location);
+    }
+  }
+
+  /**
+   * Parse the overlay instruction, to set up xul overlays
+   *
+   * @param {String} targetUrl      The chrome target url
+   * @param {String} overlayUrl     The url of the xul overlay
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseOverlay(targetUrl, overlayUrl, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.overlay.get(targetUrl).push(overlayUrl);
+    }
+  }
+
+  /**
+   * Parse the style instruction, to add stylesheets into chrome windows
+   *
+   * @param {String} uri            The uri of the chrome window
+   * @param {String} sheet          The uri of the css sheet
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseStyle(uri, sheet, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.style.get(uri).add(sheet);
+    }
+  }
+
+  /**
+   * Parse the override instruction, to set chrome uri overrides
+   *
+   * @param {String} uri            The uri being overridden
+   * @param {String} newuri         The replacement uri for the original location
+   * @param {...String} flags       The flags for this instruction
+   */
+  _parseOverride(uri, newuri, ...flags) {
+    if (this._parseFlags(flags)) {
+      this.override.set(uri, newuri);
+    }
+  }
+}
+
+/**
+ * A default map, which assumes a default value on get() if the key doesn't exist
+ */
+class DefaultMap extends Map {
+  /**
+   * Constructs the default map
+   *
+   * @param {Function} _default     A function that returns the default value for this map
+   * @param {*} iterable            An iterable to initialize the map with
+   */
+  constructor(_default, iterable) {
+    super(iterable);
+    this._default = _default;
+
+  }
+
+  /**
+   * Get the given key, creating if necessary
+   *
+   * @param {String} key            The key of the map to get
+   * @param {Boolean} create        True, if the key should be created in case it doesn't exist.
+   */
+  get(key, create=true) {
+    if (this.has(key)) {
+      return super.get(key);
+    } else if (create) {
+      this.set(key, this._default());
+      return super.get(key);
+    }
+
+    return this._default();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/common/src/Overlays.jsm
@@ -0,0 +1,429 @@
+/* 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/. */
+
+/**
+ * Load overlays in a similar way as XUL did for legacy XUL add-ons.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Overlays"];
+
+ChromeUtils.import("resource://gre/modules/Console.jsm");
+ChromeUtils.defineModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "setTimeout", "resource://gre/modules/Timer.jsm");
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+let oconsole = new ConsoleAPI({
+  prefix: "Overlays.jsm",
+  consoleID: "overlays-jsm",
+  maxLogLevel: "all" // "warn"
+});
+
+/**
+ * The overlays class, providing support for loading overlays like they used to work. This class
+ * should likely be called through its static method Overlays.load()
+ */
+class Overlays {
+  /**
+   * Load overlays for the given window using the overlay provider, which can for example be a
+   * ChromeManifest object.
+   *
+   * @param {ChromeManifest} overlayProvider        The overlay provider that contains information
+   *                                                  about styles and overlays.
+   * @param {DOMWindow} window                      The window to load into
+   */
+  static load(overlayProvider, window) {
+    let instance = new Overlays(overlayProvider, window);
+    let location = window.location.protocol + "//" +
+        window.location.host + window.location.pathname;
+
+    let urls = overlayProvider.overlay.get(location, false);
+    instance.load(urls);
+  }
+
+  /**
+   * Constructs the overlays instance. This class should be called via Overlays.load() instead.
+   *
+   * @param {ChromeManifest} overlayProvider        The overlay provider that contains information
+   *                                                  about styles and overlays.
+   * @param {DOMWindow} window                      The window to load into
+   */
+  constructor(overlayProvider, window) {
+    this.window = window;
+    this.overlayProvider = overlayProvider;
+  }
+
+  /**
+   * A shorthand to this.window.document
+   */
+  get document() {
+    return this.window.document;
+  }
+
+  /**
+   * Loads the given urls into the window, recursively loading further overlays as provided by the
+   * overlayProvider.
+   *
+   * @param {String[]} urls                         The urls to load
+   */
+  load(urls) {
+    let unloadedOverlays = urls;
+    let forwardReferences = [];
+    let unloadedScripts = [];
+    let unloadedSheets = [];
+
+    while (unloadedOverlays.length) {
+      let url = unloadedOverlays.shift();
+      let xhr = this.fetchOverlay(url);
+      let doc = xhr.responseXML;
+
+      // clean the document a bit
+      let emptyNodes = doc.evaluate("//text()[normalize-space(.) = '']", doc, null, 7, null);
+      for (let i = 0, len = emptyNodes.snapshotLength; i < len; ++i) {
+        let node = emptyNodes.snapshotItem(i);
+        node.remove();
+      }
+
+      let commentNodes = doc.evaluate("//comment()", doc, null, 7, null);
+      for (let i = 0, len = commentNodes.snapshotLength; i < len; ++i) {
+        let node = commentNodes.snapshotItem(i);
+        node.remove();
+      }
+
+      // Load css styles from the registry
+      for (let sheet of this.overlayProvider.style.get(url, false)) {
+        unloadedSheets.push(sheet);
+      }
+
+      // Load css processing instructions from the overlay
+      let stylesheets = doc.evaluate("/processing-instruction('xml-stylesheet')", doc, null, 7, null);
+      for (let i = 0, len = stylesheets.snapshotLength; i < len; ++i) {
+        let node = stylesheets.snapshotItem(i);
+        let match = node.nodeValue.match(/href=["']([^"']*)["']/);
+        if (match) {
+          unloadedSheets.push(match[1]);
+        }
+      }
+
+      // Prepare loading further nested xul overlays from the overlay
+      let xuloverlays = doc.evaluate("/processing-instruction('xul-overlay')", doc, null, 7, null);
+      for (let i = 0, len = xuloverlays.snapshotLength; i < len; ++i) {
+        let node = xuloverlays.snapshotItem(i);
+        let match = node.nodeValue.match(/href=["']([^"']*)["']/);
+        if (match) {
+          unloadedOverlays.push(match[1]);
+        }
+      }
+
+      // Prepare loading further nested xul overlays from the registry
+      for (let overlayUrl of this.overlayProvider.overlay.get(url, false)) {
+        unloadedOverlays.push(overlayUrl);
+      }
+
+      // Run through all overlay nodes on the first level (hookup nodes). Scripts will be deferred
+      // until later for simplicity (c++ code seems to process them earlier?).
+      for (let node of doc.documentElement.children) {
+        if (node.localName == "script") {
+          unloadedScripts.push(node);
+        } else {
+          forwardReferences.push(node);
+        }
+      }
+    }
+
+    // At this point, all (recursive) overlays are loaded. Unloaded scripts and sheets are ready and
+    // in order, and forward references are good to process.
+    let previous = 0;
+    while (forwardReferences.length && forwardReferences.length != previous) {
+      previous = forwardReferences.length;
+      let unresolved = [];
+
+      for (let ref of forwardReferences) {
+        if (!this._resolveForwardReference(ref)) {
+          unresolved.push(ref);
+        }
+      }
+
+      forwardReferences = unresolved;
+    }
+
+    if (forwardReferences.length) {
+      oconsole.warn(`Could not resolve ${forwardReferences.length} references`, forwardReferences);
+    }
+
+    // Loading the sheets now to avoid race conditions with xbl bindings
+    for (let sheet of unloadedSheets) {
+      this.loadCSS(sheet);
+    }
+
+    // We've resolved all the forward references we can, we can now go ahead and load the scripts
+    let deferredLoad = [];
+    for (let script of unloadedScripts) {
+      deferredLoad.push(...this.loadScript(script));
+    }
+
+    if (this.window.document.readyState == "complete") {
+      // Now here is where it gets a little tricky. The subscript loader is synchronous, but the
+      // script itself may have some asynchronous side-effects (xbl bindings attached). Throwing in a
+      // 1500ms timeout before we fire the load handlers seems to help, even though it is an ugly hack.
+      setTimeout(() => {
+        // Now execute load handlers since we are done loading scripts
+        let bubbles = [];
+        for (let { listener, useCapture } of deferredLoad) {
+          if (useCapture) {
+            this._fireEventListener(listener);
+          } else {
+            bubbles.push(listener);
+          }
+        }
+
+        for (let listener of bubbles) {
+          this._fireEventListener(listener);
+        }
+      }, 1500);
+    } else {
+      // Window load is not yet complete, just add the listener normally
+      for (let { listener, useCapture } of deferredLoad) {
+        this.window.addEventListener("load", listener, useCapture);
+      }
+    }
+  }
+
+  /**
+   * Fires a "load" event for the given listener, using the current window
+   *
+   * @param {EventListener|Function} listener       The event listener to call
+   */
+  _fireEventListener(listener) {
+    let fakeEvent = new this.window.UIEvent("load", { view: this.window });
+    if (typeof listener == "function") {
+      listener(fakeEvent);
+    } else if (listener && typeof listener == "object") {
+      listener.handleEvent(fakeEvent);
+    } else {
+      oconsole.error("Unknown listener type", listener);
+    }
+  }
+
+  /**
+   * Resolves forward references for the given node. If the node exists in the target document, it
+   * is merged in with the target node. If the node has no id it is inserted at documentElement
+   * level.
+   *
+   * @param {Element} node          The DOM Element to resolve in the target document.
+   * @return {Boolean}              True, if the node was merged/inserted, false otherwise
+   */
+  _resolveForwardReference(node) {
+    if (node.id && node.localName == "toolbarpalette") {
+      let palette = this.document.getElementById(node.id);
+      if (!palette) {
+        // These vanish from the document but still exist via the palette property
+        let boxes = [...this.document.getElementsByTagName("toolbox")];
+        let box = boxes.find(box => box.palette && box.palette.id == node.id);
+        palette = box ? box.palette : null;
+      }
+
+      if (!palette) {
+        oconsole.debug(`The palette for ${node.id} could not be found, deferring to later`);
+        return false;
+      }
+
+      this._mergeElement(palette, node);
+    } else if (node.id) {
+      let target = this.document.getElementById(node.id);
+      if (!target) {
+        oconsole.debug(`The node ${node.id} could not be found, deferring to later`);
+        return false;
+      }
+
+      this._mergeElement(target, node);
+    } else {
+       this._insertElement(this.document.documentElement, node);
+    }
+    return true;
+  }
+
+  /**
+   * Insert the node in the given parent, observing the insertbefore/insertafter/position attributes
+   *
+   * @param {Element} parent        The parent element to insert the node into.
+   * @param {Element} node          The node to insert.
+   */
+  _insertElement(parent, node) {
+    let wasInserted = false;
+    let pos = node.getAttribute("insertafter");
+    let after = true;
+
+    if (!pos) {
+      pos = node.getAttribute("insertbefore");
+      after = false;
+    }
+
+    if (pos) {
+      for (let id of pos.split(",")) {
+        let targetchild = this.document.getElementById(id);
+        if (targetchild && targetchild.parentNode == parent) {
+          parent.insertBefore(node, after ? targetchild.nextSibling : targetchild);
+          wasInserted = true;
+          // Not breaking here to match original behavior
+        }
+      }
+    }
+
+    if (!wasInserted) {
+      // position is 1-based
+      let position = parseInt(node.getAttribute("position"), 10);
+      if (position > 0 && (position - 1) <= parent.childNodes.length) {
+        parent.insertBefore(node, parent.childNodes[position - 1]);
+        wasInserted = true;
+      }
+    }
+
+    if (!wasInserted) {
+      parent.appendChild(node);
+    }
+  }
+
+  /**
+   * Merge the node into the target, adhering to the removeelement attribute, merging further
+   * attributes into the target node, and merging children as appropriate for xul nodes. If a child
+   * has an id, it will be searched in the target document and recursively merged.
+   *
+   * @param {Element} target        The node to merge into
+   * @param {Element} node          The node that is being merged
+   */
+  _mergeElement(target, node) {
+    for (let attribute of node.attributes) {
+      if (attribute.name == "id") {
+        continue;
+      }
+
+      if (attribute.name == "removeelement" && attribute.value == "true") {
+        target.remove();
+        return;
+      }
+
+      target.setAttributeNS(attribute.namespaceURI, attribute.name, attribute.value);
+    }
+
+    for (let i = 0, len = node.childElementCount; i < len; i++) {
+      let child = node.firstElementChild;
+      child.remove();
+
+      let elementInDocument = child.id ? this.document.getElementById(child.id) : null;
+      let parentId = elementInDocument ? elementInDocument.parentNode.id : null;
+
+      if (parentId && parentId == target.id) {
+        this._mergeElement(elementInDocument, child);
+      } else {
+        this._insertElement(target, child);
+      }
+    }
+  }
+
+  /**
+   * Fetches the overlay from the given chrome:// or resource:// URL. This happen synchronously so
+   * we have a chance to complete before the load event.
+   *
+   * @param {String} srcUrl                         The URL to load
+   * @return {XMLHttpRequest}                       The completed XHR.
+   */
+  fetchOverlay(srcUrl) {
+    if (!srcUrl.startsWith("chrome://") && !srcUrl.startsWith("resource://")) {
+      throw "May only load overlays from chrome:// or resource:// uris";
+    }
+
+    let xhr = new XMLHttpRequest();
+    xhr.overrideMimeType("application/xml");
+    xhr.open("GET", srcUrl, false);
+
+    // Elevate the request, so DTDs will work. Should not be a security issue since we
+    // only load chrome, resource and file URLs, and that is our privileged chrome package.
+    try {
+      xhr.channel.owner = Services.scriptSecurityManager.getSystemPrincipal();
+    } catch (ex) {
+      oconsole.error("Failed to set system principal while fetching overlay " + srcUrl);
+      xhr.close();
+      throw "Failed to set system principal";
+    }
+
+    xhr.send(null);
+    return xhr;
+  }
+
+  /**
+   * Loads scripts described by the given script node. The node can either have a src attribute, or
+   * be an inline script with textContent.
+   *
+   * @param {Element} node                          The <script> element to load the script from
+   * @return {Object[]}                             An object with listener and useCapture,
+   *                                                  describing load handlers the script creates
+   *                                                  when first run.
+   */
+  loadScript(node) {
+    let deferredLoad = [];
+
+    let oldAddEventListener = this.window.addEventListener;
+    this.window.addEventListener = function(type, listener, useCapture, ...args) {
+      if (type == "load") {
+        if (typeof useCapture == "object") {
+          useCapture = useCapture.capture;
+        }
+
+        if (typeof useCapture == "undefined") {
+          useCapture = true;
+        }
+        deferredLoad.push({ listener, useCapture });
+        return null;
+      }
+      return oldAddEventListener.call(this, type, listener, useCapture, ...args);
+    };
+
+    if (node.hasAttribute("src")) {
+      let url = node.getAttribute("src");
+      oconsole.debug(`Loading script ${url} into ${this.window.location}`);
+      try {
+        Services.scriptloader.loadSubScript(url, this.window);
+      } catch (ex) {
+        oconsole.error(`Error loading script ${url} into ${this.window.location}`, ex.message);
+      }
+    } else if (node.textContent) {
+      oconsole.debug(`Loading eval'd script into ${this.window.location}`);
+      try {
+        // It would be great if we could have script errors show the right url, but for now
+        // window.eval will have to do.
+        this.window.eval(node.textContent);
+      } catch (ex) {
+        oconsole.error(`Error loading eval script from ${node.baseURI} into ` +
+                       this.window.location, ex.message);
+      }
+    }
+
+    this.window.addEventListener = oldAddEventListener;
+
+    // This works because we only care about immediately executed addEventListener calls and
+    // loadSubScript is synchronous. Everyone else should be checking readyState anyway.
+    return deferredLoad;
+  }
+
+  /**
+   * Load the CSS stylesheet from the given url
+   *
+   * @param {String} url        The url to load from
+   */
+  loadCSS(url) {
+    oconsole.debug(`Loading ${url} into ${this.window.location}`);
+
+    // domWindowUtils.loadSheetUsingURIString doesn't record the sheet in document.styleSheets,
+    // adding a html link element seems to do so.
+    let link = this.document.createElementNS("http://www.w3.org/1999/xhtml", "link");
+    link.setAttribute("rel", "stylesheet");
+    link.setAttribute("type", "text/css");
+    link.setAttribute("href", url);
+
+    this.document.documentElement.appendChild(link);
+  }
+}
--- a/common/src/extensionSupport.jsm
+++ b/common/src/extensionSupport.jsm
@@ -1,16 +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/. */
 
 /**
  * Helper functions for use by entensions that should ease them plug
  * into the application.
- *
  */
 
 this.EXPORTED_SYMBOLS = [ "extensionDefaults", "ExtensionSupport" ];
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 // ChromeUtils.import("resource://gre/modules/Deprecated.jsm") - needed for warning.
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 
@@ -20,90 +19,98 @@ ChromeUtils.import("resource:///modules/
 var extensionHooks = new Map();
 var openWindowList;
 
 /**
  * Reads preferences from addon provided locations (defaults/preferences/*.js)
  * and stores them in the default preferences branch.
  */
 function extensionDefaults() {
+  // Fetch enabled non-bootstrapped add-ons.
+  let enabledAddons = Services.dirsvc.get("XREExtDL", Ci.nsISimpleEnumerator);
+  for (let addonFile of fixIterator(enabledAddons, Ci.nsIFile)) {
+    loadAddonPrefs(addonFile);
+  }
+}
 
-  function setPref(preferDefault, name, value) {
-    let branch = Services.prefs.getBranch("");
-    if (preferDefault) {
-      let defaultBranch = Services.prefs.getDefaultBranch("");
-      if (defaultBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) {
-        // Only use the default branch if it doesn't already have the pref set.
-        // If there is already a pref with this value on the default branch, the
-        // extension wants to override a built-in value.
-        branch = defaultBranch;
-      } else if (defaultBranch.prefHasUserValue(name)) {
-        // If a pref already has a user-set value it proper type
-        // will be returned (not PREF_INVALID). In that case keep the user's
-        // value and overwrite the default.
-        branch = defaultBranch;
+var ExtensionSupport = {
+  loadAddonPrefs(addonFile) {
+    function setPref(preferDefault, name, value) {
+      let branch = Services.prefs.getBranch("");
+      if (preferDefault) {
+        let defaultBranch = Services.prefs.getDefaultBranch("");
+        if (defaultBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) {
+          // Only use the default branch if it doesn't already have the pref set.
+          // If there is already a pref with this value on the default branch, the
+          // extension wants to override a built-in value.
+          branch = defaultBranch;
+        } else if (defaultBranch.prefHasUserValue(name)) {
+          // If a pref already has a user-set value it proper type
+          // will be returned (not PREF_INVALID). In that case keep the user's
+          // value and overwrite the default.
+          branch = defaultBranch;
+        }
+      }
+
+      if (typeof value == "boolean") {
+        branch.setBoolPref(name, value);
+      } else if (typeof value == "string") {
+        if (value.startsWith("chrome://") && value.endsWith(".properties")) {
+          let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"]
+                           .createInstance(Ci.nsIPrefLocalizedString);
+          valueLocal.data = value;
+          branch.setComplexValue(name, Ci.nsIPrefLocalizedString, valueLocal);
+        } else {
+          branch.setStringPref(name, value);
+        }
+      } else if (typeof value == "number" && Number.isInteger(value)) {
+        branch.setIntPref(name, value);
+      } else if (typeof value == "number" && Number.isFloat(value)) {
+        // Floats are set as char prefs, then retrieved using getFloatPref
+        branch.setCharPref(name, value);
       }
     }
 
-    if (typeof value == "boolean") {
-      branch.setBoolPref(name, value);
-    } else if (typeof value == "string") {
-      if (value.startsWith("chrome://") && value.endsWith(".properties")) {
-        let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"]
-                         .createInstance(Ci.nsIPrefLocalizedString);
-        valueLocal.data = value;
-        branch.setComplexValue(name, Ci.nsIPrefLocalizedString, valueLocal);
-      } else {
-        branch.setStringPref(name, value);
-      }
-    } else if (typeof value == "number" && Number.isInteger(value)) {
-      branch.setIntPref(name, value);
-    } else if (typeof value == "number" && Number.isFloat(value)) {
-      // Floats are set as char prefs, then retrieved using getFloatPref
-      branch.setCharPref(name, value);
-    }
-  }
-
-  function walkExtensionPrefs(prefFile) {
-    let foundPrefStrings = [];
-    if (!prefFile.exists())
-      return [];
-
-    if (prefFile.isDirectory()) {
-      prefFile.append("defaults");
-      prefFile.append("preferences");
-      if (!prefFile.exists() || !prefFile.isDirectory())
+    function walkExtensionPrefs(extensionRoot) {
+      let prefFile = extensionRoot.clone();
+      let foundPrefStrings = [];
+      if (!prefFile.exists())
         return [];
 
-      for (let file of fixIterator(prefFile.directoryEntries, Ci.nsIFile)) {
-        if (file.isFile() && file.leafName.toLowerCase().endsWith(".js")) {
-          foundPrefStrings.push(IOUtils.loadFileToString(file));
+      if (prefFile.isDirectory()) {
+        prefFile.append("defaults");
+        prefFile.append("preferences");
+        if (!prefFile.exists() || !prefFile.isDirectory())
+          return [];
+
+        for (let file of fixIterator(prefFile.directoryEntries, Ci.nsIFile)) {
+          if (file.isFile() && file.leafName.toLowerCase().endsWith(".js")) {
+            foundPrefStrings.push(IOUtils.loadFileToString(file));
+          }
+        }
+      } else if (prefFile.isFile() && prefFile.leafName.endsWith("xpi")) {
+        let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
+                          .createInstance(Ci.nsIZipReader);
+        zipReader.open(prefFile);
+        let entries = zipReader.findEntries("defaults/preferences/*.js");
+
+        while (entries.hasMore()) {
+          let entryName = entries.getNext();
+          let stream = zipReader.getInputStream(entryName);
+          let entrySize = zipReader.getEntry(entryName).realSize;
+          if (entrySize > 0) {
+            let content = NetUtil.readInputStreamToString(stream, entrySize, { charset: "utf-8", replacement: "?" });
+            foundPrefStrings.push(content);
+          }
         }
       }
-    } else if (prefFile.isFile() && prefFile.leafName.endsWith("xpi")) {
-      let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"]
-                        .createInstance(Ci.nsIZipReader);
-      zipReader.open(prefFile);
-      let entries = zipReader.findEntries("defaults/preferences/*.js");
 
-      while (entries.hasMore()) {
-        let entryName = entries.getNext();
-        let stream = zipReader.getInputStream(entryName);
-        let entrySize = zipReader.getEntry(entryName).realSize;
-        if (entrySize > 0) {
-          let content = NetUtil.readInputStreamToString(stream, entrySize, { charset: "utf-8", replacement: "?" });
-          foundPrefStrings.push(content);
-        }
-      }
+      return foundPrefStrings;
     }
 
-    return foundPrefStrings;
-  }
-
-  function loadAddonPrefs(addonFile) {
     let sandbox = new Cu.Sandbox(null);
     sandbox.pref = setPref.bind(undefined, true);
     sandbox.user_pref = setPref.bind(undefined, false);
 
     let prefDataStrings = walkExtensionPrefs(addonFile);
     for (let prefDataString of prefDataStrings) {
       try {
         Cu.evalInSandbox(prefDataString, sandbox);
@@ -114,26 +121,18 @@ function extensionDefaults() {
 
     /*
     TODO: decide whether we need to warn the user/make addon authors to migrate away from these pref files.
     if (prefDataStrings.length > 0) {
       Deprecated.warning(addon.defaultLocale.name + " uses defaults/preferences/*.js files to load prefs",
                          "https://bugzilla.mozilla.org/show_bug.cgi?id=1414398");
     }
     */
-  }
+  },
 
-  // Fetch enabled non-bootstrapped add-ons.
-  let enabledAddons = Services.dirsvc.get("XREExtDL", Ci.nsISimpleEnumerator);
-  for (let addonFile of fixIterator(enabledAddons, Ci.nsIFile)) {
-    loadAddonPrefs(addonFile);
-  }
-}
-
-var ExtensionSupport = {
   /**
    * Register listening for windows getting opened that will run the specified callback function
    * when a matching window is loaded.
    *
    * @param aID {String}  Some identification of the caller, usually the extension ID.
    * @param aExtensionHook {Object}  The object describing the hook the caller wants to register.
    *        Members of the object can be (all optional, but one callback must be supplied):
    *        chromeURLs {Array}         An array of strings of document URLs on which
--- a/common/src/moz.build
+++ b/common/src/moz.build
@@ -1,8 +1,17 @@
 # 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/.
 
 EXTRA_JS_MODULES += [
+    'ChromeManifest.jsm',
     'extensionSupport.jsm',
+    'Overlays.jsm'
 ]
+
+SOURCES += [
+    'nsCommonModule.cpp',
+    'nsComponentManagerExtra.cpp'
+]
+
+FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/common/src/nsCommonModule.cpp
@@ -0,0 +1,28 @@
+#include "mozilla/ModuleUtils.h"
+#include "nsCommonBaseCID.h"
+#include "nsComponentManagerExtra.h"
+
+NS_GENERIC_FACTORY_CONSTRUCTOR(nsComponentManagerExtra)
+NS_DEFINE_NAMED_CID(NS_COMPONENTMANAGEREXTRA_CID);
+
+const mozilla::Module::CIDEntry kCommonCIDs[] = {
+  { &kNS_COMPONENTMANAGEREXTRA_CID, false, nullptr, nsComponentManagerExtraConstructor },
+  { nullptr }
+};
+
+const mozilla::Module::ContractIDEntry kCommonContracts[] = {
+  { NS_COMPONENTMANAGEREXTRA_CONTRACTID, &kNS_COMPONENTMANAGEREXTRA_CID },
+  { nullptr }
+};
+
+static const mozilla::Module kCommonModule = {
+  mozilla::Module::kVersion,
+  kCommonCIDs,
+  kCommonContracts,
+  nullptr,
+  nullptr,
+  nullptr,
+  nullptr
+};
+
+NSMODULE_DEFN(nsCommonModule) = &kCommonModule;
new file mode 100644
--- /dev/null
+++ b/common/src/nsComponentManagerExtra.cpp
@@ -0,0 +1,46 @@
+/* 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/. */
+
+#include "nsIServiceManager.h"
+#include "nsCommonBaseCID.h"
+#include "nsComponentManagerExtra.h"
+#include "mozilla/Services.h"
+#include "nsXULAppAPI.h"
+
+static already_AddRefed<nsIFile>
+CloneAndAppend(nsIFile* aBase, const nsACString& aAppend)
+{
+  nsCOMPtr<nsIFile> f;
+  aBase->Clone(getter_AddRefs(f));
+  if (!f) {
+    return nullptr;
+  }
+
+  f->AppendNative(aAppend);
+  return f.forget();
+}
+
+NS_IMPL_ISUPPORTS(nsComponentManagerExtra,
+                  nsIComponentManagerExtra)
+
+NS_IMETHODIMP nsComponentManagerExtra::AddLegacyExtensionManifestLocation(nsIFile *aLocation)
+{
+  nsString path;
+  nsresult rv = aLocation->GetPath(path);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  if (Substring(path, path.Length() - 4).EqualsLiteral(".xpi")) {
+    return XRE_AddJarManifestLocation(NS_EXTENSION_LOCATION, aLocation);
+  }
+
+  nsCOMPtr<nsIFile> manifest =
+    CloneAndAppend(aLocation, NS_LITERAL_CSTRING("chrome.manifest"));
+  return XRE_AddManifestLocation(NS_EXTENSION_LOCATION, manifest);
+}
+
+nsComponentManagerExtra::~nsComponentManagerExtra()
+{
+}
new file mode 100644
--- /dev/null
+++ b/common/src/nsComponentManagerExtra.h
@@ -0,0 +1,20 @@
+
+#ifndef nsComponentManagerExtra_h__
+#define nsComponentManagerExtra_h__
+
+#include "nsCOMPtr.h"
+#include "nsIComponentManagerExtra.h"
+#include "nsLiteralString.h"
+
+class nsComponentManagerExtra : public nsIComponentManagerExtra
+{
+public:
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSICOMPONENTMANAGEREXTRA
+
+private:
+  virtual ~nsComponentManagerExtra();
+};
+
+#endif // nsComponentManagerExtra_h__
new file mode 100644
--- /dev/null
+++ b/mail/base/content/extensions.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+
+<!DOCTYPE bindings>
+
+<bindings id="thunderbird-addon-bindings"
+          xmlns="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:xbl="http://www.mozilla.org/xbl">
+  <binding id="thunderbird-addon-generic"
+           extends="chrome://mozapps/content/extensions/extensions.xml#addon-generic">
+    <implementation>
+      <constructor><![CDATA[
+        ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
+        ChromeUtils.import("resource://gre/modules/Services.jsm");
+        this._updateState = this._updateState.bind(this);
+        ExtensionParent.apiManager.on("startup", this._updateState);
+        ExtensionParent.apiManager.on("shutdown", this._updateState);
+
+        this._bundle = Services.strings.createBundle("chrome://messenger/locale/extensionsOverlay.properties");
+      ]]></constructor>
+      <destructor><![CDATA[
+        ExtensionParent.apiManager.off("startup", this._updateState);
+        ExtensionParent.apiManager.off("shutdown", this._updateState);
+      ]]></destructor>
+      <property name="webextension">
+        <getter><![CDATA[
+          if (!this._webextension) {
+            this._webextension = ExtensionParent.GlobalManager.getExtension(this.mAddon.id);
+          }
+          return this._webextension;
+        ]]></getter>
+      </property>
+      <method name="_updateState">
+        <body><![CDATA[
+          this.__proto__.__proto__._updateState.call(this);
+          let id = this.mAddon.id;
+          let webex = this.webextension;
+
+          if (webex && webex.manifest.legacy && (
+                (webex.startupReason != "APP_STARTUP" && !webex.legacyLoaded) ||
+                (this.mAddon.userDisabled && webex.legacyLoaded)
+              )) {
+            this.setAttribute("notification", "warning");
+            this._warning.textContent = this._bundle.GetStringFromName("warnLegacyRestart");
+            this._warningBtn.label = this._bundle.GetStringFromName("warnLegacyRestartButton");
+            this._warningBtn.setAttribute("oncommand", "BrowserUtils.restartApplication()");
+            this._warningBtn.hidden = false;
+            this._warningLink.hidden = true;
+          }
+        ]]></body>
+      </method>
+    </implementation>
+  </binding>
+</bindings>
--- a/mail/base/content/extensionsOverlay.css
+++ b/mail/base/content/extensionsOverlay.css
@@ -6,8 +6,12 @@
 
 .legacy-warning {
   display: none;
 }
 
 #nav-header {
   height: 50px;
 }
+
+.addon[status="installed"] {
+  -moz-binding: url("chrome://messenger/content/extensions.xml#thunderbird-addon-generic");
+}
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -6,16 +6,17 @@ messenger.jar:
 % override chrome://global/content/nsDragAndDrop.js chrome://messenger/content/nsDragAndDrop.js
 % override chrome://messagebody/skin/messageBody.css chrome://messenger/skin/messageBody.css
 #ifndef MOZILLA_OFFICIAL
 % override chrome://browser/content/browser-development-helpers.js chrome://messenger/content/browser-development-helpers.js
   content/messenger/browser-development-helpers.js (../../common/src/browser-development-helpers.js)
 #endif
     content/messenger/mailWindow.js                 (content/mailWindow.js)
     content/messenger/messageDisplay.js             (content/messageDisplay.js)
+    content/messenger/extensions.xml                (content/extensions.xml)
     content/messenger/extensionsOverlay.css         (content/extensionsOverlay.css)
     content/messenger/folderDisplay.js              (content/folderDisplay.js)
     content/messenger/mailWindowOverlay.js          (content/mailWindowOverlay.js)
     content/messenger/mail-compacttheme.js          (content/mail-compacttheme.js)
 *   content/messenger/mailOverlay.xul               (content/mailOverlay.xul)
 *   content/messenger/messageWindow.xul             (content/messageWindow.xul)
     content/messenger/messageWindow.js              (content/messageWindow.js)
     content/messenger/mailContextMenus.js           (content/mailContextMenus.js)
--- a/mail/components/extensions/ext-mail.json
+++ b/mail/components/extensions/ext-mail.json
@@ -1,9 +1,15 @@
 {
+  "legacy": {
+    "url": "chrome://messenger/content/parent/ext-legacy.js",
+    "schema": "chrome://messenger/content/schemas/legacy.json",
+    "scopes": ["addon_parent"],
+    "manifest": ["legacy"]
+  },
   "tabs": {
     "url": "chrome://messenger/content/parent/ext-tabs.js",
     "schema": "chrome://messenger/content/schemas/tabs.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["tabs"]
     ]
   },
--- a/mail/components/extensions/jar.mn
+++ b/mail/components/extensions/jar.mn
@@ -1,16 +1,18 @@
 # 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/ext-mail.json                (ext-mail.json)
 
+    content/messenger/parent/ext-legacy.js         (parent/ext-legacy.js)
     content/messenger/parent/ext-mail.js           (parent/ext-mail.js)
     content/messenger/parent/ext-tabs.js           (parent/ext-tabs.js)
     content/messenger/parent/ext-windows.js        (parent/ext-windows.js)
 
     content/messenger/child/ext-tabs.js            (child/ext-tabs.js)
     content/messenger/child/ext-mail.js            (child/ext-mail.js)
 
+    content/messenger/schemas/legacy.json   (schemas/legacy.json)
     content/messenger/schemas/tabs.json     (schemas/tabs.json)
     content/messenger/schemas/windows.json  (schemas/windows.json)
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/parent/ext-legacy.js
@@ -0,0 +1,114 @@
+/* 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/. */
+
+ChromeUtils.defineModuleGetter(this, "ChromeManifest", "resource:///modules/ChromeManifest.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionSupport", "resource:///modules/extensionSupport.jsm");
+ChromeUtils.defineModuleGetter(this, "Overlays", "resource:///modules/Overlays.jsm");
+
+Cu.importGlobalProperties(["fetch"]);
+
+var { ExtensionError } = ExtensionUtils;
+
+var loadedOnce = new Set();
+
+this.legacy = class extends ExtensionAPI {
+  async onManifestEntry(entryName) {
+    if (this.extension.manifest.legacy) {
+      await this.register();
+    }
+  }
+
+  async register() {
+    if (this.extension.startupReason != "APP_STARTUP") {
+      console.log(`Legacy WebExtension ${this.extension.id} loading for other reason than startup (${this.extension.startupReason}), refusing to load immediately`);
+      return;
+    }
+
+    this.extension.legacyLoaded = true;
+
+    if (loadedOnce.has(this.extension.id)) {
+      console.log(`Legacy WebExtension ${this.extension.id} has already been loaded in this run, refusing to do so again. Please restart`);
+      return;
+    }
+    loadedOnce.add(this.extension.id);
+
+
+    let extensionRoot;
+    if (this.extension.rootURI instanceof Ci.nsIJARURI) {
+      extensionRoot = this.extension.rootURI.JARFile.QueryInterface(Ci.nsIFileURL).file;
+      console.log("Loading packed extension from", extensionRoot.path);
+    } else {
+      extensionRoot = this.extension.rootURI.QueryInterface(Ci.nsIFileURL).file;
+      console.log("Loading unpacked extension from", extensionRoot.path);
+    }
+
+    // Have Gecko do as much loading as is still possible
+    try {
+      Cc["@mozilla.org/component-manager-extra;1"]
+        .getService(Ci.nsIComponentManagerExtra)
+        .addLegacyExtensionManifestLocation(extensionRoot);
+    } catch (e) {
+      throw new ExtensionError(e.message, e.fileName, e.lineNumber);
+    }
+
+    // Load chrome.manifest
+    let appinfo = Services.appinfo;
+    let options = {
+      application: appinfo.ID,
+      appversion: appinfo.version,
+      platformversion: appinfo.platformVersion,
+      os: appinfo.OS,
+      osversion: Services.sysinfo.getProperty("version"),
+      abi: appinfo.XPCOMABI
+    };
+    let loader = async (filename) => {
+      let url = this.extension.getURL(filename);
+      return fetch(url).then(response => response.text());
+    };
+    let chromeManifest = new ChromeManifest(loader, options);
+    await chromeManifest.parse("chrome.manifest");
+
+    // Load preference files
+    console.log("Loading add-on preferences from ", extensionRoot.path);
+    ExtensionSupport.loadAddonPrefs(extensionRoot);
+
+    // Fire profile-after-change notifications, because we are past that event by now
+    console.log("Firing profile-after-change listeners for", this.extension.id);
+    let profileAfterChange = chromeManifest.category.get("profile-after-change");
+    for (let contractid of profileAfterChange.values()) {
+      let service = contractid.startsWith("service,");
+      let instance;
+      try {
+        if (service) {
+          instance = Components.classes[contractid.substr(8)].getService(Ci.nsIObserver);
+        } else {
+          instance = Components.classes[contractid].createInstance(Ci.nsIObserver);
+        }
+
+        instance.observe(null, "profile-after-change", null);
+      } catch (e) {
+        console.error("Error firing profile-after-change listener for", contractid);
+      }
+    }
+
+    // Load overlays for each window
+    console.log("Loading legacy overlays for", this.extension.id);
+    let chromeManifestLoad = Overlays.load.bind(null, chromeManifest);
+    let targets = [...chromeManifest.overlay.keys()];
+    for (let target of targets) {
+      ExtensionSupport.registerWindowListener(this.extension.id + "-" + target, {
+        chromeURLs: [target],
+        onLoadWindow: chromeManifestLoad
+      });
+    }
+
+    this.extension.callOnClose({
+      close: () => {
+        for (let target of targets) {
+          ExtensionSupport.unregisterWindowListener(this.extension.id + "-" + target);
+        }
+      }
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/schemas/legacy.json
@@ -0,0 +1,16 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "WebExtensionManifest",
+        "properties": {
+          "legacy": {
+            "type": "boolean",
+            "optional": true
+          }
+        }
+      }
+    ]
+  }
+]
--- a/mail/locales/en-US/chrome/messenger/extensionsOverlay.properties
+++ b/mail/locales/en-US/chrome/messenger/extensionsOverlay.properties
@@ -1,6 +1,9 @@
 # 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/.
 
 cmdBackTooltip=Go back one page
 cmdForwardTooltip=Go forward one page
+
+warnLegacyRestart=This is a legacy add-on, a restart is required to continue.
+warnLegacyRestartButton=Restart
--- a/mail/moz.build
+++ b/mail/moz.build
@@ -7,17 +7,17 @@ CONFIGURE_SUBST_FILES += ['installer/Mak
 
 # app is always last as it packages up the built files on mac.
 DIRS += [
     'base',
     'locales',
     'extensions',
     'themes',
     'app',
-    '../common/src',
+    '../common',
 ]
 
 if CONFIG['MAKENSISU']:
     DIRS += ['installer/windows']
 
 if CONFIG['MOZ_BUNDLED_FONTS']:
     DIRS += ['/%s/browser/fonts' % CONFIG['mozreltopsrcdir']]