Gmail Archive Labeler - Bridging Gmail and Desktop Mail Clients
Gmail's Archive button is deceptively simple. Press it, and your message disappears from the inbox. But unlike every other email system, Gmail doesn't move the message to an "Archive" folder - it simply removes the INBOX label. The message stays in "All Mail," invisible to any mail client that relies on IMAP folder structure to identify archived messages.
If you use Apple Mail, Thunderbird, or any desktop client alongside Gmail, this creates a real problem: there is no IMAP folder representing "archived messages." Your carefully curated archive workflow breaks down at the protocol level.
This article presents a complete solution using Google Apps Script that automatically bridges this gap - making archived Gmail messages visible to desktop mail clients through a dedicated IMAP-visible label.
The Problem in Detail
Gmail's Labeling Model vs. IMAP Folders
Gmail uses labels, not folders. A message can have multiple labels simultaneously. IMAP clients see each label as a folder, and messages appear in every "folder" whose label they carry.
When you archive a message in Gmail:
- The
INBOXlabel is removed - No other label is added
- The message remains in "All Mail"
From an IMAP client's perspective, the message simply vanishes from the Inbox and appears nowhere else. There is no "Archive" folder to subscribe to, no IMAP event to trigger on.
The Desktop Client Workflow
Many users follow this workflow:
- Read email in Gmail (web or mobile)
- Archive messages they've dealt with
- Periodically move archived messages from Gmail to a local archive or different IMAP server using a desktop mail client
Step 3 is impossible without a visible marker. The desktop client has no way to ask Gmail "which messages were recently archived?"
The Thread Problem
Gmail groups messages into conversations (threads). Most Gmail scripting APIs operate at the thread level - when you label a thread, every message in that thread gets the label. This causes a subtle but painful issue:
- You archive a thread containing 5 messages
- The script labels all 5 messages
- Your mail client moves all 5 to your archive
- A new reply arrives in the same thread
- Gmail removes the INBOX label when you archive it
- The script labels the entire thread again - including the 4 messages your client already archived
- Your archive now has duplicates
The Solution
A Google Apps Script that:
- Runs automatically on a timer trigger (every 1-5 minutes)
- Detects individual messages (not threads) that were archived
- Adds a visible "Archivieren" label that appears as an IMAP folder
- Tracks processed messages with a hidden label to prevent re-labeling
- Filters by date to avoid touching historical mail
Architecture Overview
Gmail Archive Action
│
▼
Message loses INBOX label
│
▼
Apps Script trigger fires (every few minutes)
│
▼
Query: messages without INBOX, without tracking label,
not in sent/drafts/trash/spam, recent
│
▼
For each matching message:
├── Add "Archivieren" label (IMAP-visible)
└── Add "_archived_processed" label (IMAP-hidden)
│
▼
Desktop mail client sees "Archivieren" IMAP folder
│
▼
Client moves message to local archive
(tracking label prevents re-labeling)
Step-by-Step Implementation
Step 1: Create the Apps Script Project
- Go to script.google.com
- Click New project
- Name it "Gmail Archive Labeler" (click "Untitled project" at the top)
Step 2: Enable the Gmail API Advanced Service
The standard GmailApp service only operates at the thread level. To label individual messages, you need the Gmail API advanced service:
- In the Apps Script editor, click the + next to Services in the left sidebar
- Scroll to Gmail API (or search for it)
- Click Add
This gives you access to Gmail.Users.Messages.* methods that operate on individual messages.
Step 3: The Complete Code
Replace the contents of Code.gs with:
function labelArchived() {
ensureLabel_("Archivieren");
ensureHiddenLabel_("_archived_processed");
var archiveLabelId = getLabelId_("Archivieren");
var processedLabelId = getLabelId_("_archived_processed");
var response = Gmail.Users.Messages.list("me", {
q: "-in:inbox -in:sent -in:drafts -label:_archived_processed newer_than:1d after:2026/03/01 -in:trash -in:spam",
maxResults: 100
});
var messages = response.messages || [];
for (var i = 0; i < messages.length; i++) {
Gmail.Users.Messages.modify({
addLabelIds: [archiveLabelId, processedLabelId],
removeLabelIds: []
}, "me", messages[i].id);
}
}
function getLabelId_(name) {
var labels = Gmail.Users.Labels.list("me").labels;
for (var i = 0; i < labels.length; i++) {
if (labels[i].name === name) return labels[i].id;
}
return null;
}
function ensureLabel_(name) {
if (!GmailApp.getUserLabelByName(name)) {
GmailApp.createLabel(name);
}
}
function ensureHiddenLabel_(name) {
if (!getLabelId_(name)) {
Gmail.Users.Labels.create({
name: name,
labelListVisibility: "labelHide",
messageListVisibility: "hide"
}, "me");
}
}
Step 4: Understanding the Code
The Search Query
The heart of the script is the Gmail search query:
-in:inbox -in:sent -in:drafts -label:_archived_processed newer_than:1d after:2026/03/01 -in:trash -in:spam
| Operator | Purpose |
|---|---|
-in:inbox | Only messages that have been archived (INBOX label removed) |
-in:sent | Exclude sent messages (they're never in inbox either) |
-in:drafts | Exclude drafts |
-label:_archived_processed | Skip messages we already handled |
newer_than:1d | Performance optimization - limits search to last 24 hours |
after:2026/03/01 | Prevents labeling historical archived messages |
-in:trash -in:spam | Exclude deleted and spam messages |
The combination of newer_than:1d and after:2026/03/01 is intentional:
newer_than:1dkeeps each search fast by limiting scope to recent messagesafter:2026/03/01acts as a hard cutoff so that if the script were paused and resumed, it wouldn't reach back into old mail
Adjust the after: date to your deployment date.
ensureHiddenLabel_() - The Tracking Label
The _archived_processed label is created with two visibility settings:
Gmail.Users.Labels.create({
name: name,
labelListVisibility: "labelHide", // Hidden in Gmail sidebar
messageListVisibility: "hide" // Hidden in message list
}, "me");
These settings hide the label from Gmail's web interface only. To hide it from IMAP clients as well, you need a separate manual step (see Step 6).
Gmail.Users.Messages.modify() - Per-Message Labeling
This is why we need the Gmail API advanced service. The standard GmailApp.addToThread() would label all messages in a conversation. Gmail.Users.Messages.modify() targets a single message by its ID, preventing the duplicate problem described earlier.
Step 5: Set Up the Time Trigger
- In the Apps Script editor, click the clock icon (Triggers) in the left sidebar, or go to Edit > Current project's triggers
- Click + Add Trigger
- Configure:
- Function:
labelArchived - Deployment: Head
- Event source: Time-driven
- Type: Minutes timer
- Interval: Every 5 minutes (or every 1 minute for near-instant labeling)
- Click Save
- Authorize the script when prompted (it needs Gmail access)
Step 6: Hide the Tracking Label from IMAP
This is a critical step that cannot be done via the API. The labelListVisibility and messageListVisibility properties only control Gmail's web UI. IMAP visibility is a separate setting.
- Open Gmail Settings (gear icon > See all settings > Labels tab)
- Scroll down to find
_archived_processed - Uncheck "Show in IMAP"
This prevents Apple Mail and other IMAP clients from showing _archived_processed as a folder.
Important: If the
_archived_processedlabel is ever deleted and recreated by the script, you will need to uncheck "Show in IMAP" again manually. The Gmail API does not expose IMAP visibility as a property.
Step 7: Configure Your Mail Client
The "Archivieren" label now appears as an IMAP folder in your desktop mail client.
In Apple Mail:
- The "Archivieren" folder appears automatically under your Gmail account
- Messages archived in Gmail will appear here within minutes
- Select messages and move them to your local archive or another IMAP server
- The
_archived_processedtracking label ensures they won't be re-labeled
In Thunderbird or other IMAP clients:
- Subscribe to the "Archivieren" folder if it doesn't appear automatically
- Right-click the account > Subscribe > check "Archivieren"
Key Technical Insights
GmailApp vs. Gmail API
Google Apps Script provides two ways to interact with Gmail:
| Feature | GmailApp | Gmail API (Advanced Service) |
|---|---|---|
| Granularity | Thread-level | Message-level |
| Setup | Built-in, no activation needed | Must enable in Services |
| Label operations | addToThread() / removeFromThread() | Messages.modify() with label IDs |
| Search | Returns GmailThread[] | Returns {messages: [{id}]} |
| Label creation | Simple createLabel() | Full control over visibility settings |
For this use case, the Gmail API is essential because thread-level labeling causes duplicates.
Gmail Label Visibility - Three Layers
Gmail labels have three independent visibility controls:
labelListVisibility(API) - Show/hide in Gmail sidebarmessageListVisibility(API) - Show/hide in message list columns- "Show in IMAP" (Gmail Settings only) - Show/hide as IMAP folder
The first two are set via the API when creating the label. The third can only be changed in Gmail Settings and is the one that matters for desktop mail clients.
Why Per-Message Labeling Matters
Consider this scenario with thread-level labeling:
- Thread has messages A, B, C
- You archive the thread - script labels A, B, C with "Archivieren"
- Mail client moves A, B, C to local archive and removes them from Gmail
- Person replies - message D arrives in the thread
- You archive message D
- Script labels the entire thread - A, B, C, D get "Archivieren"
- A, B, C reappear in the "Archivieren" folder
- Mail client moves A, B, C again - duplicates in your archive
With per-message labeling and the _archived_processed tracking label, only message D gets labeled in step 6. Messages A, B, C already have _archived_processed and are skipped by the search query.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Tracking label visible in mail client | "Show in IMAP" is checked | Gmail Settings > Labels > uncheck "Show in IMAP" for _archived_processed |
| Old messages getting labeled | after: date too early or missing | Update the after: date in the search query |
| Duplicate messages in archive | Using GmailApp (thread-level) | Switch to Gmail.Users.Messages API (per-message) |
| Script errors on first run | Gmail API not enabled | Add Gmail API via Services > + in Apps Script editor |
| Messages not appearing in mail client | "Archivieren" label not IMAP-visible | Gmail Settings > Labels > check "Show in IMAP" for Archivieren |
| Script stopped running | Trigger removed or authorization revoked | Re-create trigger and re-authorize |
Limitations
- The
after:date is hardcoded - adjust it to your deployment date maxResults: 100processes at most 100 messages per run; for high-volume accounts, add pagination usingresponse.nextPageToken- If
_archived_processedis deleted, all recent archived messages get re-labeled on the next run - "Show in IMAP" must be set manually after label recreation - the API does not support this property
- The script runs on a timer, so there is a delay of 1-5 minutes between archiving and the label appearing
Adapting for Your Use
To use this script with your own Gmail account:
- Change
"Archivieren"to whatever label name you prefer (e.g., "Archived", "To Archive", "Move to Archive") - Update the
after:date to your deployment date - Adjust
maxResultsif you archive more than 100 messages per day - Modify the trigger interval based on how quickly you need the label to appear
The script is idempotent - running it multiple times has no side effects thanks to the tracking label. It can safely run every minute without any risk of duplicate processing.