Smart Mailbox — Scoped Graph Access
Smart Mailbox reads incoming email through the Microsoft Graph API. By default, granting the Mail.Read Application permission in Microsoft Entra gives the app access to every mailbox in the tenant — which is almost never what you want when a specific mailbox is dedicated to Smart Mailbox ingestion.
This guide walks through the modern, Microsoft-recommended approach — Exchange Online RBAC for Applications — to restrict Graph mail access to one specific mailbox (or a defined set). It replaces the older, now-deprecated Application Access Policy method.
1. Overview
At the end of this guide you'll have:
- An Entra app registration with a client secret.
- A service principal registered in both Entra and Exchange Online.
- A management scope that targets exactly the mailbox Smart Mailbox will read.
- An RBAC role assignment binding the app to
Mail.ReadWrite— the minimum required for Smart Mailbox, since it needs to move each processed message into a "Processed" folder. Optionally addMail.Sendif outbound is needed. - A verified
curlrequest that returns mail from the allowed mailbox and is denied for every other mailbox.
1.1 Prerequisites
- Microsoft 365 tenant with Exchange Online.
- Admin roles on your account:
- Application Administrator or Global Administrator in Entra (to register the app).
- Exchange Administrator or Organization Management in Exchange (to configure RBAC).
- Target mailbox is a licensed Exchange Online mailbox (
RecipientTypeDetails = UserMailboxorSharedMailbox). - PowerShell 7+ with
ExchangeOnlineManagement(both paths):
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
Optionally, if you prefer PowerShell over the Entra portal for looking up the service principal Object ID (Section 4.2):
Install-Module -Name Microsoft.Graph.Applications -Scope CurrentUser
Installing just Microsoft.Graph.Applications (~30 MB) instead of the full Microsoft.Graph meta-module (~500 MB) is sufficient and much lighter.
1.2 Approach
Two valid paths reach the same end state.
| Path | Steps | PowerShell modules required | Trade-off |
|---|---|---|---|
| A. Standard | Grant Mail.ReadWrite tenant-wide in Entra portal → configure RBAC scope in Exchange → revoke the tenant-wide grant | ExchangeOnlineManagement only | Simple, click-driven. Brief window where the app has tenant-wide access. |
| B. Zero-exposure | Register the app with no mail permissions → look up the auto-created SP → configure RBAC scope in Exchange | ExchangeOnlineManagement only (portal lookup) or + Microsoft.Graph.Applications (PowerShell lookup) | App is never over-privileged, even briefly. |
Key point: the service principal is auto-created by Entra the moment you register the app — no admin consent required. So Path B does not actually need the Microsoft.Graph PowerShell SDK if you're willing to grab the SP Object ID from the Entra portal (Section 4.1). Both paths can be completed with only ExchangeOnlineManagement.
This guide walks through Path B as the primary flow. Path A is documented in Section 9 as an alternative.
2. Register the Application in Entra
📚 Microsoft reference: Quickstart: Register an application with the Microsoft identity platform
- Go to entra.microsoft.com → Applications → App registrations → New registration.
- Name: e.g.
Smart Mailbox Reader. - Supported account types: Accounts in this organizational directory only (single tenant).
- Redirect URI: leave blank.
- Click Register.
Once registered, copy from the Overview page:
- Application (client) ID — you'll use this as
$CLIENT_ID. - Directory (tenant) ID — you'll use this as
$TENANT_ID.
The Microsoft doc shows exactly where to find these IDs — see the "Enterprise applications page" image in the Application RBAC — Service Principals section. The Application ID is the AppId; the Object ID is the ServiceId (you'll grab it in Section 4).
3. Create a Client Secret
📚 Microsoft reference: Add credentials to your application
- In the app registration → left nav → Certificates & secrets → New client secret.
- Description:
smart-mailbox-secret. - Expiration: choose per your rotation policy (6, 12, or 24 months).
- Click Add.
- Immediately copy the "Value" — this is the only chance you'll get. Save it as
$CLIENT_SECRET.
Do not add any Graph API permissions yet. That's the whole point of Path B — no tenant-wide grant is ever created.
For higher-security deployments, prefer certificate credentials over client secrets — same flow, different credential type on the token request.
4. Get the Service Principal Object ID
📚 Microsoft reference: Application and service principal objects in Microsoft Entra ID
Entra auto-creates the Enterprise Application (service principal) the moment you register an app, even before you grant any permission or admin consent. So you almost never need to create it — you just need to look up its Object ID. New-MgServicePrincipal is only needed if the auto-created SP was manually deleted, or in restricted tenants that disable auto-creation.
Pick either method:
4.1 Portal lookup (no PowerShell needed)
- Entra → Enterprise applications (left nav)
- Search for your app name → click it
- Overview → copy the Object ID
That's your SP_OBJECT_ID. Skip to Section 5.
4.2 PowerShell lookup (requires Microsoft.Graph.Applications)
Only needed for this section. If you'd rather avoid installing the Microsoft.Graph SDK, use Section 4.1 instead.
Install-Module Microsoft.Graph.Applications -Scope CurrentUser
If Connect-MgGraph fails with an MSAL / Method not found error (common on macOS/Linux when Az modules are also installed), fall back to Section 4.1.
Connect-MgGraph -Scopes "Application.Read.All"
(Get-MgServicePrincipal -Filter "AppId eq '<your-client-id>'").Id
Copy the returned GUID — that's your SP_OBJECT_ID.
If nothing is returned (rare — SP was deleted or auto-creation is disabled), create it:
Connect-MgGraph -Scopes "Application.ReadWrite.All"
New-MgServicePrincipal -AppId <your-client-id>
(Get-MgServicePrincipal -Filter "AppId eq '<your-client-id>'").Id
New-MgServicePrincipalThe service principal cannot be created … name is already in use. means the SP already exists — exactly what you want. Just run the Get-MgServicePrincipal lookup above.
5. Configure Exchange Online RBAC
This section requires the ExchangeOnlineManagement module. Install once:
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
📚 Microsoft reference: Role Based Access Control for Applications in Exchange Online
5.1 Connect to Exchange Online
📚 Connect to Exchange Online PowerShell
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com
Complete the browser sign-in prompt.
5.2 Register the service principal in Exchange
Exchange keeps its own pointer to Entra service principals. Register it:
New-ServicePrincipal `
-AppId <your-client-id> `
-ObjectId <service-principal-object-id> `
-DisplayName "Smart Mailbox Reader"
-ObjectId is the current, correct parameter. Older docs and error messages may reference -ServiceId, which is deprecated.
Verify:
Get-ServicePrincipal -Identity <your-client-id>
5.3 Create a management scope
📚 New-ManagementScope cmdlet · Application RBAC — Management Scopes
A management scope tells Exchange which mailboxes the app is allowed to touch. Use a recipient filter to target exactly one mailbox:
New-ManagementScope `
-Name "SmartMailboxScope" `
-RecipientRestrictionFilter "PrimarySmtpAddress -eq 'smartmailbox@yourdomain.com'"
Alternative filter forms:
# Multiple mailboxes by group membership (direct members only)
-RecipientRestrictionFilter "MemberOfGroup -eq 'CN=MyGroup,OU=...'"
# By custom attribute
-RecipientRestrictionFilter "CustomAttribute1 -eq 'smart-mailbox'"
# By department
-RecipientRestrictionFilter "Department -eq 'Support'"
See the filterable recipient properties reference for all supported fields.
5.4 Assign the scoped role
📚 New-ManagementRoleAssignment cmdlet
Bind the app to a built-in application role, restricted by the scope you just created.
For Smart Mailbox, Application Mail.ReadWrite is the minimum required role. Smart Mailbox moves each processed message into a "Processed" (or equivalent) folder after handling it, and move-to-folder updates the message's parentFolderId — a write operation. Mail.Read and Mail.ReadBasic cannot move messages and are not sufficient.
New-ManagementRoleAssignment `
-App <your-client-id> `
-Role "Application Mail.ReadWrite" `
-CustomResourceScope "SmartMailboxScope"
All application role choices:
| Role Name | Graph Permission | Use When |
|---|---|---|
Application Mail.ReadBasic | Mail.ReadBasic | Not sufficient for Smart Mailbox — read headers only, no folder moves |
Application Mail.Read | Mail.Read | Not sufficient for Smart Mailbox — read only, no folder moves |
Application Mail.ReadWrite | Mail.ReadWrite | Minimum required for Smart Mailbox — read + move between folders + mark read/unread + delete |
Application Mail.Send | Mail.Send | Send mail as the user (add as a second assignment only if outbound is needed) |
Application Mail Full Access | Mail.ReadWrite + Mail.Send | Combines ReadWrite and Send in one assignment |
Start with just Application Mail.ReadWrite. Add Application Mail.Send as a second role assignment (same scope) when you actually build outbound flows. This keeps blast radius small and makes it easy to revoke send access later without touching read/move.
# Add later, if/when Smart Mailbox needs to send
New-ManagementRoleAssignment `
-App <your-client-id> `
-Role "Application Mail.Send" `
-CustomResourceScope "SmartMailboxScope"
Alternatively, assign Application Mail Full Access as a single role that covers both.
Full list: Supported Application Roles.
6. Verify the Configuration
📚 Microsoft reference: Test-ServicePrincipalAuthorization cmdlet
This cmdlet bypasses the permission cache, so results are immediate.
# Allowed mailbox — should show InScope: True
Test-ServicePrincipalAuthorization -Identity <your-client-id> -Resource smartmailbox@yourdomain.com | Format-Table
# Any other mailbox — should show InScope: False
Test-ServicePrincipalAuthorization -Identity <your-client-id> -Resource other@yourdomain.com | Format-Table
Expected output:
RoleName GrantedPermissions AllowedResourceScope ScopeType InScope
-------- ------------------ -------------------- --------- -------
Application Mail.ReadWrite Mail.ReadWrite SmartMailboxScope CustomRecipientScope True
If InScope is True for the allowed mailbox and False for others, RBAC is wired correctly.
7. Test with a Real Graph API Call
📚 Microsoft references:
- Get access without a user (client credentials flow)
- List messages — Microsoft Graph API
- Use query parameters in Microsoft Graph
7.1 Acquire an app-only access token
TENANT_ID="your-tenant-id"
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "client_id=${CLIENT_ID}" \
-d "client_secret=${CLIENT_SECRET}" \
-d "scope=https://graph.microsoft.com/.default" \
-d "grant_type=client_credentials" \
| jq -r .access_token)
echo "$TOKEN" | cut -c1-40 # sanity check
The scope MUST be https://graph.microsoft.com/.default. Do not list individual permissions. Tokens live ~1 hour — cache and reuse them.
7.2 Read from the allowed mailbox (should succeed)
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages?\$top=5&\$select=subject,from,receivedDateTime,bodyPreview&\$orderby=receivedDateTime%20desc" \
| jq .
7.3 Read from a different mailbox (should be denied)
curl -s -w "\nHTTP %{http_code}\n" -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/other@yourdomain.com/messages?\$top=1"
Expected response:
{
"error": {
"code": "ErrorAccessDenied",
"message": "Access is denied. Check credentials and try again."
}
}
HTTP 403
8. Common Graph Query Patterns
8.1 Paginate with @odata.nextLink
URL="https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages?\$top=25&\$select=id,subject,receivedDateTime&\$orderby=receivedDateTime%20desc"
while [ -n "$URL" ] && [ "$URL" != "null" ]; do
RESP=$(curl -s -H "Authorization: Bearer $TOKEN" "$URL")
echo "$RESP" | jq '.value[] | {subject, receivedDateTime}'
URL=$(echo "$RESP" | jq -r '."@odata.nextLink" // empty')
done
8.2 Unread messages in Inbox only
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/mailFolders/Inbox/messages?\$filter=isRead%20eq%20false&\$top=25&\$select=id,subject,from,receivedDateTime&\$orderby=receivedDateTime%20desc" \
| jq .
8.3 Delta query — sync only what changed since last poll
📚 Get incremental changes to messages
# Initial call — save the @odata.deltaLink from the response
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/mailFolders/Inbox/messages/delta?\$select=id,subject,receivedDateTime" \
| jq .
On subsequent polls, call the saved @odata.deltaLink URL to get only new/changed/deleted messages. This is the recommended pattern for Smart Mailbox polling.
8.4 Move a message to a folder
Requires Application Mail.ReadWrite.
MSG_ID="AAMk..."
# Move to a well-known folder (no lookup needed)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "destinationId": "archive" }' \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages/${MSG_ID}/move" \
| jq .
Well-known folder names you can use directly as destinationId (no lookup needed): inbox, archive, sentitems, deleteditems, drafts, junkemail.
To move to a custom folder, first look up its ID:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/mailFolders?\$select=id,displayName" \
| jq '.value[] | {displayName, id}'
Then use that id as the destinationId in the move body.
Create a custom folder if it doesn't exist:
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "displayName": "Processed by Smart Mailbox" }' \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/mailFolders" \
| jq .
8.5 Mark a message read / unread
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "isRead": true }' \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages/${MSG_ID}"
8.6 Send an email (optional, requires Application Mail.Send)
Only works if you added the Application Mail.Send role assignment. Returns 202 Accepted with no body on success.
curl -s -w "\nHTTP %{http_code}\n" -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": {
"subject": "Ticket update from Smart Mailbox",
"body": { "contentType": "Text", "content": "Your ticket has been received." },
"toRecipients": [{ "emailAddress": { "address": "customer@example.com" } }]
},
"saveToSentItems": true
}' \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/sendMail"
8.7 Fetch one message with body and attachments
MSG_ID="AAMk..." # from a list call
# Full message
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages/${MSG_ID}" \
| jq .
# Attachments
curl -s -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/users/smartmailbox@yourdomain.com/messages/${MSG_ID}/attachments" \
| jq .
9. Alternative — Standard Flow With Revoke Step (Path A)
Path A needs only the ExchangeOnlineManagement module — the Microsoft.Graph PowerShell SDK is not required. This makes Path A the easier choice if the Microsoft.Graph module is not already installed or is failing to load.
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
If you prefer the click-driven Entra UI path (no Microsoft.Graph module needed):
- In the Entra app registration → API permissions → Add a permission → Microsoft Graph → Application permissions.
- Add
Mail.ReadWrite(or whichever role you plan to assign in Section 5.4). - Click Grant admin consent for <tenant>. This provisions the Enterprise Application and grants tenant-wide access.
- Go to Entra → Enterprise applications → search your app → Overview → copy the Object ID. That's your
SP_OBJECT_IDfor Section 5.2 — no PowerShell lookup required. - Complete Sections 5–7 above (all in the
ExchangeOnlineManagementmodule). - Revoke the tenant-wide grant: Entra → Enterprise applications → your app → Permissions → find the
Mail.ReadWritegrant → Revoke permission. - Verify with a Graph call to a non-allowed mailbox — should now return 403.
The end state is identical to Path B. The only difference is the brief window in Steps 1–6 where the app has tenant-wide access.
10. Adding More Mailboxes Later
Update the scope filter — no need to touch the app or role assignment:
Set-ManagementScope `
-Identity "SmartMailboxScope" `
-RecipientRestrictionFilter "PrimarySmtpAddress -eq 'smartmailbox@yourdomain.com' -or PrimarySmtpAddress -eq 'support@yourdomain.com'"
For a growing list, switch to a group-based filter:
# 1. Create a mail-enabled security group and add allowed mailboxes
New-DistributionGroup -Name "SmartMailboxAllowed" -Type security -PrimarySmtpAddress smart-allowed@yourdomain.com
Add-DistributionGroupMember -Identity smart-allowed@yourdomain.com -Member smartmailbox@yourdomain.com
# 2. Get the group's distinguished name
$dn = (Get-Group smart-allowed@yourdomain.com).DistinguishedName
# 3. Update the scope
Set-ManagementScope -Identity "SmartMailboxScope" -RecipientRestrictionFilter "MemberOfGroup -eq '$dn'"
Now adding a mailbox is just Add-DistributionGroupMember — no scope changes needed.
MemberOfGroup only matches direct members. Nested group membership is not considered.
11. Troubleshooting
11.1 403 ErrorAccessDenied on the allowed mailbox
The RBAC permission cache lives 30 minutes to 2 hours. If you just made changes, wait or acquire a new token. Test-ServicePrincipalAuthorization bypasses the cache and confirms whether the config itself is correct.
11.2 504 with empty UnknownError
- Almost always a transient Exchange backend timeout, especially on the first call to a mailbox. Retry 2–3 times with a few seconds' delay.
- Always use
$top(start at 25) and$selectto narrow the request — plain/messagescalls without limits are the most common trigger. - Reference the mailbox by user object ID instead of UPN to skip the UPN resolver.
11.3 401 InvalidAuthenticationToken
- Token expired (they live ~1 hour). Regenerate with the token endpoint call.
- Confirm
scope=https://graph.microsoft.com/.defaultandgrant_type=client_credentials.
11.4 Both mailboxes return 200
You still have a tenant-wide grant in Entra that is overriding your RBAC scope. Go to Entra → Enterprise applications → your app → Permissions and revoke any Mail.* Application permissions. The Entra grant and the RBAC grant are unioned, not intersected.
11.5 New-ServicePrincipal fails with "cannot find the service principal"
The Entra service principal object doesn't exist yet. Run New-MgServicePrincipal -AppId <client-id> (Section 4) first.
11.6 Test cmdlet returns nothing
The role assignment didn't take. Confirm with:
Get-ManagementRoleAssignment -RoleAssignee <your-client-id> | Format-Table Role, CustomResourceScope
12. Security Considerations
-
Secret rotation. Client secrets should rotate on a schedule (6–12 months). For production, prefer certificate authentication over client secrets.
-
Least privilege.
Application Mail.ReadWriteis the minimum role for Smart Mailbox — moving processed messages into a "Processed" (or equivalent) folder is a write operation, soMail.Read/Mail.ReadBasicare insufficient. Only addApplication Mail.Sendwhen outbound is actually implemented — do not grant it "just in case." -
Audit access. Application access to mailboxes appears in Unified Audit Log. Enable audit logging if not already on:
Get-Mailbox smartmailbox@yourdomain.com | Select-Object AuditEnabled
Set-Mailbox smartmailbox@yourdomain.com -AuditEnabled $true -
Never commit
CLIENT_SECRETto source control. Store it in a secret manager (Azure Key Vault, HashiCorp Vault, cloud provider secret store, etc.). -
Scope changes propagate slowly. Plan for up to 2 hours between RBAC changes and them being enforced at runtime, even though the config takes effect immediately.
13. Teardown / Reversal
Use this to undo the RBAC configuration cleanly — e.g. when redoing setup, decommissioning the app, or changing which mailbox is in scope.
Order matters: remove the role assignment first (it references the scope and the service principal), then the scope, then the service principal.
📚 Remove-ManagementRoleAssignment · Remove-ManagementScope · Remove-ServicePrincipal
13.1 Prerequisites
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com
Substitute your own values below:
<your-client-id>— your app's Application (client) ID.SmartMailboxScope— the scope name you used in Section 5.3.
13.2 Find and remove the role assignment
Get-ManagementRoleAssignment -RoleAssignee <your-client-id> |
Select-Object Name, Role, CustomResourceScope
Example output:
Name Role CustomResourceScope
---- ---- -------------------
Application Mail.ReadWrite-d33d3492-95ed-4b4f-84e6-4eb81a368a6e Application Mail.ReadWrite SmartMailboxScope
Remove it by exact Name:
Remove-ManagementRoleAssignment -Identity "Application Mail.ReadWrite-<sp-object-id>" -Confirm:$false
If you have multiple assignments (e.g. also Application Mail.Send), remove each by name — or use the bulk form below.
Or in one shot, remove every assignment for this app:
Get-ManagementRoleAssignment -RoleAssignee <your-client-id> |
Remove-ManagementRoleAssignment -Confirm:$false
13.3 Remove the management scope
Remove-ManagementScope -Identity "SmartMailboxScope" -Confirm:$false
If this fails with "the scope is in use," Section 13.2 didn't fully clean up. Re-run the Get-ManagementRoleAssignment query and remove any stragglers.
13.4 Remove the Exchange service principal pointer
Remove-ServicePrincipal -Identity <your-client-id> -Confirm:$false
This only removes Exchange's pointer object. The Entra app registration, its client secret, and the Enterprise Application object are all untouched — you can immediately re-run Sections 5–7 to reconfigure with different scope/role choices.
13.5 Verify the teardown
All four commands should return nothing:
Get-ManagementRoleAssignment -RoleAssignee <your-client-id>
Get-ManagementScope -Identity "SmartMailboxScope" -ErrorAction SilentlyContinue
Get-ServicePrincipal -Identity <your-client-id> -ErrorAction SilentlyContinue
Test-ServicePrincipalAuthorization -Identity <your-client-id> -Resource smartmailbox@yourdomain.com -ErrorAction SilentlyContinue
13.6 Full decommission (also remove Entra side)
If you're retiring the app entirely — not just the RBAC config:
- Revoke any Entra Graph consents — Entra admin center → Enterprise applications → your app → Permissions → Revoke each.
- Delete the Enterprise Application — same page → Properties → Delete.
- Delete the App registration — App registrations → your app → Overview → Delete.
- Rotate/delete the client secret if it was ever exposed — App registration → Certificates & secrets → delete the secret.
13.7 Runtime cache note
RBAC changes take effect immediately in Test-ServicePrincipalAuthorization, but the runtime permission cache lives 30 minutes to 2 hours. An app with an active access token may still succeed against a mailbox for up to 2 hours after you remove its assignment. Force a new token acquisition (restart the caller / clear its token cache) to see the change immediately.
14. References
- Role Based Access Control for Applications in Exchange Online
- New-ServicePrincipal (Exchange PowerShell)
- New-ManagementScope (Exchange PowerShell)
- New-ManagementRoleAssignment (Exchange PowerShell)
- Test-ServicePrincipalAuthorization (Exchange PowerShell)
- Microsoft Graph mail API reference
- Filterable recipient properties
- Related: Smart Mailbox configuration