google-apps-scriptgmail-apigoogle-workspace

How can I apply a filter to existing emails in Gmail Workspace Add-on


I am working on a Google Workspace Add-on for Gmail. The add-on is being used to help create pre-defined filters given existing messages. I am using the users.settings.filters api to create the filters.

Natively, in Gmail, when you create a filter there is an options to "Apply filter to X existing messages" but this functionality does not appear to be provided through the API.

The problem I am facing is the fact that in order to recreate this functionality myself, the execution gets timed out by App Script for a large number of messages due to the 30sec constraint.

Currently, I am using methods like GmailApp.moveThreadsToInbox and label.addToThreads. Is there a better method?

If there is not a better method, how can I improve the implementation? I have considered creating timed triggers to process large quantities but the documentation surrounding this is unclear on limitation and quotas so I would appreciate clarification there as well.

Here is a code snippet of my current implementation:

function applyActionToThreads(threads, action) {
  // Add/Remove labels to emails
  // NOTE: Add/Remove array order matters to keep this functional.
  //     i.e remove = ["INBOX", "SPAM"] will move to archive then inbox again
  //     rather than ["SPAM', "INBOX"] will move to inbox then archive.
  const existingLabels = Gmail.Users.Labels.list("me").labels;

  const actionLabels = [];
  actionLabels.push(
    ...action.addLabelIds.map((lbl) => {
      return { id: lbl, remove: false };
    })
  );
  actionLabels.push(
    ...action.removeLabelIds.map((lbl) => {
      return { id: lbl, remove: true };
    })
  );

  for (const label of actionLabels) {
    switch (label.id) {
      case "INBOX":
        if (label.remove) {
          GmailApp.moveThreadsToArchive(threads);
        } else {
          GmailApp.moveThreadsToInbox(threads);
        }
        break;
      case "SPAM":
        if (label.remove) {
          GmailApp.moveThreadsToInbox(threads);
        } else {
          GmailApp.moveThreadsToSpam(threads);
        }
        break;
      case "TRASH":
        if (label.remove) {
          GmailApp.moveThreadsToInbox(threads);
        } else {
          GmailApp.moveThreadsToTrash(threads);
        }
        break;
      case "UNREAD":
        if (label.remove) {
          GmailApp.markThreadsRead(threads);
        } else {
          GmailApp.markThreadsUnread(threads);
        }
        break;
      default:
        // Not a system label
        console.log(label);
        console.log(existingLabels);
        const userLabel = GmailApp.getUserLabelByName(
          existingLabels.find((l) => l.id == label.id).name
        );
        for (let i = 0; i < threads.length; i += 99) {
          if (label.remove) {
            userLabel.removeFromThreads(threads.slice(i, i + 100));
          } else {
            userLabel.addToThreads(threads.slice(i, i + 100));
          }
        }
    }
  }
}

Solution

  • I overlooked users.messages.batchModify in docs.

    // Apply actions to each batch
        Gmail.Users.Messages.batchModify(
          {
            ids: batch.map((message) => message.id),
            addLabelIds: filter.action.addLabelIds,
            removeLabelIds: filter.action.removeLabelIds,
          },
          "me"
        );
      }