Reading Gmail¶
Gmail is the single most common reason to reach for Ghostvault — once an account is signed in, you want to ask "what's in my inbox?" and get a usable answer. This guide covers the convenience tool, the robust fallback for when Gmail changes its DOM, and the SDK pattern.
Why this matters¶
Gmail is an aggressively dynamic SPA. The markup Google ships changes frequently — often month to month — and there is no stable public API for reading the inbox from a logged-in browser session. Ghostvault handles this two ways:
gv_read_gmail— a convenience tool that parses the Gmail DOM for you and returns a tidy list of{id, subject, sender, snippet, url, date}. Fast and clean when it works.- The fallback pattern — when the DOM shifts and the parser returns empty (or garbage), drop down to
gv_open_url+gv_get_page_content(include_screenshot=True)and let the LLM read the page directly, using vision if needed.
You should expect to need both. Treat gv_read_gmail as the happy path and the screenshot fallback as your safety net.
Prerequisites¶
You need a managed account that is already signed in. If you haven't done that yet, see Getting Started → First sign-in.
1. "Create an account called Work (e.g. provider='google')" → gv_create_account
2. "Sign in to Work" → gv_sign_in (complete login + 2FA)
3. "Read my latest emails" → gv_read_gmail ← this guide
The happy path: gv_read_gmail¶
The convenience tool takes a single optional limit (default 20, max 50) and returns recent messages:
Each message looks like:
{
"id": "18f...",
"subject": "Your weekly summary",
"sender": "noreply@example.com",
"snippet": "Here's what happened this week...",
"url": "https://mail.google.com/mail/u/0/#inbox/18f...",
"date": "2024-06-12T09:14:00Z"
}
This is all most tasks need. Hand it to the LLM and ask your question:
"Read my latest 10 emails and tell me which ones need a reply today."
The agent calls gv_read_gmail(limit=10), gets the list, and reasons over it.
The fallback: when gv_read_gmail returns empty¶
Gmail's DOM changes often. When the convenience parser can't find what it expects, it returns an empty list rather than garbage. The fix is to drive the page yourself:
# 1. Make sure the active account is the one you want
await gv.switch_account("acc_work123")
# 2. Open Gmail directly
await gv.open_url("https://mail.google.com")
# 3. Read the page text AND grab a screenshot so vision can help
result = await gv.get_page_content(include_screenshot=True, max_chars=20000)
The include_screenshot=True flag is important. Gmail renders a lot of its state visually (avatars, badges, the unread count, the promo/social tabs) that isn't reliably present in the text dump. With the screenshot attached, the LLM can actually see the inbox layout and report on it.
Gmail scraping is best-effort
There is no contract here. Google can ship a new Gmail version at any time and break both the convenience parser and the text extraction. The screenshot path is the most resilient because it relies on vision, not selectors. If you depend on Gmail reads in a script, always implement the fallback and treat an empty gv_read_gmail result as a signal to try it.
A robust two-step helper¶
Here's a pattern that tries the fast path first and falls back automatically:
import json
async def read_inbox(gv, limit=20):
"""Try the convenience tool, fall back to direct page reading."""
# Fast path
result = await gv.read_gmail(limit=limit)
messages = _parse_messages(result)
if messages:
return messages, "gv_read_gmail"
# Fallback: open Gmail + read with screenshot
await gv.open_url("https://mail.google.com")
await gv.get_page_content(include_screenshot=True, max_chars=20000)
return None, "screenshot_fallback" # caller lets the LLM read the screenshot
def _parse_messages(result):
"""Extract the messages list from a CallToolResult."""
if hasattr(result, "content"):
for block in result.content:
if getattr(block, "type", None) == "text":
try:
return json.loads(block.text).get("messages", [])
except json.JSONDecodeError:
pass
return []
Opening a specific message¶
gv_read_gmail returns a url per message (a deep link into the Gmail SPA). To read the body of one message:
# `url` came from a gv_read_gmail result
await gv.open_url("https://mail.google.com/mail/u/0/#inbox/18f...")
await gv.get_page_content(include_screenshot=True, max_chars=20000)
Again, grab the screenshot. Email bodies are where vision earns its keep — HTML email, attachments, and quoted replies all render visually.
A realistic conversation flow¶
Here's what a typical Gmail session looks like end to end, as you'd talk to it through Claude Desktop / Cursor / ZCode:
You: "Read my latest 5 emails on Work and tell me if any need an urgent reply."
Agent: calls gv_switch_account("acc_work123")
Agent: calls gv_read_gmail(limit=5)
Agent: "You have 5 messages. Two look urgent:
- 'Invoice overdue — Acme' from billing@acme.com
- 'Action required: password reset' from security@github.com
The rest are newsletters. Want me to open either?"
You: "Open the invoice one."
Agent: calls gv_open_url("https://mail.google.com/mail/u/0/#inbox/18f...")
Agent: calls gv_get_page_content(include_screenshot=True)
Agent: "The invoice is $4,200, 12 days overdue. There's a Pay Now button.
Want me to click it, or should I draft a reply to billing@acme.com?"
You: "Draft a reply asking for a 7-day extension."
Agent: (drafts text, asks for confirmation before sending)
Note the pattern: read → summarize → drill in on demand. You almost never want to open every message up front — let the agent triage from the inbox list first.
Via the SDK¶
For scripts with no LLM in the loop, the SDK call is the same tool. Here's a minimal "print my unread senders" script:
import asyncio, json
from ghostvault_sdk import GhostvaultClient
async def main():
async with GhostvaultClient() as gv:
# Assumes gv_sign_in was already completed and the session persists.
result = await gv.read_gmail(limit=20)
for block in result.content:
if getattr(block, "type", None) == "text":
data = json.loads(block.text)
for msg in data.get("messages", []):
print(f"{msg['sender']:40} {msg['subject']}")
break
asyncio.run(main())
One context per account
Ghostvault refuses to open two browser windows for the same account (it would corrupt the profile). If you're reading Gmail on Work, don't try to also open Work in another call — switch accounts instead. See Tool reference → Lifecycle.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
gv_read_gmail returns [] |
Gmail shipped a DOM change, or the session landed on a non-inbox view | Fall back to gv_open_url("https://mail.google.com") + gv_get_page_content(include_screenshot=True) |
"Session not authenticated" |
The Google session expired | Re-run gv_sign_in for that account |
| Inbox opens but is empty | You're on the wrong account, or Gmail is showing a promo tab | Check gv_get_session_status; switch tabs via gv_click_element on "Primary" |
| Reading returns login page | Profile lost its cookies | Re-run gv_sign_in; consider encryption at rest if the profile was on a moved disk |
Related guides¶
- Anonymous scraping — same
gv_get_page_contentpattern, no account needed - Human-like typing & clicks — if you need to reply to mail, type the response human-like
- Private/public profiles — hide sensitive inboxes behind a password