Skip to content

Anti-spam Outbound

This policy limits how many emails your users can send in an hour and a day to help prevent a user from sending spam or phishing campaigns out which can result in your domain being blocked by other domains.

Changes to sending limits

On February 24th, 2025, Microsoft announced changes to sending limits that will go into effect from April 3rd, 2025 to May 1st, 2025. The date targeted is dependent on how many licenses you have.

Essentially there will be a tenant outbound limit along with the user outbound limit that the outbound anti-spam policy applies to. Micrososft on the blog post states consistently this is only going to impact a small subset of organizations based on their telemetry.

At this time it is unclear to me how this may impact this policy, or if it even will since it seems like both rate limits are in place. I do believe this rate limit is even more significant now because smaller orgs getting compromised with no limits set will be limited from sending externally fairly consistently.

I have some opinions and thoughts in a blog post here.

Quickconfig

Quick configs provide a list of settings, Microsoft's recommendation, my typical recommendation, links to Microsoft's documentation, and my documentation. These are provided for convenience and educational purposes, consider your scenario and testing before using these in your environment.

Setting Msft Standard Rec Typical Recomendation MSDoc
Restrict sending to external recipients (per hour) 500 300 Docs
Restrict sending to internal recipients (per hour) 1000 300 Docs
Maximum recipient limit per day 1000 500 Docs
Over limit action Restrict the user from sending mail Restrict the user from sending mail Docs
Automatic forwarding Automatic - System-controlled Automatic - System-controlled Docs
Send a copy of messages that exceed these limits No recommendation Org preference Docs
Notify if a sender is blocked due to sending outbound spam No recommendation Avoid Docs

Considerations before deployment

Do your users have access to a bulk sending mail service? Exchange is not considered a proper tool for bulk mailing unless you are using Microsoft's High volume email service. At the time of writing this is in preview and has a 2k external email limit per day, so you will likely still be looking at a 3rd party email service to properly send bulk/marketing email.

Having such a limit determines how strict you may be able to get with this policy.

Policy Configuration

For outbound spam policies, you can set thresholds for sending and audit people hitting that threshold to identify bulk senders in your environment.

Scoping users, groups, and domains

Pay attention to the conditions on custom policies, they are evaluated together because of the AND statements. If you put a user and a group, the user must be a member of the group and anyone else who is a member of that group will not be included in this policy unless they are listed on the user list.

Distribution lists

Distribution lists created for your organization count as 1 recipient, while distribution contacts created in a user's mailbox will count for each email in the list.

Consider the following, if a user created DL contains 1,000 user emails when an email is sent to that DL it counts as 1,000 user recipients. If they use the organization created list, this counts as 1 recipient.

flowchart LR

k[1,000 users]
odl[Organization created DL]
udl[User created DL]
1r[1 recipient]
1kr[1,000 recipients]

k-->odl
k-->udl
odl-->1r
udl-->1kr

Restrict sending to external recipients (per hour)

You can use a value of 0-10,000 here, but a value of 0 takes the service limits that apply to your tenant. For most people this is 10,000 messages. Anecdotally this generally seems to be about 1,000 messages before a user hits the limit.

KQL for the External Threshold

Use the below query in Advanced Hunting to find users who may be sending bulk email or to help determine a good sending threshold in your environment by adjusting the externalHourlyLimit. It is pulling the last 30 days of EmailEvents but if you have Sentinel setup and are ingesting the EmailEvents table you likely have 90 or more days of data you can use in your hunt.

Lastly, this query groups emails by Subject, this can be helpful to identify the types of bulk emails being sent but does not give a true perspective of people who may "violate" the hourly threshold. Remove Subject on line 12 to get distinct sent email count for a more accurate number.

let externalHourlyLimit = 300;
// Get sent emails that weren't forwarded
let emails = EmailEvents
    // Filter to Sent items that weren't forwarded
    | where TimeGenerated > ago(30d)
    | where EmailDirection == 'Outbound' and isempty(ForwardingInformation)
    | extend LimitRecipient = iif(isempty(DistributionList), RecipientEmailAddress, DistributionList)
    // Account for users sending the same message to the same recipient twice when not using a Organization Distribution List
    | extend UniqueRecipient = iif(isempty(DistributionList), strcat(NetworkMessageId, LimitRecipient), strcat(EmailClusterId, LimitRecipient))
    | project TimeGenerated, UniqueRecipient, SenderFromAddress, Subject, EmailDirection;
emails
// Using 90 minutes instead of an hour because the messages could span across 2 hours depending on when they start
| summarize RecipientCount = dcount(UniqueRecipient) by SenderFromAddress, Subject, EmailDirection, bin(TimeGenerated, 90m)
| where RecipientCount >= externalHourlyLimit
| sort by RecipientCount desc

Restrict sending to internal recipients (per hour)

You can use a value of 0-10,000 here, but a value of 0 takes the service limits that apply to your tenant. For most people this is 10,000 messages. Anecdotally this generally seems to be about 1,000 messages before a user hits the limit.

KQL for the Internal Threshold

Use the below query in Advanced Hunting to find users who may be sending bulk email or to help determine a good sending threshold in your environment by adjusting the internalHourlyLimit. It is pulling the last 30 days of EmailEvents but if you have Sentinel setup and are ingesting the EmailEvents table you likely have 90 or more days of data you can use in your hunt.

Lastly, this query groups emails by Subject, this can be helpful to identify the types of bulk emails being sent but does not give a true perspective of people who may "violate" the hourly threshold. Remove Subject on line 12 to get distinct sent email count for a more accurate number.

let internalHourlyLimit = 300;
// Get sent emails that weren't forwarded
let emails = EmailEvents
    // Filter to Sent items that weren't forwarded
    | where TimeGenerated > ago(30d)
    | where EmailDirection == 'Intra-org' and isempty(ForwardingInformation)
    | extend LimitRecipient = iif(isempty(DistributionList), RecipientEmailAddress, DistributionList)
    // Account for users sending the same message to the same recipient twice when not using a Organization Distribution List
    | extend UniqueRecipient = iif(isempty(DistributionList), strcat(NetworkMessageId, LimitRecipient), strcat(EmailClusterId, LimitRecipient))
    | project TimeGenerated, UniqueRecipient, SenderFromAddress, Subject, EmailDirection;
emails
// Using 90 minutes instead of an hour because the messages could span across 2 hours depending on when they start
| summarize RecipientCount = dcount(UniqueRecipient) by SenderFromAddress, Subject, EmailDirection, bin(TimeGenerated, 90m)
| where RecipientCount >= internalHourlyLimit
| sort by RecipientCount desc

Maximum recipient limit per day

You can use a value of 0-10,000 here, but a value of 0 takes the service limits that apply to your tenant. For most people this is 10,000 messages. Anecdotally this generally seems to be about 1,000 messages before a user hits the limit.

KQL for the Daily Threshold

Use the below query in Advanced Hunting to find users who may be sending bulk email or to help determine a good sending threshold in your environment by adjusting the dailyLimit. It is pulling the last 30 days of EmailEvents but if you have Sentinel setup and are ingesting the EmailEvents table you likely have 90 or more days of data you can use in your hunt.

let dailyLimit = 500;
// Get sent emails that weren't forwarded
let emails = EmailEvents
    // Filter to Sent items that weren't forwarded
    | where TimeGenerated > ago(30d)
    | where EmailDirection in ('Intra-org', 'Outbound') and isempty(ForwardingInformation)
    | extend LimitRecipient = iif(isempty(DistributionList), RecipientEmailAddress, DistributionList)
    // Account for users sending the same message to the same recipient twice when not using a Organization Distribution List
    | extend UniqueRecipient = iif(isempty(DistributionList), strcat(NetworkMessageId, LimitRecipient), strcat(EmailClusterId, LimitRecipient))
    | project TimeGenerated, UniqueRecipient, SenderFromAddress, Subject, EmailDirection;
emails
// Using 1 day, note it is possible an email campaign starts on a previous day but at lower limits this is irrelevant
| summarize RecipientCount = dcount(UniqueRecipient) by SenderFromAddress, bin(TimeGenerated, 1d)
| where RecipientCount > dailyLimit
| sort by RecipientCount desc

Over limit action

You have three options here:

  • Restrict the user from sending mail until the following day
    • Alert generated is User restricted from sending email
    • User receives an email notification and the restrictions clears up the next day based on UTC time
  • Restrict the user from sending mail
    • Alert generated is User restricted from sending email
    • User receives an email notification and an admin is required to release them from the restricted entity list
  • No action, alert only
    • Alert generated is Email sending limit exceeded
    • Useful for piloting setting some limits on your users

When trialing sending limits it is best to use the No action, alert only setting. Eventually you will want to restrict users from sending email and create a custom policy for any exceptions you may have in your environment such as the communications or marketing department. And remember it would always be better to use a proper bulk mailing service in these cases!

Automatic forwarding

Allows you to limit the forwarding of messages to external mailboxes via mailbox forwarding or inbox rules. There are three options here:

  • Automatic - System-controlled
    • This is the same as Off or disabling the ability to forward messages
  • On
  • Off

This is usually an organization decision, if concerned about forwarding being setup after an account is compromised there are many ways to check forwarding rules and this should be a part of your checks and remediation after an account is compromised.

KQL for Forwarded Email

Use the below query in Advanced Hunting to summarize forwarded messages, comment out the summarize operator to get more detailed records. It is pulling the last 30 days of EmailEvents but if you have Sentinel setup and are ingesting the EmailEvents table you likely have 90 or more days of data you can use in your hunt.

1
2
3
4
5
6
7
EmailEvents
| where TimeGenerated > ago(30d)
| where isnotempty(ForwardingInformation)
| extend ForwardingType = parse_json(ForwardingInformation).ForwardingType
| extend RecipientDomain = substring(RecipientEmailAddress, indexof(RecipientEmailAddress,"@") + 1, strlen(RecipientEmailAddress))
| summarize Count = count() by RecipientDomain, tostring(ForwardingType)
| sort by Count desc

Send a copy of suspicious outbound messages or message that exceed these limits to these users and groups

This apparently only works in the default policy and not custom policies. This will add the specified recipient to the BCC of the sent message.

Notify these users and groups if a sender is blocked due to sending outbound spam

Warning

This setting is being deprecated as there is an alert policy, User restricted from sending email, that handles the same task.