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:
- send a message
- label that message (which appears to label the thread)
- get a reply
- then select
Archive
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:
- Search for archived email threads (up to 500 per run) which contain at least one message with no user labels attached
- Look for other labels in the thread. If it finds one label, it will add that label to all emails in the thread
- If there is no label anywhere in the thread, it will add an
X‑NO‑LABEL
label to all emails in the thread, for manual review - If it finds multiple labels, it will add an
X‑LABEL‑CONFLICT
label to all emails in the thread, for manual review
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.