Skip to content

Design: Outlook Mail#

Overview#

Purpose: Expose Microsoft 365 mailbox capabilities to AI assistants via the MCP protocol, enabling users to list, search, and read emails from their own mailbox using delegated permissions.

Scope: - Five MCP tools: list_emails, search_emails, read_email, list_folders, get_mailbox_settings - Read-only access to the authenticated user's mailbox via Microsoft Graph API - Attachment metadata retrieval (not content download by default) - OAuth2 On-Behalf-Of (OBO) authentication — see DESIGN:OAuthAuthentication

Out of Scope: - Email composition, sending, forwarding, or deletion - Attachment content download (returned as metadata only) - Shared mailboxes or delegate mailbox access - Email rules, categories, or flags management


Architecture#

┌────────────────────┐     ┌──────────────────────┐     ┌──────────────────────┐
│  AI Assistant      │────▶│  Outlook MCP Server  │────▶│  Microsoft Graph API │
│  (Claude / MCP     │     │  (FastMCP / ECS)     │     │  /v1.0/me/messages   │
│   client)          │◀────│                      │◀────│  /v1.0/me/mailFolders│
└────────────────────┘     └──────────────────────┘     └──────────────────────┘
                            ┌──────────────────────┐
                            │  OBOAuthenticator    │
                            │  (auth.py)           │
                            └──────────────────────┘
                            ┌──────────────────────┐
                            │  Token Storage       │
                            │  (DynamoDB / Memory) │
                            └──────────────────────┘

Components: - FastMCP Server (server.py) — ASGI HTTP server, MCP protocol handler, tool registration - Mail Tools (tools/mail.py) — MCP tool wrappers delegating to Graph clients - Graph Mail Client (graph/mail.py) — Microsoft Graph API calls for mail resources - Graph Client (graph/client.py) — Authenticated HTTP client (OBO token injection) - OBOAuthenticator (auth.py) — On-Behalf-Of token exchange; see DESIGN:OAuthAuthentication - Microsoft Graph API — Upstream data source; enforces user-scoped authorization


Tech Stack#

Backend: Python 3.13, FastMCP ≥3.0.0, Starlette (ASGI) HTTP Client: httpx (async) OAuth: MSAL 1.28+, OBO flow via auth.py Token Storage: py-key-value-aio (DynamoDB in production, memory in dev) Infrastructure: AWS ECS Fargate, Application Load Balancer, ECR Secret Management: AWS SSM Parameter Store (KMS encrypted) Deployment: Docker (multi-stage), GitHub Actions CI/CD


MCP Tool API#

list_emails#

Lists emails in a mailbox folder.

Parameters: - folder_id (str, default: "inbox") — folder to list from - count (int, default: 10) — maximum results to return

Returns: List of { id, subject, from, receivedDateTime }

Graph endpoint: GET /v1.0/me/mailFolders/{folder_id}/messages


search_emails#

Searches emails using OData filters with progressive fallback.

Parameters: - folder_id (str, default: "inbox") - query (str, optional) — free-text search - sender (str, optional) — filter by sender address - recipient (str, optional) — filter by recipient address - subject (str, optional) — filter by subject keywords - has_attachments (bool, optional) - unread (bool, optional) - importance (str, optional) — high, normal, or low - received_after (str, optional) — ISO 8601 datetime - received_before (str, optional) — ISO 8601 datetime - count (int, default: 10)

Returns: List of { id, subject, from, receivedDateTime }

Graph endpoint: GET /v1.0/me/mailFolders/{folder_id}/messages with $search or $filter

Search strategy: Progressive fallback — $search first (free-text), then $filter (structured filters), then recent emails if no filters provided.


read_email#

Retrieves full details of a single email, including attachment metadata.

Parameters: - email_id (str) — unique message ID

Returns: { email: { id, subject, from, to, body, receivedDateTime, hasAttachments, ... }, attachments: [{ id, name, size, contentType }] }

Graph endpoints: - GET /v1.0/me/messages/{id} - GET /v1.0/me/messages/{id}/attachments (only if hasAttachments=true)


list_folders#

Lists all mail folders in the user's mailbox.

Parameters: None

Returns: List of { id, displayName, parentFolderId, childFolderCount, unreadItemCount }

Graph endpoint: GET /v1.0/me/mailFolders


get_mailbox_settings#

Retrieves mailbox configuration for the authenticated user.

Parameters: None

Returns: { timeZone, workingHours, automaticRepliesSetting, ... }

Graph endpoint: GET /v1.0/me/mailboxSettings


Security#

Authentication: OAuth2 OBO flow — see DESIGN:OAuthAuthentication

Authorization: - All Graph API calls use /me/ endpoint — access scoped to the authenticated user - No parameterized user IDs accepted (prevents cross-user access) - Microsoft Graph API provides secondary authorization layer

Graph Permission Scope: Mail.Read only (no write, send, or delete permissions)

Data Protection: - Email content never persisted — stateless HTTP mode (stateless_http=True) - DynamoDB stores OAuth tokens only — no email content or metadata - Email subject, body, sender, and recipient fields excluded from application logs - LOG_LEVEL=INFO in production — DEBUG logging disabled - Transport: TLS 1.3 (enforced at ALB layer)

Attachment Handling: - list_emails and search_emails return attachment count/flag only - read_email returns attachment metadata (name, size, contentType) — no content download - No automatic attachment content retrieval


Data Model#

No persistent data model for email content. The only data stored is OAuth tokens:

DynamoDB table: mcp-oauth-storage-outlook-mcp-{env}
  Partition key: user_id (string)
  Sort key:      session_id (string)
  Attributes:    access_token, refresh_token, expires_at
  TTL:           expires_at + 90 days

All email data is fetched on-demand from Microsoft Graph API and returned directly to the MCP client. No caching, no secondary storage.


Integrations#

Microsoft Graph API - Protocol: REST over HTTPS - Auth: OAuth2 Bearer token (OBO-exchanged, user-scoped) - Scopes: Mail.Read, MailboxSettings.Read, offline_access - Base URL: https://graph.microsoft.com/v1.0 - Rate limits: 10,000 requests per 10 minutes per user - Error handling: HTTP 429 propagated to client (with Retry-After); HTTP 401/403 re-raised

Azure AD (authentication) - See DESIGN:OAuthAuthentication for full flow - Tenant: Novo Nordisk single-tenant


Performance#

Expected Load: Low — per-user assistant sessions, not batch operations Response Time Target: < 2s for list_emails / search_emails, < 3s for read_email with attachments OBO Exchange Overhead: ~200–500ms per tool call (see DESIGN:OAuthAuthentication)

Optimization Notes: - No Graph token caching (security > latency) - search_emails uses progressive fallback to avoid unsupported filter combinations - Attachment metadata fetched only when hasAttachments=true (avoids redundant API calls)


Deployment#

Environments:

Env URL ECS sizing Storage
Dev outlook.dev.connectors.novo-genai.com 0.25 vCPU / 512 MB Memory
Prod outlook.connectors.novo-genai.com 0.5 vCPU / 1 GB DynamoDB

CI/CD: .github/workflows/deploy-outlook-mcp.yml - lintbuild (push to dev ECR) → deploy-devdeploy-prod (gated: environment: Production) - Prod promotes image from dev ECR (no rebuild)

Secrets (SSM Parameter Store):

/aiconnectors/{env}/outlook-mcp/client_id
/aiconnectors/{env}/outlook-mcp/client_secret
/aiconnectors/{env}/outlook-mcp/dynamodb_table_name


Monitoring#

Logs: JSON structured logs → CloudWatch Logs (/ecs/outlook-mcp-{env}) Retention: 90 days (GDPR Article 30 compliance)

Key Events Logged: - email_list_requested — user identity, folder, count - email_search_requested — user identity, filter strategy used - email_read_requested — user identity, message ID - graph_request — method, endpoint, HTTP status (no content) - obo_exchange_failed — error code, user identity

NOT Logged: - Email subject, body, sender/recipient fields - Attachment names or content - OAuth tokens or authorization codes

Alerts: - Error rate > 5% over 5 minutes - Graph API 5xx rate > 10 per minute - ECS health check failures


Key Decisions#

Why /me/ endpoint only (no /users/{id}/)?#

Decision: All Graph API calls use /me/ — no parameterized user ID paths.

Rationale: Prevents cross-user access. The /users/{id}/messages endpoint requires Mail.ReadWrite.All (application permission), which is intentionally not requested. Using /me/ ensures the Graph API resolves the mailbox from the OBO token's identity claim, making cross-user access structurally impossible even if the server code has a bug.

Why no attachment content download?#

Decision: read_email returns attachment metadata only. Content retrieval is not implemented.

Rationale: Attachment content (particularly executables, macros, scripts) represents a higher risk surface for the AI assistant. Returning metadata (name, size, contentType) allows the user to decide whether to retrieve the content via other means. This is consistent with R5 in RISK:OutlookMail (attachment injection risk).

Why progressive search fallback?#

Decision: search_emails tries $search first, then $filter, then falls back to recent emails.

Rationale: Microsoft Graph API does not support combining $search with all $filter operators. The progressive strategy maximizes compatibility across different filter combinations without requiring the MCP client to understand Graph API limitations.


URS: @URS:OutlookMail Risk Assessment: @RISK:OutlookMail OAuth Design: @DESIGN:OAuthAuthentication

Risks Addressed by This Design: - R1 (token compromise) — OBO flow, Mail.Read scope only - R2 (content in logs) — structured logging with field exclusion - R3 (persistent storage) — stateless HTTP, no email caching - R4 (excessive permissions) — Mail.Read only, no write scope - R5 (attachment injection) — metadata-only retrieval - R6 (cross-user access) — /me/ endpoint, no user ID parameters - R7 (audit trail) — structured logging with user identity and message IDs


Version: 1.0 Date: 2026-04-22 Author: AILab Approved by: Pending PR review