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.

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 available
  • fetch_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 user
  • source_type – always GMAIL for Gmail-originated threads
  • link_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:

  1. Link creation – The google_gmail OIDC identity provider completes OAuth and stores the refresh token in email_links (see tooling/seed_cli/src/entity/gmail/mod.rs for the table structure)

  2. Watch registration – Macro calls Gmail's watch API to subscribe the account to Pub/Sub notifications

  3. Token retrieval – Any Gmail API operation calls fetch_gmail_access_token(&link) to obtain account-specific credentials

  4. Message ingestion – Incoming push notifications arrive via the forwarder sidecar, are routed by link_id, and stored with provider = GMAIL and the corresponding link_id

  5. Unified presentation – Search queries aggregate across all link_id values 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

  • Link identifiers decouple multi-account complexity from business logic — every operation receives an explicit account context
  • Redis-cached tokens in gmail_token_provider.rs provide 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_service aggregates threads across all linked Gmail accounts using standard database queries on owner_id and source_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →