AgentStack
SKILL verified MIT Self-run

Adding Azure Communication Services Email

skill-alexpizarro-azure-lean-stack-skills-adding-azure-communication-services-email · by alexpizarro

Adds transactional email to an Azure web app via Azure Communication Services (ACS) Email — verification emails, password resets, notifications. Encodes the three ACS quirks that consistently break Bicep deploys (location:'global' literal, dataLocation in plain English not Azure region IDs, declare-order to avoid circular dependency), and the safeSend() wrapper that prevents email failures from c…

No reviews yet
0 installs
6 views
0.0% view→install

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

✓ Passed

No 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.

Are you the author of Adding Azure Communication Services Email? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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:

  1. ACS gives you DNS records (SPF, DKIM, DMARC) to add
  2. After DNS propagates, run az communication email domain initiate-verification
  3. 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.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.