Skip to main content

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 add Mail.Send if outbound is needed.
  • A verified curl request 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 = UserMailbox or SharedMailbox).
  • 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.

PathStepsPowerShell modules requiredTrade-off
A. StandardGrant Mail.ReadWrite tenant-wide in Entra portal → configure RBAC scope in Exchange → revoke the tenant-wide grantExchangeOnlineManagement onlySimple, click-driven. Brief window where the app has tenant-wide access.
B. Zero-exposureRegister the app with no mail permissions → look up the auto-created SP → configure RBAC scope in ExchangeExchangeOnlineManagement 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

  1. Go to entra.microsoft.comApplicationsApp registrationsNew registration.
  2. Name: e.g. Smart Mailbox Reader.
  3. Supported account types: Accounts in this organizational directory only (single tenant).
  4. Redirect URI: leave blank.
  5. 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.
Screenshot reference

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

  1. In the app registration → left nav → Certificates & secretsNew client secret.
  2. Description: smart-mailbox-secret.
  3. Expiration: choose per your rotation policy (6, 12, or 24 months).
  4. Click Add.
  5. Immediately copy the "Value" — this is the only chance you'll get. Save it as $CLIENT_SECRET.
caution

Do not add any Graph API permissions yet. That's the whole point of Path B — no tenant-wide grant is ever created.

Production recommendation

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

Good news — the SP already exists

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)

  1. Entra → Enterprise applications (left nav)
  2. Search for your app name → click it
  3. Overview → copy the Object ID

That's your SP_OBJECT_ID. Skip to Section 5.

4.2 PowerShell lookup (requires Microsoft.Graph.Applications)

Optional module

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
409 Conflict on New-MgServicePrincipal

The 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

Required module

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

📚 New-ServicePrincipal cmdlet

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"
Parameter name note

-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 NameGraph PermissionUse When
Application Mail.ReadBasicMail.ReadBasicNot sufficient for Smart Mailbox — read headers only, no folder moves
Application Mail.ReadMail.ReadNot sufficient for Smart Mailbox — read only, no folder moves
Application Mail.ReadWriteMail.ReadWriteMinimum required for Smart Mailbox — read + move between folders + mark read/unread + delete
Application Mail.SendMail.SendSend mail as the user (add as a second assignment only if outbound is needed)
Application Mail Full AccessMail.ReadWrite + Mail.SendCombines ReadWrite and Send in one assignment
Add Send only when you need it

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:

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
note

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

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

📚 Move a message — Graph API

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)

📚 Send mail — Graph API

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)

Required module

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):

  1. In the Entra app registration → API permissionsAdd a permissionMicrosoft GraphApplication permissions.
  2. Add Mail.ReadWrite (or whichever role you plan to assign in Section 5.4).
  3. Click Grant admin consent for <tenant>. This provisions the Enterprise Application and grants tenant-wide access.
  4. Go to Entra → Enterprise applications → search your app → Overview → copy the Object ID. That's your SP_OBJECT_ID for Section 5.2 — no PowerShell lookup required.
  5. Complete Sections 5–7 above (all in the ExchangeOnlineManagement module).
  6. Revoke the tenant-wide grant: Entra → Enterprise applications → your app → Permissions → find the Mail.ReadWrite grant → Revoke permission.
  7. 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.

caution

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 $select to narrow the request — plain /messages calls 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/.default and grant_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.ReadWrite is the minimum role for Smart Mailbox — moving processed messages into a "Processed" (or equivalent) folder is a write operation, so Mail.Read / Mail.ReadBasic are insufficient. Only add Application Mail.Send when 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_SECRET to 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
note

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:

  1. Revoke any Entra Graph consents — Entra admin center → Enterprise applications → your app → Permissions → Revoke each.
  2. Delete the Enterprise Application — same page → PropertiesDelete.
  3. Delete the App registrationApp registrations → your app → OverviewDelete.
  4. 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