# How Macro’s Email Service Handles Multi-Account Gmail Integration and Unified Inboxes

> Macro’s email service seamlessly integrates unlimited Gmail accounts into one unified inbox using per-account identifiers, cached OAuth tokens, and an aggregated search layer.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: architecture
- Published: 2026-08-20

---

**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`](https://github.com/macro-inc/macro/blob/main/crates/email/src/domain/ports.rs), domain ports expose methods that require a `&Link` parameter:

```rust
// 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`](https://github.com/macro-inc/macro/blob/main/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

```rust
// 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`](https://github.com/macro-inc/macro/blob/main/tooling/xtask/src/local/gen_compose.rs):

```toml

# 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/crates/search_service/src/api/search/unified.rs), the `sort_unified_search_results` function merges entities from multiple indices:

```rust
// 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`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/crates/email/src/outbound/gmail_token_provider.rs) | OAuth token caching and refresh per `Link` |
| [`crates/email/src/domain/ports.rs`](https://github.com/macro-inc/macro/blob/main/crates/email/src/domain/ports.rs) | Service contract requiring `Link` for account-scoped operations |
| [`crates/email/src/inbound/axum/thread_labels_router.rs`](https://github.com/macro-inc/macro/blob/main/crates/email/src/inbound/axum/thread_labels_router.rs) | Webhook ingestion with `link_id` routing |
| [`tooling/xtask/src/local/gen_compose.rs`](https://github.com/macro-inc/macro/blob/main/tooling/xtask/src/local/gen_compose.rs) | Sidecar configuration for Gmail Pub/Sub forwarding |
| [`crates/search_service/src/api/search/unified.rs`](https://github.com/macro-inc/macro/blob/main/crates/search_service/src/api/search/unified.rs) | Cross-account result aggregation and sorting |
| [`tooling/seed_cli/src/entity/gmail/mod.rs`](https://github.com/macro-inc/macro/blob/main/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`](https://github.com/macro-inc/macro/blob/main/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.