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

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 module handles token lifecycle operations:

// 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:

// 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:

// 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:

{
  "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":

// 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:

// 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 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.

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 →