Outlook Sensor
What it ingests
Mail messages from the Inbox and Sent Items of your organization's members — the threads where decisions, commitments, and customer conversation happen outside a meeting or a channel.
Each message becomes one Message in the ontology, grouped into a Conversation by Graph's thread id. The sender and To/Cc recipients resolve to Person entities by email, so they canonicalize against people already ingested from Slack, GitHub, Teams, or Calendar rather than minting duplicates.
Drafts, Deleted Items, and Junk are never ingested. Attachments are noted (has_attachments) but not downloaded. Calendar stays on the Outlook Calendar sensor; channel posts and meeting transcripts stay on the Microsoft Teams sensor.
Credential
References the same microsoft_graph credential as the Microsoft Teams, SharePoint / OneDrive, and Outlook Calendar sources — a JSON object describing an Entra ID app registration:
{
"tenant_id": "72f988bf-86f1-41af-91ab-2d7cd011db47",
"client_id": "a1b2c3d4-e5f6-4789-8abc-def012345678",
"client_secret": "abc8Q~…"
}Already running Teams, SharePoint, or Calendar? Reuse that credential.
Point this source at the same vault row. A tenant that has connected any of those does not need a second app registration — only one additional consented permission on the one it already has (Mail.Read). That is why the credential type was named for Graph rather than for Teams.
Config fields
| Key | Required | Notes |
|---|---|---|
cloud | no | global (default) for commercial Microsoft 365 — including GCC, which is served by the commercial endpoints — usgov for GCC-High (L4), or usgovdod for DoD (L5). |
lookback_days | no | How far back a newly added mailbox (not yet in the cursor) is read, and the default Backfill window. First poll on an empty cursor seeds watermarks to now without emitting — history is Backfill's job. Default 7, maximum 30. |
poll_overlap_minutes | no | Minutes subtracted from each mailbox watermark so a message that landed while the previous poll was running is still caught. Default 15. |
folders | no | Comma-separated well-known folders. Allowed: Inbox, SentItems, Archive. Default Inbox, SentItems. Drafts, Deleted Items, and Junk cannot be opted in. |
member_allowlist | no | Comma-separated mailbox UPNs. When set, fan-out is restricted to these members (staged rollout / testing). |
workspace_domains | no | Comma-separated email domains. When set, only members in these domains are polled. |
group_ids | no | Comma-separated Microsoft 365 group IDs. Fan-out becomes the intersection of org members and those groups' members. Requires GroupMember.Read.All. |
Provider-side setup
1. Use (or create) the app registration
If you already configured the Teams, SharePoint, or Outlook Calendar source, reuse that app registration and skip to step 2. Otherwise follow the Teams guide's app-registration steps — the registration itself is identical; only the permissions differ.
2. Add application permissions and grant admin consent
In Entra ID → App registrations → your app → API permissions, add these Application (not Delegated) permissions and click Grant admin consent:
| Permission | Why |
|---|---|
Mail.Read | Read each member's Inbox / Sent Items via GET /users/{id}/mailFolders/{folder}/messages |
User.Read.All | Test connection (lists one user to probe) and address mailboxes by UPN. Teams already requires this. |
GroupMember.Read.All | Only if group_ids is set |
Missing Mail.Read fails Test connection with a named permission error, not a generic 500.
Mail.Read is tenant-wide application access to every mailbox the sensor's fan-out can reach. Stage with member_allowlist before opening the crawl to the whole org.
3. GCC-High / DoD tenants
L4 and L5 are different hosts. Set cloud to usgov for GCC-High and usgovdod for DoD:
cloud | Graph host | Token scope |
|---|---|---|
usgov | graph.microsoft.us | https://graph.microsoft.us/.default |
usgovdod | dod-graph.microsoft.us | https://dod-graph.microsoft.us/.default |
Both authenticate against login.microsoftonline.us. Plain GCC (not GCC-High) is served by the commercial endpoints — use global there.
List messages is ✅ in GCC-High (L4) and DoD (L5) — List messages.
Wire-up
POST /api/sensors
Every org member, default Inbox + Sent Items:
{
"sensor_type": "outlook",
"name": "Contoso Mail",
"config": {},
"credentials": { "graph_credential": "<microsoft_graph credential id>" }
}Staged rollout to two mailboxes, GCC-High:
{
"sensor_type": "outlook",
"name": "Agency Mail (pilot)",
"config": {
"cloud": "usgov",
"member_allowlist": "ada@agency.gov, bob@agency.gov",
"lookback_days": "3",
"folders": "Inbox, SentItems"
},
"credentials": { "graph_credential": "<microsoft_graph credential id>" }
}Verify
Run Test connection. It probes Inbox on one mailbox (the first allowlisted address, or one user listed from the directory), so a missing Mail.Read consent fails here rather than silently ingesting nothing.
A mailbox that 404s (no Exchange license, a guest without a mailbox) is skipped and named on the source's status line; the other members keep ingesting. Sensors only begin polling once you confirm their bindings.
After the first poll, check that:
sensor_eventsexist for mail across more than one member- the same message sitting in Alice's Inbox and Bob's Sent Items produced one Message, not two
- the sender and To/Cc recipients resolved to existing Person entities (same email as Slack / GitHub / Teams) rather than duplicates
- a reply in a thread
BELONGS_TOthe same Conversation as the original
Notes and limits
Coverage is the Corveil org, not the whole Microsoft 365 tenant. Fan-out walks this organization's members (filtered by allowlist, domain, or group). A Microsoft 365 user who is not a Corveil member is not polled.
Fan-out is capped at 100 mailboxes per poll, and a poll emits at most 400 messages. Hitting either cap is not silent: the source status names how many eligible mailboxes or messages were deferred. Remaining mailboxes resume from their watermarks next tick. Narrow with member_allowlist, workspace_domains, or group_ids.
The same message is identified by internetMessageId, not Graph's id. Graph's message id is mailbox-local — Alice and Bob have different ids for the same mail. RFC 5322 internetMessageId is the same on every copy and is the stable entity key.
Bodies are stored in full (stripped to plain text). HTML is converted before it reaches the ontology. That is the ingestion-fidelity contract (ADR 0010): a snippet is not enough to drill from an entity back to the source. Bodies over 40,000 characters are truncated.
Keep the backfill window modest. Mail volume is high. First poll seeds watermarks to now without emitting; historical ingest is the worker's one-shot Backfill. Start with the 7-day default (or a two-mailbox allowlist) and widen only if you need the history. Subsequent polls are incremental.
Attachments, rules, and categories are out of v1. So are Drafts, Deleted Items, Junk, and 1:1 Teams chats.