Bug 1461330 - Add enterprise policy support to Thunderbird with a limited set of policies; r=mkmelin
authorGeoff Lankow <geoff@darktrojan.net>
Mon, 13 May 2019 19:53:04 +1200
changeset 73611 973cbf59e0bc89dbb150cd662389904c8bbdbfce
parent 73594 00dbf8bfc8eb2a26dcdbea3a13bb2e9085dde109
child 73612 7f4b7f71868b363d54443e2ecaf8ec29f11c39cc
push id8446
push usermozilla@jorgk.com
push dateMon, 13 May 2019 12:48:50 +0000
treeherdertry-comm-central@44a039417a1c [default view] [failures only]
reviewersmkmelin
bugs1461330
Bug 1461330 - Add enterprise policy support to Thunderbird with a limited set of policies; r=mkmelin
.eslintignore
mail/components/aboutRedirector.js
mail/components/enterprisepolicies/Policies.jsm
mail/components/enterprisepolicies/content/aboutPolicies.css
mail/components/enterprisepolicies/content/aboutPolicies.js
mail/components/enterprisepolicies/content/aboutPolicies.xhtml
mail/components/enterprisepolicies/content/policies-active.svg
mail/components/enterprisepolicies/content/policies-documentation.svg
mail/components/enterprisepolicies/content/policies-error.svg
mail/components/enterprisepolicies/helpers/ProxyPolicies.jsm
mail/components/enterprisepolicies/helpers/moz.build
mail/components/enterprisepolicies/jar.mn
mail/components/enterprisepolicies/moz.build
mail/components/enterprisepolicies/schemas/configuration.json
mail/components/enterprisepolicies/schemas/moz.build
mail/components/enterprisepolicies/schemas/policies-schema.json
mail/components/enterprisepolicies/schemas/schema.jsm
mail/components/enterprisepolicies/tests/browser/.eslintrc.js
mail/components/enterprisepolicies/tests/browser/browser.ini
mail/components/enterprisepolicies/tests/browser/browser_policies_macosparser_unflatten.js
mail/components/enterprisepolicies/tests/browser/browser_policies_runOnce_helper.js
mail/components/enterprisepolicies/tests/browser/browser_policies_setAndLockPref_API.js
mail/components/enterprisepolicies/tests/browser/browser_policies_sorted_alphabetically.js
mail/components/enterprisepolicies/tests/browser/browser_policy_app_update.js
mail/components/enterprisepolicies/tests/browser/browser_policy_app_update_URL.js
mail/components/enterprisepolicies/tests/browser/browser_policy_disable_masterpassword.js
mail/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
mail/components/enterprisepolicies/tests/browser/browser_policy_locale.js
mail/components/enterprisepolicies/tests/browser/browser_policy_preferences.js
mail/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
mail/components/enterprisepolicies/tests/browser/head.js
mail/components/enterprisepolicies/tests/moz.build
mail/components/mailComponents.manifest
mail/components/moz.build
mail/installer/package-manifest.in
mail/locales/en-US/messenger/policies/aboutPolicies.ftl
mail/locales/en-US/messenger/policies/policies-descriptions.ftl
--- a/.eslintignore
+++ b/.eslintignore
@@ -68,16 +68,20 @@ mailnews/mime/*
 
 # mail exclusions
 mail/app/profile/all-thunderbird.js
 mail/app/profile/channel-prefs.js
 mail/app/profile/prefs.js
 mail/base/content/protovis-r2.6-modded.js
 mail/branding/nightly/thunderbird-branding.js
 mail/branding/thunderbird/thunderbird-branding.js
+# This file is split into two in order to keep it as a valid json file
+# for documentation purposes (policies.json) but to be accessed by the
+# code as a .jsm (schema.jsm)
+mail/components/enterprisepolicies/schemas/schema.jsm
 mail/components/im/all-im.js
 mail/locales/en-US/all-l10n.js
 mail/test/mozmill/**
 
 # calendar/ exclusions
 
 # prefs files
 calendar/lightning/content/lightning.js
--- a/mail/components/aboutRedirector.js
+++ b/mail/components/aboutRedirector.js
@@ -20,16 +20,18 @@ AboutRedirector.prototype = {
                flags: (Ci.nsIAboutModule.ALLOW_SCRIPT |
                        Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT)},
     "support": {url: "chrome://messenger/content/about-support/aboutSupport.xhtml",
                 flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
     "preferences": {url: "chrome://messenger/content/preferences/aboutPreferences.xul",
                     flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
     "downloads": {url: "chrome://messenger/content/downloads/aboutDownloads.xul",
                   flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
+    "policies": {url: "chrome://messenger/content/policies/aboutPolicies.xhtml",
+                 flags: Ci.nsIAboutModule.ALLOW_SCRIPT},
   },
 
   /**
    * Gets the module name from the given URI.
    */
   _getModuleName(aURI) {
     // Strip out the first ? or #, and anything following it
     let name = (/[^?#]+/.exec(aURI.pathQueryRef))[0];
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/Policies.jsm
@@ -0,0 +1,193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  ProxyPolicies: "resource:///modules/policies/ProxyPolicies.jsm",
+});
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+  return new ConsoleAPI({
+    prefix: "Policies.jsm",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+var EXPORTED_SYMBOLS = ["Policies"];
+
+/*
+ * ============================
+ * = POLICIES IMPLEMENTATIONS =
+ * ============================
+ *
+ * The Policies object below is where the implementation for each policy
+ * happens. An object for each policy should be defined, containing
+ * callback functions that will be called by the engine.
+ *
+ * See the _callbacks object in EnterprisePolicies.js for the list of
+ * possible callbacks and an explanation of each.
+ *
+ * Each callback will be called with two parameters:
+ * - manager
+ *   This is the EnterprisePoliciesManager singleton object from
+ *   EnterprisePolicies.js
+ *
+ * - param
+ *   The parameter defined for this policy in policies-schema.json.
+ *   It will be different for each policy. It could be a boolean,
+ *   a string, an array or a complex object. All parameters have
+ *   been validated according to the schema, and no unknown
+ *   properties will be present on them.
+ *
+ * The callbacks will be bound to their parent policy object.
+ */
+var Policies = {
+  "AppUpdateURL": {
+    onBeforeAddons(manager, param) {
+      setDefaultPref("app.update.url", param.href);
+    },
+  },
+
+  "DisableMasterPasswordCreation": {
+    onBeforeUIStartup(manager, param) {
+      if (param) {
+        manager.disallowFeature("createMasterPassword");
+      }
+    },
+  },
+
+  "ExtensionSettings": {
+    onBeforeAddons(manager, param) {
+      manager.setExtensionSettings(param);
+    },
+  },
+
+  "Preferences": {
+    onBeforeAddons(manager, param) {
+      for (let preference in param) {
+        setAndLockPref(preference, param[preference]);
+      }
+    },
+  },
+
+  "Proxy": {
+    onBeforeAddons(manager, param) {
+      if (param.Locked) {
+        manager.disallowFeature("changeProxySettings");
+        ProxyPolicies.configureProxySettings(param, setAndLockPref);
+      } else {
+        ProxyPolicies.configureProxySettings(param, setDefaultPref);
+      }
+    },
+  },
+
+  "RequestedLocales": {
+    onBeforeAddons(manager, param) {
+      if (Array.isArray(param)) {
+        Services.locale.requestedLocales = param;
+      } else {
+        Services.locale.requestedLocales = param.split(",");
+      }
+    },
+  },
+};
+
+/*
+ * ====================
+ * = HELPER FUNCTIONS =
+ * ====================
+ *
+ * The functions below are helpers to be used by several policies.
+ */
+
+/**
+ * setAndLockPref
+ *
+ * Sets the _default_ value of a pref, and locks it (meaning that
+ * the default value will always be returned, independent from what
+ * is stored as the user value).
+ * The value is only changed in memory, and not stored to disk.
+ *
+ * @param {string} prefName
+ *        The pref to be changed
+ * @param {boolean,number,string} prefValue
+ *        The value to set and lock
+ */
+function setAndLockPref(prefName, prefValue) {
+  setDefaultPref(prefName, prefValue, true);
+}
+
+/**
+ * setDefaultPref
+ *
+ * Sets the _default_ value of a pref and optionally locks it.
+ * The value is only changed in memory, and not stored to disk.
+ *
+ * @param {string} prefName
+ *        The pref to be changed
+ * @param {boolean,number,string} prefValue
+ *        The value to set
+ * @param {boolean} locked
+ *        Optionally lock the pref
+ */
+function setDefaultPref(prefName, prefValue, locked = false) {
+  if (Services.prefs.prefIsLocked(prefName)) {
+    Services.prefs.unlockPref(prefName);
+  }
+
+  let defaults = Services.prefs.getDefaultBranch("");
+
+  switch (typeof(prefValue)) {
+    case "boolean":
+      defaults.setBoolPref(prefName, prefValue);
+      break;
+
+    case "number":
+      if (!Number.isInteger(prefValue)) {
+        throw new Error(`Non-integer value for ${prefName}`);
+      }
+
+      defaults.setIntPref(prefName, prefValue);
+      break;
+
+    case "string":
+      defaults.setStringPref(prefName, prefValue);
+      break;
+  }
+
+  if (locked) {
+    Services.prefs.lockPref(prefName);
+  }
+}
+
+/**
+ * runOnce
+ *
+ * Helper function to run a callback only once per policy.
+ *
+ * @param {string} actionName
+ *        A given name which will be used to track if this callback has run.
+ * @param {Functon} callback
+ *        The callback to run only once.
+ */
+ // eslint-disable-next-line no-unused-vars
+function runOnce(actionName, callback) {
+  let prefName = `browser.policies.runonce.${actionName}`;
+  if (Services.prefs.getBoolPref(prefName, false)) {
+    log.debug(`Not running action ${actionName} again because it has already run.`);
+    return;
+  }
+  Services.prefs.setBoolPref(prefName, true);
+  callback();
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/aboutPolicies.css
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url("chrome://global/skin/in-content/common.css");
+
+html {
+  height: 100%;
+}
+
+body {
+  display: flex;
+  align-items: stretch;
+  height: 100%;
+}
+
+#sectionTitle {
+  float: left;
+  padding-inline-start: 1rem;
+}
+
+#sectionTitle:dir(rtl) {
+  float: right;
+  padding-inline-end: 1rem;
+}
+
+/** Categories **/
+
+.category {
+  cursor: pointer;
+  /* Center category names */
+  display: flex;
+  align-items: center;
+}
+
+.category .category-name {
+  pointer-events: none;
+}
+
+#categories hr {
+  border-top-color: rgba(255,255,255,0.15);
+}
+
+/** Content area **/
+
+.main-content {
+  flex: 1;
+}
+
+.tab {
+  padding: 0.5em 0;
+}
+
+.tab table {
+  width: 100%;
+}
+
+tbody tr {
+  transition: background cubic-bezier(.07, .95, 0, 1) 250ms;
+}
+
+tbody tr:hover {
+  background-color: var(--in-content-item-hover);
+}
+
+th, td, table {
+  border-collapse: collapse;
+  border: none;
+  text-align: start;
+}
+
+th {
+  padding: 1rem;
+  font-size: larger;
+}
+
+td {
+  padding: 1rem;
+}
+
+/*
+ * In Documentation Tab, this property sets the policies row in an
+ * alternate color scheme of white and grey as each policy comprises
+ * of two tbody tags, one for the description and the other for the
+ * collapsible information block.
+ */
+
+.active-policies tr.odd:not(:hover),
+.errors tr:nth-child(odd):not(:hover),
+tbody:nth-child(4n + 1) {
+  background-color: var(--in-content-box-background-odd);
+}
+
+.arr_sep.odd:not(:last-child) td:not(:first-child) {
+  border-bottom: 2px solid #f9f9fa;
+}
+
+.arr_sep.even:not(:last-child) td:not(:first-child) {
+  border-bottom: 2px solid #ededf0;
+}
+
+.last_row:not(:last-child) td {
+  border-bottom: 2px solid #d7d7db !important;
+}
+
+.icon {
+  background-position: center center;
+  background-repeat: no-repeat;
+  background-size: 16px;
+  -moz-context-properties: fill;
+  display: inline-block;
+  fill: var(--newtab-icon-primary-color);
+  height: 14px;
+  vertical-align: middle;
+  width: 14px;
+  margin-top: -.125rem;
+  margin-left: .5rem;
+}
+
+.collapsible {
+  cursor: pointer;
+  border: none;
+  outline: none;
+}
+
+.content {
+  display: none;
+}
+
+.content-style {
+  background-color: var(--in-content-box-background);
+  color: var(--blue-50);
+}
+
+tbody.collapsible td {
+  padding-bottom: 1rem;
+}
+
+.schema {
+  font-family: monospace;
+  white-space: pre;
+  direction: ltr;
+}
+
+/*
+ * The Active tab has two messages: one for when the policy service
+ * is inactive and another for when the there are no specified
+ * policies. The three classes below control which message to display
+ * or to show the policy table.
+ */
+.no-specified-policies > table,
+.inactive-service > table {
+  display: none;
+}
+
+:not(.no-specified-policies) > .no-specified-policies-message,
+:not(.inactive-service) > .inactive-service-message {
+  display: none;
+}
+
+.no-specified-policies-message,
+.inactive-service-message {
+  padding: 1rem;
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/aboutPolicies.js
@@ -0,0 +1,345 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  schema: "resource:///modules/policies/schema.jsm",
+});
+
+function col(text, className) {
+  let column = document.createElement("td");
+  if (className) {
+    column.classList.add(className);
+  }
+  let content = document.createTextNode(text);
+  column.appendChild(content);
+  return column;
+}
+
+function addMissingColumns() {
+  const table = document.getElementById("activeContent");
+  let maxColumns = 0;
+
+  // count the number of columns per row and set the max number of columns
+  for (let i = 0, length = table.rows.length; i < length; i++) {
+    if (maxColumns < table.rows[i].cells.length) {
+      maxColumns = table.rows[i].cells.length;
+    }
+  }
+
+  // add the missing columns
+  for (let i = 0, length = table.rows.length; i < length; i++) {
+    const rowLength = table.rows[i].cells.length;
+
+    if (rowLength < maxColumns) {
+      let missingColumns = maxColumns - rowLength;
+
+      while (missingColumns > 0) {
+        table.rows[i].insertCell();
+        missingColumns--;
+      }
+    }
+  }
+}
+
+/*
+ * This function generates the Active Policies content to be displayed by calling
+ * a recursive function called generatePolicy() according to the policy schema.
+ */
+
+function generateActivePolicies(data) {
+  let new_cont = document.getElementById("activeContent");
+  new_cont.classList.add("active-policies");
+
+  let policy_count = 0;
+
+  for (let policyName in data) {
+    const color_class = ++policy_count % 2 === 0 ? "even" : "odd";
+
+    if (schema.properties[policyName].type == "array") {
+      for (let count in data[policyName]) {
+        let isFirstRow = (count == 0);
+        let isLastRow = (count == data[policyName].length - 1);
+        let row = document.createElement("tr");
+        row.classList.add(color_class);
+        row.appendChild(col(isFirstRow ? policyName : ""));
+        generatePolicy(data[policyName][count], row, 1, new_cont, isLastRow, data[policyName].length > 1);
+      }
+    } else if (schema.properties[policyName].type == "object") {
+      let count = 0;
+      for (let obj in data[policyName]) {
+        let isFirstRow = (count == 0);
+        let isLastRow = (count == Object.keys(data[policyName]).length - 1);
+        let row = document.createElement("tr");
+        row.classList.add(color_class);
+        row.appendChild(col(isFirstRow ? policyName : ""));
+        row.appendChild(col(obj));
+        generatePolicy(data[policyName][obj], row, 2, new_cont, isLastRow, true);
+        count++;
+      }
+    } else {
+      let row = document.createElement("tr");
+      row.appendChild(col(policyName));
+      row.appendChild(col(JSON.stringify(data[policyName])));
+      row.classList.add(color_class, "last_row");
+      new_cont.appendChild(row);
+    }
+  }
+
+  if (policy_count < 1) {
+    let current_tab = document.querySelector(".active");
+    if (Services.policies.status == Services.policies.ACTIVE) {
+      current_tab.classList.add("no-specified-policies");
+    } else {
+      current_tab.classList.add("inactive-service");
+    }
+  }
+
+  addMissingColumns();
+}
+
+/*
+ * This is a helper recursive function that iterates levels of each
+ * policy and formats the content to be displayed accordingly.
+ */
+
+function generatePolicy(data, row, depth, new_cont, islast, arr_sep = false) {
+  const color_class = row.classList.contains("odd") ? "odd" : "even";
+
+  if (Array.isArray(data)) {
+    for (let count in data) {
+      if (count == 0) {
+        if (count == data.length - 1) {
+          generatePolicy(data[count], row, depth + 1, new_cont, islast ? islast : false, true);
+        } else {
+          generatePolicy(data[count], row, depth + 1, new_cont, false, false);
+        }
+      } else if (count == data.length - 1) {
+        let last_row = document.createElement("tr");
+        last_row.classList.add(color_class, "arr_sep");
+
+        for (let i = 0; i < depth; i++) {
+            last_row.appendChild(col(""));
+        }
+
+        generatePolicy(data[count], last_row, depth + 1, new_cont, islast ? islast : false, arr_sep);
+      } else {
+        let new_row = document.createElement("tr");
+        new_row.classList.add(color_class);
+
+        for (let i = 0; i < depth; i++) {
+          new_row.appendChild(col(""));
+        }
+
+        generatePolicy(data[count], new_row, depth + 1, new_cont, false, false);
+      }
+    }
+  } else if (typeof data == "object" && Object.keys(data).length > 0) {
+    let count = 0;
+      for (let obj in data) {
+        if (count == 0) {
+          row.appendChild(col(obj));
+          if (count == Object.keys(data).length - 1) {
+            generatePolicy(data[obj], row, depth + 1, new_cont, islast ? islast : false, arr_sep);
+          } else {
+            generatePolicy(data[obj], row, depth + 1, new_cont, false, false);
+          }
+        } else if (count == Object.keys(data).length - 1) {
+          let last_row = document.createElement("tr");
+          for (let i = 0; i < depth; i++) {
+            last_row.appendChild(col(""));
+          }
+
+          last_row.appendChild(col(obj));
+          last_row.classList.add(color_class);
+
+          if (arr_sep) {
+            last_row.classList.add("arr_sep");
+          }
+
+          generatePolicy(data[obj], last_row, depth + 1, new_cont, islast ? islast : false, false);
+        } else {
+          let new_row = document.createElement("tr");
+          new_row.classList.add(color_class);
+
+          for (let i = 0; i < depth; i++) {
+            new_row.appendChild(col(""));
+          }
+
+          new_row.appendChild(col(obj));
+          generatePolicy(data[obj], new_row, depth + 1, new_cont, false, false);
+        }
+        count++;
+      }
+  } else {
+    row.appendChild(col(JSON.stringify(data)));
+
+    if (arr_sep) {
+      row.classList.add("arr_sep");
+    }
+    if (islast) {
+      row.classList.add("last_row");
+    }
+    new_cont.appendChild(row);
+  }
+}
+
+function generateErrors() {
+  const consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  const storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+  const consoleEvents = storage.getEvents();
+  const prefixes = ["Enterprise Policies",
+                    "JsonSchemaValidator.jsm",
+                    "Policies.jsm",
+                    "GPOParser.jsm",
+                    "Enterprise Policies Child",
+                    "BookmarksPolicies.jsm",
+                    "ProxyPolicies.jsm",
+                    "WebsiteFilter Policy",
+                    "macOSPoliciesParser.jsm"];
+
+  let new_cont = document.getElementById("errorsContent");
+  new_cont.classList.add("errors");
+
+  let flag = false;
+  for (let err of consoleEvents) {
+    if (prefixes.includes(err.prefix)) {
+      flag = true;
+      let row = document.createElement("tr");
+      row.appendChild(col(err.arguments[0]));
+      new_cont.appendChild(row);
+    }
+  }
+  if (!flag) {
+    let errors_tab = document.getElementById("category-errors");
+    errors_tab.style.display = "none";
+  }
+}
+
+function generateDocumentation() {
+  let new_cont = document.getElementById("documentationContent");
+  new_cont.setAttribute("id", "documentationContent");
+
+  // map specific policies to a different string ID, to allow updates to
+  // existing descriptions
+  let string_mapping = {
+  };
+
+  for (let policyName in schema.properties) {
+    let main_tbody = document.createElement("tbody");
+    main_tbody.classList.add("collapsible");
+    main_tbody.addEventListener("click", function() {
+      let content = this.nextElementSibling;
+      content.classList.toggle("content");
+    });
+    let row = document.createElement("tr");
+    row.appendChild(col(policyName));
+    let descriptionColumn = col("");
+    let stringID = string_mapping[policyName] || policyName;
+    descriptionColumn.setAttribute("data-l10n-id", `policy-${stringID}`);
+    row.appendChild(descriptionColumn);
+    main_tbody.appendChild(row);
+    let sec_tbody = document.createElement("tbody");
+    sec_tbody.classList.add("content");
+    sec_tbody.classList.add("content-style");
+    let schema_row = document.createElement("tr");
+    if (schema.properties[policyName].properties) {
+      let column = col(JSON.stringify(schema.properties[policyName].properties, null, 1), "schema");
+      column.colSpan = "2";
+      schema_row.appendChild(column);
+      sec_tbody.appendChild(schema_row);
+    } else if (schema.properties[policyName].items) {
+      let column = col(JSON.stringify(schema.properties[policyName], null, 1), "schema");
+      column.colSpan = "2";
+      schema_row.appendChild(column);
+      sec_tbody.appendChild(schema_row);
+    } else {
+      let column = col("type: " + schema.properties[policyName].type, "schema");
+      column.colSpan = "2";
+      schema_row.appendChild(column);
+      sec_tbody.appendChild(schema_row);
+      if (schema.properties[policyName].enum) {
+        let enum_row = document.createElement("tr");
+        column = col("enum: " + JSON.stringify(schema.properties[policyName].enum, null, 1), "schema");
+        column.colSpan = "2";
+        enum_row.appendChild(column);
+        sec_tbody.appendChild(enum_row);
+      }
+    }
+    new_cont.appendChild(main_tbody);
+    new_cont.appendChild(sec_tbody);
+  }
+}
+
+let gInited = false;
+function init() {
+  if (gInited) {
+    return;
+  }
+  gInited = true;
+
+  let data = Services.policies.getActivePolicies();
+  generateActivePolicies(data);
+  generateErrors();
+  generateDocumentation();
+
+  // Event delegation on #categories element
+  let menu = document.getElementById("categories");
+  for (let category of menu.children) {
+    category.addEventListener("click", () => show(category));
+  }
+
+  if (location.hash) {
+    let sectionButton = document.getElementById("category-" + location.hash.substring(1));
+    if (sectionButton) {
+      sectionButton.click();
+    }
+  }
+
+  window.addEventListener("hashchange", function() {
+   if (location.hash) {
+      let sectionButton = document.getElementById("category-" + location.hash.substring(1));
+      sectionButton.click();
+    }
+  });
+}
+
+function show(button) {
+  let current_tab = document.querySelector(".active");
+  let category = button.getAttribute("id").substring("category-".length);
+  let content = document.getElementById(category);
+  if (current_tab == content)
+    return;
+  saveScrollPosition(current_tab.id);
+  current_tab.classList.remove("active");
+  current_tab.hidden = true;
+  content.classList.add("active");
+  content.hidden = false;
+
+  let current_button = document.querySelector("[selected=true]");
+  current_button.removeAttribute("selected");
+  button.setAttribute("selected", "true");
+
+  let title = document.getElementById("sectionTitle");
+  title.textContent = button.children[1].textContent;
+  location.hash = category;
+  restoreScrollPosition(category);
+}
+
+const scrollPositions = {};
+function saveScrollPosition(category) {
+  const mainContent = document.querySelector(".main-content");
+  scrollPositions[category] = mainContent.scrollTop;
+}
+
+function restoreScrollPosition(category) {
+  const scrollY = scrollPositions[category] || 0;
+  const mainContent = document.querySelector(".main-content");
+  mainContent.scrollTo(0, scrollY);
+}
+
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/aboutPolicies.xhtml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <title data-l10n-id="about-policies-title"/>
+        <link rel="stylesheet" href="chrome://messenger/content/policies/aboutPolicies.css" type="text/css" />
+        <link rel="localization" href="branding/brand.ftl"/>
+        <link rel="localization" href="messenger/policies/aboutPolicies.ftl"/>
+        <link rel="localization" href="messenger/policies/policies-descriptions.ftl"/>
+        <script type="application/javascript" src="chrome://messenger/content/policies/aboutPolicies.js" />
+    </head>
+    <body id="body" onload="init()">
+        <div id="categories">
+            <div class="category" selected="true" id="category-active">
+                <img class="category-icon" src="chrome://messenger/content/policies/policies-active.svg"></img>
+                <label class="category-name" data-l10n-id="active-policies-tab"></label>
+            </div>
+            <div class="category" id="category-documentation">
+                <img class="category-icon" src="chrome://messenger/content/policies/policies-documentation.svg"></img>
+                <label class="category-name" data-l10n-id="documentation-tab"></label>
+            </div>
+            <div class="category" id="category-errors">
+                <img class="category-icon" src="chrome://messenger/content/policies/policies-error.svg"></img>
+                <label class="category-name" data-l10n-id="errors-tab"></label>
+            </div>
+        </div>
+        <div class="main-content">
+            <div class="header">
+                <div id="sectionTitle" class="header-name" data-l10n-id="active-policies-tab"/>
+            </div>
+
+            <div id="active" class="tab active">
+                <h3 class="inactive-service-message" data-l10n-id="inactive-message"></h3>
+                <h3 class="no-specified-policies-message" data-l10n-id="no-specified-policies-message"></h3>
+                <table>
+                    <thead>
+                        <tr>
+                            <th data-l10n-id="policy-name"/>
+                            <th data-l10n-id="policy-value"/>
+                        </tr>
+                    </thead>
+                    <tbody id="activeContent" />
+                </table>
+            </div>
+
+            <div id="documentation" class="tab" hidden="true">
+                <table>
+                    <thead>
+                        <tr>
+                            <th data-l10n-id="policy-name"/>
+                        </tr>
+                    </thead>
+                    <tbody id="documentationContent" />
+                </table>
+            </div>
+
+             <div id="errors" class="tab" hidden="true">
+                <table>
+                    <thead>
+                        <tr>
+                            <th data-l10n-id="policy-errors"/>
+                        </tr>
+                    </thead>
+                    <tbody id="errorsContent" />
+                </table>
+            </div>
+        </div>
+    </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/policies-active.svg
@@ -0,0 +1,3 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="m11 10c-.5522847 0-1-.44771525-1-1v-4h-7v8c0 .5522847.44771525 1 1 1h2c.55228475 0 1 .4477153 1 1s-.44771525 1-1 1h-3c-1.1045695 0-2-.8954305-2-2v-10c0-1.1045695.8954305-2 2-2h1.05c.23659623-1.16516199 1.26105919-2.00250628 2.45-2.00250628s2.21340377.83734429 2.45 2.00250628h1.05c1.1045695 0 2 .8954305 2 2v5c0 .55228475-.4477153 1-1 1zm-1-6v-1h-1.051c-.47526862.000097-.88494628-.33433375-.98-.8-.14293517-.69793844-.7570756-1.19905191-1.4695-1.19905191s-1.32656483.50111347-1.4695 1.19905191c-.09505372.46566625-.50473138.800097-.98.8h-1.05v1zm-3.5-2c.27614237 0 .5.22385763.5.5s-.22385763.5-.5.5-.5-.22385763-.5-.5.22385763-.5.5-.5zm-2 5c-.27614237 0-.5-.22385763-.5-.5s.22385763-.5.5-.5h4c.27614237 0 .5.22385763.5.5s-.22385763.5-.5.5zm0 2c-.27614237 0-.5-.22385763-.5-.5s.22385763-.5.5-.5h2c.27614237 0 .5.22385763.5.5s-.22385763.5-.5.5zm0 2c-.27614237 0-.5-.2238576-.5-.5s.22385763-.5.5-.5h3c.27614237 0 .5.2238576.5.5s-.22385763.5-.5.5zm5.16250363 4.9969649c-.17706448-.0000378-.34686306-.070407-.47204764-.1956294l-2.00303103-2.003031c-.25303103-.2619823-.24941233-.6784164.00813326-.935962s.67397967-.2611643.93596203-.0081333l1.44017931 1.4401793 4.21704794-6.02444961c.2127301-.29815587.6259441-.36927468.9261129-.15939456.3001689.20988012.3752209.62239779.1682098.92455241l-4.6737391 6.67677006c-.1126024.1627768-.29162177.2672048-.48873957.2850981-.01935032.0009766-.03873758.0009766-.0580879 0z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/policies-documentation.svg
@@ -0,0 +1,3 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="m11 7.00250628c-.5522847 0-1-.44771525-1-1v-1h-7v8.00000002c0 .5522847.44771525 1 1 1h2c.55228475 0 1 .4477152 1 1 0 .5522847-.44771525 1-1 1h-3c-1.1045695 0-2-.8954305-2-2v-10.00000002c0-1.1045695.8954305-2 2-2h1.05c.23659623-1.16516199 1.26105919-2.00250628 2.45-2.00250628s2.21340377.83734429 2.45 2.00250628h1.05c1.1045695 0 2 .8954305 2 2v2c0 .55228475-.4477153 1-1 1zm-1-3v-1h-1.051c-.47526862.000097-.88494628-.33433374-.98-.8-.14293517-.69793844-.7570756-1.19905191-1.4695-1.19905191s-1.32656483.50111347-1.4695 1.19905191c-.09505372.46566626-.50473138.800097-.98.8h-1.05v1zm-3.5-2c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5s-.5-.22385762-.5-.5c0-.27614237.22385763-.5.5-.5zm-2 5c-.27614237 0-.5-.22385762-.5-.5 0-.27614237.22385763-.5.5-.5h4c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5zm0 2c-.27614237 0-.5-.22385762-.5-.5 0-.27614237.22385763-.5.5-.5h2c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5zm0 2.00000002c-.27614237 0-.5-.2238576-.5-.5s.22385763-.5.5-.5h1c.27614237 0 .5.2238576.5.5s-.22385763.5-.5.5zm6.5 4.9974937c-2.2092 0-4-1.7908-4-4s1.7908-4 4-4 4 1.7908 4 4-1.7908 4-4 4zm.46-2c0-.254051-.205949-.46-.46-.46s-.46.205949-.46.46.205949.46.46.46.46-.205949.46-.46zm-1.06-3c0-.3056.2464-.6.6-.6s.6.2944.6.6c0 .1244-.0576.254-.1632.3896-.0796.1016-.1544.172-.228.24-.0308.0288-.0612.0572-.092.0876-.0062816.0061251-.0126828.0121262-.0192.018-.052.0468-.1864.168-.2792.3028-.1304.1896-.2184.434-.2184.762 0 .2209139.1790861.4.4.4s.4-.1790861.4-.4c0-.1724.0424-.258.0776-.3088.0204-.0296.0456-.0576.0788-.09.016-.0156.032-.03.0516-.048l.0012-.0008.0036-.0032c.02-.018.0456-.0416.07-.0664l.0344-.032c.1262825-.1138504.2434416-.2374293.3504-.3696.16-.2048.3324-.5056.3324-.8812 0-.6944-.5536-1.4-1.4-1.4s-1.4.7056-1.4 1.4c0 .2209139.1790861.4.4.4s.4-.1790861.4-.4z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/content/policies-error.svg
@@ -0,0 +1,3 @@
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
+    <path fill="context-fill" fill-opacity="context-fill-opacity" d="m11 7.00250628c-.5522847 0-1-.44771525-1-1v-1h-7v8.00000002c0 .5522847.44771525 1 1 1h1c.55228475 0 1 .4477152 1 1 0 .5522847-.44771525 1-1 1h-2c-1.1045695 0-2-.8954305-2-2v-10.00000002c0-1.1045695.8954305-2 2-2h1.05c.23659623-1.16516199 1.26105919-2.00250628 2.45-2.00250628s2.21340377.83734429 2.45 2.00250628h1.05c1.1045695 0 2 .8954305 2 2v2c0 .55228475-.4477153 1-1 1zm-1-3v-1h-1.051c-.47526862.000097-.88494628-.33433374-.98-.8-.14293517-.69793844-.7570756-1.19905191-1.4695-1.19905191s-1.32656483.50111347-1.4695 1.19905191c-.09505372.46566626-.50473138.800097-.98.8h-1.05v1zm-3.5-2c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5s-.5-.22385762-.5-.5c0-.27614237.22385763-.5.5-.5zm-2 5c-.27614237 0-.5-.22385762-.5-.5 0-.27614237.22385763-.5.5-.5h4c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5zm0 2c-.27614237 0-.5-.22385762-.5-.5 0-.27614237.22385763-.5.5-.5h3c.27614237 0 .5.22385763.5.5 0 .27614238-.22385763.5-.5.5zm0 2.00000002c-.27614237 0-.5-.2238576-.5-.5s.22385763-.5.5-.5h2c.27614237 0 .5.2238576.5.5s-.22385763.5-.5.5zm10.4094737 3.3385315c.1818196.3660605.1556801.8011195-.0686611 1.1427768-.2243412.3416572-.6131685.5385659-1.0213389.5172232h-5.70000002c-.39222659-.0104155-.75207561-.2200879-.95454083-.5561802-.20246523-.3360923-.21960272-.7522174-.04545917-1.1038198l2.85-5.69999999c.19423612-.39115456.59327402-.63853153 1.03000002-.63853153.4367259 0 .8357638.24737697 1.03.63853153zm-4.45-3.78v1.73c.0381214.2884432.2840486.5040066.575.5040066s.5368786-.2155634.575-.5040066v-1.73c.0295426-.2235324-.0731401-.4439283-.2632845-.56510838-.1901445-.12118005-.4332866-.12118005-.623431 0-.1901445.12118008-.2928271.34157598-.2632845.56510838zm.57 4.13c.3755536 0 .68-.3044464.68-.68 0-.2750343-.1656767-.522987-.4197753-.6282381s-.5465787-.0470731-.7410573.1474055c-.1944787.1944786-.2526566.4869588-.1474055.7410573.1052511.2540986.3532038.4197753.6282381.4197753z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/helpers/ProxyPolicies.jsm
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+  return new ConsoleAPI({
+    prefix: "ProxyPolicies.jsm",
+    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+    // messages during development. See LOG_LEVELS in Console.jsm for details.
+    maxLogLevel: "error",
+    maxLogLevelPref: PREF_LOGLEVEL,
+  });
+});
+
+// Don't use const here because this is acessed by
+// tests through the BackstagePass object.
+var PROXY_TYPES_MAP = new Map([
+  ["none", Ci.nsIProtocolProxyService.PROXYCONFIG_DIRECT],
+  ["system", Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM],
+  ["manual", Ci.nsIProtocolProxyService.PROXYCONFIG_MANUAL],
+  ["autoDetect", Ci.nsIProtocolProxyService.PROXYCONFIG_WPAD],
+  ["autoConfig", Ci.nsIProtocolProxyService.PROXYCONFIG_PAC],
+]);
+
+var EXPORTED_SYMBOLS = [ "ProxyPolicies" ];
+
+var ProxyPolicies = {
+  configureProxySettings(param, setPref) {
+    if (param.Mode) {
+      setPref("network.proxy.type", PROXY_TYPES_MAP.get(param.Mode));
+    }
+
+    if (param.AutoConfigURL) {
+      setPref("network.proxy.autoconfig_url", param.AutoConfigURL.href);
+    }
+
+    if (param.UseProxyForDNS !== undefined) {
+      setPref("network.proxy.socks_remote_dns", param.UseProxyForDNS);
+    }
+
+    if (param.AutoLogin !== undefined) {
+      setPref("signon.autologin.proxy", param.AutoLogin);
+    }
+
+    if (param.SOCKSVersion !== undefined) {
+      if (param.SOCKSVersion != 4 && param.SOCKSVersion != 5) {
+        log.error("Invalid SOCKS version");
+      } else {
+        setPref("network.proxy.socks_version", param.SOCKSVersion);
+      }
+    }
+
+    if (param.Passthrough !== undefined) {
+      setPref("network.proxy.no_proxies_on", param.Passthrough);
+    }
+
+    if (param.UseHTTPProxyForAllProtocols !== undefined) {
+      setPref("network.proxy.share_proxy_settings", param.UseHTTPProxyForAllProtocols);
+    }
+
+    function setProxyHostAndPort(type, address) {
+      let url;
+      try {
+        // Prepend https just so we can use the URL parser
+        // instead of parsing manually.
+        url = new URL(`https://${address}`);
+      } catch (e) {
+        log.error(`Invalid address for ${type} proxy: ${address}`);
+        return;
+      }
+
+      setPref(`network.proxy.${type}`, url.hostname);
+      if (url.port) {
+        setPref(`network.proxy.${type}_port`, Number(url.port));
+      }
+    }
+
+    if (param.HTTPProxy) {
+      setProxyHostAndPort("http", param.HTTPProxy);
+
+      // network.proxy.share_proxy_settings is a UI feature, not handled by the
+      // network code. That pref only controls if the checkbox is checked, and
+      // then we must manually set the other values.
+      if (param.UseHTTPProxyForAllProtocols) {
+        param.FTPProxy = param.SSLProxy = param.SOCKSProxy = param.HTTPProxy;
+      }
+    }
+
+    if (param.FTPProxy) {
+      setProxyHostAndPort("ftp", param.FTPProxy);
+    }
+
+    if (param.SSLProxy) {
+      setProxyHostAndPort("ssl", param.SSLProxy);
+    }
+
+    if (param.SOCKSProxy) {
+      setProxyHostAndPort("socks", param.SOCKSProxy);
+    }
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/helpers/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+with Files('**'):
+    BUG_COMPONENT = ('Thunderbird', 'OS Integration')
+
+EXTRA_JS_MODULES.policies += [
+    'ProxyPolicies.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/jar.mn
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+messenger.jar:
+    content/messenger/policies/aboutPolicies.css              (content/aboutPolicies.css)
+    content/messenger/policies/aboutPolicies.xhtml            (content/aboutPolicies.xhtml)
+    content/messenger/policies/aboutPolicies.js               (content/aboutPolicies.js)
+    content/messenger/policies/policies-active.svg            (content/policies-active.svg)
+    content/messenger/policies/policies-documentation.svg     (content/policies-documentation.svg)
+    content/messenger/policies/policies-error.svg             (content/policies-error.svg)
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+with Files('**'):
+    BUG_COMPONENT = ('Thunderbird', 'OS Integration')
+
+DIRS += [
+    'helpers',
+    'schemas',
+]
+
+TEST_DIRS += [
+    'tests'
+]
+
+EXTRA_JS_MODULES.policies += [
+    'Policies.jsm',
+]
+
+JAR_MANIFESTS += [
+    'jar.mn',
+]
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/schemas/configuration.json
@@ -0,0 +1,10 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "properties": {
+    "policies": {
+      "$ref": "policies.json"
+    }
+  },
+  "required": ["policies"]
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/schemas/moz.build
@@ -0,0 +1,12 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+with Files('**'):
+    BUG_COMPONENT = ('Thunderbird', 'OS Integration')
+
+EXTRA_PP_JS_MODULES.policies += [
+    'schema.jsm',
+]
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/schemas/policies-schema.json
@@ -0,0 +1,113 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "type": "object",
+  "properties": {
+    "AppUpdateURL": {
+      "type": "URL"
+    },
+
+    "DisableMasterPasswordCreation": {
+      "type": "boolean"
+    },
+
+    "ExtensionSettings": {
+      "type": "object",
+      "patternProperties": {
+        "^.*$": {
+          "type": "object",
+          "properties": {
+            "blocked_install_message": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    },
+
+    "Preferences": {
+      "type": "object",
+      "properties": {
+        "network.IDN_show_punycode": {
+          "type": "boolean"
+        },
+        "browser.fixup.dns_first_for_single_words": {
+          "type": "boolean"
+        },
+        "browser.cache.disk.parent_directory": {
+          "type": "string"
+        },
+        "browser.urlbar.suggest.openpage": {
+          "type": "boolean"
+        },
+        "browser.urlbar.suggest.history": {
+          "type": "boolean"
+        },
+        "browser.urlbar.suggest.bookmark": {
+          "type": "boolean"
+        }
+      }
+    },
+
+    "Proxy": {
+      "type": "object",
+      "properties": {
+        "Mode": {
+          "type": "string",
+          "enum": ["none", "system", "manual", "autoDetect", "autoConfig"]
+        },
+
+        "Locked": {
+          "type": "boolean"
+        },
+
+        "AutoConfigURL": {
+          "type": "URLorEmpty"
+        },
+
+        "FTPProxy": {
+          "type": "string"
+        },
+
+        "HTTPProxy": {
+          "type": "string"
+        },
+
+        "SSLProxy": {
+          "type": "string"
+        },
+
+        "SOCKSProxy": {
+          "type": "string"
+        },
+
+        "SOCKSVersion": {
+          "type": "number",
+          "enum": [4, 5]
+        },
+
+        "UseHTTPProxyForAllProtocols": {
+          "type": "boolean"
+        },
+
+        "Passthrough": {
+          "type": "string"
+        },
+
+        "UseProxyForDNS": {
+          "type": "boolean"
+        },
+
+        "AutoLogin": {
+          "type": "boolean"
+        }
+      }
+    },
+
+    "RequestedLocales": {
+      "type": ["string", "array"],
+      "items": {
+        "type": "string"
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/schemas/schema.jsm
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["schema"];
+
+this.schema =
+#include policies-schema.json
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/.eslintrc.js
@@ -0,0 +1,10 @@
+"use strict";
+
+module.exports = {
+  "extends": "plugin:mozilla/browser-test",
+
+  "rules": {
+    "func-names": "off",
+    "mozilla/import-headjs-globals": "error",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+head = head.js
+prefs =
+  ldap_2.servers.osx.description=
+  ldap_2.servers.osx.dirType=-1
+  ldap_2.servers.osx.uri=
+  mail.provider.suppress_dialog_on_startup=true
+  mail.spotlight.firstRunDone=true
+  mail.winsearch.firstRunDone=true
+  mailnews.start_page.override_url=about:blank
+  mailnews.start_page.url=about:blank
+subsuite = thunderbird
+
+[browser_policies_macosparser_unflatten.js]
+skip-if = os != 'mac'
+[browser_policies_runOnce_helper.js]
+[browser_policies_setAndLockPref_API.js]
+[browser_policies_sorted_alphabetically.js]
+[browser_policy_app_update.js]
+[browser_policy_app_update_URL.js]
+[browser_policy_extensionsettings.js]
+[browser_policy_locale.js]
+[browser_policy_proxy.js]
+[browser_policy_preferences.js]
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policies_macosparser_unflatten.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { macOSPoliciesParser } = ChromeUtils.import("resource://gre/modules/policies/macOSPoliciesParser.jsm");
+
+add_task(async function test_object_unflatten() {
+  // Note: these policies are just examples and they won't actually
+  // run through the policy engine on this test. We're just testing
+  // that the unflattening algorithm produces the correct output.
+  let input = {
+    "DisplayBookmarksToolbar": true,
+
+    "Homepage__URL": "https://www.mozilla.org",
+    "Homepage__Locked": "true",
+    "Homepage__Additional__0": "https://extra-homepage-1.example.com",
+    "Homepage__Additional__1": "https://extra-homepage-2.example.com",
+
+    "WebsiteFilter__Block__0": "*://*.example.org/*",
+    "WebsiteFilter__Block__1": "*://*.example.net/*",
+    "WebsiteFilter__Exceptions__0": "*://*.example.org/*exception*",
+
+    "Permissions__Camera__Allow__0": "https://www.example.com",
+
+    "Permissions__Notifications__Allow__0": "https://www.example.com",
+    "Permissions__Notifications__Allow__1": "https://www.example.org",
+    "Permissions__Notifications__Block__0": "https://www.example.net",
+
+    "Permissions__Notifications__BlockNewRequests": true,
+    "Permissions__Notifications__Locked": true,
+
+    "Bookmarks__0__Title": "Bookmark 1",
+    "Bookmarks__0__URL": "https://bookmark1.example.com",
+
+    "Bookmarks__1__Title": "Bookmark 2",
+    "Bookmarks__1__URL": "https://bookmark2.example.com",
+    "Bookmarks__1__Folder": "Folder",
+  };
+
+  let expected = {
+    "DisplayBookmarksToolbar": true,
+
+    "Homepage": {
+      "URL": "https://www.mozilla.org",
+      "Locked": "true",
+      "Additional": [
+        "https://extra-homepage-1.example.com",
+        "https://extra-homepage-2.example.com",
+      ],
+    },
+
+    "WebsiteFilter": {
+      "Block": [
+        "*://*.example.org/*",
+        "*://*.example.net/*",
+      ],
+      "Exceptions": [
+        "*://*.example.org/*exception*",
+      ],
+    },
+
+    "Permissions": {
+      "Camera": {
+        "Allow": [
+          "https://www.example.com",
+        ],
+      },
+
+      "Notifications": {
+        "Allow": [
+          "https://www.example.com",
+          "https://www.example.org",
+        ],
+        "Block": [
+          "https://www.example.net",
+        ],
+        "BlockNewRequests": true,
+        "Locked": true,
+      },
+    },
+
+    "Bookmarks": [
+      {
+        "Title": "Bookmark 1",
+        "URL": "https://bookmark1.example.com",
+      },
+      {
+        "Title": "Bookmark 2",
+        "URL": "https://bookmark2.example.com",
+        "Folder": "Folder",
+      },
+    ],
+  };
+
+  let unflattened = macOSPoliciesParser.unflatten(input);
+
+  Assert.deepEqual(unflattened, expected, "Input was unflattened correctly.");
+});
+
+add_task(async function test_array_unflatten() {
+  let input = {
+    "Foo__1": 1,
+    "Foo__5": 5,
+    "Foo__10": 10,
+    "Foo__30": 30,
+    "Foo__51": 51, // This one should not be included as the limit is 50
+  };
+
+  let unflattened = macOSPoliciesParser.unflatten(input);
+  is(unflattened.Foo.length, 31, "Array size is correct");
+
+  let expected = {
+    Foo: [, 1, , , , 5], // eslint-disable-line no-sparse-arrays
+  };
+  expected.Foo[10] = 10;
+  expected.Foo[30] = 30;
+
+  Assert.deepEqual(unflattened, expected, "Array was unflattened correctly.");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policies_runOnce_helper.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { runOnce } = ChromeUtils.import("resource:///modules/policies/Policies.jsm", null);
+
+let runCount = 0;
+function callback() {
+  runCount++;
+}
+
+add_task(async function test_runonce_helper() {
+  runOnce("test_action", callback);
+  is(runCount, 1, "Callback ran for the first time.");
+
+  runOnce("test_action", callback);
+  is(runCount, 1, "Callback didn't run again.");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policies_setAndLockPref_API.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let {
+  Policies,
+  setAndLockPref,
+  setDefaultPref,
+} = ChromeUtils.import("resource:///modules/policies/Policies.jsm", null);
+
+add_task(async function test_API_directly() {
+  await setupPolicyEngineWithJson("");
+  setAndLockPref("policies.test.boolPref", true);
+  checkLockedPref("policies.test.boolPref", true);
+
+  // Check that a previously-locked pref can be changed
+  // (it will be unlocked first).
+  setAndLockPref("policies.test.boolPref", false);
+  checkLockedPref("policies.test.boolPref", false);
+
+  setAndLockPref("policies.test.intPref", 0);
+  checkLockedPref("policies.test.intPref", 0);
+
+  setAndLockPref("policies.test.stringPref", "policies test");
+  checkLockedPref("policies.test.stringPref", "policies test");
+
+  setDefaultPref("policies.test.lockedPref", "policies test", true);
+  checkLockedPref("policies.test.lockedPref", "policies test");
+
+  // Test that user values do not override the prefs, and the get*Pref call
+  // still return the value set through setAndLockPref
+  Services.prefs.setBoolPref("policies.test.boolPref", true);
+  checkLockedPref("policies.test.boolPref", false);
+
+  Services.prefs.setIntPref("policies.test.intPref", 10);
+  checkLockedPref("policies.test.intPref", 0);
+
+  Services.prefs.setStringPref("policies.test.stringPref", "policies test");
+  checkLockedPref("policies.test.stringPref", "policies test");
+
+  try {
+    // Test that a non-integer value is correctly rejected, even though
+    // typeof(val) == "number"
+    setAndLockPref("policies.test.intPref", 1.5);
+    ok(false, "Integer value should be rejected");
+  } catch (ex) {
+    ok(true, "Integer value was rejected");
+  }
+});
+
+add_task(async function test_API_through_policies() {
+  // Ensure that the values received by the policies have the correct
+  // type to make sure things are properly working.
+
+  // Implement functions to handle the three simple policies
+  // that will be added to the schema.
+  Policies.bool_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.boolPref", param);
+    },
+  };
+
+  Policies.int_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.intPref", param);
+    },
+  };
+
+  Policies.string_policy = {
+    onBeforeUIStartup(manager, param) {
+      setAndLockPref("policies.test2.stringPref", param);
+    },
+  };
+
+  await setupPolicyEngineWithJson(
+    // policies.json
+    {
+      "policies": {
+        "bool_policy": true,
+        "int_policy": 42,
+        "string_policy": "policies test 2",
+      },
+    },
+
+    // custom schema
+    {
+      properties: {
+        "bool_policy": {
+          "type": "boolean",
+        },
+
+        "int_policy": {
+          "type": "integer",
+        },
+
+        "string_policy": {
+          "type": "string",
+        },
+      },
+    }
+  );
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+
+  // The expected values come from config_setAndLockPref.json
+  checkLockedPref("policies.test2.boolPref", true);
+  checkLockedPref("policies.test2.intPref", 42);
+  checkLockedPref("policies.test2.stringPref", "policies test 2");
+
+  delete Policies.bool_policy;
+  delete Policies.int_policy;
+  delete Policies.string_policy;
+});
+
+add_task(async function test_pref_tracker() {
+  // Tests the test harness functionality that tracks usage of
+  // the setAndLockPref and setDefualtPref APIs.
+
+  let defaults = Services.prefs.getDefaultBranch("");
+
+  // Test prefs that had a default value and got changed to another
+  defaults.setIntPref("test1.pref1", 10);
+  defaults.setStringPref("test1.pref2", "test");
+
+  setAndLockPref("test1.pref1", 20);
+  setDefaultPref("test1.pref2", "NEW VALUE");
+  setAndLockPref("test1.pref3", "NEW VALUE");
+  setDefaultPref("test1.pref4", 20);
+
+  PoliciesPrefTracker.restoreDefaultValues();
+
+  is(Services.prefs.getIntPref("test1.pref1"), 10, "Expected value for test1.pref1");
+  is(Services.prefs.getStringPref("test1.pref2"), "test", "Expected value for test1.pref2");
+  is(Services.prefs.prefIsLocked("test1.pref1"), false, "test1.pref1 got unlocked");
+  is(Services.prefs.getStringPref("test1.pref3", undefined), undefined, "test1.pref3 should have had its value unset");
+  is(Services.prefs.getIntPref("test1.pref4", -1), -1, "test1.pref4 should have had its value unset");
+
+  // Test a pref that had a default value and a user value
+  defaults.setIntPref("test2.pref1", 10);
+  Services.prefs.setIntPref("test2.pref1", 20);
+
+  setAndLockPref("test2.pref1", 20);
+
+  PoliciesPrefTracker.restoreDefaultValues();
+
+  is(Services.prefs.getIntPref("test2.pref1"), 20, "Correct user value");
+  is(defaults.getIntPref("test2.pref1"), 10, "Correct default value");
+  is(Services.prefs.prefIsLocked("test2.pref1"), false, "felipe pref is not locked");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policies_sorted_alphabetically.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function checkArrayIsSorted(array, msg) {
+  let sorted = true;
+  let sortedArray = array.slice().sort(function(a, b) { return a.localeCompare(b); });
+
+  for (let i = 0; i < array.length; i++) {
+    if (array[i] != sortedArray[i]) {
+      sorted = false;
+      break;
+    }
+  }
+  ok(sorted, msg);
+}
+
+add_task(async function test_policies_sorted() {
+  let { schema } = ChromeUtils.import("resource:///modules/policies/schema.jsm");
+  let { Policies } = ChromeUtils.import("resource:///modules/policies/Policies.jsm");
+
+  checkArrayIsSorted(Object.keys(schema.properties), "policies-schema.json is alphabetically sorted.");
+  checkArrayIsSorted(Object.keys(Policies), "Policies.jsm is alphabetically sorted.");
+});
+
+add_task(async function check_naming_conventions() {
+  let { schema } = ChromeUtils.import("resource:///modules/policies/schema.jsm");
+  is(Object.keys(schema.properties).some(key => key.includes("__")), false, "Can't use __ in a policy name as it's used as a delimiter");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_app_update.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+ChromeUtils.defineModuleGetter(this, "UpdateUtils",
+                               "resource://gre/modules/UpdateUtils.jsm");
+var updateService = Cc["@mozilla.org/updates/update-service;1"].
+                    getService(Ci.nsIApplicationUpdateService);
+
+// This test is intended to ensure that nsIUpdateService::canCheckForUpdates
+// is true before the "DisableAppUpdate" policy is applied. Testing that
+// nsIUpdateService::canCheckForUpdates is false after the "DisableAppUpdate"
+// policy is applied needs to occur in a different test since the policy does
+// not properly take effect unless it is applied during application startup.
+add_task(async function test_updates_pre_policy() {
+  // Turn off automatic update before we set app.update.disabledForTesting to
+  // false so that we don't cause an actual update.
+  let originalUpdateAutoValue = await UpdateUtils.getAppUpdateAutoEnabled();
+  await UpdateUtils.setAppUpdateAutoEnabled(false);
+  registerCleanupFunction(async () => {
+    await UpdateUtils.setAppUpdateAutoEnabled(originalUpdateAutoValue);
+  });
+
+  await SpecialPowers.pushPrefEnv({"set": [["app.update.disabledForTesting", false]]});
+
+  is(Services.policies.isAllowed("appUpdate"), true,
+     "Since no policies have been set, appUpdate should be allowed by default");
+
+  is(updateService.canCheckForUpdates, true,
+     "Should be able to check for updates before any policies are in effect.");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_app_update_URL.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_app_update_URL() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "AppUpdateURL": "https://www.example.com/",
+    },
+  });
+
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.ACTIVE, "Engine is active");
+
+  // The app.update.url preference is read from the default preferences.
+  let expected = Services.prefs.getDefaultBranch(null).getCharPref("app.update.url", undefined);
+
+  is("https://www.example.com/", expected, "Correct app update URL");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_disable_masterpassword.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const MASTER_PASSWORD = "omgsecret!";
+const mpToken = Cc["@mozilla.org/security/pk11tokendb;1"]
+                  .getService(Ci.nsIPK11TokenDB)
+                  .getInternalKeyToken();
+
+async function checkDeviceManager({buttonIsDisabled}) {
+  let deviceManagerWindow = window.openDialog("chrome://pippki/content/device_manager.xul", "", "");
+  await BrowserTestUtils.waitForEvent(deviceManagerWindow, "load");
+
+  let tree = deviceManagerWindow.document.getElementById("device_tree");
+  ok(tree, "The device tree exists");
+
+  // Find and select the item related to the internal key token
+  for (let i = 0; i < tree.view.rowCount; i++) {
+    tree.view.selection.select(i);
+
+    try {
+      let selected_token = deviceManagerWindow.selected_slot.getToken();
+      if (selected_token.isInternalKeyToken) {
+        break;
+      }
+    } catch (e) {}
+  }
+
+  // Check to see if the button was updated correctly
+  let changePwButton = deviceManagerWindow.document.getElementById("change_pw_button");
+  is(changePwButton.getAttribute("disabled") == "true", buttonIsDisabled,
+     "Change Password button is in the correct state: " + buttonIsDisabled);
+
+  await BrowserTestUtils.closeWindow(deviceManagerWindow);
+}
+
+async function checkAboutPreferences({checkboxIsDisabled}) {
+  await BrowserTestUtils.withNewTab("about:preferences#privacy", async browser => {
+  is(browser.contentDocument.getElementById("useMasterPassword").disabled, checkboxIsDisabled,
+    "Master Password checkbox is in the correct state: " + checkboxIsDisabled);
+});
+}
+
+add_task(async function test_policy_disable_masterpassword() {
+  ok(!mpToken.hasPassword, "Starting the test with no password");
+
+  // No password and no policy: access to setting a master password
+  // should be enabled.
+  await checkDeviceManager({buttonIsDisabled: false});
+  await checkAboutPreferences({checkboxIsDisabled: false});
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "DisableMasterPasswordCreation": true,
+    },
+  });
+
+  // With the `DisableMasterPasswordCreation: true` policy active, the
+  // UI entry points for creating a Master Password should be disabled.
+  await checkDeviceManager({buttonIsDisabled: true});
+  await checkAboutPreferences({checkboxIsDisabled: true});
+
+  mpToken.changePassword("", MASTER_PASSWORD);
+  ok(mpToken.hasPassword, "Master password was set");
+
+  // If a Master Password is already set, there's no point in disabling
+  // the
+  await checkDeviceManager({buttonIsDisabled: false});
+  await checkAboutPreferences({checkboxIsDisabled: false});
+
+  // Clean up
+  mpToken.changePassword(MASTER_PASSWORD, "");
+  ok(!mpToken.hasPassword, "Master password was cleaned up");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function test_extensionsettings() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "ExtensionSettings": {
+        "extension1@mozilla.com": {
+          "blocked_install_message": "Extension1 error message.",
+        },
+        "*": {
+          "blocked_install_message": "Generic error message.",
+        },
+      },
+    },
+  });
+
+  let extensionSettings =  Services.policies.getExtensionSettings("extension1@mozilla.com");
+  is(extensionSettings.blocked_install_message, "Extension1 error message.", "Should have extension specific message.");
+  extensionSettings =  Services.policies.getExtensionSettings("extension2@mozilla.com");
+  is(extensionSettings.blocked_install_message, "Generic error message.", "Should have generic message.");
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_locale.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const REQ_LOC_CHANGE_EVENT = "intl:requested-locales-changed";
+
+function promiseLocaleChanged(requestedLocale) {
+  return new Promise(resolve => {
+    let localeObserver = {
+      observe(aSubject, aTopic, aData) {
+        switch (aTopic) {
+          case REQ_LOC_CHANGE_EVENT:
+            let reqLocs = Services.locale.requestedLocales;
+            is(reqLocs[0], requestedLocale);
+            Services.obs.removeObserver(localeObserver, REQ_LOC_CHANGE_EVENT);
+            resolve();
+        }
+      },
+    };
+    Services.obs.addObserver(localeObserver, REQ_LOC_CHANGE_EVENT);
+  });
+}
+
+add_task(async function test_requested_locale_array() {
+  let originalLocales = Services.locale.requestedLocales;
+  let localePromise = promiseLocaleChanged("de");
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "RequestedLocales": ["de"],
+    },
+  });
+  await localePromise;
+  Services.locale.requestedLocales = originalLocales;
+});
+
+add_task(async function test_requested_locale_string() {
+  let originalLocales = Services.locale.requestedLocales;
+  let localePromise = promiseLocaleChanged("fr");
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "RequestedLocales": "fr",
+    },
+  });
+  await localePromise;
+  Services.locale.requestedLocales = originalLocales;
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_preferences.js
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test is not intended to test all preferences that
+   can be set with Preferences, just a subset to verify
+   the overall functionality */
+
+"use strict";
+
+add_task(async function test_policy_preferences() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Preferences": {
+        "network.IDN_show_punycode": true,
+        "app.update.log": true,
+      },
+    },
+  });
+
+  checkLockedPref("network.IDN_show_punycode", true);
+  is(Services.prefs.getBoolPref("app.update.log"), false, "Disallowed pref was not been changed");
+ });
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/browser_policy_proxy.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function test_proxy_modes_and_autoconfig() {
+  // Directly test the proxy Mode and AutoconfigURL parameters through
+  // the API instead of the policy engine, because the test harness
+  // uses these prefs, and changing them interfere with the harness.
+
+  // Checks that every Mode value translates correctly to the expected pref value
+  let { ProxyPolicies, PROXY_TYPES_MAP } = ChromeUtils.import("resource:///modules/policies/ProxyPolicies.jsm", null);
+
+  for (let [mode, expectedValue] of PROXY_TYPES_MAP) {
+    ProxyPolicies.configureProxySettings({Mode: mode}, (_, value) => {
+      is(value, expectedValue, "Correct proxy mode");
+    });
+  }
+
+  let autoconfigURL = new URL("data:text/plain,test");
+  ProxyPolicies.configureProxySettings({AutoConfigURL: autoconfigURL}, (_, value) => {
+    is(value, autoconfigURL.href, "AutoconfigURL correctly set");
+  });
+});
+
+add_task(async function test_proxy_boolean_settings() {
+  // Tests that both false and true values are correctly set and locked
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "UseProxyForDNS": false,
+        "AutoLogin": false,
+      },
+    },
+  });
+
+  checkUnlockedPref("network.proxy.socks_remote_dns", false);
+  checkUnlockedPref("signon.autologin.proxy", false);
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "UseProxyForDNS": true,
+        "AutoLogin": true,
+      },
+    },
+  });
+
+  checkUnlockedPref("network.proxy.socks_remote_dns", true);
+  checkUnlockedPref("signon.autologin.proxy", true);
+});
+
+add_task(async function test_proxy_socks_and_passthrough() {
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "SOCKSVersion": 4,
+        "Passthrough": "a, b, c",
+      },
+    },
+  });
+
+  checkUnlockedPref("network.proxy.socks_version", 4);
+  checkUnlockedPref("network.proxy.no_proxies_on", "a, b, c");
+});
+
+add_task(async function test_proxy_addresses() {
+  function checkProxyPref(proxytype, address, port) {
+    checkUnlockedPref(`network.proxy.${proxytype}`, address);
+    checkUnlockedPref(`network.proxy.${proxytype}_port`, port);
+  }
+
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "HTTPProxy": "http.proxy.example.com:10",
+        "FTPProxy": "ftp.proxy.example.com:20",
+        "SSLProxy": "ssl.proxy.example.com:30",
+        "SOCKSProxy": "socks.proxy.example.com:40",
+      },
+    },
+  });
+
+  checkProxyPref("http", "http.proxy.example.com", 10);
+  checkProxyPref("ftp", "ftp.proxy.example.com", 20);
+  checkProxyPref("ssl", "ssl.proxy.example.com", 30);
+  checkProxyPref("socks", "socks.proxy.example.com", 40);
+
+  // Do the same, but now use the UseHTTPProxyForAllProtocols option
+  // and check that it takes effect.
+  await setupPolicyEngineWithJson({
+    "policies": {
+      "Proxy": {
+        "HTTPProxy": "http.proxy.example.com:10",
+        "FTPProxy": "ftp.proxy.example.com:20",
+        "SSLProxy": "ssl.proxy.example.com:30",
+        "SOCKSProxy": "socks.proxy.example.com:40",
+        "UseHTTPProxyForAllProtocols": true,
+      },
+    },
+  });
+
+
+  checkProxyPref("http", "http.proxy.example.com", 10);
+  checkProxyPref("ftp", "http.proxy.example.com", 10);
+  checkProxyPref("ssl", "http.proxy.example.com", 10);
+  checkProxyPref("socks", "http.proxy.example.com", 10);
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/browser/head.js
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {
+  EnterprisePolicyTesting,
+  PoliciesPrefTracker,
+} = ChromeUtils.import("resource://testing-common/EnterprisePolicyTesting.jsm", null);
+const {TestUtils} = ChromeUtils.import("resource://testing-common/TestUtils.jsm", null);
+
+PoliciesPrefTracker.start();
+
+async function setupPolicyEngineWithJson(json, customSchema) {
+  PoliciesPrefTracker.restoreDefaultValues();
+  if (typeof(json) != "object") {
+    let filePath = getTestFilePath(json ? json : "non-existing-file.json");
+    return EnterprisePolicyTesting.setupPolicyEngineWithJson(filePath, customSchema);
+  }
+  return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema);
+}
+
+function checkLockedPref(prefName, prefValue) {
+  EnterprisePolicyTesting.checkPolicyPref(prefName, prefValue, true);
+}
+
+function checkUnlockedPref(prefName, prefValue) {
+  EnterprisePolicyTesting.checkPolicyPref(prefName, prefValue, false);
+}
+
+// Checks that a page was blocked by seeing if it was replaced with about:neterror
+async function checkBlockedPage(url, expectedBlocked) {
+  await BrowserTestUtils.withNewTab({
+    gBrowser,
+    url,
+    waitForLoad: false,
+    waitForStateStop: true,
+  }, async function() {
+    await BrowserTestUtils.waitForCondition(async function() {
+      let blocked = await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() {
+        return content.document.documentURI.startsWith("about:neterror");
+      });
+      return blocked == expectedBlocked;
+    }, `Page ${url} block was correct (expected=${expectedBlocked}).`);
+  });
+}
+
+add_task(async function policies_headjs_startWithCleanSlate() {
+  if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
+    await setupPolicyEngineWithJson("");
+  }
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive at the start of the test");
+});
+
+registerCleanupFunction(async function policies_headjs_finishWithCleanSlate() {
+  if (Services.policies.status != Ci.nsIEnterprisePolicies.INACTIVE) {
+    await setupPolicyEngineWithJson("");
+  }
+  is(Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive at the end of the test");
+
+  EnterprisePolicyTesting.resetRunOnceState();
+  PoliciesPrefTracker.stop();
+});
new file mode 100644
--- /dev/null
+++ b/mail/components/enterprisepolicies/tests/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += [
+    'browser/browser.ini',
+]
--- a/mail/components/mailComponents.manifest
+++ b/mail/components/mailComponents.manifest
@@ -1,16 +1,17 @@
 component {8cc51368-6aa0-43e8-b762-bde9b9fd828c} aboutRedirector.js
 # Each addition here should be coupled with a corresponding addition in
 # aboutRedirector.js.
 contract @mozilla.org/network/protocol/about;1?what=newserror {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=rights {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=support {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=preferences {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 contract @mozilla.org/network/protocol/about;1?what=downloads {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
+contract @mozilla.org/network/protocol/about;1?what=policies {8cc51368-6aa0-43e8-b762-bde9b9fd828c}
 
 component {44346520-c5d2-44e5-a1ec-034e04d7fac4} nsMailDefaultHandler.js
 contract @mozilla.org/mail/clh;1 {44346520-c5d2-44e5-a1ec-034e04d7fac4}
 category command-line-handler x-default @mozilla.org/mail/clh;1
 category command-line-validator b-default @mozilla.org/mail/clh;1
 
 component {1c73f03a-b817-4640-b984-18c3478a9ae3} mailContentHandler.js
 contract @mozilla.org/uriloader/content-handler;1?type=text/html {1c73f03a-b817-4640-b984-18c3478a9ae3}
--- a/mail/components/moz.build
+++ b/mail/components/moz.build
@@ -1,32 +1,33 @@
 # 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/.
 
 # Only Mac and Windows have search integration components, but we include at
 # least one module from search/ on all platforms
 DIRS += [
+    'about-support',
+    'accountcreation',
+    'activity',
+    'addrbook',
+    'cloudfile',
     'compose',
-    'cloudfile',
     'devtools',
     'downloads',
+    'enterprisepolicies',
     'extensions',
+    'im',
+    'migration',
+    'newmailaccount',
     'preferences',
-    'addrbook',
-    'migration',
-    'activity',
     'search',
     'shell',
-    'about-support',
-    'accountcreation',
     'wintaskbar',
-    'newmailaccount',
-    'im',
 ]
 
 TEST_DIRS += ['test']
 
 DIRS += ['build']
 
 XPIDL_SOURCES += [
     'nsIMailGlue.idl',
--- a/mail/installer/package-manifest.in
+++ b/mail/installer/package-manifest.in
@@ -159,16 +159,19 @@
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 @RESPATH@/defaults/messenger/mailViews.dat
 @RESPATH@/defaults/profile/prefs.js
 
 @RESPATH@/isp/*
 
 @RESPATH@/components/aboutRedirector.js
 @RESPATH@/components/activityComponents.manifest
+@RESPATH@/components/EnterprisePolicies.js
+@RESPATH@/components/EnterprisePolicies.manifest
+@RESPATH@/components/EnterprisePoliciesContent.js
 @RESPATH@/components/folderLookupService.js
 ; interfaces.manifest doesn't get packaged because it is dynamically
 ; re-created at packaging time when linking the xpts that will actually
 ; go into the package, so the test related interfaces aren't included.
 @RESPATH@/components/mimeJSComponents.js
 @RESPATH@/components/msgMime.manifest
 @RESPATH@/components/msgAsyncPrompter.js
 @RESPATH@/components/msgBase.manifest
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/messenger/policies/aboutPolicies.ftl
@@ -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/.
+
+about-policies-title = Enterprise Policies
+
+# 'Active' is used to describe the policies that are currently active
+active-policies-tab = Active
+errors-tab = Errors
+documentation-tab = Documentation
+
+no-specified-policies-message = The Enterprise Policies service is active but there are no policies enabled.
+inactive-message = The Enterprise Policies service is inactive.
+
+policy-name = Policy Name
+policy-value = Policy Value
+policy-errors = Policy Errors
new file mode 100644
--- /dev/null
+++ b/mail/locales/en-US/messenger/policies/policies-descriptions.ftl
@@ -0,0 +1,108 @@
+# 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/.
+
+## The Enterprise Policies feature is aimed at system administrators
+## who want to deploy these settings across several Thunderbird installations
+## all at once. This is traditionally done through the Windows Group Policy
+## feature, but the system also supports other forms of deployment.
+## These are short descriptions for individual policies, to be displayed
+## in the documentation section in about:policies.
+
+policy-3rdparty = Set policies that WebExtensions can access via chrome.storage.managed.
+
+policy-AppUpdateURL = Set custom app update URL.
+
+policy-Authentication = Configure integrated authentication for websites that support it.
+
+policy-BlockAboutAddons = Block access to the Add-ons Manager (about:addons).
+
+policy-BlockAboutConfig = Block access to the about:config page.
+
+policy-BlockAboutProfiles = Block access to the about:profiles page.
+
+policy-BlockAboutSupport = Block access to the about:support page.
+
+policy-CaptivePortal = Enable or disable captive portal support.
+
+policy-CertificatesDescription = Add certificates or use built-in certificates.
+
+policy-Cookies = Allow or deny websites to set cookies.
+
+policy-DefaultDownloadDirectory = Set the default download directory.
+
+policy-DisableAppUpdate = Prevent { -brand-short-name } from updating.
+
+policy-DisableDeveloperTools = Block access to the developer tools.
+
+policy-DisableFeedbackCommands = Disable commands to send feedback from the Help menu (Submit Feedback and Report Deceptive Site).
+
+policy-DisableForgetButton = Prevent access to the Forget button.
+
+policy-DisableMasterPasswordCreation = If true, a master password can’t be created.
+
+policy-DisableProfileImport = Disable the menu command to Import data from another application.
+
+policy-DisableSafeMode = Disable the feature to restart in Safe Mode. Note: the Shift key to enter Safe Mode can only be disabled on Windows using Group Policy.
+
+policy-DisableSecurityBypass = Prevent the user from bypassing certain security warnings.
+
+policy-DisableSystemAddonUpdate = Prevent { -brand-short-name } from installing and updating system add-ons.
+
+policy-DisableTelemetry = Turn off Telemetry.
+
+policy-DisplayMenuBar = Display the Menu Bar by default.
+
+policy-DNSOverHTTPS = Configure DNS over HTTPS.
+
+policy-DontCheckDefaultClient = Disable check for default client on startup.
+
+policy-DownloadDirectory = Set and lock the download directory.
+
+# “lock” means that the user won’t be able to change this setting
+policy-EnableTrackingProtection = Enable or disable Content Blocking and optionally lock it.
+
+# A “locked” extension can’t be disabled or removed by the user. This policy
+# takes 3 keys (“Install”, ”Uninstall”, ”Locked”), you can either keep them in
+# English or translate them as verbs.
+policy-Extensions = Install, uninstall or lock extensions. The Install option takes URLs or paths as parameters. The Uninstall and Locked options take extension IDs.
+
+policy-ExtensionUpdate = Enable or disable automatic extension updates.
+
+policy-HardwareAcceleration = If false, turn off hardware acceleration.
+
+policy-InstallAddonsPermission = Allow certain websites to install add-ons.
+
+policy-LocalFileLinks = Allow specific websites to link to local files.
+
+policy-NetworkPrediction = Enable or disable network prediction (DNS prefetching).
+
+policy-OfferToSaveLogins = Enforce the setting to allow { -brand-short-name } to offer to remember saved logins and passwords. Both true and false values are accepted.
+
+policy-OverrideFirstRunPage = Override the first run page. Set this policy to blank if you want to disable the first run page.
+
+policy-OverridePostUpdatePage = Override the post-update “What’s New” page. Set this policy to blank if you want to disable the post-update page.
+
+policy-Preferences = Set and lock the value for a subset of preferences.
+
+policy-PromptForDownloadLocation = Ask where to save files when downloading.
+
+policy-Proxy = Configure proxy settings.
+
+policy-RequestedLocales = Set the list of requested locales for the application in order of preference.
+
+policy-SanitizeOnShutdown2 = Clear navigation data on shutdown.
+
+policy-SearchEngines = Configure search engine settings. This policy is only available on the Extended Support Release (ESR) version.
+
+# For more information, see https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/PKCS11/Module_Installation
+policy-SecurityDevices = Install PKCS #11 modules.
+
+policy-SSLVersionMax = Set the maximum SSL version.
+
+policy-SSLVersionMin = Set the minimum SSL version.
+
+policy-SupportMenu = Add a custom support menu item to the help menu.
+
+# “format” refers to the format used for the value of this policy.
+policy-WebsiteFilter = Block websites from being visited. See documentation for more details on the format.