imported patch legacy draft
authorGeoff Lankow <geoff@darktrojan.net>
Sun, 28 Oct 2018 15:23:01 +1300
changeset 68602 9b0ae238f56b056cd16d180a0fb97f8a5e15bc09
parent 68601 342a301ded26733882b7887343fd7f83d29093c4
child 68603 9278bd3ae6e642823daaab98d57d00f5cc4f28cc
push id7205
push usergeoff@darktrojan.net
push dateSun, 28 Oct 2018 02:24:55 +0000
treeherdertry-comm-central@9278bd3ae6e6 [default view] [failures only]
imported patch legacy
common/public/moz.build
common/public/nsCommonBaseCID.h
common/public/nsIComponentManagerExtra.idl
common/src/ChromeManifest.jsm
common/src/extensionSupport.jsm
common/src/moz.build
common/src/nsCommonModule.cpp
common/src/nsComponentManagerExtra.cpp
common/src/nsComponentManagerExtra.h
mail/base/content/aboutAddonsExtra.js
mail/base/content/extensions.xml
mail/base/content/extensionsOverlay.css
mail/base/content/specialTabs.js
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/components/mailGlue.js
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,17 @@
+/* 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 \
+  { 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,332 @@
+/* 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"];
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * 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.trim().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) {
+    if (flags.length == 0) {
+      return true;
+    }
+
+    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();
+  }
+}
--- a/common/src/extensionSupport.jsm
+++ b/common/src/extensionSupport.jsm
@@ -1,94 +1,147 @@
 /* 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 = [ "extensionDefaults" ];
+/**
+ * Helper functions for use by extensions that should ease them plug
+ * into the application.
+ */
 
+this.EXPORTED_SYMBOLS = ["ExtensionSupport"];
+
+ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 // ChromeUtils.import("resource://gre/modules/Deprecated.jsm") - needed for warning.
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 
-ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
+var { fixIterator } = ChromeUtils.import("resource:///modules/iteratorUtils.jsm", null);
 ChromeUtils.import("resource:///modules/IOUtils.js");
 
-/**
- * Reads preferences from addon provided locations (defaults/preferences/*.js)
- * and stores them in the default preferences branch.
- */
-function extensionDefaults() {
+var extensionHooks = new Map();
+var legacyExtensions = new Map();
+var openWindowList;
 
-  function setPref(preferDefault, name, value) {
-    let branch = preferDefault ? Services.prefs.getDefaultBranch("") : Services.prefs.getBranch("");
+var ExtensionSupport = {
+  /**
+   * A Map-like object which tracks legacy extension status. The "has" method
+   * returns only active extensions for compatibility with existing code.
+   */
+  loadedLegacyExtensions: {
+    set(id, state) {
+      legacyExtensions.set(id, state);
+    },
+    get(id) {
+      return legacyExtensions.get(id);
+    },
+    has(id) {
+      if (!legacyExtensions.has(id))
+        return false;
+
+      let state = legacyExtensions.get(id);
+      return !["install", "enable"].includes(state.pendingOperation);
+    },
+    hasAnyState(id) {
+      return legacyExtensions.has(id);
+    },
+    _maybeDelete(id, newPendingOperation) {
+      if (!legacyExtensions.has(id))
+        return;
 
-    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);
+      let state = legacyExtensions.get(id);
+      if (state.pendingOperation == "enable" && newPendingOperation == "disable") {
+        legacyExtensions.delete(id);
+        this.notifyObservers(state);
+      } else if (state.pendingOperation == "install" && newPendingOperation == "uninstall") {
+        legacyExtensions.delete(id);
+        this.notifyObservers(state);
       }
-    } 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);
-    }
-  }
+    },
+    notifyObservers(state) {
+      let wrappedState = { wrappedJSObject: state };
+      Services.obs.notifyObservers(wrappedState, "legacy-addon-status-changed");
+    },
+    // AddonListener
+    onDisabled(ev) {
+      this._maybeDelete(ev.id, "disable");
+    },
+    onUninstalled(ev) {
+      this._maybeDelete(ev.id, "uninstall");
+    },
+  },
+
+  loadAddonPrefs(addonFile) {
+    function setPref(preferDefault, name, value) {
+      let branch = preferDefault ? Services.prefs.getDefaultBranch("") : Services.prefs.getBranch("");
 
-  function walkExtensionPrefs(prefFile) {
-    let foundPrefStrings = [];
-    if (!prefFile.exists())
-      return [];
+      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 (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 [];
 
-      let unsortedFiles = [];
-      for (let file of fixIterator(prefFile.directoryEntries, Ci.nsIFile)) {
-        if (file.isFile() && file.leafName.toLowerCase().endsWith(".js")) {
-          unsortedFiles.push(file);
+      if (prefFile.isDirectory()) {
+        prefFile.append("defaults");
+        prefFile.append("preferences");
+        if (!prefFile.exists() || !prefFile.isDirectory())
+          return [];
+
+        let unsortedFiles = [];
+        for (let file of fixIterator(prefFile.directoryEntries, Ci.nsIFile)) {
+          if (file.isFile() && file.leafName.toLowerCase().endsWith(".js")) {
+            unsortedFiles.push(file);
+          }
+        }
+
+        for (let file of unsortedFiles.sort((a, b) => a.path < b.path ? 1 : -1)) {
+          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");
+        let unsortedEntries = [];
+        while (entries.hasMore()) {
+          unsortedEntries.push(entries.getNext());
+        }
+
+        for (let entryName of unsortedEntries.sort().reverse()) {
+          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);
+          }
         }
       }
 
-      for (let file of unsortedFiles.sort((a, b) => a.path < b.path ? 1 : -1)) {
-        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");
-      let unsortedEntries = [];
-      while (entries.hasMore()) {
-        unsortedEntries.push(entries.getNext());
-      }
-
-      for (let entryName of unsortedEntries.sort().reverse()) {
-        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);
@@ -99,16 +152,207 @@ 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");
     }
     */
-  }
+  },
+
+  /**
+   * 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
+   *                                   the given callback should run. If not specified,
+   *                                   run on all windows.
+   *        onLoadWindow {function}    The callback function to run when window loads
+   *                                   the matching document.
+   *        onUnloadWindow {function}  The callback function to run when window
+   *                                   unloads the matching document.
+   *        Both callbacks receive the matching window object as argument.
+   *
+   * @return {boolean}  True if the passed arguments were valid and the caller could be registered.
+   *                    False otherwise.
+   */
+  registerWindowListener(aID, aExtensionHook) {
+    if (!aID) {
+      Cu.reportError("No extension ID provided for the window listener");
+      return false;
+    }
+
+    if (extensionHooks.has(aID)) {
+      Cu.reportError("Window listener for extension + '" + aID + "' already registered");
+      return false;
+    }
+
+    if (!("onLoadWindow" in aExtensionHook) && !("onUnloadWindow" in aExtensionHook)) {
+      Cu.reportError("The extension + '" + aID + "' does not provide any callbacks");
+      return false;
+    }
+
+    extensionHooks.set(aID, aExtensionHook);
+
+    // Add our global listener if there isn't one already
+    // (only when we have first caller).
+    if (extensionHooks.size == 1) {
+      Services.wm.addListener(this._windowListener);
+    }
+
+    if (openWindowList) {
+      // We already have a list of open windows, notify the caller about them.
+      openWindowList.forEach(domWindow =>
+        ExtensionSupport._checkAndRunMatchingExtensions(domWindow, "load", aID));
+    } else {
+      openWindowList = new Set();
+      // Get the list of windows already open.
+      let windows = Services.wm.getEnumerator(null);
+      while (windows.hasMoreElements()) {
+        let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+        if (domWindow.document.location.href === "about:blank") {
+          ExtensionSupport._waitForLoad(domWindow, aID);
+        } else {
+          ExtensionSupport._addToListAndNotify(domWindow, aID);
+        }
+      }
+    }
+
+    return true;
+  },
+
+  /**
+   * Unregister listening for windows for the given caller.
+   *
+   * @param aID {String}  Some identification of the caller, usually the extension ID.
+   *
+   * @return {boolean}  True if the passed arguments were valid and the caller could be unregistered.
+   *                    False otherwise.
+   */
+  unregisterWindowListener(aID) {
+    if (!aID) {
+      Cu.reportError("No extension ID provided for the window listener");
+      return false;
+    }
+
+    let windowListener = extensionHooks.get(aID);
+    if (!windowListener) {
+      Cu.reportError("Couldn't remove window listener for extension + '" + aID + "'");
+      return false;
+    }
+
+    extensionHooks.delete(aID);
+    // Remove our global listener if there are no callers registered anymore.
+    if (extensionHooks.size == 0) {
+      Services.wm.removeListener(this._windowListener);
+      openWindowList.clear();
+      openWindowList = undefined;
+    }
+
+    return true;
+  },
 
-  // Fetch enabled non-bootstrapped add-ons.
-  let enabledAddons = Services.dirsvc.get("XREExtDL", Ci.nsISimpleEnumerator);
-  for (let addonFile of fixIterator(enabledAddons, Ci.nsIFile)) {
-    loadAddonPrefs(addonFile);
-  }
-}
+  get openWindows() {
+    return openWindowList.values();
+  },
+
+  _windowListener: {
+    // nsIWindowMediatorListener functions
+    onOpenWindow(xulWindow) {
+      // A new window has opened.
+      let domWindow = xulWindow.docShell.domWindow;
+
+      // Here we pass no caller ID, so all registered callers get notified.
+      ExtensionSupport._waitForLoad(domWindow);
+    },
+
+    onCloseWindow(xulWindow) {
+      // One of the windows has closed.
+      let domWindow = xulWindow.docShell.domWindow;
+      openWindowList.delete(domWindow);
+    },
+  },
+
+  /**
+   * Set up listeners to run the callbacks on the given window.
+   *
+   * @param aWindow {nsIDOMWindow}  The window to set up.
+   * @param aID {String} Optional.  ID of the new caller that has registered right now.
+   */
+  _waitForLoad(aWindow, aID) {
+    // Wait for the load event of the window. At that point
+    // aWindow.document.location.href will not be "about:blank" any more.
+    aWindow.addEventListener("load", function() {
+      ExtensionSupport._addToListAndNotify(aWindow, aID);
+    }, { once: true });
+  },
+
+  /**
+   * Once the window is fully loaded with the href referring to the XUL document,
+   * add it to our list, attach the "unload" listener to it and notify interested
+   * callers.
+   *
+   * @param aWindow {nsIDOMWindow}  The window to process.
+   * @param aID {String} Optional.  ID of the new caller that has registered right now.
+   */
+  _addToListAndNotify(aWindow, aID) {
+    openWindowList.add(aWindow);
+    aWindow.addEventListener("unload", function() {
+      ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "unload");
+    }, { once: true });
+    ExtensionSupport._checkAndRunMatchingExtensions(aWindow, "load", aID);
+  },
+
+  /**
+   * Check if the caller matches the given window and run its callback function.
+   *
+   * @param aWindow {nsIDOMWindow}  The window to run the callbacks on.
+   * @param aEventType {String}     Which callback to run if caller matches (load/unload).
+   * @param aID {String}            Optional ID of the caller whose callback is to be run.
+   *                                If not given, all registered callers are notified.
+   */
+  _checkAndRunMatchingExtensions(aWindow, aEventType, aID) {
+    if (aID) {
+      checkAndRunExtensionCode(extensionHooks.get(aID));
+    } else {
+      for (let extensionHook of extensionHooks.values()) {
+        checkAndRunExtensionCode(extensionHook);
+      }
+    }
+
+    /**
+     * Check if the single given caller matches the given window
+     * and run its callback function.
+     *
+     * @param aExtensionHook {Object}  The object describing the hook the caller
+     *                                 has registered.
+     */
+    function checkAndRunExtensionCode(aExtensionHook) {
+      let windowChromeURL = aWindow.document.location.href;
+      // Check if extension applies to this document URL.
+      if (("chromeURLs" in aExtensionHook) &&
+          (!aExtensionHook.chromeURLs.some(url => url == windowChromeURL)))
+        return;
+
+      // Run the relevant callback.
+      switch (aEventType) {
+        case "load":
+          if ("onLoadWindow" in aExtensionHook)
+            aExtensionHook.onLoadWindow(aWindow);
+          break;
+        case "unload":
+          if ("onUnloadWindow" in aExtensionHook)
+            aExtensionHook.onUnloadWindow(aWindow);
+          break;
+      }
+    }
+  },
+
+  get registeredWindowListenerCount() {
+    return extensionHooks.size;
+  },
+};
+
+AddonManager.addAddonListener(ExtensionSupport.loadedLegacyExtensions);
--- a/common/src/moz.build
+++ b/common/src/moz.build
@@ -1,8 +1,16 @@
 # 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',
 ]
+
+SOURCES += [
+    'nsCommonModule.cpp',
+    'nsComponentManagerExtra.cpp',
+]
+
+FINAL_LIBRARY = 'xul'
new file mode 100644
--- /dev/null
+++ b/common/src/nsCommonModule.cpp
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/TransactionManager.h"
+#include "nsBaseCommandController.h"
+#include "nsCommonBaseCID.h"
+#include "nsComponentManagerExtra.h"
+#include "nsSyncStreamListener.h"
+
+using mozilla::TransactionManager;
+
+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/aboutAddonsExtra.js
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals AddonManager, Services, gDetailView, gStrings */
+
+const { ExtensionSupport } = ChromeUtils.import("resource:///modules/extensionSupport.jsm", null);
+
+window._oldSortElements = window.sortElements;
+window.sortElements = function(aElements, aSortBy, aAscending) {
+  if (aSortBy.length != 2 || aSortBy[0] != "uiState" || aSortBy[1] != "name") {
+    window._oldSortElements(aElements, aSortBy, aAscending);
+  }
+
+  let getUIState = function(addon) {
+    if (addon.pendingOperations == AddonManager.PENDING_DISABLE) {
+      return "pendingDisable";
+    }
+    if (ExtensionSupport.loadedLegacyExtensions.has(addon.id) && addon.userDisabled) {
+      return "pendingDisable";
+    }
+    if (addon.pendingOperations == AddonManager.PENDING_UNINSTALL) {
+      return "pendingUninstall";
+    }
+    if (!addon.isActive &&
+        (addon.pendingOperations != AddonManager.PENDING_ENABLE &&
+         addon.pendingOperations != AddonManager.PENDING_INSTALL)) {
+      return "disabled";
+    }
+    return "enabled";
+  };
+
+  aElements.sort((a, b) => {
+    const UISTATE_ORDER = ["enabled", "askToActivate", "pendingDisable", "pendingUninstall", "disabled"];
+
+    let aState = UISTATE_ORDER.indexOf(getUIState(a.mAddon));
+    let bState = UISTATE_ORDER.indexOf(getUIState(b.mAddon));
+    if (aState < bState) {
+      return -1;
+    }
+    if (aState > bState) {
+      return 1;
+    }
+    if (a.mAddon.name < b.mAddon.name) {
+      return -1;
+    }
+    if (a.mAddon.name > b.mAddon.name) {
+      return 1;
+    }
+    return 0;
+  });
+};
+if (window.gViewController.currentViewObj == window.gListView) {
+  window.sortList(window.gListView._listBox, ["uiState", "name"], true);
+}
+
+gDetailView._oldDetailUpdateState = gDetailView.updateState;
+gDetailView.updateState = function() {
+  this._oldDetailUpdateState();
+
+  if (ExtensionSupport.loadedLegacyExtensions.has(this._addon.id)) {
+    this.node.setAttribute("active", "true");
+  }
+
+  if (ExtensionSupport.loadedLegacyExtensions.hasAnyState(this._addon.id, true)) {
+    let { stringName, undoCommand, version } = getTrueState(this._addon, "gDetailView._addon");
+
+    if (stringName) {
+      this.node.removeAttribute("notification");
+      this.node.setAttribute("pending", "true");
+
+      let warning = document.getElementById("detail-warning");
+      document.getElementById("detail-warning-link").hidden = true;
+      warning.textContent = gStrings.ext.formatStringFromName(
+        stringName, [this._addon.name, gStrings.brandShortName], 2
+      );
+
+      if (version) {
+        document.getElementById("detail-version").value = version;
+      }
+
+      if (undoCommand) {
+      }
+      return;
+    }
+  }
+};
+
+/**
+ * Update the UI when things change.
+ */
+function statusChangedObserver(subject, topic, data) {
+  let { id } = subject.wrappedJSObject;
+
+  if (gViewController.currentViewObj == gListView) {
+    let listItem = gListView.getListItemForID(id);
+    if (listItem) {
+      setTimeout(() => listItem._updateState());
+    }
+  } else if (gViewController.currentViewObj == gDetailView) {
+    setTimeout(() => gDetailView.updateState());
+  }
+}
+Services.obs.addObserver(statusChangedObserver, "legacy-addon-status-changed");
+window.addEventListener("unload", () => {
+  Services.obs.removeObserver(statusChangedObserver, "legacy-addon-status-changed");
+});
+
+/**
+ * The true status of legacy extensions, which AddonManager doesn't know
+ * about because it thinks all extensions are restartless.
+ *
+ * @return An object of three properties:
+ *         stringName: a string to display to the user, from extensions.properties.
+ *         undoCommand: code to run, should the user want to return to the previous state.
+ *         version: the current version of the extension.
+ */
+function getTrueState(addon, addonRef) {
+  let state = ExtensionSupport.loadedLegacyExtensions.get(addon.id);
+  let returnObject = {};
+
+  if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL &&
+      ExtensionSupport.loadedLegacyExtensions.has(addon.id)) {
+    returnObject.stringName = "notification.uninstall";
+    returnObject.undoCommand = `${addonRef}.cancelUninstall()`;
+
+  } else if (state.pendingOperation == "install") {
+    returnObject.stringName = "notification.install";
+    returnObject.undoCommand = `${addonRef}.uninstall()`;
+
+  } else if (addon.userDisabled) {
+    returnObject.stringName = "notification.disable";
+    returnObject.undoCommand = `${addonRef}.enable()`;
+
+  } else if (state.pendingOperation == "enable") {
+    returnObject.stringName = "notification.enable";
+    returnObject.undoCommand = `${addonRef}.disable()`;
+
+  } else if (state.pendingOperation == "upgrade") {
+    returnObject.stringName = "notification.upgrade";
+    returnObject.version = state.version;
+
+  } else if (state.pendingOperation == "downgrade") {
+    returnObject.stringName = "notification.upgrade";
+    returnObject.version = state.version;
+  }
+
+  return returnObject;
+}
+
+(function() {
+  window.isCorrectlySigned = function() { return true; };
+
+  let contentStylesheet = document.createProcessingInstruction(
+    "xml-stylesheet",
+    'href="chrome://messenger/content/extensionsOverlay.css" type="text/css"');
+  document.insertBefore(contentStylesheet, document.documentElement);
+})();
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);
+      ]]></constructor>
+      <destructor><![CDATA[
+        ExtensionParent.apiManager.off("startup", this._updateState);
+        ExtensionParent.apiManager.off("shutdown", this._updateState);
+      ]]></destructor>
+      <method name="_updateState">
+        <body><![CDATA[
+          this.__proto__.__proto__._updateState.call(this);
+
+          if (ExtensionSupport.loadedLegacyExtensions.has(this.mAddon.id)) {
+            this.setAttribute("active", "true");
+          }
+
+          if (ExtensionSupport.loadedLegacyExtensions.hasAnyState(this.mAddon.id, true)) {
+            let {
+              stringName,
+              undoCommand,
+            } = getTrueState(this.mAddon, "document.getBindingParent(this).mAddon");
+
+            if (stringName) {
+              this.removeAttribute("notification");
+              this.setAttribute("pending", "true");
+
+              this._pending.textContent = gStrings.ext.formatStringFromName(
+                stringName, [this.mAddon.name, gStrings.brandShortName], 2
+              );
+
+              if (undoCommand) {
+              }
+              return;
+            }
+          }
+        ]]></body>
+      </method>
+    </implementation>
+  </binding>
+</bindings>
--- a/mail/base/content/extensionsOverlay.css
+++ b/mail/base/content/extensionsOverlay.css
@@ -2,8 +2,12 @@
  * 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 url("chrome://messenger/skin/extensionsOverlay.css");
 
 .legacy-warning {
   display: none;
 }
+
+.addon[status="installed"] {
+  -moz-binding: url("chrome://messenger/content/extensions.xml#thunderbird-addon-generic");
+}
--- a/mail/base/content/specialTabs.js
+++ b/mail/base/content/specialTabs.js
@@ -260,21 +260,19 @@ var contentTabBaseType = {
   // The array members (functions) are for the respective document URLs
   // as specified in inContentWhitelist.
   inContentOverlays: [
     // about:addons
     function (aDocument, aTab) {
       // Switch off the context menu.
       aTab.browser.removeAttribute("context");
 
-      // Fix the "Search on addons.mozilla.org" placeholder text in the searchbox.
-      let textbox = aDocument.getElementById("header-search");
-      let placeholder = textbox.getAttribute("placeholder");
-      placeholder = placeholder.replace("addons.mozilla.org", "addons.thunderbird.net");
-      textbox.setAttribute("placeholder", placeholder);
+      Services.scriptloader.loadSubScript(
+        "chrome://messenger/content/aboutAddonsExtra.js", aDocument.defaultView
+      );
     },
 
     // Let's not mess with about:blank.
     null,
 
     // Other about:* pages.
     function (aDocument, aTab) {
       // Provide context menu for about:* pages.
--- a/mail/base/jar.mn
+++ b/mail/base/jar.mn
@@ -4,28 +4,28 @@ messenger.jar:
 % content messagebody %content/messagebody/ contentaccessible=yes
 % content messenger %content/messenger/
 % override chrome://global/content/nsDragAndDrop.js chrome://messenger/content/nsDragAndDrop.js
 % override chrome://messagebody/skin/messageBody.css chrome://messenger/skin/messageBody.css
 % overlay chrome://messenger/content/viewSource.xul chrome://messenger/content/viewSourceOverlay.xul
 % overlay chrome://global/content/config.xul chrome://messenger/content/configEditorOverlay.xul
 % overlay chrome://editor/content/EdSpellCheck.xul chrome://messenger/content/EdSpellCheckOverlay.xul
 % overlay about:addons chrome://messenger/content/extensionsOverlay.xul
-% style about:addons chrome://messenger/content/extensionsOverlay.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/content/messenger.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/addressbook/addressbook.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/messengercompose/messengercompose.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/smime/msgCompSMIMEOverlay.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/messageHeader.css
 % style chrome://messenger/content/customizeToolbar.xul chrome://messenger/skin/primaryToolbar.css
     content/messenger/mailWindow.js                 (content/mailWindow.js)
     content/messenger/messageDisplay.js             (content/messageDisplay.js)
     content/messenger/extensionsOverlay.css         (content/extensionsOverlay.css)
     content/messenger/extensionsOverlay.xul         (content/extensionsOverlay.xul)
+    content/messenger/extensions.xml                (content/extensions.xml)
     content/messenger/folderDisplay.js              (content/folderDisplay.js)
     content/messenger/mail-compacttheme.js          (content/mail-compacttheme.js)
     content/messenger/mailWindowOverlay.js          (content/mailWindowOverlay.js)
 *   content/messenger/mailWindowOverlay.xul         (content/mailWindowOverlay.xul)
     content/messenger/extraCustomizeItems.xul       (content/extraCustomizeItems.xul)
 *   content/messenger/mailOverlay.xul               (content/mailOverlay.xul)
 *   content/messenger/messageWindow.xul             (content/messageWindow.xul)
     content/messenger/messageWindow.js              (content/messageWindow.js)
@@ -66,16 +66,17 @@ messenger.jar:
     content/messenger/SearchDialog.js               (content/SearchDialog.js)
 *   content/messenger/ABSearchDialog.xul            (content/ABSearchDialog.xul)
     content/messenger/ABSearchDialog.js             (content/ABSearchDialog.js)
     content/messenger/FilterListDialog.xul          (content/FilterListDialog.xul)
     content/messenger/FilterListDialog.js           (content/FilterListDialog.js)
     content/messenger/plugins.js                    (content/plugins.js)
     content/messenger/specialTabs.js                (content/specialTabs.js)
     content/messenger/specialTabs.xul               (content/specialTabs.xul)
+    content/messenger/aboutAddonsExtra.js           (content/aboutAddonsExtra.js)
     content/messenger/aboutDialog-appUpdater.js     (content/aboutDialog-appUpdater.js)
 *   content/messenger/aboutDialog.xul               (content/aboutDialog.xul)
     content/messenger/aboutDialog.js                (content/aboutDialog.js)
 *   content/messenger/aboutRights.xhtml             (content/aboutRights.xhtml)
 *   content/messenger/systemIntegrationDialog.xul   (content/systemIntegrationDialog.xul)
     content/messenger/systemIntegrationDialog.js    (content/systemIntegrationDialog.js)
     content/messenger/folderPane.js                 (content/folderPane.js)
     content/messenger/searchBar.js                  (content/searchBar.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/tabs.json     (schemas/tabs.json)
-    content/messenger/schemas/windows.json  (schemas/windows.json)
+    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,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineModuleGetter(this, "ChromeManifest", "resource:///modules/ChromeManifest.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionSupport", "resource:///modules/extensionSupport.jsm");
+ChromeUtils.defineModuleGetter(this, "XPIInternal", "resource://gre/modules/addons/XPIProvider.jsm");
+ChromeUtils.defineModuleGetter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
+
+Cu.importGlobalProperties(["fetch"]);
+
+var { ExtensionError } = ExtensionUtils;
+
+this.legacy = class extends ExtensionAPI {
+  async onManifestEntry(entryName) {
+    if (this.extension.manifest.legacy) {
+      await this.register();
+    }
+  }
+
+  async register() {
+    this.extension.legacyLoaded = true;
+
+    let state = {
+      id: this.extension.id,
+      pendingOperation: null,
+      version: this.extension.version,
+    };
+    if (ExtensionSupport.loadedLegacyExtensions.has(this.extension.id)) {
+      state = ExtensionSupport.loadedLegacyExtensions.get(this.extension.id);
+      let versionComparison = Services.vc.compare(this.extension.version, state.version);
+      if (versionComparison > 0) {
+        state.pendingOperation = "upgrade";
+        ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
+      } else if (versionComparison < 0) {
+        state.pendingOperation = "downgrade";
+        ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
+      }
+      console.log(`Legacy WebExtension ${this.extension.id} has already been loaded in this run, refusing to do so again. Please restart.`);
+      return;
+    }
+
+    ExtensionSupport.loadedLegacyExtensions.set(this.extension.id, state);
+    if (this.extension.startupReason == "ADDON_INSTALL") {
+      // Usually, sideloaded extensions are disabled when they first appear,
+      // but for MozMill to run calendar tests, we disable this.
+      let location = XPIInternal.XPIStates.findAddon(this.extension.id).location;
+      let scope = XPIProvider.installLocations.find(l => l._name == location.name)._scope;
+      let autoDisableScopes = Services.prefs.getIntPref("extensions.autoDisableScopes");
+
+      // If the extension was just installed from the distribution folder,
+      // it's in the profile extensions folder. We don't want to disable it.
+      let isDistroAddon = Services.prefs.getBoolPref(
+        "extensions.installedDistroAddon." + this.extension.id, false
+      );
+
+      if (!isDistroAddon && (scope & autoDisableScopes)) {
+        state.pendingOperation = "install";
+        console.log(`Legacy WebExtension ${this.extension.id} loading for other reason than startup (${this.extension.startupReason}), refusing to load immediately.`);
+        ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
+        return;
+      }
+    }
+    if (this.extension.startupReason == "ADDON_ENABLE") {
+      state.pendingOperation = "enable";
+      console.log(`Legacy WebExtension ${this.extension.id} loading for other reason than startup (${this.extension.startupReason}), refusing to load immediately.`);
+      ExtensionSupport.loadedLegacyExtensions.notifyObservers(state);
+      return;
+    }
+
+    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);
+      }
+    }
+  }
+};
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/components/mailGlue.js
+++ b/mail/components/mailGlue.js
@@ -4,17 +4,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
 ChromeUtils.import("resource://gre/modules/LightweightThemeConsumer.jsm");
 ChromeUtils.import("resource:///modules/distribution.js");
 ChromeUtils.import("resource:///modules/mailMigrator.js");
-ChromeUtils.import("resource:///modules/extensionSupport.jsm");
 
 // lazy module getters
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   LightweightThemeManager: "resource://gre/modules/LightweightThemeManager.jsm",
 });
 
 XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
@@ -44,27 +43,25 @@ function MailGlue() {
 
 MailGlue.prototype = {
   // init (called at app startup)
   _init: function MailGlue__init() {
     Services.obs.addObserver(this, "xpcom-shutdown");
     Services.obs.addObserver(this, "final-ui-startup");
     Services.obs.addObserver(this, "mail-startup-done");
     Services.obs.addObserver(this, "handle-xul-text-link");
-    Services.obs.addObserver(this, "profile-after-change");
     Services.obs.addObserver(this, "chrome-document-global-created");
   },
 
   // cleanup (called at shutdown)
   _dispose: function MailGlue__dispose() {
     Services.obs.removeObserver(this, "xpcom-shutdown");
     Services.obs.removeObserver(this, "final-ui-startup");
     Services.obs.removeObserver(this, "mail-startup-done");
     Services.obs.removeObserver(this, "handle-xul-text-link");
-    Services.obs.removeObserver(this, "profile-after-change");
     Services.obs.removeObserver(this, "chrome-document-global-created");
   },
 
   // nsIObserver implementation
   observe: function MailGlue_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
     case "xpcom-shutdown":
       this._dispose();
@@ -73,19 +70,16 @@ MailGlue.prototype = {
       this._onProfileStartup();
       break;
     case "mail-startup-done":
       this._onMailStartupDone();
       break;
     case "handle-xul-text-link":
       this._handleLink(aSubject, aData);
       break;
-    case "profile-after-change":
-      extensionDefaults(); // extensionSupport.jsm
-      break;
     case "chrome-document-global-created":
       // Set up lwt, but only if the "lightweightthemes" attr is set on the root
       // (i.e. in messenger.xul).
       aSubject.addEventListener("DOMContentLoaded", () => {
         if (aSubject.document.documentElement.hasAttribute("lightweightthemes")) {
           new LightweightThemeConsumer(aSubject.document);
         }
       }, {once: true});