# How Macro's Unified Inbox Aggregates Multiple Gmail Accounts: OAuth to Indexed Search

> Learn how Macro unifies multiple Gmail accounts using OAuth and indexed search. Access all your messages in one chronological inbox for efficient email management.

- Repository: [Macro/macro](https://github.com/macro-inc/macro)
- Tags: deep-dive
- Published: 2026-08-16

---

**Macro aggregates multiple Gmail accounts by syncing each account via OAuth-authorized Gmail API access, storing normalized messages in a central database, and indexing them for unified search—presenting a single chronological inbox view regardless of source account.**

Macro's unified inbox is a core feature of the open-source email platform that eliminates the need to switch between Gmail accounts. Instead of treating each account as an isolated silo, Macro normalizes, stores, and indexes email from all connected sources into one queryable system. This article breaks down the technical implementation based on the macro-inc/macro source code.

## OAuth 2.0 Authorization Flow for Gmail Accounts

Before any email can be fetched, Macro establishes a secure, revocable connection to each Gmail account through Google's standard OAuth 2.0 flow.

### Token Acquisition and Storage

When a user adds a Gmail account through the Settings UI, Macro redirects to Google's consent screen using the `google_gmail` identity provider configuration. The resulting access and refresh tokens are stored by the `authentication_service` and associated with the user's Macro identity.

The [`google_access_token.rs`](https://github.com/macro-inc/macro/blob/main/google_access_token.rs) module handles token lifecycle operations:

```rust
// crates/authentication_service_client/src/google_access_token.rs
// Retrieves valid access tokens, refreshing expired ones automatically
pub async fn get_google_access_token(
    account_id: &str,
) -> Result<String, AuthError> {
    // Checks token expiry, refreshes if needed via Google's token endpoint
    // Returns bearer token for Gmail API calls
}

```

This centralized token management ensures the `email_service` can continuously sync without re-prompting users for consent.

## Gmail Watch and Real-Time Sync

Macro doesn't rely solely on polling. Instead, it leverages Gmail's **push notification** system via Google Cloud Pub/Sub.

### Watch Subscription Setup

For each connected account, the `email_service` registers a watch on the user's mailbox:

```rust
// Conceptual flow based on tooling/xtask/crates/xtask_local/src/local/validate.rs
pub async fn setup_gmail_watch(
    access_token: &str,
    user_id: &str,
) -> Result<WatchResponse, EmailError> {
    let watch_request = GmailWatchRequest {
        label_ids: vec!["INBOX".to_string()],
        topic_name: format!("projects/{}/topics/{}", PROJECT_ID, TOPIC_NAME),
    };
    // POST https://gmail.googleapis.com/gmail/v1/users/{userId}/watch
}

```

The `xtask_local` configuration references the development topic `gmail-gcp-queue-local`, while production deployments use project-specific topics. This watch subscription triggers notifications for new messages, label changes, and deletes.

### Pub/Sub Message Consumption

A `gmail_forwarder` side-car service consumes these Pub/Sub messages and routes them to the appropriate `email_service` worker for processing. This architecture decouples notification receipt from message retrieval, improving reliability under load.

## Message Retrieval and Normalization

When a notification arrives, Macro fetches the actual message content and converts it to an internal representation.

### Gmail API Integration

The `seed_cli` crate's Gmail entity module demonstrates the API interaction pattern:

```rust
// tooling/seed_cli/src/entity/gmail/mod.rs
pub async fn fetch_message(
    access_token: &str,
    message_id: &str,
) -> Result<MimeMessage, EmailError> {
    let url = format!(
        "https://gmail.googleapis.com/gmail/v1/users/me/messages/{}?format=raw",
        message_id
    );
    let response = reqwest::Client::new()
        .get(&url)
        .bearer_auth(access_token)
        .send()
        .await?;
    
    let gmail_msg: GmailApiMessage = response.json().await?;
    // Base64url-decode the 'raw' field to get full MIME
    parse_mime_message(&gmail_msg.raw)
}

```

### Storage in MacroDB

Parsed messages are persisted to **MacroDB** with a normalized schema. Key fields include:

- `message_id` – Globally unique identifier
- `account_id` – Foreign key linking to the source Gmail account
- `thread_id` – For conversation grouping across accounts
- `headers` – Parsed From, To, Cc, Subject, Date
- `body_text` / `body_html` – Extracted content
- `received_at` – Timestamp for chronological sorting

This normalization is critical: it allows Macro to treat a Gmail message, a Slack DM, or a Notion comment as interchangeable **entity types** within the same storage layer.

## Centralized Search Indexing

Raw storage alone doesn't enable the unified inbox. Macro indexes all messages into **OpenSearch** via the `search_service` for sub-second querying across millions of records.

### Unified Entity Indexing

All email messages, regardless of originating account, are written to a single `email` index:

```json
{
  "entity_type": "email",
  "entity_id": "msg_abc123",
  "account_id": "gmail_user@example.com",
  "account_type": "gmail",
  "subject": "Project update",
  "body": "Here's the latest...",
  "sent_at": "2024-01-15T09:30:00Z",
  "is_read": false,
  "labels": ["INBOX", "IMPORTANT"],
  "participants": ["sender@example.com", "recipient@example.com"]
}

```

The `account_id` field enables filtering when needed, but the default inbox query omits this filter—returning all email chronologically.

### Signal vs. Noise Classification

During indexing, Macro applies heuristics to classify each message:

- **Signal**: Unread, mentions the user, contains action items, or originates from high-priority contacts
- **Noise**: Everything else, including newsletters and automated notifications

This classification is stored as `signal_score` and `noise_bucket` fields, allowing the UI to present focused views without re-querying.

## Unified Inbox Query and Presentation

The front-end `apps/web` retrieves the unified view through the `ListEntities` tool endpoint.

### Backend Aggregation

The `email_service` handles the `ListEntities` request for `entity_type: "inbox"`:

```typescript
// apps/web/src/components/app/app-sidebar/sidebar.tsx usage pattern
const fetchUnifiedInbox = async (params: InboxQueryParams) => {
  const response = await fetch("/api/entities", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      tool: "ListEntities",
      entity_type: "inbox",
      limit: 50,
      cursor: params.nextCursor,
      filters: {
        // Optional: filter by account_id if user selects specific account
        account_id: params.selectedAccountId,
        signal_only: params.signalOnly,
      }
    }),
  });
  return response.json();
};

```

The backend executes an OpenSearch query against the unified `email` index, sorts by `sent_at` descending, and returns results. Because all accounts share one index, sorting is truly global—not per-account stitching.

### Account-Aware Composition

When composing a reply, the UI presents a sender selector populated from connected accounts:

```typescript
// Sending from a specific Gmail address
await fetch("/api/email/send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    entity_type: "email",
    account_id: "gmail_user@example.com", // Selected in dropdown
    thread_id: "thread_xyz789",
    to: ["recipient@example.com"],
    subject: "Re: Project update",
    body: { text: "Thanks for the update!", html: "<p>Thanks...</p>" }
  }),
});

```

The `email_service` routes this through the appropriate Gmail account's API credentials using the stored OAuth tokens.

## Multi-Account Configuration Flow

Users add accounts through Settings, documented in the getting-started guide:

```

apps/docs/getting-started.mdx
  → "Connecting Your Accounts" section

```

The flow: Settings → Add Account → Select Gmail → OAuth consent → Immediate background sync → Inbox unified.

## Summary

- **OAuth 2.0 authorization** via `authentication_service` establishes secure, refreshable API access for each Gmail account
- **Gmail Watch + Pub/Sub** enables real-time push notifications instead of inefficient polling
- **MIME parsing and normalization** converts diverse Gmail formats into consistent `EmailMessage` entities in MacroDB
- **Unified OpenSearch indexing** treats all email as queryable documents with `account_id` for optional filtering
- **`ListEntities` API** returns chronologically merged results across accounts for the inbox UI
- **Account-aware sending** lets users reply from any connected address using stored credentials

## Frequently Asked Questions

### How does Macro handle Gmail authentication securely?

Macro uses standard OAuth 2.0 with offline refresh tokens. Tokens are encrypted at rest in the `authentication_service` database and never exposed to clients. The [`google_access_token.rs`](https://github.com/macro-inc/macro/blob/main/google_access_token.rs) module automatically refreshes expired tokens before API calls.

### Can I search across all Gmail accounts at once?

Yes. The unified search index in `search_service` contains email from all connected accounts. Queries without an `account_id` filter return global results. The search index includes full-text body content, headers, and metadata for comprehensive cross-account search.

### What happens when I disconnect a Gmail account?

Disconnection triggers cleanup workflows: the `email_service` stops the Gmail Watch subscription, marks the account's messages as `archived_account` in the index (optional retention), and deletes OAuth tokens. Previously synced messages may be retained based on user data retention settings.

### Does Macro support Google Workspace accounts in addition to personal Gmail?

Yes. The same `google_gmail` OAuth provider configuration works for both consumer Gmail and Google Workspace domains. Workspace admins may need to approve the Macro OAuth app at the organization level for domain-wide deployment.