Email with Azure Communication Services
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Commands on this page target the preview communication Azure CLI extension (auto-installs on first use,
requires Azure CLI 2.67.0 or later; this page’s examples are re-verified against Azure CLI 2.90.0, checked
2026-09-27) — re-verify flag names against az communication --help before relying on them in a pipeline, since
a preview extension’s surface can change between releases faster than GA command groups do.
Bookshelf’s notifier worker (the Container Apps job introduced on
Container Apps: Scaling, Jobs and Dapr) sends order-confirmation emails once orders-api
publishes an order. This page covers the Azure resource that makes that possible — an Email Communication
Service, acs-bookshelf, linked to a Communication Services resource, cs-bookshelf — domain verification,
sending from the CLI, from the Email SDK, and over SMTP, delivery/engagement tracking, quotas, the outbound
port 25 block every subscription hits eventually, SendGrid as a Marketplace alternative, and deliverability
basics.
The resource model
Sending email through Azure Communication Services always involves two ARM resources plus one channel identity:
- Email Communication Service (
Microsoft.Communication/emailServices) -
The resource that owns one or more domains. It has no data-plane endpoint of its own — it exists purely to hold domain configuration (verification records, sender usernames, engagement-tracking settings) independently of any particular Communication Services resource.
- Domain (
Microsoft.Communication/emailServices/domains) -
A child resource of the Email Communication Service, one of two kinds:
-
An Azure-managed domain (
--domain-management AzureManaged, domain name literallyAzureManagedDomain) — a subdomain of*.azurecomm.netthat Azure provisions and verifies automatically, with no DNS work on the caller’s side. It sends immediately, but its sender address can’t be personalized beyond thedonotreply@local part, its sending volume is capped well below a custom domain’s ([Quotas and limits]), and it can’t enable user-engagement tracking. -
A custom domain (
--domain-management CustomerManaged, e.g.bookshelf.example) — a domain the caller owns, proved via a Domain TXT ownership record and then authenticated with SPF (aTXTrecord) and two DKIMCNAMErecords (DKIMandDKIM2, Azure’s two rotating signing selectors); DMARC alignment is the caller’s own DNS record layered on top of SPF/DKIM, not a record Azure issues ([Deliverability basics]). A custom domain sends from an address the caller controls and can raise its default quota ([Quotas and limits]).
-
- Communication Services resource (
Microsoft.Communication/communicationServices) -
The resource applications and the CLI actually address to send — it exposes the data-plane endpoint (
https://cs-bookshelf.<region> .communication.azure.com) and the connection string/access keys. A Communication Services resource has no domains of its own; one or more Email Communication Service domains are linked to it, and only a linked domain’s sender addresses can be used from that Communication Services resource. - Sender username (
MailFrom) -
The local part of the
Fromaddress for a given domain —DoNotReplyby default, with additional usernames addable per domain (az communication email domain sender-username create) up to 100 per domain ([Quotas and limits]). Bookshelf sends order confirmations fromDoNotReply@<managed-domain>on the Azure-managed domain in the examples below; switching to a custom domain later needs no application-side change beyond the sender address string.
Both the Email Communication Service and the Communication Services resource are created with --location
Global — Communication Services is a data-residency-based, not region-based, service — and a separate
--data-location parameter (e.g. United States, Europe) picks which geography actually stores the data; this
is a different axis from the $LOCATION (eastus2) used by every other Bookshelf resource on this site, and
worth not confusing with it.
In the Azure portal
-
Search for Email Communication Services → Create:
$RG, nameacs-bookshelf, Data Location (e.g. United States). -
Once created, open
acs-bookshelf→ Provision domains → Add domain → Azure Managed Domain for the no-DNS path (see [Domain verification] for the custom-domain path from the same blade). -
Search for Communication Services → Create:
$RG, namecs-bookshelf, Data Location matching the Email Communication Service above. -
Open
cs-bookshelf→ Email → Domains → Connect domain, and pick the Email Communication Service (acs-bookshelf) and the domain to link.
With the Azure CLI
export RG=rg-bookshelf
export LOCATION=eastus2
az group create --name "$RG" --location "$LOCATION"
az extension add --name communication
# Email Communication Service -- owns domains
az communication email create \
--name acs-bookshelf \
--resource-group "$RG" \
--location Global \
--data-location "United States"
# Azure-managed domain -- no DNS work, sends immediately
az communication email domain create \
--domain-name AzureManagedDomain \
--email-service-name acs-bookshelf \
--resource-group "$RG" \
--location Global \
--domain-management AzureManaged
# Communication Services resource -- the data-plane endpoint applications send through
az communication create \
--name cs-bookshelf \
--resource-group "$RG" \
--location Global \
--data-location "United States"
# Link the Azure-managed domain to the Communication Services resource
MANAGED_DOMAIN=$(az communication email domain list \
--email-service-name acs-bookshelf \
--resource-group "$RG" \
--query "[0].name" -o tsv)
az communication update \
--name cs-bookshelf \
--resource-group "$RG" \
--set "linkedDomains=[\"$(az communication email domain show --domain-name "$MANAGED_DOMAIN" --email-service-name acs-bookshelf --resource-group "$RG" --query id -o tsv)\"]"
Derived from Create and manage Email Communication Service resources, Quickstart: Create and manage Communication Services resources, and Azure CLI reference — az communication email.
[[Domain verification]] == Domain verification
An Azure-managed domain needs no verification step — it is created already verified. A custom domain moves
through three verification passes, each triggered with az communication email domain initiate-verification
--verification-type <type> (Domain, SPF, DKIM, DKIM2, DMARC) after the matching DNS record is in place;
DNS propagation typically takes 15-30 minutes before a verification attempt succeeds.
| Verification type | Record | Purpose |
|---|---|---|
|
|
Proves ownership of the domain before anything else is accepted. |
|
|
Authorizes Azure Communication Services' sending infrastructure to send as this domain. |
|
|
The first of two rotating DKIM signing keys Azure Communication Services cryptographically signs outgoing mail with. |
|
|
The second rotating DKIM selector, kept in place alongside |
|
|
Declares how a receiving mail server should treat a message that fails SPF/DKIM alignment ([Deliverability basics]); Azure verifies the record exists but the policy itself is the domain owner’s choice. |
In the Azure portal
-
Open
acs-bookshelf→ Provision domains → Add domain → Custom domain; enter the domain name twice to confirm, then Add. -
Verify Domain: copy the generated
TXTrecord into the domain’s DNS zone, wait for propagation, then Done. -
Once Domain Status reads Verified, Configure under the same domain adds the SPF
TXTrecord and the two DKIMCNAMErecords; add them to DNS, then Done. -
Optionally enable User Engagement Tracking on the domain (needed for the engagement events in [Delivery status and engagement tracking]) — disabled by default and unavailable on Azure-managed domains.
With the Azure CLI
az communication email domain create \
--domain-name bookshelf.example \
--email-service-name acs-bookshelf \
--resource-group "$RG" \
--location Global \
--domain-management CustomerManaged
# after adding the generated TXT record to DNS and waiting for propagation
az communication email domain initiate-verification \
--domain-name bookshelf.example \
--email-service-name acs-bookshelf \
--resource-group "$RG" \
--verification-type Domain
# after adding the SPF TXT and both DKIM CNAME records
az communication email domain initiate-verification \
--domain-name bookshelf.example --email-service-name acs-bookshelf --resource-group "$RG" --verification-type SPF
az communication email domain initiate-verification \
--domain-name bookshelf.example --email-service-name acs-bookshelf --resource-group "$RG" --verification-type DKIM
az communication email domain initiate-verification \
--domain-name bookshelf.example --email-service-name acs-bookshelf --resource-group "$RG" --verification-type DKIM2
az communication email domain show \
--domain-name bookshelf.example --email-service-name acs-bookshelf --resource-group "$RG" \
--query verificationStates
Sending email
With the Azure CLI
az communication email send is a synchronous convenience command that submits the message and polls
az communication email status get internally until the message reaches a final state:
CONNECTION_STRING=$(az communication list-key \
--name cs-bookshelf --resource-group "$RG" --query primaryConnectionString -o tsv)
az communication email send \
--connection-string "$CONNECTION_STRING" \
--sender "DoNotReply@<managed-domain>" \
--to "reader@example.com" \
--subject "Your Bookshelf order is confirmed" \
--text "Thanks for your order -- it ships within 2 business days."
# Check status of a message sent earlier by its message ID
az communication email status get \
--connection-string "$CONNECTION_STRING" \
--message-id "<message-id>"
A connection string is the only way the communication CLI extension itself authenticates — there is no
az login-based option for these two commands — so treat $CONNECTION_STRING the way any other credential on
this site is treated: read it from Key Vault (Key Vault and Secrets) for a real
pipeline, never hard-code it in a script or commit it to source control. Application code has the better option
below.
From application code
The Email SDK authenticates with DefaultAzureCredential against the Communication Services resource’s
endpoint, exactly like every other Bookshelf service on this site
(Identity and Access) — notifier’s managed identity needs no stored connection string
at all, only a role assignment (`Contributor or the narrower built-in Communication and Email Service Owner
role) scoped to cs-bookshelf:
package com.example.bookshelf.notifier;
import com.azure.communication.email.EmailClient;
import com.azure.communication.email.EmailClientBuilder;
import com.azure.communication.email.models.EmailMessage;
import com.azure.communication.email.models.EmailSendResult;
import com.azure.core.util.polling.SyncPoller;
import com.azure.identity.DefaultAzureCredentialBuilder;
public class OrderConfirmationSender {
private final EmailClient emailClient = new EmailClientBuilder()
.endpoint("https://cs-bookshelf.unitedstates.communication.azure.com/")
.credential(new DefaultAzureCredentialBuilder().build())
.buildClient();
public void sendOrderConfirmation(String recipient, String orderId) {
EmailMessage message = new EmailMessage()
.setSenderAddress("DoNotReply@<managed-domain>")
.setToRecipients(recipient)
.setSubject("Your Bookshelf order " + orderId + " is confirmed")
.setBodyPlainText("Thanks for your order -- it ships within 2 business days.");
SyncPoller<EmailSendResult, EmailSendResult> poller = emailClient.beginSend(message);
poller.waitForCompletion();
}
}
beginSend returns a long-running-operation poller rather than blocking synchronously, so notifier (running
as a Container Apps job, per Container Apps: Scaling, Jobs and Dapr) can fire the send and
either wait on the poller inline for a short job or check status on a later invocation via
emailClient.getSendStatus(operationId). The .NET and Python Email SDKs expose the same
EmailClient/DefaultAzureCredential shape and are the better starting point than Java only when the rest of
the calling service is already .NET or Python (Bookshelf’s own services are Spring Boot, so Java is the default
here).
Derived from Azure Communication Email client library for Java and Send an email using Azure Communication Services.
[[SMTP relay]] == SMTP relay
Where a library or platform only speaks SMTP — an existing JavaMailSender bean, a legacy on-call alerting
tool, a CMS plugin — Azure Communication Services also exposes an authenticated SMTP relay at
smtp.azurecomm.net:587, STARTTLS, rather than requiring every caller to adopt the Email SDK. Authentication is
still Microsoft Entra ID, not a shared SMTP password: a Microsoft Entra application is registered, granted the
Communication and Email Service Owner role (or Contributor) on cs-bookshelf, and its client secret becomes
the SMTP password. The SMTP username is not the app’s client ID alone — it is the three-part string
<communication-services-resource-name>.<entra-app-id>.<entra-tenant-id> (e.g.
cs-bookshelf.11111111-1111-1111-1111-111111111111.22222222-2222-2222-2222-222222222222), which is how the
relay knows which Communication Services resource and which Entra tenant to authenticate the connection against.
spring:
mail:
host: smtp.azurecomm.net
port: 587
username: cs-bookshelf.${ENTRA_APP_ID}.${ENTRA_TENANT_ID}
password: ${ACS_SMTP_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
With spring-boot-starter-mail on the classpath, this configuration is all Spring Boot needs to autowire a
JavaMailSender — no code change from the plain-SMTP MailSender/JavaMailSender setup already shown on
Configuration and Profiles, just a different host/username shape and
${ACS_SMTP_PASSWORD} sourced from Key Vault rather than committed to a properties file. The Quarkus Mailer
extension configures the same relay through quarkus.mailer.* properties exactly as
Scheduling and Mail describes for any other SMTP provider, pointed at
smtp.azurecomm.net:587 with start-tls=REQUIRED and this same three-part username.
[[Delivery status and engagement tracking]] == Delivery status and engagement tracking via Event Grid
Rather than polling az communication email status get or the SDK’s getSendStatus, Azure Communication
Services publishes delivery outcomes — and, on a domain with User Engagement Tracking enabled, opens and link
clicks — directly to Event Grid:
Microsoft.Communication.EmailDeliveryReportReceived-
Fired once a message reaches a final delivery state (
Delivered,Bounced,Suppressed,Failed,Quarantined,FilteredSpam), carrying the message ID, the final status, and, for a bounce, a per-recipient failure reason. Microsoft.Communication.EmailEngagementTrackingReportReceived-
Fired when a recipient opens the message or clicks a tracked link, for a domain with engagement tracking enabled — the mechanism that made
--user-engmnt-tracking Enabled/Disabledonaz communication email domain updaterelevant above.
az eventgrid event-subscription create \
--name acs-bookshelf-email-events \
--source-resource-id "$(az communication show --name cs-bookshelf --resource-group "$RG" --query id -o tsv)" \
--endpoint "$(az functionapp function show --name notifier --resource-group "$RG" --function-name EmailStatusHandler --query invokeUrlTemplate -o tsv)" \
--included-event-types Microsoft.Communication.EmailDeliveryReportReceived Microsoft.Communication.EmailEngagementTrackingReportReceived
Subscribing `notifier’s own status handler this way lets Bookshelf record a bounce against the customer’s order without adding a poll loop anywhere in the request path.
Derived from Azure Communication Services — Email events and Handle Email events.
[[Quotas and limits]] == Quotas and limits
Every subscription starts with conservative send rate limits and grows them only on request, since Azure Communication Services shares outbound IP reputation across every tenant using the service:
| Operation | Domain type | Per minute | Per hour |
|---|---|---|---|
Send email |
Azure-managed domain |
5 |
10 |
Send email |
Custom domain |
30 |
100 |
Get email status |
Azure-managed domain |
10 |
20 |
Get email status |
Custom domain |
60 |
200 |
Azure-managed domains cannot request a higher limit at all; a verified custom domain can, and Azure Communication
Services documents supporting up to 1-2 million messages per hour for accounts that earn it. A handful of
other size/resource limits apply regardless of domain type: at most 50 recipients across to/cc/bcc per
message (more needs a support request), a 10 MB cap on the total request size including attachments (roughly
7.5 MB of actual attachment content once Base64’s ~33% overhead is accounted for; larger files are better sent
as a Blob Storage link with a SAS, per Blob Storage), at most 100 sender usernames per
domain, and at most 100 domains linked to one Communication Services resource.
Requesting a quota increase is a standard Azure support request (Service and Subscription Limits (Quotas) as the issue type), evaluated on sender reputation rather than granted automatically: Microsoft looks at the domain’s failure rate (bounces, complaints, blocks), and asks for it to stay under 1% before approving a higher limit — requests typically take up to 72 hours, and Microsoft’s own guidance is to ramp volume up gradually over two to four weeks on a new domain rather than starting at, or requesting, a high limit on day one, so that third-party mail providers have time to build trust in the domain’s new sending IPs.
Derived from Service limits for Azure Communication Services — Email and Quota increase for email domains.
[[The outbound port 25 block]] == The outbound port 25 block
Azure blocks outbound TCP port 25 from most compute services — virtual machines, VM Scale Sets, App Service, Container Apps, AKS nodes — on most subscription types, to protect the shared IP reputation of Azure’s own address space from being used for unauthenticated spam relaying; there is no per-resource opt-out, and the block applies independent of any NSG rule the caller configures on their own subnet. This is precisely why Azure Communication Services (both the Email SDK and the SMTP relay above) and third-party relays such as SendGrid exist as the recommended path: they listen on port 587 with mandatory authenticated SMTP (STARTTLS plus a credential, never anonymous relay), which Azure does not block on any subscription type, and they additionally carry their own established sending reputation rather than an ordinary VM’s arbitrary outbound IP. An application or script that tries to talk directly to a recipient’s port 25 mail server from an Azure compute resource — rather than through an authenticated relay like ACS or SendGrid — fails outright rather than degrading, which is the most common report behind "email works locally but not from our VM."
Derived from Troubleshoot outbound SMTP connectivity in Azure.
SendGrid via Azure Marketplace as an alternative
Twilio SendGrid is offered through Azure Marketplace as a SaaS listing (the same Marketplace SaaS billing mechanism described for Elastic Cloud and the partner databases on Redis and Partner Databases on Azure): subscribing creates a SendGrid account tied to the Azure subscription and billed on the regular Azure invoice, while SendGrid itself operates the sending infrastructure, API, and its own SMTP relay entirely outside Azure Communication Services. It is worth considering instead of Azure Communication Services Email when a team already standardizes on SendGrid’s templates, marketing-email features, or existing integrations, or is migrating an app that already speaks SendGrid’s API; Azure Communication Services remains the more integrated choice for a Bookshelf-style application that also uses Communication Services' SMS/chat/calling channels or wants delivery/engagement events to land on the same Event Grid topic as everything else on Identity and Access's managed identities.
Derived from Twilio SendGrid — Azure Marketplace listing.
[[Deliverability basics]] == Deliverability basics
SPF, DKIM, and DMARC together are what lets a receiving mail server decide whether a message claiming to be from
bookshelf.example actually came from an authorized sender:
-
SPF lists which sending infrastructure (Azure Communication Services' own, once the SPF
TXTrecord above is in place) is authorized to send as the domain in theReturn-Path/envelope-from address. -
DKIM cryptographically signs each outgoing message with a private key only Azure Communication Services holds; the two
CNAMErecords above let a receiver fetch the matching public key and verify the signature wasn’t tampered with in transit. -
DMARC is the policy layered on top: it declares what a receiver should do (
none,quarantine,reject) when a message fails alignment — SPF’s envelope-from domain or DKIM’s signing domain not matching the visibleFrom:domain — and where to send aggregate/failure reports. Azure Communication Services verifies that a DMARC record exists on the domain but does not generate or manage its policy value; that record is authored and owned the same way any other DNS record for the domain is.
Alongside authentication, Azure Communication Services maintains a per-domain suppression list — addresses
that hard-bounced, marked a prior message as spam, or were added manually — and automatically skips sending to
any address already on it, which protects the domain’s own reputation from repeatedly hammering a dead or
complaining address. The list is inspectable and manageable through the management SDKs (there is no dedicated
az communication command for it as of this writing); an address can be removed once whatever caused the
bounce/complaint is understood to be resolved.
Troubleshooting
-
A custom domain’s SPF/DKIM show "Not configured" despite correct-looking DNS — almost always a record added at the wrong zone level (root vs. subdomain, see the zone-level examples in [Domain verification]) or verification attempted before DNS has actually propagated; re-run
az communication email domain initiate-verificationafter confirming the record resolves publicly (dig TXT/dig CNAMEfrom outside the corporate network). -
az communication email sendreturns403/Unauthorized— the connection string is for the wrong Communication Services resource, or the sender address’s domain isn’t linked tocs-bookshelfyet (The resource model). -
Messages accepted by Azure Communication Services never arrive — check the domain’s suppression list first ([Deliverability basics]), then the recipient-side spam folder; a
Microsoft.Communication. EmailDeliveryReportReceivedevent with statusFilteredSpamorQuarantined([Delivery status and engagement tracking]) confirms it left Azure successfully and was judged suspicious downstream, which points at DMARC alignment or sender reputation rather than at Azure Communication Services itself. -
SMTP relay authentication fails with a generic
535— double-check the three-part username (<communication-services-resource>.<entra-app-id>.<entra-tenant-id>) against the exact Communication Services resource name and that the Entra app’s client secret (not its client ID) is the password, and that the app actually holdsCommunication and Email Service Owner(orContributor) on that resource.
Clean up
az group delete --name "$RG" --yes --no-wait
Deleting the resource group removes acs-bookshelf, cs-bookshelf, their domains, and any Event Grid
subscriptions on them; there is no soft-delete/purge step for Communication Services resources, unlike Key
Vault or API Management.
Related pages
-
Key Vault and Secrets covers storing the SMTP client secret and any connection string this page uses as a Key Vault secret rather than in configuration.
-
Identity and Access covers
DefaultAzureCredential, managed identities, and Azure RBAC role assignment in depth. -
Container Apps: Scaling, Jobs and Dapr covers
notifieras an event-driven Container Apps job. -
Configuration and Profiles covers the Spring Boot
MailSender/JavaMailSenderbean shape this page’s SMTP configuration plugs into. -
Scheduling and Mail covers the Quarkus Mailer extension’s blocking/reactive APIs, attachments, and Qute mail templates in depth.
-
Blob Storage covers SAS-based links for attachments too large to send inline.
-
Redis and Partner Databases on Azure covers Azure Marketplace SaaS billing in more depth, as used by the SendGrid listing above.
References
-
Microsoft Learn — Create and manage Email Communication Service resources
-
Microsoft Learn — Quickstart: Create and manage Communication Services resources
-
Microsoft Learn — Azure CLI reference: az communication email
-
Microsoft Learn — Azure CLI reference: az communication email domain
-
Microsoft Learn — Add Azure Managed Domains to Email Communication Service
-
Microsoft Learn — Send an email using Azure Communication Services
-
Microsoft Learn — Azure Communication Email client library for Java
-
Microsoft Learn — Authenticate to the Azure Communication Services SMTP relay
-
Microsoft Learn — Azure Communication Services — Email events
-
Microsoft Learn — Service limits for Azure Communication Services
-
Microsoft Learn — Troubleshoot outbound SMTP connectivity in Azure
-
Microsoft Learn — Best practices for sender authentication support
-
Microsoft Learn — Manage sender reputation with the managed suppression list