Let an app send mail from the mailboxes you choose, and no others
Why this matters
CRMs, newsletter tools, ticketing systems and monitoring platforms send email through Microsoft Graph as an application, without a signed-in user. Granted the usual way in Microsoft Entra ID, the Mail.Send application permission is tenant-wide: the app can send as any mailbox in your organisation, your CEO's included. A leaked client secret then becomes a perfect phishing tool, sending from addresses everyone trusts.
Nothing in the Entra portal warns you about this. The fix is a few lines of Exchange Online PowerShell that limit the app to a group of mailboxes you choose, for example newsletter@ and sales@. Microsoft's current way to do it is RBAC for Applications, which replaces the older Application Access Policies (covered at the end, for tenants that still use them).
What you need
- An account with the Exchange Administrator role in Entra ID, which is a member of the Organization Management role group in Exchange Online.
- Rights to register applications in Entra ID (Application Administrator or Global Administrator).
- PowerShell 7 with the Exchange Online module:
Install-Module ExchangeOnlineManagement -Scope CurrentUser
In the commands below, replace contoso.com and the example names with your own.
1. Create the sending mailbox
Use a shared mailbox: it needs no licence and nobody signs in to it. In the Exchange admin centre: Recipients → Mailboxes → Add a shared mailbox, or in PowerShell:
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
New-Mailbox -Shared -Name "Newsletter" -DisplayName "Contoso Newsletter" -PrimarySmtpAddress newsletter@contoso.com
Then block sign-in for its account: Microsoft 365 admin centre → Users → Active users → Newsletter → Block sign-in. The app never uses a password, so nothing breaks.
2. Put the allowed mailboxes in one group
One mail-enabled security group holds every mailbox the app may use. Adding or removing a member later is how you widen or narrow the app's reach.
New-DistributionGroup -Name "App mailboxes - Mailer" -Type Security -PrimarySmtpAddress app-mailboxes@contoso.com
Add-DistributionGroupMember -Identity app-mailboxes@contoso.com -Member newsletter@contoso.com
Add-DistributionGroupMember -Identity app-mailboxes@contoso.com -Member sales@contoso.com
Only direct members count: a mailbox inside a nested group is out of scope.
3. Register the application in Entra ID
- Entra admin centre → Applications → App registrations → New registration. Name it, choose Accounts in this organizational directory only, and leave the redirect URI empty: the app never signs a user in.
- On its Overview page, note the Application (client) ID and the Directory (tenant) ID.
- Certificates & secrets: upload a certificate (preferred), or create a client secret of at most 24 months and copy its Value at once (not the Secret ID). Put the expiry date in someone's calendar.
- Do not add
Mail.Sendor any other mail permission under API permissions. The Exchange assignment below grants it, scoped. A grant in Entra would be added on top of it, unscoped, and undo the whole exercise. - Entra admin centre → Applications → Enterprise applications → your app: note its Object ID. Use this page, not App registrations, which shows a different object ID.
4. Scope the app in Exchange Online
Four commands: a pointer to the app, a scope made from the group, the role assignment, and a check.
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
$appId = "<Application (client) ID>"
$objectId = "<Object ID from Enterprise applications>"
# 1. Tell Exchange about the app
New-ServicePrincipal -AppId $appId -ObjectId $objectId -DisplayName "Contoso Mailer"
# 2. A scope that contains exactly the group's members
$dn = (Get-DistributionGroup app-mailboxes@contoso.com).DistinguishedName
New-ManagementScope -Name "Contoso Mailer mailboxes" -RecipientRestrictionFilter "MemberOfGroup -eq '$dn'"
# 3. Allow sending, only within that scope
New-ManagementRoleAssignment -App $objectId -Role "Application Mail.Send" -CustomResourceScope "Contoso Mailer mailboxes"
# Only if the app must also read the mailboxes (for example to match replies):
New-ManagementRoleAssignment -App $objectId -Role "Application Mail.Read" -CustomResourceScope "Contoso Mailer mailboxes"
Grant only the roles the app needs. Get-ManagementRole | Where-Object Name -like "Application *" lists them all.
5. Test it both ways
A test that only checks the allowed mailbox proves nothing: an unscoped app passes it too. Ask about a mailbox the app must not reach as well.
Test-ServicePrincipalAuthorization -Identity $objectId -Resource newsletter@contoso.com | Format-Table
# InScope: True (allowed)
Test-ServicePrincipalAuthorization -Identity $objectId -Resource ceo@contoso.com | Format-Table
# InScope: False (refused, as it must be)
Then make sure no unscoped grant is left in Entra ID: Enterprise applications → your app → Permissions must show no Mail.* application permission. If one is there, revoke it: grants from the two systems add up.
To prove it end to end, send a real message as the app. Changes reach live calls after 30 minutes to 2 hours (the test command above sees them at once), so a refusal right after setup may just be the cache.
$tenant = "<Directory (tenant) ID>"
$secret = "<client secret Value>"
$token = (Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$tenant/oauth2/v2.0/token" -Body @{
client_id = $appId; client_secret = $secret; grant_type = "client_credentials"
scope = "https://graph.microsoft.com/.default" }).access_token
$headers = @{ Authorization = "Bearer $token" }
$mail = @{ message = @{ subject = "App scope test"; body = @{ contentType = "Text"; content = "Sent by the app." }
toRecipients = @(@{ emailAddress = @{ address = "you@contoso.com" } }) } } | ConvertTo-Json -Depth 6
# Allowed: accepted without output, and the message arrives
Invoke-RestMethod -Method Post -Headers $headers -ContentType "application/json" -Body $mail `
-Uri "https://graph.microsoft.com/v1.0/users/newsletter@contoso.com/sendMail"
# Not allowed: must fail with 403 ErrorAccessDenied
Invoke-RestMethod -Method Post -Headers $headers -ContentType "application/json" -Body $mail `
-Uri "https://graph.microsoft.com/v1.0/users/ceo@contoso.com/sendMail"
Remove-Variable secret, token # do not leave the secret in your session
Adding or removing a mailbox later
Change the group, nothing else, then run the test for that mailbox:
Add-DistributionGroupMember -Identity app-mailboxes@contoso.com -Member events@contoso.com
Remove-DistributionGroupMember -Identity app-mailboxes@contoso.com -Member sales@contoso.com -Confirm:$false
Test-ServicePrincipalAuthorization -Identity $objectId -Resource events@contoso.com | Format-Table
When something fails
| Symptom | Usual cause |
|---|---|
AADSTS7000215 invalid client secret | The Secret ID was copied instead of its Value |
| Token works, the call returns 403 | The mailbox is not a direct member of the group, or the change has not reached the cache yet (up to 2 hours) |
| The app reaches mailboxes outside the group | A Mail.* application permission is still consented in Entra ID: revoke it |
New-ServicePrincipal refuses the IDs | The object ID came from App registrations, not Enterprise applications |
| 404 on the mailbox | The address is a distribution list or a typo, not a mailbox |
Already using an Application Access Policy?
Many tenants scoped their apps the older way. It still works, but Microsoft has marked it legacy and asks new setups to use RBAC for Applications. For reference, the legacy commands (they constrain a Mail.Send grant that is consented in Entra ID):
New-ApplicationAccessPolicy -AppId $appId -PolicyScopeGroupId app-mailboxes@contoso.com `
-AccessRight RestrictAccess -Description "Mailer: chosen mailboxes only"
Test-ApplicationAccessPolicy -Identity newsletter@contoso.com -AppId $appId # Granted
Test-ApplicationAccessPolicy -Identity ceo@contoso.com -AppId $appId # Denied
To move to RBAC for Applications without interruption: create the scope and the role assignment above (steps 4 and 5) for the same group, revoke the Mail.* consent in Entra ID, then remove the old policy with Remove-ApplicationAccessPolicy.
Need a hand?
Pro CISO® reviews Microsoft 365 tenants for exactly this kind of quiet over-permission: apps, mail flow, conditional access and admin roles. Ask us, or read the rest of our security news.
Sources: Microsoft Learn, Role Based Access Control for Applications in Exchange Online and Application Access Policies (legacy).