Bug 1600547 - Allow browser.messages.query to find messages with specified tag. r=mkmelin DONTBUILD
authorGeoff Lankow <geoff@darktrojan.net>
Tue, 31 Dec 2019 15:06:42 +1300
changeset 38153 5c29bf6eb78be3eda24e484bc4bf8ed5f7cfe3aa
parent 38152 3f3fc2c0d80474dff5953969f765e16937dde150
child 38154 c4ca8ba04d380749dda576fd4fc84de059abaed2
push id398
push userclokep@gmail.com
push dateMon, 09 Mar 2020 19:10:28 +0000
reviewersmkmelin
bugs1600547
Bug 1600547 - Allow browser.messages.query to find messages with specified tag. r=mkmelin DONTBUILD
mail/components/extensions/parent/ext-messages.js
mail/components/extensions/schemas/mailTabs.json
mail/components/extensions/schemas/messages.json
mail/components/extensions/test/xpcshell/test_ext_messages_query.js
--- a/mail/components/extensions/parent/ext-messages.js
+++ b/mail/components/extensions/parent/ext-messages.js
@@ -229,16 +229,28 @@ this.messages = class extends ExtensionA
             let folder = MailServices.folderLookup.getFolderForURL(
               folderPathToURI(queryInfo.folder.accountId, queryInfo.folder.path)
             );
             query.folder(folder);
           }
           if (queryInfo.fromDate || queryInfo.toDate) {
             query.dateRange([queryInfo.fromDate, queryInfo.toDate]);
           }
+          let validTags;
+          if (queryInfo.tags) {
+            validTags = MailServices.tags
+              .getAllTags()
+              .filter(tag => tag.key in queryInfo.tags.tags && queryInfo.tags.tags[tag.key]);
+            if (validTags.length === 0) {
+              // No messages will match this. Just return immediately.
+              return messageListTracker.startList([], context.extension);
+            }
+            query.tags(...validTags);
+            validTags = validTags.map(tag => tag.key);
+          }
 
           let collectionArray = await new Promise(resolve => {
             query.getCollection({
               onItemsAdded(items, collection) {},
               onItemsModified(items, collection) {},
               onItemsRemoved(items, collection) {},
               onQueryCompleted(collection) {
                 resolve(
@@ -250,16 +262,22 @@ this.messages = class extends ExtensionA
             });
           });
 
           if (queryInfo.unread !== null) {
             collectionArray = collectionArray.filter(
               msg => msg.isRead == !queryInfo.unread
             );
           }
+          if (validTags && queryInfo.tags.mode == "all") {
+            collectionArray = collectionArray.filter(msg => {
+              let messageTags = msg.getStringProperty("keywords").split(" ");
+              return validTags.every(tag => messageTags.includes(tag));
+            });
+          }
 
           return messageListTracker.startList(
             collectionArray,
             context.extension
           );
         },
         async update(messageId, newProperties) {
           let msgHdr = messageTracker.getMessage(messageId);
--- a/mail/components/extensions/schemas/mailTabs.json
+++ b/mail/components/extensions/schemas/mailTabs.json
@@ -1,36 +1,13 @@
 [
   {
     "namespace": "mailTabs",
     "types": [
       {
-        "id": "QuickFilterTagsDetail",
-        "type": "object",
-        "properties": {
-          "tags": {
-            "type": "object",
-            "description": "Object keys are tags to filter on, values are <code>true</code> if the message must have the tag, or <code>false</code> if it must not have the tag. For a list of available tags, call the :ref:`messages.listTags` method.",
-            "patternProperties": {
-              ".*": {
-                "type": "boolean"
-              }
-            }
-          },
-          "mode": {
-            "type": "string",
-            "description": "Whether all of the tag filters must apply, or any of them.",
-            "enum": [
-              "all",
-              "any"
-            ]
-          }
-        }
-      },
-      {
         "id": "QuickFilterTextDetail",
         "type": "object",
         "properties": {
           "text": {
             "type": "string",
             "description": "String to match against the <var>recipients</var>, <var>author</var>, <var>subject</var>, or <var>body</var>."
           },
           "recipients": {
@@ -229,17 +206,17 @@
               },
               "tags": {
                 "optional": true,
                 "choices": [
                   {
                     "type": "boolean"
                   },
                   {
-                    "$ref": "QuickFilterTagsDetail"
+                    "$ref": "messages.TagsDetail"
                   }
                 ],
                 "description": "Shows only messages with tags on them."
               },
               "attachment": {
                 "type": "boolean",
                 "description": "Shows only messages with attachments.",
                 "optional": true
--- a/mail/components/extensions/schemas/messages.json
+++ b/mail/components/extensions/schemas/messages.json
@@ -16,16 +16,42 @@
       }
     ]
   },
   {
     "namespace": "messages",
     "permissions": [
       "messagesRead"
     ],
+    "types": [
+      {
+        "id": "TagsDetail",
+        "type": "object",
+        "description": "Used for filtering messages by tag in various methods. Note that functions using this type may have a partial implementation.",
+        "properties": {
+          "tags": {
+            "type": "object",
+            "description": "Object keys are tags to filter on, values are <code>true</code> if the message must have the tag, or <code>false</code> if it must not have the tag. For a list of available tags, call the :ref:`messages.listTags` method.",
+            "patternProperties": {
+              ".*": {
+                "type": "boolean"
+              }
+            }
+          },
+          "mode": {
+            "type": "string",
+            "description": "Whether all of the tag filters must apply, or any of them.",
+            "enum": [
+              "all",
+              "any"
+            ]
+          }
+        }
+      }
+    ],
     "functions": [
       {
         "name": "list",
         "type": "function",
         "description": "Gets all messages in a folder.",
         "async": true,
         "parameters": [
           {
@@ -132,16 +158,24 @@
                 "optional": true,
                 "description": "Returns only unread (or read if false) messages."
               },
               "flagged": {
                 "type": "boolean",
                 "optional": true,
                 "description": "Returns only flagged (or unflagged if false) messages."
               },
+              "tags": {
+                "$ref": "TagsDetail",
+                "optional": true,
+                "items": {
+                  "type": "string"
+                },
+                "description": "Returns only messages with the specified tags. For a list of available tags, call the listTags method. Querying for messages that must *not* have a tag does not work."
+              },
               "folder": {
                 "$ref": "folders.MailFolder",
                 "optional": true,
                 "description": "Returns only messages from the specified folder."
               },
               "fromDate": {
                 "$ref": "extensionTypes.Date",
                 "optional": true,
--- a/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
+++ b/mail/components/extensions/test/xpcshell/test_ext_messages_query.js
@@ -1,14 +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/. */
 
 "use strict";
 
+var { toXPCOMArray } = ChromeUtils.import(
+  "resource:///modules/iteratorUtils.jsm"
+);
 var { ExtensionTestUtils } = ChromeUtils.import(
   "resource://testing-common/ExtensionXPCShellUtils.jsm"
 );
 ExtensionTestUtils.init(this);
 
 var { Gloda } = ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
 var { GlodaIndexer } = ChromeUtils.import(
   "resource:///modules/gloda/GlodaIndexer.jsm"
@@ -31,16 +34,29 @@ add_task(async function setup() {
   createMessages(subFolders.test2, 9);
 
   let messages = [...subFolders.test1.messages];
   // NB: Here, the messages are zero-indexed. In the test they're one-indexed.
   messages[0].markRead(true);
   messages[1].markFlagged(true);
   messages[6].markFlagged(true);
 
+  subFolders.test1.addKeywordsToMessages(
+    toXPCOMArray(messages.slice(0, 1), Ci.nsIMutableArray),
+    "notATag"
+  );
+  subFolders.test1.addKeywordsToMessages(
+    toXPCOMArray(messages.slice(2, 4), Ci.nsIMutableArray),
+    "$label2"
+  );
+  subFolders.test1.addKeywordsToMessages(
+    toXPCOMArray(messages.slice(3, 6), Ci.nsIMutableArray),
+    "$label3"
+  );
+
   addIdentity(account, messages[5].author.replace(/.*<(.*)>/, "$1"));
   addIdentity(account, messages[2].recipients.replace(/.*<(.*)>/, "$1"));
   Gloda._initMyIdentities();
 
   // Wait for Gloda to re-index the added messages.
   await new Promise(resolve => {
     let waiting = false;
     GlodaIndexer.addListener(function indexListener(status) {
@@ -162,16 +178,40 @@ add_task(async function() {
       await subtest({ recipients: keyword }, 8);
       await subtest({ fullText: keyword }, 8);
       await subtest({ body: keyword }, 8);
 
       // From Me and To Me. These use the identities added to account.
       await subtest({ fromMe: true }, 6);
       await subtest({ toMe: true }, 3);
 
+      // Tags query.
+      await subtest({ tags: { mode: "any", tags: { notATag: true } } });
+      await subtest({ tags: { mode: "any", tags: { $label2: true } } }, 3, 4);
+      await subtest(
+        { tags: { mode: "any", tags: { $label3: true } } },
+        4,
+        5,
+        6
+      );
+      await subtest(
+        { tags: { mode: "any", tags: { $label2: true, $label3: true } } },
+        3,
+        4,
+        5,
+        6
+      );
+      await subtest({
+        tags: { mode: "all", tags: { $label1: true, $label2: true } },
+      });
+      await subtest(
+        { tags: { mode: "all", tags: { $label2: true, $label3: true } } },
+        4
+      );
+
       browser.test.notifyPass("finished");
     },
     manifest: { permissions: ["accountsRead", "messagesRead"] },
   });
 
   await extension.startup();
   extension.sendMessage(account.key);
   await extension.awaitFinish("finished");