feat(gmail): re-fetch failed documents by id (Resolver)
Description
GmailConnector now implements Resolver.reindex. Targeted reindex can re-fetch failed Gmail threads by document id.
Document types
- Covered: email threads. Gmail emits only this type. The document id is the Gmail thread id, the same id the crawl yields.
- Not covered: none.
How reindex finds the mailbox
A Gmail thread id is unique only inside one mailbox. The id does not name the mailbox. The crawl reads every Workspace user's mailbox through service-account impersonation (or one mailbox with OAuth).
- Crawl failures now link to their mailbox:
https://mail.google.com/mail/u/<email>/#all/<thread id>. Reindex reads the mailbox from this link and fetches there first. This changes only theConnectorFailurelink, not the document's section links. - Ids without that link (admin-requested targets, older failures) are looked up in every mailbox the crawl reads, the primary admin's first. Reindex stops when all ids resolve.
- Each mailbox lookup uses batched
threads.getcalls (50 per batch). 403/404 means "not in this mailbox". Rate limits and 5xx errors are retried, and become per-thread failures if they persist. They never count as "not found". - A mailbox that cannot be impersonated (suspended user) is skipped. If the primary admin's mailbox fails, the error is raised, because the credential is bad. Missing scopes raise
PermissionError, as in the crawl.
Failures
- An id that is not a hex thread id yields a
ConnectorFailurewith no API call. - An id found in no mailbox yields a
ConnectorFailure. - Entity failures are skipped, as in Confluence.
Filter parity with the crawl
The crawl applies these filters. Reindex applies each one:
- Mailbox scope: the crawl reads only the users from
_get_all_user_emails. Reindex searches only those mailboxes. A mailbox link that names another user is ignored. threads.listwithoutincludeSpamTrash: the crawl never lists a thread whose messages are all in Spam or Trash. Reindex readslabelIdsand yields aConnectorFailurefor such a thread. It then removes the labels, so the document text matches the crawl's.- Mailbox not provisioned (400
failedPrecondition): the crawl skips the mailbox. Reindex treats it as "not in this mailbox". - Thread with no messages: the crawl yields nothing. Reindex yields a
ConnectorFailure. - The time-range query is not an access filter, so reindex does not apply it.
- The connector has no other config (only
batch_size).
Permissions
The crawl always sets external_access to the mailbox owner's email (thread_to_document). Reindex uses the same function, so include_permissions changes nothing. Perm sync (ee/.../gmail/doc_sync.py) gives the same single-owner access.
How Has This Been Tested?
Unit: uv run pytest -v backend/tests/unit/onyx/connectors/gmail/ gives 10 passed. Daily: uv run --env-file .vscode/.env pytest -v backend/tests/daily/connectors/gmail/ gave 10 passed before the rebase onto main (8 resolver, 2 existing crawl tests).
- New
backend/tests/daily/connectors/gmail/test_gmail_resolver.py(service account, real test workspace): one thread from a non-admin mailbox, all four threads across four mailboxes, mailbox link hint, bogus but well-formed id, unparseable id, empty input, entity-only input, andinclude_permissions=Truewithexternal_accesschecked per owner. - New unit tests in
backend/tests/unit/onyx/connectors/gmail/test_connector.py: a thread with all messages in Spam/Trash yields a failure; a listed thread yields a document without label text; OAuth path with a mocked Gmail client: the Admin SDK returns 403, so reindex reads only the admin mailbox, and a thread from another mailbox yields a failure. - OAuth is not tested live. CI keeps no Gmail OAuth credential, because a token that needs refreshing can make CI fail from time to time. The unit test above covers the OAuth path.
Additional Options
- [Optional] Please cherry-pick this PR to the latest release version.
- [Optional] Override Linear Check
🤖 Generated with Claude Code
Summary by cubic
Implements Resolver.reindex on GmailConnector so documents that failed during a crawl can be re-fetched by Gmail thread id.
- Gmail thread ids don't name their mailbox: reindex reads the mailbox from the crawl failure link when available, otherwise searches every mailbox the crawl reads with batched
threads.getcalls (50 per batch), admin first, stopping when all ids resolve. - Rate limits and 5xx errors are retried at the request and whole-batch level; persistent ones become per-thread failures and never count as "not found".
- Reindex applies the crawl's filters: threads in Spam/Trash, empty threads, and threads in no crawled mailbox yield
ConnectorFailures; invalid ids fail without an API call.
Summary by CodeRabbit
- Improvements
- Gmail reindexing retrieves threads in batches for faster, more reliable processing.
- Temporary rate-limit and server errors are retried automatically.
- Reindexing selects the appropriate mailbox and excludes Spam- and Trash-only threads to match crawl results.
- Mailbox-specific links and permissions are preserved in failure details.
- Invalid, inaccessible, unavailable, or unresolved threads now produce item-level failures without blocking other items.
- OAuth reindexing is restricted to the administrator mailbox.