Install
$ agentstack add skill-alexpizarro-azure-lean-stack-skills-adding-azure-communication-services-email ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Adding Azure Communication Services Email
Transactional email via Azure Communication Services. Free tier: 100 emails/day, then $0.00025/email. Cheaper than SendGrid for low-volume projects.
When to use ACS Email
| Need | Use | |------|-----| | Account verification, password reset, transactional notifications | ACS Email (this skill) | | Marketing campaigns, newsletters, A/B testing | A marketing-focused provider (Mailchimp, etc.) — ACS doesn't do that | | High-volume bulk send (>100k/day) | SendGrid Pro or Mailgun (ACS gets expensive at scale) |
The three ACS quirks
1. location: 'global' (literal string, not a real Azure region)
resource emailService 'Microsoft.Communication/emailServices@2023-04-01' = {
name: emailServiceName
location: 'global' // ← NOT 'australiaeast', NOT location param
properties: { dataLocation: 'Australia' }
}
If you pass the project's location param (australiaeast), Azure rejects the deploy.
2. dataLocation uses plain English
The values for dataLocation are not Azure region IDs. They're region groupings:
| dataLocation value | Where data is stored | |---------------------|---------------------| | 'Australia' | AU data centres | | 'Europe' | EU | | 'United States' | US | | 'Asia Pacific' | APAC excluding AU | | 'Africa' | Africa | | 'Brazil' | Brazil | | 'Canada' | Canada | | 'France' | France | | 'Germany' | Germany | | 'India' | India | | 'Japan' | Japan | | 'Korea' | Korea | | 'Norway' | Norway | | 'Switzerland' | Switzerland | | 'UAE' | UAE | | 'United Kingdom' | UK |
Use 'Australia' — not 'australiaeast', not 'au', not 'aus'.
3. Declare-order — no dependsOn on email service + domain
ACS resources have an awkward circular dependency: the ACS resource needs to know about the domain, but linkedDomains + dependsOn causes deployment failures.
Declare in this order, with no dependsOn:
// 1. Email service (no dependencies)
resource emailService 'Microsoft.Communication/emailServices@2023-04-01' = {
name: emailServiceName
location: 'global'
properties: { dataLocation: 'Australia' }
}
// 2. Domain — child of emailService
resource emailDomain 'Microsoft.Communication/emailServices/domains@2023-04-01' = {
parent: emailService
name: 'AzureManagedDomain' // or your custom domain
location: 'global'
properties: {
domainManagement: 'AzureManaged' // or 'CustomerManaged'
userEngagementTracking: 'Disabled'
}
}
// 3. Comms service — links to the domain via linkedDomains
resource acs 'Microsoft.Communication/communicationServices@2023-04-01' = {
name: acsName
location: 'global'
properties: {
dataLocation: 'Australia'
linkedDomains: [ emailDomain.id ]
}
}
No dependsOn blocks anywhere — the implicit dependency through parent: and linkedDomains: is enough.
The Azure-managed domain has an unknown name
When using AzureManagedDomain, the actual sending address looks like:
DoNotReply@.azurecomm.net
You can't predict the hash before deployment. Retrieve it post-deploy:
az communication email domain show \
--resource-group "$RG" \
--email-service-name "$EMAIL_SERVICE_NAME" \
--name AzureManagedDomain \
--query "fromSenderDomain" -o tsv
Set EMAIL_FROM as a SWA / Function App setting after the first deploy.
The safeSend() wrapper
@azure/communication-email uses an async poller — beginSend() returns immediately, you call pollUntilDone(). If pollUntilDone() throws (network error, quota, etc.), your HTTP handler crashes with a 500.
Wrap it:
import { EmailClient } from '@azure/communication-email';
const client = new EmailClient(process.env.ACS_CONNECTION_STRING!);
export async function safeSend(message: {
senderAddress: string;
recipients: { to: { address: string }[] };
content: { subject: string; plainText?: string; html?: string };
}): Promise {
try {
const poller = await client.beginSend(message);
const result = await poller.pollUntilDone();
if (result.status === 'Succeeded') {
return { ok: true, messageId: result.id };
}
return { ok: false, error: result.error?.message ?? `status=${result.status}` };
} catch (err: any) {
// Log but never throw — email failure must not crash the HTTP handler
console.error('Email send failed:', err);
return { ok: false, error: err.message ?? String(err) };
}
}
Then call from your handler:
const result = await safeSend({
senderAddress: process.env.EMAIL_FROM!,
recipients: { to: [{ address: user.email }] },
content: {
subject: 'Verify your email',
html: renderVerifyEmail(token),
},
});
if (!result.ok) {
ctx.warn(`Email send failed for ${user.email}: ${result.error}`);
// Continue — perhaps queue for retry, but don't fail the user's signup
}
Custom domains
To send from noreply@yourdomain.com, set domainManagement: 'CustomerManaged'. Then:
- ACS gives you DNS records (SPF, DKIM, DMARC) to add
- After DNS propagates, run
az communication email domain initiate-verification - Use the custom sender address in
senderAddress
The Azure-managed domain is fine for prototypes and internal tools.
Pricing
- 100 emails/day free
- $0.00025/email above that
- Attachments billed extra at $0.0002/MB
For a 1000-user app doing ~5 transactional emails/user/month (5k/month), cost is ~$1.25/month.
Composes with
- [scaffolding-azure-bicep-infrastructure](../scaffolding-azure-bicep-infrastructure/SKILL.md) — add ACS as an optional module
- [deploying-azure-static-web-apps](../deploying-azure-static-web-apps/SKILL.md) — the API calls
safeSend()from a managed function - [diagnosing-azure-deployment-failures](../diagnosing-azure-deployment-failures/SKILL.md) — for ACS-specific deploy errors
Templates
| File | Purpose | |------|---------| | [templates/acs.bicep](templates/acs.bicep) | Email service + Azure-managed domain + ACS resource |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: alexpizarro
- Source: alexpizarro/azure-lean-stack-skills
- License: MIT
- Homepage: https://github.com/alexpizarro/azure-lean-stack-skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.