Guide

Gmail API: List Spam and Trash Messages

Your script lists every message in the mailbox, yet the phishing sample you're hunting never shows up. That's not a bug in your code. The Gmail API excludes SPAM and TRASH from messages.list results unless you ask for them explicitly. This guide covers the three documented ways to read spam and trash: the includeSpamTrash parameter, the labelIds filter, and in:spam search queries, plus the one-flag CLI equivalent.

Written by Aaron de Mello Senior Engineering Manager

VerifiedCLI 3.1.16 · Gmail · last tested June 6, 2026

Command references used in this guide: nylas email list and nylas email folders list.

Why doesn't the Gmail API return spam or trash messages?

The Gmail API excludes the SPAM and TRASH system labels from users.messages.list responses by default. The includeSpamTrash query parameter controls this behavior, defaults to false, and per the messages.list reference will "include messages from SPAM and TRASH in the results" when set to true.

The default makes sense for inbox-style apps, but it breaks three real workloads: abuse and phishing analysis, full-mailbox backup, and false-positive recovery scripts. Each messages.list call costs 5 quota units and returns 100 message IDs by default (500 with maxResults), so the fix is a parameter change, not extra requests.

How do you include spam and trash with includeSpamTrash?

Setting includeSpamTrash=true on messages.list returns spam and trash messages alongside everything else. It widens the result set rather than filtering it, so use it when you need a complete mailbox sweep. Combined with maxResults=500, one request covers 5x the default page size.

The Python example below uses google-api-python-client and counts how many of the first 500 results carry the SPAM or TRASH label. Note that each follow-up messages.get costs 20 quota units, 4x the list call itself.

from googleapiclient.discovery import build

service = build("gmail", "v1", credentials=creds)

# Include spam and trash in a full-mailbox listing
results = service.users().messages().list(
    userId="me",
    includeSpamTrash=True,
    maxResults=500,
).execute()

ids = [m["id"] for m in results.get("messages", [])]
print(f"{len(ids)} messages, spam and trash included")

# Check which labels a specific message carries
msg = service.users().messages().get(
    userId="me", id=ids[0], format="minimal"
).execute()
print(msg["labelIds"])  # e.g. ['SPAM'] or ['TRASH']

Without the parameter, the same call silently drops any message labeled SPAM or TRASH. There's no warning and no count of what was excluded, which is why backup scripts that skip this flag under-report mailbox size and miss recoverable mail.

How do you list only spam messages with labelIds?

Passing labelIds=["SPAM"] to messages.list returns spam messages only, no includeSpamTrash needed. The API reference states the filter returns messages "with labels that match all of the specified label IDs", so a single label ID gives you exactly that folder. Use ["TRASH"] the same way for deleted mail.

The third option is the q parameter, which per the same reference "supports the same query format as the Gmail search box". That makes q="in:spam" equivalent to the labelIds filter, and it composes with other operators like from: and after: in one string.

# Spam only, via labelIds
spam = service.users().messages().list(
    userId="me", labelIds=["SPAM"], maxResults=100
).execute()

# Trash only
trash = service.users().messages().list(
    userId="me", labelIds=["TRASH"], maxResults=100
).execute()

# Search-style: spam from one sender in the last week
recent = service.users().messages().list(
    userId="me", q="in:spam from:billing@suspicious.example newer_than:7d"
).execute()

Pick by intent: labelIds for a clean single-folder read, q when you need to combine spam scope with sender, date, or attachment conditions. Both approaches cost the same 5 quota units per list call. SPAM and TRASH are 2 of Gmail's 13 built-in system labels, listed in the Gmail labels guide.

How long does Gmail keep trash messages?

Gmail keeps a deleted message in Trash for up to 30 days. According to Google's recovery documentation, "Up to 30 days after deletion: You can find the message in Trash" and after 30 days "The message is permanently deleted." Any recovery script you build against the TRASH label is racing that clock.

That window shapes how you schedule automation. A weekly cron job that exports trash to JSON has 4 chances to catch a message before Gmail purges it; a monthly one can miss messages entirely. For spam triage, the practical pattern is the same: list the SPAM label on a schedule shorter than your team's incident-response window, and persist anything you might need as evidence.

How do you read spam and trash from the CLI?

The nylas email list --folder SPAM command returns Gmail spam messages as JSON with no Google Cloud project, OAuth consent screen, or Python client setup. The CLI's --folder flag accepts any Gmail system label ID, so TRASH works identically, and --all-folders sweeps every folder in one run. Auth takes one nylas auth login.

# List spam messages
nylas email list --folder SPAM --limit 20

# List trash as JSON for scripting
nylas email list --folder TRASH --json --limit 50

# Sweep every folder, spam and trash included
nylas email list --all-folders --json --limit 200

# Extract sender addresses from spam for a blocklist review
nylas email list --folder SPAM --json --limit 50 | \
  jq -r '.[].from[0].email' | sort | uniq -c | sort -rn

These commands were verified against a live Gmail account on CLI 3.1.16. The same --folder syntax works on Outlook and IMAP accounts with their folder names, so a spam-review script written for Gmail ports to other providers without touching the Gmail API's label model. Run nylas email folders list to see the exact folder IDs on any connected account.

Next steps