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 identifieraccount_id– Foreign key linking to the source Gmail accountthread_id– For conversation grouping across accountsheaders– Parsed From, To, Cc, Subject, Datebody_text/body_html– Extracted contentreceived_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_serviceestablishes 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
EmailMessageentities in MacroDB - Unified OpenSearch indexing treats all email as queryable documents with
account_idfor optional filtering ListEntitiesAPI 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →