Setup
This guide walks through everything needed to poll a Microsoft 365 mailbox from
OctoMesh — first in the Azure Portal, then the equivalent Azure CLI (az)
commands, and finally the OctoMesh side (credentials, pipeline, deploy, test).
Unlike the Teams bot, this integration uses an app-only (client-credentials) flow: there is no Azure Bot resource and no user sign-in. The app registration authenticates as itself and reads/moves mail through Microsoft Graph application permissions.
Prerequisites
- An Azure subscription and permission to create an App registration in Microsoft Entra ID.
- A Global Administrator (or Privileged Role Administrator) to grant admin consent for the Graph application permission.
- A Microsoft 365 mailbox to poll, and the folder structure you want to use as the
work queue (e.g. a
ToDoand aDonefolder). - A running OctoMesh mesh adapter for the target tenant.
The examples use the app name octo-mail-import, the mailbox
accounting@company.com, the source folder Archive/Invoices/ToDo and the success
folder Archive/Invoices/Done. Replace them with your own values.
Step 1 — App registration + client secret
The app registration is the identity the adapter authenticates as. Its client
ID/secret and tenant id go into the OctoMesh MicrosoftGraphConfiguration.
Portal
- Microsoft Entra ID → App registrations → + New registration.
- Name:
octo-mail-import. - Supported account types: Accounts in this organizational directory only (Single tenant).
- Register, then note Application (client) ID and Directory (tenant) ID.
- Certificates & secrets → + New client secret → set an expiry → Add → copy the Value immediately (it is shown only once).
Azure CLI
# create a single-tenant app registration
az ad app create --display-name "octo-mail-import" --sign-in-audience AzureADMyOrg
# -> note the "appId" from the output
# add a client secret (prints the secret once)
az ad app credential reset --id <APP_ID> --append --display-name mail-import --years 1 \
--query password -o tsv
# your tenant id
az account show --query tenantId -o tsv
Keep three values for later: App (client) ID, client secret, tenant ID.
Step 2 — Graph application permission Mail.ReadWrite
The trigger both reads messages and moves them between folders, so it needs the
application permission Mail.ReadWrite (not the delegated one), followed by admin
consent.
Portal
- On the app registration: API permissions → + Add a permission → Microsoft Graph → Application permissions.
- Search
Mail.ReadWrite, tick it, Add permissions. - Click Grant admin consent for <tenant> and confirm — the Status column must show a green Granted check.
Azure CLI
GRAPH=00000003-0000-0000-c000-000000000000 # Microsoft Graph resource appId
MAIL_RW=e2a3a72e-5f79-4c64-b1b1-878b674786c9 # Mail.ReadWrite (application) role id
# add the application permission
az ad app permission add --id <APP_ID> \
--api $GRAPH --api-permissions ${MAIL_RW}=Role
# grant admin consent (requires an admin)
az ad app permission admin-consent --id <APP_ID>
# verify the grant
az ad app permission list-grants --id <APP_ID> -o table
Mail.ReadWrite application permission grants the app access to every mailbox in
the tenant, not just the one you configure. Before using this in production, scope it
to the specific mailbox(es) with an Application Access Policy in Exchange Online
(create a mail-enabled security group containing only the mailbox(es) to expose, then):
# Exchange Online PowerShell
New-ApplicationAccessPolicy -AppId <APP_ID> `
-PolicyScopeGroupId mail-import-scope@company.com `
-AccessRight RestrictAccess `
-Description "Restrict octo-mail-import to the accounting mailbox"
# confirm the app can access the target mailbox but not others
Test-ApplicationAccessPolicy -AppId <APP_ID> -Identity accounting@company.com
Step 3 — Prepare the mailbox folders
The trigger treats a folder as a work queue. Create the source and (optional) success folders in the mailbox, for example:
Archive
└── Invoices
├── ToDo ← trigger polls this (folderPath)
└── Done ← messages moved here on success (moveToFolderPathOnSuccess)
folderPath segments must match the Graph display names, which are not
localized. Use Archive, not the localized Outlook name (e.g. Archivieren).
Well-known folders (inbox, archive, …) also resolve by their well-known name.
The success folder's leaf is auto-created if missing, but its parent path must
already exist.
Route incoming invoices into the ToDo folder however you like (an Outlook rule, a
shared mailbox workflow, or manual filing).
Step 4 — Configure OctoMesh
1. Credentials (MicrosoftGraphConfiguration)
Provide the tenant id / client id / secret as a
System.Communication/MicrosoftGraphConfiguration entity. Store the secret outside
of source control (a local secrets file, a key vault, …). This is the same
configuration type the Teams triggers use.
- rtId: <config-rtId>
ckTypeId: System.Communication/MicrosoftGraphConfiguration
rtWellKnownName: MicrosoftGraphDocuments
attributes:
- id: System.Communication/AzureTenantId
value: "<tenant id>"
- id: System.Communication/ClientId
value: "<app (client) id>"
- id: System.Communication/ClientSecret
value: "<client secret>"
2. Pipeline with FromMicrosoftGraphEmail@1
The pipeline must carry a System.Communication/Uses association to the
MicrosoftGraphConfiguration (the GlobalConfiguration is built only from Uses
associations — the name alone is not resolved).
# on the Pipeline entity
associations:
- roleId: System.Communication/Uses
targetRtId: <config-rtId>
targetCkTypeId: System.Communication/MicrosoftGraphConfiguration
A minimal pipeline definition that stages each PDF attachment (the trigger delivers one
message per run, so the outer ForEach@1 iterates a single element):
triggers:
- type: FromMicrosoftGraphEmail@1
serverConfiguration: MicrosoftGraphDocuments
mailbox: accounting@company.com
folderPath: Archive/Invoices/ToDo
moveToFolderPathOnSuccess: Archive/Invoices/Done
pollingIntervalSeconds: 120
transformations:
- type: ForEach@1
iterationPath: $.Emails
targetPath: $.emailResults
targetValueKind: Simple
targetValueWriteMode: Overwrite
transformations:
- type: ForEach@1
iterationPath: $.key.Attachments
targetPath: $.attachmentResults
targetValueKind: Simple
targetValueWriteMode: Overwrite
transformations:
- type: If@1
path: $.key.ContentType
operator: Equals
value: "application/pdf"
valueType: String
transformations:
# ... store $.key.Data, run OCR / AI, create entities ...
The message metadata ($.key.Subject, $.key.FromAddress, $.key.Date,
$.key.Body) and each attachment's FileName / ContentType / Length / Data
are available under the ForEach@1 element key — see the
unified message shape.
3. Deploy
Import the configuration and pipeline and deploy the data flow with the octo-cli
(ImportRt, DeployDataFlow). After deployment the adapter starts polling on the
configured interval.
Step 5 — Test
- Drop a test message with a PDF attachment into the
Archive/Invoices/ToDofolder. - Within one polling interval (default 120 s) the pipeline runs for that message.
- On success the message disappears from
ToDoand appears inDone; on failure it stays inToDoand is retried up tomaxAttemptsPerMessagetimes.
Watch the adapter log to confirm the round-trip — a token error, a permission error or a missing folder all surface there (see Troubleshooting).
Node reference
FromMicrosoftGraphEmail@1 (trigger)
| Property | Default | Description |
|---|---|---|
serverConfiguration | — | WellKnownName of the MicrosoftGraphConfiguration (tenant id + client id/secret). |
mailbox | — | Mailbox UPN to poll (e.g. accounting@company.com). |
folderPath | — | /-separated folder path resolved from the mailbox root. |
moveToFolderPathOnSuccess | (none) | Folder the message is moved to after a successful run (leaf auto-created). Omitted → message stays in place. |
pollingIntervalSeconds | 120 | Seconds between polling cycles. |
maxMessagesPerPoll | 25 | Max messages fetched per cycle (oldest first). |
senderFilter | (none) | Only process messages whose sender address contains this string. |
maxAttemptsPerMessage | 3 | Retries for a failing message before it is skipped until the adapter restarts. |
Output: $.Emails[] (one element per run) — see the
Overview. See also the concise
FromMicrosoftGraphEmail@1 node reference.
Security
- App-only access. The adapter authenticates as the app registration via client-credentials; there is no user context. Treat the client secret like a password — store it outside source control and rotate it before expiry.
- Least privilege.
Mail.ReadWriteapplication permission spans the whole tenant. Scope it to the target mailbox(es) with an Application Access Policy (see the admonition in Step 2). Without it, a leaked secret exposes every mailbox. - Read/write footprint. The trigger reads messages, downloads attachments and moves messages between folders. It does not delete or send mail.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
invalid_client / AADSTS7000215 when acquiring the token | Wrong or expired client secret, or wrong AzureTenantId. Reset the secret (Step 1) and re-check the tenant id. |
ErrorAccessDenied / 403 from Graph | The Mail.ReadWrite application permission is missing or admin consent was not granted (Step 2), or an Application Access Policy is blocking this mailbox. Verify with az ad app permission list-grants and Test-ApplicationAccessPolicy. |
ErrorItemNotFound / folder not found | folderPath uses a localized name (e.g. Archivieren) or a non-existent path. Use the Graph (non-localized) display names; the parent of the success folder must exist. |
| Nothing is polled although mail is in the folder | mailbox UPN wrong, folderPath points at the wrong folder, or senderFilter excludes the messages. Confirm the folder actually holds the messages and check the adapter log. |
| A message is processed repeatedly | The pipeline run keeps failing, so the message is never moved out of the source folder. Fix the pipeline error; after maxAttemptsPerMessage failures the message is skipped until the adapter restarts. |
| Attachments are empty | Only fileAttachment contents are downloaded — inline/item/reference attachments are skipped. Check ContentType/Length on $.Emails[0].Attachments[]. |
| No polling activity at all in the log | The data flow is not deployed, or the adapter build predates the node — redeploy and restart the adapter. |