Bug 1193200 - Remote content exceptions: Cannot add email address to blocklist with Block and Allow buttons; adding web site seems to succeed but also invisible (no new strings version). r=aceman, a=rkent
authorMagnus Melin <mkmelin+mozilla@iki.fi>
Sat, 26 Sep 2015 22:06:08 +0300
changeset 26391 ca6e9c82d923d8ab289721f9d01dbf661baa664f
parent 26390 39ebb9295094005aefb47c89ec9cf4f068247e36
child 26392 a3e5dfc44beb9c3c3237d00e46791ac96c5dc013
push id1850
push userclokep@gmail.com
push dateWed, 08 Mar 2017 19:29:12 +0000
treeherdercomm-esr52@028df196b2d9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaceman, rkent
bugs1193200
Bug 1193200 - Remote content exceptions: Cannot add email address to blocklist with Block and Allow buttons; adding web site seems to succeed but also invisible (no new strings version). r=aceman, a=rkent We used to use mailto: uris to store the permission, but that has no principal.origin which the permission manager requires nowadays. So instead we use a chrome uri, the magic chrome://messenger/content/?email=whatever@example.com to check against.
mail/base/content/mailWindowOverlay.js
mail/components/preferences/cookies.js
mail/components/preferences/jar.mn
mail/components/preferences/permissions.js
mail/components/preferences/permissions.xul
mail/components/preferences/permissionsutils.js
mail/locales/en-US/chrome/messenger/preferences/preferences.properties
mail/test/mozmill/content-policy/test-general-content-policy.js
mailnews/base/src/nsMsgContentPolicy.cpp
mailnews/base/test/unit/test_accountMigration.js
mailnews/base/util/mailnewsMigrator.js
--- a/mail/base/content/mailWindowOverlay.js
+++ b/mail/base/content/mailWindowOverlay.js
@@ -3017,22 +3017,24 @@ var gMessageNotificationBar =
 
     if (!this.isShowingRemoteContentNotification()) {
       this.msgNotificationBar.appendNotification(remoteContentMsg, "remoteContent",
         "chrome://messenger/skin/icons/remote-blocked.png",
         this.msgNotificationBar.PRIORITY_WARNING_MEDIUM,
         buttons);
     }
 
-    // The popup value is a space separated list of all the blocked hosts.
+    // The popup value is a space separated list of all the blocked origins.
     let popup = document.getElementById("remoteContentOptions");
-    let hosts = popup.value ? popup.value.split(" ") : [];
-    if (hosts.indexOf(aContentURI.host) == -1)
-      hosts.push(aContentURI.host);
-    popup.value = hosts.join(" ");
+    let principal = Services.scriptSecurityManager
+      .createCodebasePrincipal(aContentURI, {});
+    let origins = popup.value ? popup.value.split(" ") : [];
+    if (!origins.includes(principal.origin))
+      origins.push(principal.origin);
+    popup.value = origins.join(" ");
   },
 
   isShowingRemoteContentNotification: function() {
     return !!this.msgNotificationBar.getNotificationWithValue("remoteContent");
   },
 
   setPhishingMsg: function()
   {
@@ -3151,45 +3153,45 @@ function LoadMsgWithRemoteContent()
   setMsgHdrPropertyAndReload("remoteContentPolicy", kAllowRemoteContent);
   window.content.focus();
 }
 
 /**
  * Populate the remote content options for the current message.
  */
 function onRemoteContentOptionsShowing(aEvent) {
-  let hosts = aEvent.target.value ? aEvent.target.value.split(" ") : [];
+  let origins = aEvent.target.value ? aEvent.target.value.split(" ") : [];
 
   let addresses = {};
   MailServices.headerParser.parseHeadersWithArray(
     gMessageDisplay.displayedMessage.author, addresses, {}, {});
   let authorEmailAddress = addresses.value[0];
-  // Needs bug 457296 policy patch to actually work, but I don't want to
-  // keep this bug hostage for that, so just if-false it for now.
-  if (authorEmailAddress)
-    hosts.splice(0, 0, authorEmailAddress);
+  let authorEmailAddressURI = Services.io.newURI(
+    "chrome://messenger/content/?email=" + authorEmailAddress, null, null);
+  let mailPrincipal = Services.scriptSecurityManager
+    .createCodebasePrincipal(authorEmailAddressURI, {});
+  // Put author email first in the menu.
+  origins.splice(0, 0, mailPrincipal.origin);
 
   let messengerBundle = document.getElementById("bundle_messenger");
 
   // Out with the old...
   let childNodes = aEvent.target.childNodes;
   for (let i = childNodes.length - 1; i >= 0; i--) {
     if (childNodes[i].getAttribute("class") == "allow-remote-uri")
       childNodes[i].remove();
   }
 
   // ... and in with the new.
-  for (let host of hosts) {
-    let uri = Services.io.newURI(
-      host.includes("@") ? "mailto:" + host : "http://" + host, null, null);
-
+  for (let origin of origins) {
     let menuitem = document.createElement("menuitem");
     menuitem.setAttribute("label",
-      messengerBundle.getFormattedString("remoteAllow", [host]));
-    menuitem.setAttribute("value", uri.spec);
+      messengerBundle.getFormattedString("remoteAllow",
+        [origin.replace("chrome://messenger/content/?email=", "")]));
+    menuitem.setAttribute("value", origin);
     menuitem.setAttribute("class", "allow-remote-uri");
     menuitem.setAttribute("oncommand", "allowRemoteContentForURI(this.value);");
     aEvent.target.appendChild(menuitem);
   }
 }
 
 /**
  * Add privileges to display remote content for the given uri.
--- a/mail/components/preferences/cookies.js
+++ b/mail/components/preferences/cookies.js
@@ -509,17 +509,17 @@ var gCookiesWindow = {
                                      this._ds.timeFormatSeconds,
                                      date.getFullYear(),
                                      date.getMonth() + 1,
                                      date.getDate(),
                                      date.getHours(),
                                      date.getMinutes(),
                                      date.getSeconds());
     }
-    return this._bundle.getString("AtEndOfSession");
+    return this._bundle.getString("expireAtEndOfSession");
   },
 
   _updateCookieData: function (aItem)
   {
     var seln = this._view.selection;
     var ids = ["name", "value", "host", "path", "isSecure", "expires"];
     var properties;
 
--- a/mail/components/preferences/jar.mn
+++ b/mail/components/preferences/jar.mn
@@ -46,11 +46,10 @@ messenger.jar:
     content/messenger/preferences/fonts.js
 *   content/messenger/preferences/fonts.xul
     content/messenger/preferences/notifications.xul
     content/messenger/preferences/offline.js
     content/messenger/preferences/offline.xul
     content/messenger/preferences/cookies.js
 *   content/messenger/preferences/cookies.xul
     content/messenger/preferences/permissions.js
-*   content/messenger/preferences/permissions.xul
-    content/messenger/preferences/permissionsutils.js
+    content/messenger/preferences/permissions.xul
     content/messenger/preferences/subdialogs.js
--- a/mail/components/preferences/permissions.js
+++ b/mail/components/preferences/permissions.js
@@ -2,131 +2,222 @@
  * 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/. */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
 const nsICookiePermission = Components.interfaces.nsICookiePermission;
 
-function Permission(host, rawHost, type, capability, perm)
+const NOTIFICATION_FLUSH_PERMISSIONS = "flush-pending-permissions";
+
+/**
+ * Magic URI base used so the permission manager can store
+ * remote content permissions for a given email address.
+ */
+const MAILURI_BASE = "chrome://messenger/content/?email=";
+
+function Permission(principal, type, capability)
 {
-  this.host = host;
-  this.rawHost = rawHost;
+  this.principal = principal;
+  this.origin = principal.origin;
   this.type = type;
   this.capability = capability;
-  this.perm = perm;
 }
 
 var gPermissionManager = {
-  _type         : "",
-  _permissions  : [],
-  _bundle       : null,
-  _tree         : null,
+  _type                 : "",
+  _permissions          : [],
+  _permissionsToAdd     : new Map(),
+  _permissionsToDelete  : new Map(),
+  _bundle               : null,
+  _tree                 : null,
+  _observerRemoved      : false,
 
   _view: {
     _rowCount: 0,
     get rowCount()
     {
       return this._rowCount;
     },
     getCellText: function (aRow, aColumn)
     {
       if (aColumn.id == "siteCol")
-        return gPermissionManager._permissions[aRow].rawHost;
+        return gPermissionManager._permissions[aRow].origin
+          .replace(MAILURI_BASE, "");
       else if (aColumn.id == "statusCol")
         return gPermissionManager._permissions[aRow].capability;
       return "";
     },
 
     isSeparator: function(aIndex) { return false; },
     isSorted: function() { return false; },
     isContainer: function(aIndex) { return false; },
     setTree: function(aTree){},
     getImageSrc: function(aRow, aColumn) {},
     getProgressMode: function(aRow, aColumn) {},
     getCellValue: function(aRow, aColumn) {},
-    cycleHeader: function(aColumn) {},
-    getRowProperties: function(aRow) { return ""; },
-    getColumnProperties: function(aColumn) { return ""; },
-    getCellProperties: function(aRow, aColumn) {
-      return (aColumn.element.getAttribute("id") == "siteCol") ? "ltr" : "";
+    cycleHeader: function(column) {},
+    getRowProperties: function(row){ return ""; },
+    getColumnProperties: function(column){ return ""; },
+    getCellProperties: function(row,column){
+      if (column.element.getAttribute("id") == "siteCol")
+        return "ltr";
+      return "";
     }
   },
 
   _getCapabilityString: function (aCapability)
   {
     var stringKey = null;
     switch (aCapability) {
     case nsIPermissionManager.ALLOW_ACTION:
       stringKey = "can";
       break;
     case nsIPermissionManager.DENY_ACTION:
       stringKey = "cannot";
       break;
+    case nsICookiePermission.ACCESS_ALLOW_FIRST_PARTY_ONLY:
+      stringKey = "canAccessFirstParty";
+      break;
     case nsICookiePermission.ACCESS_SESSION:
       stringKey = "canSession";
       break;
     }
     return this._bundle.getString(stringKey);
   },
 
   addPermission: function (aCapability)
   {
     var textbox = document.getElementById("url");
-    let scheme = textbox.value.includes("@") ? "mailto:" : "http://";
-    let host = textbox.value.replace(/^\s*([-\w]*:\/*)?/, ""); // trim any leading space and scheme
+    var input_url = textbox.value.trim();
+    let principal;
     try {
-      let uri = Services.io.newURI(scheme + host, null, null);
-      host = uri.spec.startsWith("mailto:") ? uri.spec : uri.host;
+      // The origin accessor on the principal object will throw if the
+      // principal doesn't have a canonical origin representation. This will
+      // help catch cases where the URI parser parsed something like
+      // `localhost:8080` as having the scheme `localhost`, rather than being
+      // an invalid URI. A canonical origin representation is required by the
+      // permission manager for storage, so this won't prevent any valid
+      // permissions from being entered by the user.
+      let uri;
+      try {
+        uri = Services.io.newURI(input_url, null, null);
+        principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+        // If we have ended up with an unknown scheme, the following will throw.
+        principal.origin;
+      } catch(ex) {
+        let scheme = (this._type != "image" || !input_url.includes("@")) ?
+          "http://" : MAILURI_BASE;
+        uri = Services.io.newURI(scheme + input_url, null, null);
+        principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+        // If we have ended up with an unknown scheme, the following will throw.
+        principal.origin;
+      }
     } catch(ex) {
       var message = this._bundle.getString("invalidURI");
       var title = this._bundle.getString("invalidURITitle");
       Services.prompt.alert(window, title, message);
       return;
     }
 
     var capabilityString = this._getCapabilityString(aCapability);
 
     // check whether the permission already exists, if not, add it
-    var exists = false;
+    let permissionExists = false;
+    let capabilityExists = false;
     for (var i = 0; i < this._permissions.length; ++i) {
-      if (this._permissions[i].rawHost == host) {
-        // Avoid calling the permission manager if the capability settings are
-        // the same. Otherwise allow the call to the permissions manager to
-        // update the listbox for us.
-        exists = this._permissions[i].perm == aCapability;
+      // Thunderbird compares origins, not principals here.
+      if (this._permissions[i].principal.origin == principal.origin) {
+        permissionExists = true;
+        capabilityExists = this._permissions[i].capability == capabilityString;
+        if (!capabilityExists) {
+          this._permissions[i].capability = capabilityString;
+        }
         break;
       }
     }
 
-    if (!exists) {
-      host = host.startsWith(".") ? host.substr(1) : host;
-      // For mailto: uris permissions are keyed off the address.
-      host = host.replace(/^mailto:/, "");
-      let uri = Services.io.newURI(scheme + host, null, null);
-      Services.perms.add(uri, this._type, aCapability);
+    let permissionParams = {principal: principal, type: this._type, capability: aCapability};
+    if (!permissionExists) {
+      this._permissionsToAdd.set(principal.origin, permissionParams);
+      this._addPermission(permissionParams);
     }
+    else if (!capabilityExists) {
+      this._permissionsToAdd.set(principal.origin, permissionParams);
+      this._handleCapabilityChange();
+    }
+
     textbox.value = "";
     textbox.focus();
 
     // covers a case where the site exists already, so the buttons don't disable
     this.onHostInput(textbox);
 
     // enable "remove all" button as needed
     document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0;
   },
 
+  _removePermission: function(aPermission)
+  {
+    this._removePermissionFromList(aPermission.principal);
+
+    // If this permission was added during this session, let's remove
+    // it from the pending adds list to prevent calls to the
+    // permission manager.
+    let isNewPermission = this._permissionsToAdd.delete(aPermission.principal.origin);
+
+    if (!isNewPermission) {
+      this._permissionsToDelete.set(aPermission.principal.origin, aPermission);
+    }
+
+  },
+
+  _handleCapabilityChange: function ()
+  {
+    // Re-do the sort, if the status changed from Block to Allow
+    // or vice versa, since if we're sorted on status, we may no
+    // longer be in order.
+    if (this._lastPermissionSortColumn == "statusCol") {
+      this._resortPermissions();
+    }
+    this._tree.treeBoxObject.invalidate();
+  },
+
+  _addPermission: function(aPermission)
+  {
+    this._addPermissionToList(aPermission);
+    ++this._view._rowCount;
+    this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, 1);
+    // Re-do the sort, since we inserted this new item at the end.
+    this._resortPermissions();
+  },
+
+  _resortPermissions: function()
+  {
+    gTreeUtils.sort(this._tree, this._view, this._permissions,
+                    this._lastPermissionSortColumn,
+                    this._permissionsComparator,
+                    this._lastPermissionSortColumn,
+                    !this._lastPermissionSortAscending); // keep sort direction
+  },
+
   onHostInput: function (aSiteField)
   {
     document.getElementById("btnSession").disabled = !aSiteField.value;
     document.getElementById("btnBlock").disabled = !aSiteField.value;
     document.getElementById("btnAllow").disabled = !aSiteField.value;
   },
 
+  onWindowKeyPress: function (aEvent)
+  {
+    if (aEvent.keyCode == KeyEvent.DOM_VK_ESCAPE)
+      window.close();
+  },
+
   onHostKeyPress: function (aEvent)
   {
     if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
       document.getElementById("btnAllow").click();
   },
 
   onLoad: function ()
   {
@@ -142,17 +233,17 @@ var gPermissionManager = {
       this.uninit();
     }
 
     this._type = aParams.permissionType;
     this._manageCapability = aParams.manageCapability;
 
     var permissionsText = document.getElementById("permissionsText");
     while (permissionsText.hasChildNodes())
-      permissionsText.lastChild.remove();
+      permissionsText.removeChild(permissionsText.firstChild);
     permissionsText.appendChild(document.createTextNode(aParams.introText));
 
     document.title = aParams.windowTitle;
 
     document.getElementById("btnBlock").hidden    = !aParams.blockVisible;
     document.getElementById("btnSession").hidden  = !aParams.sessionVisible;
     document.getElementById("btnAllow").hidden    = !aParams.allowVisible;
 
@@ -162,62 +253,71 @@ var gPermissionManager = {
     urlField.value = aParams.prefilledHost;
     urlField.hidden = !urlFieldVisible;
 
     this.onHostInput(urlField);
 
     var urlLabel = document.getElementById("urlLabel");
     urlLabel.hidden = !urlFieldVisible;
 
+    let treecols = document.getElementsByTagName("treecols")[0];
+    treecols.addEventListener("click", event => {
+      if (event.target.nodeName != "treecol" || event.button != 0) {
+        return;
+      }
+
+      let sortField = event.target.getAttribute("data-field-name");
+      if (!sortField) {
+        return;
+      }
+
+      gPermissionManager.onPermissionSort(sortField);
+    });
+
+    Services.obs.notifyObservers(null, NOTIFICATION_FLUSH_PERMISSIONS, this._type);
     Services.obs.addObserver(this, "perm-changed", false);
 
     this._loadPermissions();
 
     urlField.focus();
   },
 
   uninit: function ()
   {
-    Services.obs.removeObserver(this, "perm-changed");
+    if (!this._observerRemoved) {
+      Services.obs.removeObserver(this, "perm-changed");
+
+      this._observerRemoved = true;
+    }
   },
 
   observe: function (aSubject, aTopic, aData)
   {
     if (aTopic == "perm-changed") {
       var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission);
+
+      // Ignore unrelated permission types.
+      if (permission.type != this._type)
+        return;
+
       if (aData == "added") {
-        this._addPermissionToList(permission);
-        ++this._view._rowCount;
-        this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, 1);
-        // Re-do the sort, since we inserted this new item at the end.
-        gTreeUtils.sort(this._tree, this._view, this._permissions,
-                        this._lastPermissionSortColumn,
-                        this._lastPermissionSortAscending);
+        this._addPermission(permission);
       }
       else if (aData == "changed") {
         for (var i = 0; i < this._permissions.length; ++i) {
-          if (this._permissions[i].host == permission.host) {
+          if (permission.matches(this._permissions[i].principal, true)) {
             this._permissions[i].capability = this._getCapabilityString(permission.capability);
             break;
           }
         }
-        // Re-do the sort, if the status changed from Block to Allow
-        // or vice versa, since if we're sorted on status, we may no
-        // longer be in order.
-        if (this._lastPermissionSortColumn.id == "statusCol") {
-          gTreeUtils.sort(this._tree, this._view, this._permissions,
-                          this._lastPermissionSortColumn,
-                          this._lastPermissionSortAscending);
-        }
-        this._tree.treeBoxObject.invalidate();
+        this._handleCapabilityChange();
       }
-      // No UI other than this window causes this method to be sent a "deleted"
-      // notification, so we don't need to implement it since Delete is handled
-      // directly by the Permission Removal handlers. If that ever changes, those
-      // implementations will have to move into here.
+      else if (aData == "deleted") {
+        this._removePermissionFromList(permission);
+      }
     }
   },
 
   onPermissionSelected: function ()
   {
     var hasSelection = this._tree.view.selection.count > 0;
     var hasRows = this._tree.view.rowCount > 0;
     document.getElementById("removePermission").disabled = !hasRows || !hasSelection;
@@ -227,104 +327,142 @@ var gPermissionManager = {
   onPermissionDeleted: function ()
   {
     if (!this._view.rowCount)
       return;
     var removedPermissions = [];
     gTreeUtils.deleteSelectedItems(this._tree, this._view, this._permissions, removedPermissions);
     for (var i = 0; i < removedPermissions.length; ++i) {
       var p = removedPermissions[i];
-      Services.perms.remove(p.host, p.type);
+      this._removePermission(p);
     }
     document.getElementById("removePermission").disabled = !this._permissions.length;
     document.getElementById("removeAllPermissions").disabled = !this._permissions.length;
   },
 
   onAllPermissionsDeleted: function ()
   {
     if (!this._view.rowCount)
       return;
     var removedPermissions = [];
     gTreeUtils.deleteAll(this._tree, this._view, this._permissions, removedPermissions);
     for (var i = 0; i < removedPermissions.length; ++i) {
       var p = removedPermissions[i];
-      Services.perms.remove(p.host, p.type);
+      this._removePermission(p);
     }
     document.getElementById("removePermission").disabled = true;
     document.getElementById("removeAllPermissions").disabled = true;
   },
 
   onPermissionKeyPress: function (aEvent)
   {
-    if (aEvent.keyCode == 46)
+    if (aEvent.keyCode == KeyEvent.DOM_VK_DELETE
+        || (Application.platformIsMac && aEvent.keyCode == KeyEvent.DOM_VK_BACK_SPACE)
+       )
       this.onPermissionDeleted();
   },
 
   _lastPermissionSortColumn: "",
   _lastPermissionSortAscending: false,
+  _permissionsComparator : function (a, b)
+  {
+    return a.toLowerCase().localeCompare(b.toLowerCase());
+  },
+
 
   onPermissionSort: function (aColumn)
   {
     this._lastPermissionSortAscending = gTreeUtils.sort(this._tree,
                                                         this._view,
                                                         this._permissions,
                                                         aColumn,
+                                                        this._permissionsComparator,
                                                         this._lastPermissionSortColumn,
                                                         this._lastPermissionSortAscending);
     this._lastPermissionSortColumn = aColumn;
   },
 
+  onApplyChanges: function()
+  {
+    // Stop observing permission changes since we are about
+    // to write out the pending adds/deletes and don't need
+    // to update the UI
+    this.uninit();
+
+    for (let permissionParams of this._permissionsToAdd.values()) {
+      Services.perms.addFromPrincipal(permissionParams.principal, permissionParams.type, permissionParams.capability);
+    }
+
+    for (let p of this._permissionsToDelete.values()) {
+      Services.perms.removeFromPrincipal(p.principal, p.type);
+    }
+
+    window.close();
+  },
+
   _loadPermissions: function ()
   {
     this._tree = document.getElementById("permissionsTree");
     this._permissions = [];
 
     // load permissions into a table
     var count = 0;
     var enumerator = Services.perms.enumerator;
     while (enumerator.hasMoreElements()) {
       var nextPermission = enumerator.getNext().QueryInterface(Components.interfaces.nsIPermission);
       this._addPermissionToList(nextPermission);
     }
 
     this._view._rowCount = this._permissions.length;
 
     // sort and display the table
-    this._tree.treeBoxObject.view = this._view;
-    this.onPermissionSort("rawHost", false);
+    this._tree.view = this._view;
+    this.onPermissionSort("origin");
 
     // disable "remove all" button if there are none
     document.getElementById("removeAllPermissions").disabled = this._permissions.length == 0;
   },
 
   _addPermissionToList: function (aPermission)
   {
+
     if (aPermission.type == this._type &&
         (!this._manageCapability ||
          (aPermission.capability == this._manageCapability))) {
 
-      var host = aPermission.host;
+      var principal = aPermission.principal;
       var capabilityString = this._getCapabilityString(aPermission.capability);
-      var p = new Permission(host,
-                             (host.startsWith(".")) ? host.substring(1, host.length) : host,
+      var p = new Permission(principal,
                              aPermission.type,
-                             capabilityString,
-                             aPermission.capability);
+                             capabilityString);
       this._permissions.push(p);
     }
   },
 
-  setHost: function (aHost)
+  _removePermissionFromList: function (aPrincipal)
   {
-    document.getElementById("url").value = aHost;
+    for (let i = 0; i < this._permissions.length; ++i) {
+      // Thunderbird compares origins, not principals here.
+      if (this._permissions[i].principal.origin == aPrincipal.origin) {
+        this._permissions.splice(i, 1);
+        this._view._rowCount--;
+        this._tree.treeBoxObject.rowCountChanged(this._view.rowCount - 1, -1);
+        this._tree.treeBoxObject.invalidate();
+        break;
+      }
+    }
+  },
+
+  setOrigin: function (aOrigin)
+  {
+    document.getElementById("url").value = aOrigin;
   }
 };
 
-function setHost(aHost)
+function setOrigin(aOrigin)
 {
-  gPermissionManager.setHost(aHost);
+  gPermissionManager.setOrigin(aOrigin);
 }
 
 function initWithParams(aParams)
 {
   gPermissionManager.init(aParams);
 }
-
--- a/mail/components/preferences/permissions.xul
+++ b/mail/components/preferences/permissions.xul
@@ -1,30 +1,33 @@
 <?xml version="1.0"?>
 
-# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
 <?xml-stylesheet href="chrome://messenger/skin/preferences/preferences.css" type="text/css"?>
 
-<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/preferences/permissions.dtd" >
+<!DOCTYPE dialog [
+  <!ENTITY % dtd1 SYSTEM "chrome://messenger/locale/preferences/permissions.dtd"> %dtd1;
+  <!ENTITY % dtd2 SYSTEM "chrome://messenger/locale/editContactOverlay.dtd"> %dtd2;
+]>
 
 <window id="PermissionsDialog" class="windowDialog"
         windowtype="mailnews:permissions"
         title="&window.title;"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         style="width: &window.width;;"
         onload="gPermissionManager.onLoad();"
         onunload="gPermissionManager.uninit();"
-        persist="screenX screenY width height">
+        persist="screenX screenY width height"
+        onkeypress="gPermissionManager.onWindowKeyPress(event);">
 
-  <script src="chrome://messenger/content/preferences/permissionsutils.js"/>
+  <script src="chrome://global/content/treeUtils.js"/>
   <script src="chrome://messenger/content/preferences/permissions.js"/>
 
   <stringbundle id="bundlePreferences"
                 src="chrome://messenger/locale/preferences/preferences.properties"/>
 
   <keyset>
     <key key="&windowClose.key;" modifiers="accel" oncommand="window.close();"/>
   </keyset>
@@ -48,35 +51,36 @@
     </hbox>
     <separator class="thin"/>
     <tree id="permissionsTree" flex="1" style="height: 18em;"
           hidecolumnpicker="true"
           onkeypress="gPermissionManager.onPermissionKeyPress(event)"
           onselect="gPermissionManager.onPermissionSelected();">
       <treecols>
         <treecol id="siteCol" label="&treehead.sitename.label;" flex="3"
-                onclick="gPermissionManager.onPermissionSort('rawHost');" persist="width"/>
+                 data-field-name="rawHost" persist="width"/>
         <splitter class="tree-splitter"/>
         <treecol id="statusCol" label="&treehead.status.label;" flex="1"
-                onclick="gPermissionManager.onPermissionSort('capability');" persist="width"/>
+                 data-field-name="capability" persist="width"/>
       </treecols>
       <treechildren/>
     </tree>
   </vbox>
-  <hbox align="end">
-    <hbox class="actionButtons" flex="1">
+  <vbox>
+    <hbox class="actionButtons" align="left" flex="1">
       <button id="removePermission" disabled="true"
               accesskey="&removepermission.accesskey;"
               icon="remove" label="&removepermission.label;"
               oncommand="gPermissionManager.onPermissionDeleted();"/>
       <button id="removeAllPermissions"
               icon="clear" label="&removeallpermissions.label;"
               accesskey="&removeallpermissions.accesskey;"
               oncommand="gPermissionManager.onAllPermissionsDeleted();"/>
-      <spacer flex="1"/>
-#ifndef XP_MACOSX
+    </hbox>
+    <spacer flex="1"/>
+    <hbox class="actionButtons" align="right" flex="1">
       <button oncommand="close();" icon="close"
-              label="&button.close.label;" accesskey="&button.close.accesskey;"/>
-#endif
+              label="&editContactPanelCancel.label;" accesskey="&editContactPanelCancel.accesskey;" />
+      <button id="btnApplyChanges" oncommand="gPermissionManager.onApplyChanges();" icon="save"
+              label="&editContactPanelDone.label;" accesskey="&editContactPanelDone.accesskey;"/>
     </hbox>
-    <resizer dir="bottomend"/>
-  </hbox>
+  </vbox>
 </window>
deleted file mode 100644
--- a/mail/components/preferences/permissionsutils.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-var gTreeUtils = {
-  deleteAll: function (aTree, aView, aItems, aDeletedItems)
-  {
-    for (var i = 0; i < aItems.length; ++i)
-      aDeletedItems.push(aItems[i]);
-    aItems.splice(0);
-    var oldCount = aView.rowCount;
-    aView._rowCount = 0;
-    aTree.treeBoxObject.rowCountChanged(0, -oldCount);
-  },
-
-  deleteSelectedItems: function (aTree, aView, aItems, aDeletedItems)
-  {
-    var selection = aTree.view.selection;
-    selection.selectEventsSuppressed = true;
-
-    var rc = selection.getRangeCount();
-    for (var i = 0; i < rc; ++i) {
-      var min = { }; var max = { };
-      selection.getRangeAt(i, min, max);
-      for (var j = min.value; j <= max.value; ++j) {
-        aDeletedItems.push(aItems[j]);
-        aItems[j] = null;
-      }
-    }
-
-    var nextSelection = 0;
-    for (i = 0; i < aItems.length; ++i) {
-      if (!aItems[i]) {
-        var j = i;
-        while (j < aItems.length && !aItems[j])
-          ++j;
-        aItems.splice(i, j - i);
-        nextSelection = j < aView.rowCount ? j - 1 : j - 2;
-        aView._rowCount -= j - i;
-        aTree.treeBoxObject.rowCountChanged(i, i - j);
-      }
-    }
-
-    if (aItems.length) {
-      selection.select(nextSelection);
-      aTree.treeBoxObject.ensureRowIsVisible(nextSelection);
-      aTree.focus();
-    }
-    selection.selectEventsSuppressed = false;
-  },
-
-  sort: function (aTree, aView, aDataSet, aColumn,
-                  aLastSortColumn, aLastSortAscending)
-  {
-    var ascending = (aColumn == aLastSortColumn) ? !aLastSortAscending : true;
-    aDataSet.sort(function (a, b) { return a[aColumn].toLowerCase().localeCompare(b[aColumn].toLowerCase()); });
-    if (!ascending)
-      aDataSet.reverse();
-
-    aTree.view.selection.select(-1);
-    aTree.view.selection.select(0);
-    aTree.treeBoxObject.invalidate();
-    aTree.treeBoxObject.ensureRowIsVisible(0);
-
-    return ascending;
-  }
-};
-
--- a/mail/locales/en-US/chrome/messenger/preferences/preferences.properties
+++ b/mail/locales/en-US/chrome/messenger/preferences/preferences.properties
@@ -78,18 +78,19 @@ cookiepermissionstext=You can specify wh
 invalidURI=Please enter a valid hostname
 invalidURITitle=Invalid Hostname Entered
 
 #### Cookie Viewer
 hostColon=Host:
 domainColon=Domain:
 forSecureOnly=Encrypted connections only
 forAnyConnection=Any type of connection
-AtEndOfSession=at end of session
+expireAtEndOfSession=At end of session
 can=Allow
+canAccessFirstParty=Allow first party only
 canSession=Allow for Session
 cannot=Block
 noCookieSelected=<no cookie selected>
 cookiesAll=The following cookies are stored on your computer:
 cookiesFiltered=The following cookies match your search:
 removeCookies=Remove Cookies
 removeCookie=Remove Cookie
 
--- a/mail/test/mozmill/content-policy/test-general-content-policy.js
+++ b/mail/test/mozmill/content-policy/test-general-content-policy.js
@@ -267,17 +267,17 @@ function checkAllowFeedMsg(test) {
 function checkAllowForSenderWithPerms(test) {
   let msgDbHdr = addToFolder(test.type + " priv sender test message " + gMsgNo,
                              msgBodyStart + test.body + msgBodyEnd, folder);
 
   let addresses = {};
   MailServices.headerParser.parseHeadersWithArray(msgDbHdr.author, addresses, {}, {});
   let authorEmailAddress = addresses.value[0];
 
-  let uri = Services.io.newURI("mailto:" + authorEmailAddress, null, null);
+  let uri = Services.io.newURI("chrome://messenger/content/?email=" + authorEmailAddress, null, null);
   Services.perms.add(uri, "image", Services.perms.ALLOW_ACTION);
   assert_true(Services.perms.testPermission(uri, "image") ==
               Services.perms.ALLOW_ACTION);
 
   // select the newly created message
   let msgHdr = select_click_row(gMsgNo);
 
   assert_equals(msgDbHdr, msgHdr);
--- a/mailnews/base/src/nsMsgContentPolicy.cpp
+++ b/mailnews/base/src/nsMsgContentPolicy.cpp
@@ -105,17 +105,17 @@ nsMsgContentPolicy::ShouldAcceptRemoteCo
   nsCString emailAddress; 
   ExtractEmail(EncodedHeader(author), emailAddress);
   if (emailAddress.IsEmpty())
     return false;
 
   nsCOMPtr<nsIIOService> ios = do_GetService("@mozilla.org/network/io-service;1", &rv);
   NS_ENSURE_SUCCESS(rv, false);
   nsCOMPtr<nsIURI> mailURI;
-  emailAddress.Insert("mailto:", 0);
+  emailAddress.Insert("chrome://messenger/content/?email=", 0);
   rv = ios->NewURI(emailAddress, nullptr, nullptr, getter_AddRefs(mailURI));
   NS_ENSURE_SUCCESS(rv, false);
 
   // check with permission manager
   uint32_t permission = 0;
   rv = mPermissionManager->TestPermission(mailURI, "image", &permission);
   NS_ENSURE_SUCCESS(rv, false);
 
--- a/mailnews/base/test/unit/test_accountMigration.js
+++ b/mailnews/base/test/unit/test_accountMigration.js
@@ -27,19 +27,22 @@ function run_test() {
   Services.prefs.setCharPref("mail.accountmanager.accounts",
                              "account1,account2");
 
   let testAB = do_get_file("data/remoteContent.mab");
 
   // Copy the file to the profile directory for a PAB.
   testAB.copyTo(do_get_profile(), kPABData.fileName);
 
-  let uriAllowed = Services.io.newURI("mailto:yes@test.invalid", null, null);
-  let uriAllowed2 = Services.io.newURI("mailto:yes2@test.invalid", null, null);
-  let uriDisallowed = Services.io.newURI("mailto:no@test.invalid", null, null);
+  let uriAllowed = Services.io.newURI(
+    "chrome://messenger/content/?email=yes@test.invalid", null, null);
+  let uriAllowed2 = Services.io.newURI(
+    "chrome://messenger/content/?email=yes2@test.invalid", null, null);
+  let uriDisallowed = Services.io.newURI(
+    "chrome://messenger/content/?email=no@test.invalid", null, null);
 
   // Check that this email that according to the ab data has (had!)
   // remote content premissions, has no premissions pre migration.
   do_check_eq(Services.perms.testPermission(uriAllowed, "image"),
               Services.perms.UNKNOWN_ACTION);
   do_check_eq(Services.perms.testPermission(uriAllowed2, "image"),
               Services.perms.UNKNOWN_ACTION);
   do_check_eq(Services.perms.testPermission(uriDisallowed, "image"),
--- a/mailnews/base/util/mailnewsMigrator.js
+++ b/mailnews/base/util/mailnewsMigrator.js
@@ -126,17 +126,18 @@ function MigrateABRemoteContentSettings(
   if (Services.prefs.prefHasUserValue("mail.ab_remote_content.migrated"))
     return;
 
   // Search through all of our local address books looking for a match.
   let enumerator = MailServices.ab.directories;
   while (enumerator.hasMoreElements())
   {
     let migrateAddress = function(aEmail) {
-      let uri = Services.io.newURI("mailto:" + aEmail, null, null);
+      let uri = Services.io.newURI(
+        "chrome://messenger/content/?email=" + aEmail, null, null);
       Services.perms.add(uri, "image", Services.perms.ALLOW_ACTION);
     }
 
     let addrbook = enumerator.getNext()
       .QueryInterface(Components.interfaces.nsIAbDirectory);
     try {
       // If it's a read-only book, don't try to find a card as we we could never
       // have set the AllowRemoteContent property.