Bug 1488176 - Accounts and messages WebExtensions API; r=Fallen
authorGeoff Lankow <geoff@darktrojan.net>
Mon, 14 Jan 2019 14:22:59 +1300
changeset 33331 18f7dcdd1bed
parent 33330 f9b760462530
child 33332 7acd73ce23af
push id2368
push userclokep@gmail.com
push dateMon, 28 Jan 2019 21:12:50 +0000
treeherdercomm-beta@56d23c07d815 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersFallen
bugs1488176
Bug 1488176 - Accounts and messages WebExtensions API; r=Fallen
mail/components/extensions/ext-mail.json
mail/components/extensions/jar.mn
mail/components/extensions/parent/.eslintrc.js
mail/components/extensions/parent/ext-accounts.js
mail/components/extensions/parent/ext-mail.js
mail/components/extensions/parent/ext-mailTabs.js
mail/components/extensions/parent/ext-messages.js
mail/components/extensions/schemas/accounts.json
mail/components/extensions/schemas/messages.json
mail/locales/en-US/chrome/messenger/addons.properties
--- a/mail/components/extensions/ext-mail.json
+++ b/mail/components/extensions/ext-mail.json
@@ -1,9 +1,17 @@
 {
+  "accounts": {
+    "url": "chrome://messenger/content/parent/ext-accounts.js",
+    "schema": "chrome://messenger/content/schemas/accounts.json",
+    "scopes": ["addon_parent"],
+    "paths": [
+      ["accounts"]
+    ]
+  },
   "addressBook": {
     "url": "chrome://messenger/content/parent/ext-addressBook.js",
     "schema": "chrome://messenger/content/schemas/addressBook.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["addressBooks"], ["contacts"], ["mailingLists"]
     ]
   },
@@ -66,16 +74,25 @@
   "menus": {
     "url": "chrome://messenger/content/parent/ext-menus.js",
     "schema": "chrome://messenger/content/schemas/menus.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["menus"]
     ]
   },
+  "messages": {
+    "url": "chrome://messenger/content/parent/ext-messages.js",
+    "schema": "chrome://messenger/content/schemas/messages.json",
+    "scopes": ["addon_parent"],
+    "manifest": ["messages"],
+    "paths": [
+      ["messages"]
+    ]
+  },
   "pkcs11": {
     "url": "chrome://messenger/content/parent/ext-pkcs11.js",
     "schema": "chrome://messenger/content/schemas/pkcs11.json",
     "scopes": ["addon_parent"],
     "paths": [
       ["pkcs11"]
     ]
   },
--- a/mail/components/extensions/jar.mn
+++ b/mail/components/extensions/jar.mn
@@ -6,33 +6,37 @@ messenger.jar:
     content/messenger/ext-mail.json                (ext-mail.json)
     content/messenger/extension.svg                (extension.svg)
 
     content/messenger/child/ext-mail.js            (child/ext-mail.js)
     content/messenger/child/ext-menus-child.js     (child/ext-menus-child.js)
     content/messenger/child/ext-menus.js           (child/ext-menus.js)
     content/messenger/child/ext-tabs.js            (child/ext-tabs.js)
 
+    content/messenger/parent/ext-accounts.js       (parent/ext-accounts.js)
     content/messenger/parent/ext-addressBook.js    (parent/ext-addressBook.js)
     content/messenger/parent/ext-browserAction.js  (parent/ext-browserAction.js)
     content/messenger/parent/ext-cloudFile.js      (parent/ext-cloudFile.js)
     content/messenger/parent/ext-commands.js       (parent/ext-commands.js)
     content/messenger/parent/ext-composeAction.js  (parent/ext-composeAction.js)
     content/messenger/parent/ext-legacy.js         (parent/ext-legacy.js)
     content/messenger/parent/ext-mail.js           (parent/ext-mail.js)
     content/messenger/parent/ext-mailTabs.js       (parent/ext-mailTabs.js)
     content/messenger/parent/ext-menus.js          (parent/ext-menus.js)
+    content/messenger/parent/ext-messages.js       (parent/ext-messages.js)
     content/messenger/parent/ext-pkcs11.js         (../../../../browser/components/extensions/parent/ext-pkcs11.js)
     content/messenger/parent/ext-tabs.js           (parent/ext-tabs.js)
     content/messenger/parent/ext-windows.js        (parent/ext-windows.js)
 
+    content/messenger/schemas/accounts.json        (schemas/accounts.json)
     content/messenger/schemas/addressBook.json     (schemas/addressBook.json)
     content/messenger/schemas/browserAction.json   (schemas/browserAction.json)
     content/messenger/schemas/cloudFile.json       (schemas/cloudFile.json)
     content/messenger/schemas/commands.json        (schemas/commands.json)
     content/messenger/schemas/composeAction.json   (schemas/composeAction.json)
     content/messenger/schemas/legacy.json          (schemas/legacy.json)
     content/messenger/schemas/mailTabs.json        (schemas/mailTabs.json)
     content/messenger/schemas/menus.json           (schemas/menus.json)
     content/messenger/schemas/menus_child.json     (schemas/menus_child.json)
+    content/messenger/schemas/messages.json        (schemas/messages.json)
     content/messenger/schemas/pkcs11.json          (../../../../browser/components/extensions/schemas/pkcs11.json)
     content/messenger/schemas/tabs.json            (schemas/tabs.json)
     content/messenger/schemas/windows.json         (schemas/windows.json)
--- a/mail/components/extensions/parent/.eslintrc.js
+++ b/mail/components/extensions/parent/.eslintrc.js
@@ -38,16 +38,18 @@ module.exports = {
     "Window": true,
     "WindowEventManager": true,
     "convertFolder": true,
     "convertMessage": true,
     "folderPathToURI": true,
     "folderURIToPath": true,
     "getTabBrowser": true,
     "makeWidgetId": true,
+    "messageListTracker": true,
+    "messageTracker": true,
     "tabGetSender": true,
     "tabTracker": true,
     "windowTracker": true,
 
     // ext-browserAction.js
     "browserActionFor": true,
   },
 };
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/parent/ext-accounts.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+var { fixIterator } = ChromeUtils.import("resource:///modules/iteratorUtils.jsm", null);
+
+function convertAccount(account) {
+  account = account.QueryInterface(Ci.nsIMsgAccount);
+  let server = account.incomingServer;
+  if (server.type == "im") {
+    return null;
+  }
+
+  let folders = [];
+  let traverse = function(folder, accountId) {
+    for (let subFolder of fixIterator(folder.subFolders, Ci.nsIMsgFolder)) {
+      folders.push(convertFolder(subFolder, accountId));
+      traverse(subFolder, accountId);
+    }
+  };
+  traverse(account.incomingServer.rootFolder, account.key);
+
+  return {
+    id: account.key,
+    name: account.incomingServer.prettyName,
+    type: account.incomingServer.type,
+    folders,
+  };
+}
+
+this.accounts = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      accounts: {
+        async list() {
+          let accounts = [];
+          for (let account of fixIterator(MailServices.accounts.accounts, Ci.nsIMsgAccount)) {
+            account = convertAccount(account);
+            if (account) {
+              accounts.push(account);
+            }
+          }
+          return accounts;
+        },
+        async get(accountId) {
+          let account = MailServices.accounts.getAccount(accountId);
+          if (account) {
+            return convertAccount(account);
+          }
+          return null;
+        },
+      },
+    };
+  }
+};
--- a/mail/components/extensions/parent/ext-mail.js
+++ b/mail/components/extensions/parent/ext-mail.js
@@ -1,13 +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/. */
 
 ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(
+  this, "uuidGenerator", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"
+);
 
 var {
   ExtensionError,
 } = ExtensionUtils;
 
 var {
   defineLazyGetter,
 } = ExtensionCommon;
@@ -1058,24 +1062,169 @@ function convertFolder(folder, accountId
   };
 }
 
 /**
  * Converts an nsIMsgHdr to a simle object for use in messages.
  * This function WILL change as the API develops.
  * @return {Object}
  */
-function convertMessage(msgHdr) {
+function convertMessage(msgHdr, context) {
   if (!msgHdr) {
     return null;
   }
 
-  return {
-    messageId: msgHdr.messageId,
+  let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"]
+                        .createInstance(Ci.nsIMsgCompFields);
+
+  let messageObject = {
+    id: messageTracker.getId(msgHdr),
+    date: new Date(msgHdr.dateInSeconds * 1000),
+    author: msgHdr.mime2DecodedAuthor,
+    recipients: composeFields.splitRecipients(msgHdr.mime2DecodedRecipients, false, {}),
+    ccList: composeFields.splitRecipients(msgHdr.ccList, false, {}),
+    bccList: composeFields.splitRecipients(msgHdr.bccList, false, {}),
+    subject: msgHdr.mime2DecodedSubject,
     read: msgHdr.isRead,
     flagged: msgHdr.isFlagged,
-    ccList: msgHdr.ccList,
-    bccList: msgHdr.bccList,
-    author: msgHdr.mime2DecodedAuthor,
-    subject: msgHdr.mime2DecodedSubject,
-    recipients: msgHdr.mime2DecodedRecipients,
   };
+  if (context.extension.hasPermission("accountsRead")) {
+    messageObject.folder = convertFolder(msgHdr.folder, msgHdr.accountKey);
+  }
+  let tags = msgHdr.getProperty("keywords");
+  messageObject.tags = tags ? tags.split(" ") : [];
+  return messageObject;
 }
+
+/**
+ * A map of numeric identifiers to messages for easy reference.
+ */
+var messageTracker = {
+  _nextId: 1,
+  _messages: new Map(),
+
+  /**
+   * Finds a message in the map or adds it to the map.
+   * @return {int} The identifier of the message
+   */
+  getId(msgHdr) {
+    for (let [key, value] of this._messages.entries()) {
+      if (value.folderURI == msgHdr.folder.URI && value.messageId == msgHdr.messageId) {
+        return key;
+      }
+    }
+    let id = this._nextId++;
+    this.setId(msgHdr, id);
+    return id;
+  },
+
+  /**
+   * Retrieves a message from the map. If the message no longer exists,
+   * it is removed from the map.
+   * @return {nsIMsgHdr} The identifier of the message
+   */
+  getMessage(id) {
+    let value = this._messages.get(id);
+    if (!value) {
+      return null;
+    }
+
+    let folder = MailServices.folderLookup.getFolderForURL(value.folderURI);
+    if (folder) {
+      let msgHdr = folder.msgDatabase.getMsgHdrForMessageID(value.messageId);
+      if (msgHdr) {
+        return msgHdr;
+      }
+    }
+
+    this._messages.delete(id);
+    return null;
+  },
+
+  /**
+   * Adds a message to the map.
+   */
+  setId(msgHdr, id) {
+    this._messages.set(id, { folderURI: msgHdr.folder.URI, messageId: msgHdr.messageId });
+  },
+};
+
+/**
+ * Tracks lists of messages so that an extension can consume them in chunks.
+ * Any WebExtensions method that could return multiple messages should instead call
+ * messageListTracker.startList and return the results, which contain the first
+ * chunk. Further chunks can be fetched by the extension calling
+ * browser.messages.continueList. Chunk size is controlled by a pref.
+ */
+var messageListTracker = {
+  _contextLists: new WeakMap(),
+
+  /**
+   * Takes an array or enumerator of messages and returns the first chunk.
+   * @returns {Object}
+   */
+  startList(messageList, context) {
+    if (Array.isArray(messageList)) {
+      messageList = this._createEnumerator(messageList);
+    }
+    let firstPage = this._getNextPage(messageList);
+    let messageListId = null;
+    if (messageList.hasMoreElements()) {
+      messageListId = uuidGenerator.generateUUID().number.substring(1, 37);
+      let lists = this._contextLists.get(context);
+      if (!lists) {
+        lists = new Map();
+        this._contextLists.set(context, lists);
+      }
+      lists.set(messageListId, messageList);
+    }
+
+    return {
+      id: messageListId,
+      messages: firstPage.map(message => convertMessage(message, context)),
+    };
+  },
+
+  /**
+   * Returns any subsequent chunk of messages.
+   * @returns {Object}
+   */
+  continueList(messageListId, context) {
+    let lists = this._contextLists.get(context);
+    let messageList = lists ? lists.get(messageListId, null) : null;
+    if (!messageList) {
+      throw new ExtensionError(
+        `No message list for id ${messageListId}. Have you reached the end of a list?`
+      );
+    }
+
+    let nextPage = this._getNextPage(messageList);
+    if (!messageList.hasMoreElements()) {
+      lists.delete(messageListId);
+      messageListId = null;
+    }
+    return {
+      id: messageListId,
+      messages: nextPage.map(message => convertMessage(message, context)),
+    };
+  },
+
+  _createEnumerator(array) {
+    let current = 0;
+    return {
+      hasMoreElements() {
+        return current < array.length;
+      },
+      getNext() {
+        return array[current++];
+      },
+    };
+  },
+
+  _getNextPage(messageList) {
+    let messageCount = Services.prefs.getIntPref("extensions.webextensions.messagesPerPage", 100);
+    let page = [];
+    for (let i = 0; i < messageCount && messageList.hasMoreElements(); i++) {
+      page.push(messageList.getNext().QueryInterface(Ci.nsIMsgDBHdr));
+    }
+    return page;
+  },
+};
--- a/mail/components/extensions/parent/ext-mailTabs.js
+++ b/mail/components/extensions/parent/ext-mailTabs.js
@@ -239,17 +239,18 @@ this.mailTabs = class extends ExtensionA
           if (!extension.hasPermission("messagesRead")) {
             throw new ExtensionError(
               `The "messagesRead" permission is required to use mailTabs.getSelectedMessages.`
             );
           }
 
           let tab = getTabOrActive(tabId);
           let { folderDisplay } = tab.nativeTab;
-          return [...folderDisplay.view.dbView.getSelectedMsgHdrs()].map(convertMessage);
+          let messageList = folderDisplay.view.dbView.getSelectedMsgHdrs();
+          return messageListTracker.startList(messageList, context);
         },
 
         async setQuickFilter(tabId, state) {
           let tab = getTabOrActive(tabId);
           let nativeTab = tab.nativeTab;
           let window = Cu.getGlobalForObject(nativeTab);
 
           let filterer;
@@ -324,17 +325,17 @@ this.mailTabs = class extends ExtensionA
         }).api(),
 
         onSelectedMessagesChanged: new PermissionedEventManager({
           permission: "messagesRead",
           context,
           name: "mailTabs.onSelectedMessagesChanged",
           register: (fire) => {
             let listener = (event, tab, messages) => {
-              fire.sync(tabTracker.getId(tab), messages.map(convertMessage));
+              fire.sync(tabTracker.getId(tab), messageListTracker.startList(messages, context));
             };
 
             uiListener.on("messages-changed", listener);
             uiListener.incrementListeners();
             return () => {
               uiListener.off("messages-changed", listener);
               uiListener.decrementListeners();
             };
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/parent/ext-messages.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ChromeUtils.defineModuleGetter(this, "MailServices", "resource:///modules/MailServices.jsm");
+ChromeUtils.defineModuleGetter(this, "MsgHdrToMimeMessage", "resource:///modules/gloda/mimemsg.js");
+
+/**
+ * Takes a part of a MIME message (as retrieved with MsgHdrToMimeMessage) and filters
+ * out the properties we don't want to send to extensions.
+ */
+function convertMessagePart(part) {
+  let partObject = {};
+  for (let key of ["body", "contentType", "headers", "name", "partName", "size"]) {
+    if (key in part) {
+      partObject[key] = part[key];
+    }
+  }
+  if (Array.isArray(part.parts) && part.parts.length > 0) {
+    partObject.parts = part.parts.map(convertMessagePart);
+  }
+  return partObject;
+}
+
+this.messages = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      messages: {
+        async list({ accountId, path }) {
+          let uri = folderPathToURI(accountId, path);
+          let folder = MailServices.folderLookup.getFolderForURL(uri);
+
+          return messageListTracker.startList(folder.messages, context);
+        },
+        async continueList(messageListId) {
+          return messageListTracker.continueList(messageListId, context);
+        },
+        async get(messageId) {
+          return convertMessage(messageTracker.getMessage(messageId), context);
+        },
+        async getFull(messageId) {
+          return new Promise(resolve => {
+            let msgHdr = messageTracker.getMessage(messageId);
+            MsgHdrToMimeMessage(msgHdr, null, (_msgHdr, mimeMsg) => {
+              resolve(convertMessagePart(mimeMsg));
+            });
+          });
+        },
+        async update(messageId, newProperties) {
+          let msgHdr = messageTracker.getMessage(messageId);
+          if (!msgHdr) {
+            return;
+          }
+          if (newProperties.read !== null) {
+            msgHdr.markRead(newProperties.read);
+          }
+          if (newProperties.flagged !== null) {
+            msgHdr.markFlagged(newProperties.flagged);
+          }
+          if (Array.isArray(newProperties.tags)) {
+            newProperties.tags = newProperties.tags.filter(MailServices.tags.isValidKey);
+            msgHdr.setProperty("keywords", newProperties.tags.join(" "));
+            for (let window of Services.wm.getEnumerator("mail:3pane")) {
+              window.OnTagsChange();
+            }
+          }
+        },
+        async listTags() {
+          return MailServices.tags.getAllTags({}).map(({ key, tag, color, ordinal }) => {
+            return {
+              key,
+              tag,
+              color,
+              ordinal,
+            };
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/schemas/accounts.json
@@ -0,0 +1,64 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "OptionalPermission",
+        "choices": [
+          {
+            "type": "string",
+            "enum": [
+              "accountsRead"
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "namespace": "accounts",
+    "permissions": [
+      "accountsRead"
+    ],
+    "types": [
+      {
+        "id": "MailFolder",
+        "type": "object",
+        "description": "A folder object, as returned by the <var>list</var> and <var>get</var> methods.",
+        "properties": {
+          "accountId": {
+            "type": "string"
+          },
+          "name": {
+            "type": "string",
+            "optional": true
+          },
+          "path": {
+            "type": "string"
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "list",
+        "type": "function",
+        "description": "Returns all mail accounts.",
+        "async": true,
+        "parameters": []
+      },
+      {
+        "name": "get",
+        "type": "function",
+        "description": "Returns details of the requested account, or null if it doesn't exist.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "accountId",
+            "type": "string"
+          }
+        ]
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mail/components/extensions/schemas/messages.json
@@ -0,0 +1,117 @@
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "OptionalPermission",
+        "choices": [
+          {
+            "type": "string",
+            "enum": [
+              "messagesRead"
+            ]
+          }
+        ]
+      }
+    ]
+  },
+  {
+    "namespace": "messages",
+    "permissions": [
+      "messagesRead"
+    ],
+    "functions": [
+      {
+        "name": "list",
+        "type": "function",
+        "description": "Gets all messages in a folder.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "folder",
+            "$ref": "accounts.MailFolder"
+          }
+        ]
+      },
+      {
+        "name": "continueList",
+        "type": "function",
+        "description": "Returns the next chunk of messages in a list. See :doc:`how-to/messageLists` for more information.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "messageListId",
+            "type": "string"
+          }
+        ]
+      },
+      {
+        "name": "get",
+        "type": "function",
+        "description": "Returns a specified message.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "messageId",
+            "type": "integer"
+          }
+        ]
+      },
+      {
+        "name": "getFull",
+        "type": "function",
+        "description": "Returns a specified message, including all headers and MIME parts.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "messageId",
+            "type": "integer"
+          }
+        ]
+      },
+      {
+        "name": "update",
+        "type": "function",
+        "description": "Marks or unmarks a message as read, starred, or tagged.",
+        "async": true,
+        "parameters": [
+          {
+            "name": "messageId",
+            "type": "integer"
+          },
+          {
+            "name": "newProperties",
+            "type": "object",
+            "properties": {
+              "read": {
+                "type": "boolean",
+                "description": "Marks the message as read or unread.",
+                "optional": true
+              },
+              "flagged": {
+                "type": "boolean",
+                "description": "Marks the message as starred or unstarred.",
+                "optional": true
+              },
+              "tags": {
+                "type": "array",
+                "description": "Sets the tags on the message. For a list of available tags, call the listTags method.",
+                "optional": true,
+                "items": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        ]
+      },
+      {
+        "name": "listTags",
+        "type": "function",
+        "description": "Returns a list of tags that can be set on messages, and their human-friendly name, colour, and sort order.",
+        "async": true,
+        "parameters": []
+      }
+    ]
+  }
+]
--- a/mail/locales/en-US/chrome/messenger/addons.properties
+++ b/mail/locales/en-US/chrome/messenger/addons.properties
@@ -143,30 +143,32 @@ webextPerms.updateAccept.accessKey=U
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.optionalPermsHeader=%S requests additional permissions.
 webextPerms.optionalPermsListIntro=It wants to:
 webextPerms.optionalPermsAllow.label=Allow
 webextPerms.optionalPermsAllow.accessKey=A
 webextPerms.optionalPermsDeny.label=Deny
 webextPerms.optionalPermsDeny.accessKey=D
 
+webextPerms.description.accountsRead=See your mail accounts and their folders
 webextPerms.description.addressBooks=Read and modify your address books and contacts
 webextPerms.description.bookmarks=Read and modify bookmarks
 webextPerms.description.browserSettings=Read and modify browser settings
 webextPerms.description.browsingData=Clear recent browsing history, cookies, and related data
 webextPerms.description.clipboardRead=Get data from the clipboard
 webextPerms.description.clipboardWrite=Input data to the clipboard
 webextPerms.description.devtools=Extend developer tools to access your data in open tabs
 webextPerms.description.dns=Access IP address and hostname information
 webextPerms.description.downloads=Download files and read and modify the browser’s download history
 webextPerms.description.downloads.open=Open files downloaded to your computer
 webextPerms.description.find=Read the text of all open tabs
 webextPerms.description.geolocation=Access your location
 webextPerms.description.history=Access browsing history
 webextPerms.description.management=Monitor extension usage and manage themes
+webextPerms.description.messagesRead=Read your email messages
 # LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
 # %S will be replaced with the name of the application
 webextPerms.description.nativeMessaging=Exchange messages with programs other than %S
 webextPerms.description.notifications=Display notifications to you
 webextPerms.description.pkcs11=Provide cryptographic authentication services
 webextPerms.description.privacy=Read and modify privacy settings
 webextPerms.description.proxy=Control browser proxy settings
 webextPerms.description.sessions=Access recently closed tabs