Gmail Archive Labeler - Bridging Gmail and Desktop Mail Clients

Bring back the "Archive" Functionality to Google Mail

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:

  1. The INBOX label is removed
  2. No other label is added
  3. 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:

  1. Read email in Gmail (web or mobile)
  2. Archive messages they've dealt with
  3. 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

  1. Go to script.google.com
  2. Click New project
  3. 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:

  1. In the Apps Script editor, click the + next to Services in the left sidebar
  2. Scroll to Gmail API (or search for it)
  3. 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

OperatorPurpose
-in:inboxOnly messages that have been archived (INBOX label removed)
-in:sentExclude sent messages (they're never in inbox either)
-in:draftsExclude drafts
-label:_archived_processedSkip messages we already handled
newer_than:1dPerformance optimization - limits search to last 24 hours
after:2026/03/01Prevents labeling historical archived messages
-in:trash -in:spamExclude deleted and spam messages

The combination of newer_than:1d and after:2026/03/01 is intentional:

  • newer_than:1d keeps each search fast by limiting scope to recent messages
  • after:2026/03/01 acts 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

  1. In the Apps Script editor, click the clock icon (Triggers) in the left sidebar, or go to Edit > Current project's triggers
  2. Click + Add Trigger
  3. Configure:
  • Function: labelArchived
  • Deployment: Head
  • Event source: Time-driven
  • Type: Minutes timer
  • Interval: Every 5 minutes (or every 1 minute for near-instant labeling)
  1. Click Save
  2. 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.

  1. Open Gmail Settings (gear icon > See all settings > Labels tab)
  2. Scroll down to find _archived_processed
  3. Uncheck "Show in IMAP"

This prevents Apple Mail and other IMAP clients from showing _archived_processed as a folder.

Important: If the _archived_processed label 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:

  1. The "Archivieren" folder appears automatically under your Gmail account
  2. Messages archived in Gmail will appear here within minutes
  3. Select messages and move them to your local archive or another IMAP server
  4. The _archived_processed tracking label ensures they won't be re-labeled

In Thunderbird or other IMAP clients:

  1. Subscribe to the "Archivieren" folder if it doesn't appear automatically
  2. Right-click the account > Subscribe > check "Archivieren"

Key Technical Insights

GmailApp vs. Gmail API

Google Apps Script provides two ways to interact with Gmail:

FeatureGmailAppGmail API (Advanced Service)
GranularityThread-levelMessage-level
SetupBuilt-in, no activation neededMust enable in Services
Label operationsaddToThread() / removeFromThread()Messages.modify() with label IDs
SearchReturns GmailThread[]Returns {messages: [{id}]}
Label creationSimple 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:

  1. labelListVisibility (API) - Show/hide in Gmail sidebar
  2. messageListVisibility (API) - Show/hide in message list columns
  3. "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:

  1. Thread has messages A, B, C
  2. You archive the thread - script labels A, B, C with "Archivieren"
  3. Mail client moves A, B, C to local archive and removes them from Gmail
  4. Person replies - message D arrives in the thread
  5. You archive message D
  6. Script labels the entire thread - A, B, C, D get "Archivieren"
  7. A, B, C reappear in the "Archivieren" folder
  8. 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

ProblemCauseFix
Tracking label visible in mail client"Show in IMAP" is checkedGmail Settings > Labels > uncheck "Show in IMAP" for _archived_processed
Old messages getting labeledafter: date too early or missingUpdate the after: date in the search query
Duplicate messages in archiveUsing GmailApp (thread-level)Switch to Gmail.Users.Messages API (per-message)
Script errors on first runGmail API not enabledAdd Gmail API via Services > + in Apps Script editor
Messages not appearing in mail client"Archivieren" label not IMAP-visibleGmail Settings > Labels > check "Show in IMAP" for Archivieren
Script stopped runningTrigger removed or authorization revokedRe-create trigger and re-authorize

Limitations

  • The after: date is hardcoded - adjust it to your deployment date
  • maxResults: 100 processes at most 100 messages per run; for high-volume accounts, add pagination using response.nextPageToken
  • If _archived_processed is 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:

  1. Change "Archivieren" to whatever label name you prefer (e.g., "Archived", "To Archive", "Move to Archive")
  2. Update the after: date to your deployment date
  3. Adjust maxResults if you archive more than 100 messages per day
  4. 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.