Entra / Microsoft 365 · Exchange Online
Send azure communications email
An example script to show how to send email from Azure Communication Services using PowerShell.
Connect & set up
Run these once per session. All scopes are read-only unless the script makes changes.
# Review required modules and connection steps before running.# Connect to Microsoft Graph or Exchange Online as needed for this script.
Run it
The main script. Copy it, or download the .ps1 and run it from your console.
param([string] $TenantId = "",[string] $AppId = "")function Get-AccessToken {# Get an access token for Azure Communication Services$params = @{Uri = "https://login.microsoftonline.com/$($TenantId)/oauth2/v2.0/token"Method = "POST"Body = @{client_id = $AppIdclient_secret = $AppSecretgrant_type = "client_credentials"scope = "https://communication.azure.com/.default"}}$token = Invoke-RestMethod @paramsreturn $token.access_token}Function Get-MessageRecipients { # Build a list of the recipients for a message[cmdletbinding()]Param([array]$ListOfAddresses )$To = @()ForEach ($SMTPAddress in $ListOfAddresses) {$Recipient = @{Address = $SMTPAddressdisplayName = $SMTPAddress}$To += $Recipient}Return $To}# Replace the identifiers with appropriate values for your tenant. Application (client) identifier, app secret, and tenant id# I use an app called Office365ITPros Email Communication Service for this script$AppSecret = 'q7n8Q~xwBCGPz--2sVtDvQGgK4k-CsFtkKQxDb3~'# The sender's email address. This must be an address defined as a sender with Email Communication Services# See https://learn.microsoft.com/azure/communication-services/quickstarts/email/add-multiple-senders for how to# configure additional email addresses as senders$SenderAddress = 'DoNotReply@office365itpros.com'# Endpoint for the Azure Communication Services API$CommunicationEndPoint = "office365itpros.unitedstates.communication.azure.com"# Define the headers for the REST API call. This includes the access token to identify the app as coming from the domain$headers = @{"Content-Type" = "application/json""Authorization" = "Bearer $(Get-AccessToken)"}# Define the content of the email - handcrafted HTML here. This is an actual message that we sent to# subscribers of the Office 365 for IT Pros eBook$Content = "<p>Dear Subscriber:</p>" +"<p>As a subscriber to Office 365 for IT Pros (2024 edition), you are entitled to upgrade and extend your " +"subscription to cover Office 365 for IT Pros (2025 edition). Time is running out for this offer, " +"which closes on July 21." +"<p>To avoid problems with messages sent using the Gumroad email system being tagged as spam or ending up in " +"quarantine, we are sending this message using the Azure Email communication service (ECS). It is a relatively new " +"service that is designed to handle mass mailings of the type we do when communicating with subscribers. " +"It has been an interesting project to work out how to process email with ECS.</p>" +"<p>If you have any questions, please check <a href='https://office365itpros.com/office-365-for-it-pros-faq/'>our FAQ</a> " +"or send email to o365itprosrenewals@office365itpros.com.</p>" +"<p>We hope that you like Office 365 for IT Pros (2025 edition).</p>" +"<p>Best Regards,</p><p>Tony</p><p><p>" +"<p>------------------------------------------------------------------------------</p>" +"<p>This email was sent using Azure Email Communication Service"# Read in email addresses to process. This script uses a simple CSV file with a single column called Email[array]$RecipientList = Import-CSV 'c:\temp\MessageRecipients.CSV'# Remove any duplicates found in the set imported from the CSV$RecipientList = $RecipientList | Sort-Object Email -Unique# ECS has a limit of 100 recipients per message. If the list is longer, we'll only process the first 100If ($RecipientList.count -gt 100) {Write-Host "Too many recipients to process in one go. Limiting the set to the first 100"$RecipientList = $RecipientList | Select-Object -First 100}$Report = [System.Collections.Generic.List[Object]]::new()$MessageIds = [System.Collections.Generic.List[Object]]::new()[int]$i = 0Write-Host "Processing messages... "ForEach ($Recipient in $RecipientList.Email) {# Construct the TO addresses for the message[array]$ToRecipientAddress = Get-MessageRecipients -ListOfAddresses $Recipient$i++Write-Host ("Sending email to {0} ({1}/{2})" -f $Recipient, $i, $RecipientList.count)# Build a hash table containing the settings for the message$Email = @{# The sender's email addresssenderAddress = $senderAddress# Create a unique identifier for this messageheaders = @{id = ("{0}-{1}" -f (Get-Date -format s), $ToRecipientAddress.address)}# The content of the email, including the subject and HTML bodycontent = @{subject = "Office 365 for IT Pros Subscription update"html = $Content}# The recipients of the emailrecipients = @{to = $ToRecipientAddressbcc = @(@{address = "o365itprosrenewals@office365itpros.com"displayname = "Office 365 for IT Pros Support"})}# The reply-to addresses for the email - doesn't have to be the same as the sender addressReplyTo = @(@{address = "o365itprosrenewals@office365itpros.com"displayName = "Office 365 for IT Pros Support"})userEngagementTrackingDisabled = $false}# Convert the email settings structure to JSON$EmailSettings = $Email | ConvertTo-Json -Depth 10$MailStatus = $null# Define the URI to post to when sending a message with ECS.# The same URI is used for all messages. The body of the message dictates who receives the email$Uri = ("https://{0}/emails:send?api-version=2023-03-31" -f $CommunicationEndpoint)# Submit the message to the Email Communication servicetry {$MailStatus = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $EmailSettings -UseBasicParsing$ReportLine = [PSCustomObject][Ordered]@{Id = $MailStatus.idTimestamp = (Get-Date -format s)Recipient = $Recipient}$MessageIds.Add($ReportLine)}catch {Write-Host ("Failed to send email to {0}" -f $Recipient)$ReportLine = [PSCustomObject][Ordered]@{Timestamp = (Get-Date -format s)Recipient = $RecipientStatus = $Mailstatus.statusId = $Mailstatus.id}$Report.Add($ReportLine)}Start-Sleep -Seconds 2$Recipient = $null}Write-Host "Checking status of messages sent..."# All messages are sent. Let's find out what happenedForEach ($Message in $MessageIds) {$Uri = ("https://{0}/emails/operations/{1}?api-version=2023-03-31" -f $CommunicationEndpoint, $Message.id)Try {$MailStatus = Invoke-RestMethod -Uri $uri -Method Get -Headers $headersWrite-Host ("Message sent to {0} returned status of {1}" -f $Message.Recipient, $MailStatus.status)$ReportLine = [PSCustomObject][Ordered]@{Timestamp = $Message.TimestampRecipient = $Message.RecipientStatus = $MailStatus.status}$Report.Add($ReportLine)} Catch {Write-Error $_.Exception.Message}}Clear-HostWrite-Host "Summary of email transmission session"Write-Host "-------------------------------------"Write-Host ""Write-Host ("Messages sent via ECS endpoint {0}" -f $CommunicationEndPoint)Write-Host ""Write-Host ("Number of messages sent: {0}" -f $Report.Count)Write-Host ("Number of messages sent successfully: {0}" -f ($Report | Where-Object { $_.Status -eq "Succeeded" }).Count)Write-Host ("Number of messages failed: {0}" -f ($Report | Where-Object { $_.Status -eq "Failed" }).Count)Write-Host ("Number of messages pending: {0}" -f ($Report | Where-Object { $_.Status -eq "Running" }).Count)Write-Host ""Write-Host "Message status"Write-Host ""$Report
Parameters
ParameterDefaultNotes
-TenantId""Microsoft Entra tenant ID for app-only Graph authentication.-AppId""Application (client) ID for the app registration used to connect.Attribution
Author
Office365itpros