Azure Email Communication Services (ECS)

Azure Email Communication Services (ECS)

Hi All,

Azure Communication Services (ACS) and Email Communication Services (ECS) enables developers to send transactional, bulk, and marketing emails directly from applications using Microsoft Azure. It supports both SMTP and modern SDKs, offers custom domain integration with SPF and DKIM authentication, and provides delivery tracking, analytics, and scalable cloud-based email delivery. As part of Azure Communication Services, it can be combined with SMS, voice, video, and chat capabilities to build rich customer communication solutions

Let’s calculate 100'000 Mails with a Size of 1 MB

Mails per Month Price per Unit Total
100'000 Mails 0.00021 CHF 21 CHF
100'000 Mails x 1MB 0.00010 CHF 10 CHF
TOTAL 31 CHF

Setup

First of all we need the AZ PowerShell Modules and also AZ.Communication

Get-InstalledPSResource -Name Az.Communication -Scope CurrentUser
Install-PSResource -Name Az.Communication -Scope CurrentUser
Get-InstalledPSResource -Name Az.Communication -Scope CurrentUser

Connect to your Azure Account

# Connect-AzAccount
$SubscriptionID = "fb33f2b7-e082-4028-9fa2-98d7ecaaa105"
Connect-AzAccount -Tenant icewolfch.onmicrosoft.com -Subscription $SubscriptionID

Let’s create a Resource Group

# Create Resource Group
$RGName = "RG_ACS"
$Location = "westeurope"
New-AzResourceGroup -Name $RGName -Location $Location

Now we can add the Email Communication Services (ECS)

The Region is always Global, but the Data Location can be selected. But it’s a limited List.

I’ve created it in the Azure Portal, but this is the Code to create it with PowerShell

# Create ECS Service
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
$DataLocation = "Switzerland"
New-AzEmailService -Name $EmailServiceName -ResourceGroupName $ResourceGroup -Location "global" -DataLocation $DataLocation

Now we have an Email Communication Services (ECS) Instance in Azure

Get ECS with PowerShell

# Get ECS Service
$RGName = "RG_ACS"
Get-AzEmailService -ResourceGroup $RGName

Now we can add a custom domain - i have choosen ecs.icewolf.ch a Subdomain of icewolf.ch

Add Custom Domain

# Custom Domain
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
$CustomDomain = "ecs.icewolf.ch"
New-AzEmailServiceDomain -ResourceGroupName $RGName -EmailServiceName $EmailServiceName -Name $CustomDomain -DomainManagement "CustomerManaged"

The custom Domain needs to be verified by several DNS Records

You can see the Verification Records that need to be created

# Get CustomDomain 
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
Get-AzEmailServiceDomain -ResourceGroup $RGName -EmailServiceName $EmailServiceName

Verification Records

# Get CustomDomain Verification Records
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
$ESD = Get-AzEmailServiceDomain -ResourceGroup $RGName -EmailServiceName $EmailServiceName
$ESD.VerificationRecord

DNS Zone and Records

Let’s create a new Zone for the subdomain *ecs.icewolf.ch

# Create DNS Zone
$RGName = "RG_ACS"
$DnsZoneName = "ecs.icewolf.ch"
$Location = "global"
$DnsZone = New-AzDnsZone -Name $DnsZoneName -ResourceGroupName $RGName

Show the Nameservers - we will need them later when we add a delegation in the parent DNS Zone icewolf.ch

# Name Servers for the DNS Zone
$DNSZone
$DnsZone.NameServers

Add the TXT Records

# Add TXT Records (ms-domain-verification / SPF)
$DnsRecordConfig = @(
    New-AzDnsRecordConfig -Value "ms-domain-verification=5b84b425-eb9f-4ac1-adc2-9be1a6c83a62"
    New-AzDnsRecordConfig -Value "v=spf1 include:spf.protection.outlook.com -all"
)
New-AzDnsRecordSet -Name "@" -RecordType TXT -ZoneName $DnsZoneName -ResourceGroupName $RGName -Ttl 3600 -DnsRecords $DnsRecordConfig

Add the DKIM Records

# DKIM Selector 1
$DnsRecordConfig = New-AzDnsRecordConfig -Cname "selector1-azurecomm-prod-net._domainkey.azurecomm.net"
New-AzDnsRecordSet -Name "selector1-azurecomm-prod-net._domainkey" -RecordType CNAME -ZoneName $DnsZoneName -ResourceGroupName $RGName -Ttl 3600 -DnsRecords $DnsRecordConfig

# DKIM Selector 2
$DnsRecordConfig = New-AzDnsRecordConfig -Cname "selector2-azurecomm-prod-net._domainkey.azurecomm.net"
New-AzDnsRecordSet -Name "selector2-azurecomm-prod-net._domainkey" -RecordType CNAME -ZoneName $DnsZoneName -ResourceGroupName $RGName -Ttl 3600 -DnsRecords $DnsRecordConfig

Add the DMARC Record

# DMARC
$DnsRecordConfig = New-AzDnsRecordConfig -Value "v=DMARC1; p=reject; sp=reject; "
New-AzDnsRecordSet -Name "_dmarc" -RecordType TXT -ZoneName $DnsZoneName -ResourceGroupName $RGName -Ttl 3600 -DnsRecords $DnsRecordConfig

This is how it looks in the Azure Portal

DNS Delegation in Parent Zone

Now we need to create a DNS Delegation for the Subdomain ecs.icewolf.ch in the icewolf.ch parent DNS Zone. We simply add the NS Records for ecs

# Create Delegation in parent Zone
$SubscriptionID = "42ecead4-eae9-4456-997c-1580c58b54ba"
$RGName = "RG_Prod"
$Null = Set-AzContext -SubscriptionId $SubscriptionId

$DnsZoneName = "icewolf.ch"
$NsConfig = @()
foreach ($NS in $DnsZone.NameServers)
{
    $NsConfig += New-AzDnsRecordConfig -Nsdname $NS
}
New-AzDnsRecordSet -Name "ecs" -RecordType NS -ZoneName $DnsZoneName -ResourceGroupName $RGName -Ttl 3600 -DnsRecords $NsConfig

Verify Domain / DNS Records

You can start the verification Process in the Azure Portal

It takes a while until the DNS Records are verified

We can see that the Verification has the Status of “NotStarted”

# Get CustomDomain 
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
Get-AzEmailServiceDomain -ResourceGroup $RGName -EmailServiceName $EmailServiceName

We can invoke the Verification for the Types:

  • Domain
  • SPF
  • DKIM
  • DKIM2

There is no DMARC Verification Record but there is a DMARCStatus but can not be invoked

# Invoke DNS Verification
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
$CustomDomain = "ecs.icewolf.ch"
Invoke-AzEmailServiceInitiateDomainVerification -DomainName $CustomDomain -EmailServiceName $EmailServiceName -ResourceGroupName $RGName -VerificationType Domain
Invoke-AzEmailServiceInitiateDomainVerification -DomainName $CustomDomain -EmailServiceName $EmailServiceName -ResourceGroupName $RGName -VerificationType SPF
Invoke-AzEmailServiceInitiateDomainVerification -DomainName $CustomDomain -EmailServiceName $EmailServiceName -ResourceGroupName $RGName -VerificationType DKIM
Invoke-AzEmailServiceInitiateDomainVerification -DomainName $CustomDomain -EmailServiceName $EmailServiceName -ResourceGroupName $RGName -VerificationType DKIM2
#Invoke-AzEmailServiceInitiateDomainVerification -DomainName $CustomDomain -EmailServiceName $EmailServiceName -ResourceGroupName $RGName -VerificationType DMARC

Check if the DNS Records have been verified

# Get CustomDomain 
$EmailServiceName = "IcewolfECS"
$RGName = "RG_ACS"
Get-AzEmailServiceDomain -ResourceGroup $RGName -EmailServiceName $EmailServiceName

Also the Azure Portal is happy with the Verification

Azure Communication Services

Now we need to deploy an Azure Communication Services (ACS) Instance

# Create ACS
$ACSName = "IcewolfACS"
$RGName = "RG_ACS"
$DataLocation = "Switzerland"
New-AzCommunicationService -ResourceGroupName $RGName -Name $ACSName -DataLocation $DataLocation -Location Global

Let’s check the details

# Get ACS
Get-AzCommunicationService -ResourceGroupName $RGName

Now we need to Connect or Link the ECS Domain with ACS

There is no Command in the AZ.Communication Module but we can use the Azure Management Rest API

# Connect ECS Domain to ACS
$SubscriptionID = "fb33f2b7-e082-4028-9fa2-98d7ecaaa105"
$RGName = "RG_ACS"
$ECSName = "IcewolfECS"
$ACSName = "IcewolfACS"

$ContentType = "application/json"
$DomainResourceID = (Get-AzEmailServiceDomain -ResourceGroup $RGName -EmailServiceName $ECSName).Id
$URI = "https://management.azure.com/subscriptions/$SubscriptionID/resourceGroups/$RGName/providers/Microsoft.Communication/CommunicationServices/$ACSName`?api-version=2023-03-31"
$Body = @"
{
    "properties": {
        "linkedDomains": ["$DomainResourceID"]
    }
}
"@
$Body = $Body | ConvertTo-Json -Depth 10
Invoke-AzRestMethod -Method Patch -Uri $URI -Body $Body -ContentType $ContentType

Now we can see the Linked Domain

# Get ACS
$RGName = "RG_ACS"
Get-AzCommunicationService -ResourceGroupName $RGName

These are the Resources in the Resource Group

Send Mail with AZ PowerShell

Now we are able to send An Email with AZ PowerShell

Connect-AzAccount -AuthScope AzureCommunicationEmailEndpointResourceId -Subscription "fb33f2b7-e082-4028-9fa2-98d7ecaaa105" -WarningAction SilentlyContinue
$Endpoint = "https://icewolfacs.switzerland.communication.azure.com"

# Recipient Information
$emailRecipientTo = @(
   @{
        Address = "<a.bohren@icewolf.ch>"
        DisplayName = "Andres Bohren"
    }
)

# Message Content
$message = @{
    ContentSubject = "Test Email"
    RecipientTo = @($emailRecipientTo)  # Array of email address objects
    SenderAddress = '<donotreply@ecs.icewolf.ch>'
    ReplyTo = @('andres.bohren@gmail.com')
    ContentPlainText = "This is the first email from ACS - Azure PowerShell"
}

# Send Email
Send-AzEmailServicedataEmail -Message $Message -endpoint $Endpoint

Mail in Outlook

Headers in Message Header Analyzer - SPF / DKIM / DMARC have passed 😎

SMTP Authentication

I also wanted to try out SMTP Authentication. This was more complicated than i was expecting.

Requirements

  • Entra Application / Entra Service Principal
  • Client Secret (will figure as SMTP Password)
  • In Azure add “Communication and Email Service Owner” Role to Service Principal of the Entra Application to ACS
  • Create SMTP Username

Create Entra Application

# Create Entra Application for SMTP Authentication
$AppName = "SMTPAuthApp"
$App = New-AzADApplication -DisplayName $AppName
$AppID = $App.AppID
$AppID

Optional: Add Owner to Entra Applicaton

# Add Owner to the Entra Application
Connect-MgGraph -Scopes Application.ReadWrite.All -NoWelcome
$App = Get-MgApplication -Filter "displayName eq 'SMTPAuthApp'"
$User = Get-MgUser -UserId "a.bohren@icewolf.ch"
$params = @{
    "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$($User.Id)"
}
New-MgApplicationOwnerByRef -ApplicationId $App.Id -BodyParameter $params

Now i wanted to add a Client Secret - but thats blocked by a Policy

Entra Admin Center > Enterprise Applications > Application Policies > Password restrictions > Block password addition

Here we can select “All applications with exclusions” and add the App to the exclusion list

Let’s have a look at the AppManagement Policy

# Get Entra Application Policy
Connect-MgGraph -Scopes Policy.ReadWrite.ApplicationConfiguration
Get-MgPolicyAppManagementPolicy | ConvertTo-Json -Depth 20

Now we add the Application to the exlusion List

# Exclude the Entra Application from the default app management policy
$PolicyID = (Get-MgPolicyAppManagementPolicy).Id
$App = Get-MgApplication -Filter "displayName eq 'SMTPAuthApp'"
$App.Id      # Object ID
$App.AppId   # Client ID

$URI = "https://graph.microsoft.com/beta/applications/$($App.Id)/appManagementPolicies/`$ref"
$Body = @{
    "@odata.id" = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$PolicyId"
}
Invoke-MgGraphRequest -Method "POST" -Uri $URI -Body $Body

The App is now added in the exlusion list

Now we can add ClientSecrets - it’s no longer blocked by the Policy

# Add ClientSecret to the Entra Application
$App = Get-MgApplication -Filter "appId eq '$AppID'"

$PasswordCredential = @{
    DisplayName = "SMTPPassword"
    EndDateTime = (Get-Date).AddYears(2)
}
$Secret = Add-MgApplicationPassword -ApplicationId $App.Id -PasswordCredential $PasswordCredential
$ClientSecret = $Secret.SecretText

The ClientSecret has been addet

Create Service Principal

# Create Service Principal for the Entra Application
$AppName = "SMTPAuthApp"
Get-AzADApplication -DisplayName $AppName  | New-AzADServicePrincipal

Add Azure Permission “Communication and Email Service Owner” on ACS for Service Principal

# Add Azure Permission "Communication and Email Service Owner" to Service Principal for ACS
$RGName = "RG_ACS"
$AppName = "SMTPAuthApp"

# Get the Service Principal Object ID from the App ID
$SP = Get-AzADApplication -DisplayName $AppName | Get-AzADServicePrincipal

# Assign the role on the ACS instance
$ACSID = (Get-AzCommunicationService -ResourceGroupName $RGName).id
New-AzRoleAssignment -ObjectId $($SP.Id) -RoleDefinitionName "Communication and Email Service Owner" -Scope $ACSID

Permissions set in Azure Portal

Now we can add the SMTP Username

# Register SMTP Username
$ACSName = "IcewolfACS"
$RGName = "RG_ACS"
$AppID = "1364a53d-5997-4f7f-8df4-e4ee34c86ce3"
$TenantID = "46bbad84-29f0-4e03-8d34-f6841a5071ad"
New-AzCommunicationServiceSmtpUsername -CommunicationServiceName $ACSName -ResourceGroupName $RGName -SmtpUsername "donotreply" -EntraApplicationId $AppId -TenantId $TenantId -Username "donotreply@ecs.icewolf.ch"

SMTP Username has been added

# Send Authenticated Email
$Password = ConvertTo-SecureString -AsPlainText -Force -String "$ClientSecret"
$Cred = New-Object -TypeName PSCredential -ArgumentList "donotreply@ecs.icewolf.ch", $Password
Send-MailMessage -From "donotreply@ecs.icewolf.ch" -To "a.bohren@icewolf.ch" -Subject "Test mail" -Body "Test" -SmtpServer "smtp.azurecomm.net" -Port 587 -Credential $Cred -UseSsl -WarningAction SilentlyContinue

Mail in Outlook

Headers in Message Header Analyzer

The Mail has gone to the Junk-E-Mail Folder. Maybe the text was just to short. But SPF / DKIM / DMARC have passed 😎

Summary

The Azure Email Communication Service takes many steps to configure. Especially the SMTP Authentication is way more complex than i exptected to be. Requiring an Entra App just for the ClientSecret as Password and needing the ServicePrincipal have Permissions on the ACS seems a bit overengineered.

In addition the costs are quite significant, as soon as you send more than a couple of Mails.

Good to know how it works and beeing able to provide most of it as PowerShell Scripts will hopefully others help too.

Note: All the PowerShell Code is also available in my GitHub Repo SetupECS_ACS.ps1

Regards
Andres Bohren

Azure Logo

PowerShell Logo