How Macro’s Email Service Handles Multi-Account Gmail Integration and Unified Inboxes
Macro's email service connects unlimited Gmail accounts to a single unified inbox by using per-account Link identifiers, Redis-cached OAuth tokens, and a unified search layer that aggregates messages across all linked accounts.
The macro-inc/macro repository implements a Rust-based email service designed for power users who manage multiple Gmail addresses. This article explains how the system authenticates each account separately, ingests messages via Gmail's Pub/Sub API, and presents everything in one chronological feed.
The Link Abstraction: One Identifier Per Gmail Account
Every connected Gmail account in Macro is represented by a Link struct. This abstraction lives throughout the email stack and serves as the primary key for account-specific operations.
In crates/email/src/domain/ports.rs, domain ports expose methods that require a &Link parameter:
// crates/email/src/domain/ports.rs
fn fetch_gmail_access_token(&self, link: &Link) -> impl Future<Output = Result<String>>;
fn enqueue_gmail_ops_modify_labels_batch(&self, link: &Link, ops: Vec<LabelOp>) -> impl Future<Output = Result<()>>;
The Link contains a link-id — the primary key of the email_links table. This design eliminates ambiguity: every token fetch, webhook event, and label modification targets exactly one Gmail account.
Gmail Token Provider: Per-Account OAuth with Redis Caching
The GmailTokenProvider in crates/email/src/outbound/gmail_token_provider.rs handles OAuth token lifecycle management. It provides two entry points:
fetch_gmail_access_token– Returns a cached access token from Redis when availablefetch_gmail_access_token_no_cache– Forces a fresh token refresh using the stored refresh token
// crates/email/src/outbound/gmail_token_provider.rs
let token = self
.fetch_gmail_access_token(&link)
.await?;
The cache key derivation incorporates the link-id, ensuring tokens for different Gmail accounts never collide in Redis. When a cached token expires or is absent, the provider exchanges the refresh token (stored in email_links during initial OAuth consent) for a new access token.
This architecture scales to any number of Gmail accounts per user without complicating the call sites — each operation simply passes the appropriate Link.
Real-Time Ingestion: Gmail Watch and the Forwarder Sidecar
Macro receives Gmail push notifications through a sidecar container rather than direct webhook exposure. The gmail_forwarder configuration appears in tooling/xtask/src/local/gen_compose.rs:
# tooling/xtask/src/local/gen_compose.rs (excerpt)
"gmail-forwarder".to_string(),
"http://email-service:8080/gmail/webhook",
"projects/macro-email-testing/subscriptions/gmail-local-watch-${instance}",
The forwarder subscribes to a GCP Pub/Sub subscription and routes notifications to POST /gmail/webhook. Critically, the webhook payload includes the link_id of the account that triggered the event.
In crates/email/src/inbound/axum/thread_labels_router.rs, the webhook handler extracts this link-id and routes the event to the correct processing pipeline. This decouples the single Pub/Sub subscription from multi-account awareness — the email service learns which account generated each event from the payload itself.
Unified Inbox: Aggregating Across All Linked Accounts
The unified inbox is implemented in the search service rather than the email service directly. In crates/search_service/src/api/search/unified.rs, the sort_unified_search_results function merges entities from multiple indices:
// crates/search_service/src/api/search/unified.rs
let results = sort_unified_search_results(results);
For email specifically, the enrich_emails function groups Gmail message-level search hits into thread-level items. Every thread record stores:
owner_id– the Macro usersource_type– alwaysGMAILfor Gmail-originated threadslink_id– the specific Gmail account
A unified search request with entity_type = EMAIL queries all threads where owner_id matches the requesting user, regardless of link_id. The results are sorted by a common updated_at timestamp, producing a single chronological feed spanning every connected Gmail account.
Account Linking Flow: From OAuth to Searchable Thread
The complete lifecycle of a new Gmail account connection:
-
Link creation – The
google_gmailOIDC identity provider completes OAuth and stores the refresh token inemail_links(seetooling/seed_cli/src/entity/gmail/mod.rsfor the table structure) -
Watch registration – Macro calls Gmail's
watchAPI to subscribe the account to Pub/Sub notifications -
Token retrieval – Any Gmail API operation calls
fetch_gmail_access_token(&link)to obtain account-specific credentials -
Message ingestion – Incoming push notifications arrive via the forwarder sidecar, are routed by
link_id, and stored withprovider = GMAILand the correspondinglink_id -
Unified presentation – Search queries aggregate across all
link_idvalues for the user, presenting one inbox
Key Implementation Files
| File | Responsibility |
|---|---|
crates/email/src/outbound/gmail_token_provider.rs |
OAuth token caching and refresh per Link |
crates/email/src/domain/ports.rs |
Service contract requiring Link for account-scoped operations |
crates/email/src/inbound/axum/thread_labels_router.rs |
Webhook ingestion with link_id routing |
tooling/xtask/src/local/gen_compose.rs |
Sidecar configuration for Gmail Pub/Sub forwarding |
crates/search_service/src/api/search/unified.rs |
Cross-account result aggregation and sorting |
tooling/seed_cli/src/entity/gmail/mod.rs |
email_links table structure and seed data |
Summary
Linkidentifiers decouple multi-account complexity from business logic — every operation receives an explicit account context- Redis-cached tokens in
gmail_token_provider.rsprovide fast, isolated credential access per account - Pub/Sub webhook forwarding through a sidecar enables secure, scalable real-time ingestion without exposing the email service directly
- Unified search in
search_serviceaggregates threads across all linked Gmail accounts using standard database queries onowner_idandsource_type
Frequently Asked Questions
How many Gmail accounts can a single Macro user connect?
There is no hard limit enforced by the architecture. Each account receives a distinct Link with a unique link_id, and all data structures — token cache, email storage, search indices — are scoped by this identifier. Performance depends on database and search index capacity rather than application-level constraints.
What happens if a Gmail token expires mid-operation?
The fetch_gmail_access_token method automatically refreshes expired tokens using the stored refresh token. Callers receive a valid access token transparently. The no-cache variant exists for operations that explicitly require fresh credentials, such as retrying after a 401 response.
Does the unified inbox mix Gmail with other email providers?
The search service architecture supports multiple source_type values. While this analysis focuses on GMAIL, the sort_unified_search_results function and enrich_emails pipeline are designed to merge threads from any provider implementing the same interface — the aggregation logic is provider-agnostic.
How does Macro ensure webhook security for multiple accounts?
The gmail_forwarder sidecar runs inside the same infrastructure as the email service and uses internal networking (http://email-service:8080). The Gmail API signs push notifications; Macro verifies these signatures before processing. The link_id in each payload ensures account isolation without relying on URL path segmentation for security.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →