Fixing Gmail labels to attach to messages, not threads

Posted 13th February 2023
Except for the code below, nothing on this page is AI-assisted

The problem

Gmail labels attach to messages, not threads. This means that if you:

then the reply and any further messages are not given that label. This is an awful design decision by Google that is completely hidden in Gmail's web UI, unless you’re one of the very few people who turn conversation view off.

As a result, if you use IMAP or API-based products like MailStore to back up your email, many messages are not properly tagged into folders — which, depending on your configuration, may mean they are missed from the backup entirely. Emails in Google Takeout backups will not be properly labelled.

The solution

Below is a Google Apps Script function that will:

To use it, copy the below into a Google Apps Script, and after testing, set it to run on a ten-minute trigger so it can work through your email history. Runtimes and logs will let you know when it’s done. By default, debug mode is on, so it will log its intentions but won’t make any actual changes. It’s also set to only do 5 emails at a time. Those are to stop people who don’t read from blowing up their entire email archive accidentally. The settings are easy to change, and if you can’t figure out how to change them, you shouldn’t be running this script.

Important warranty stuff

This worked for me, but I offer no warranty for it. I strongly advise you to test it on a disposable account, to use the debug mode, and to restrict it to just accessing a few emails at first. Please do not contact me for support or feature requests — I can’t help and I’m not intending to turn this into anything bigger. You may also need to modify it to fit your own filing system.

Copyright

I wrote an English description of the task; ChatGPT translated that to Google Apps Script; I then made several modifications myself, because it was quicker than asking. Here’s the full record of the conversation.

I am unsure of the copyright status of this. As far as I’m legally able, I license the code below as CC-0. But it was created in collaboration with ChatGPT by OpenAI, who say they “will not claim copyright”. I am not a copyright lawyer, this is uncharted territory, use at your own risk.

Known issues

Gmail’s search sometimes ignores -in:draft and -in:inbox. No idea why. So if you’re writing a draft, and this happens to run in the background at the same time, that draft may get tagged as X‑NO‑LABEL while you’re working on it. The easiest way around this is to reduce the trigger time to hourly or daily once the script has worked through the backlog.

The code

function fixGmailConversationLabels() {
  const debugMode = true;
  const messageLimit = 5;

  // creates the labels if they don't exist.
  if(!GmailApp.getUserLabelByName("X-NO-LABEL")){
    GmailApp.createLabel("X-NO-LABEL");
  }
  if(!GmailApp.getUserLabelByName("X-LABEL-CONFLICT")){
    GmailApp.createLabel("X-LABEL-CONFLICT");
  }

  var threads = GmailApp.search('has:nouserlabels -in:inbox -in:draft -label:X-NO-LABEL -label:X-LABEL-CONFLICT',0, messageLimit);
  for (var i = 0; i < threads.length; i++) {
    var thread = threads[i];
    var labels = thread.getLabels();
    var labelToApply = '';
    if (labels.length === 0) {
      labelToApply = 'X-NO-LABEL';
    } else if (labels.length === 1) {
      labelToApply = labels[0].getName();
    } else {
      labelToApply = 'X-LABEL-CONFLICT';
    }
    if (labelToApply) {
      Logger.log("Thread: " + thread.getFirstMessageSubject() + "\nLabel: " + labelToApply + "\nMessage count: " + thread.getMessageCount() + "\nDebug mode: " + debugMode);
      if (!debugMode) {
        thread.addLabel(GmailApp.getUserLabelByName(labelToApply));
      }
    }
  }
}

PS: this wasn’t just a one-off fluke. It was even better at working with node.js and the now-deprecated Twitter API. Almost right first time, and I wouldn’t have used anything from ES6.