agentsclimarketplace

Adding azure communication services email

Skill alexpizarro/azure-lean-stack-skills/skills/adding-azure-communication-services-email

Azure apps that cost nothing when nobody's using them. A Claude Code skill pack — 14 composable skills, 37+ documented gotchas, branch-per-env CI/CD.

Install
npx -y skills add alexpizarro/azure-lean-stack-skills --skill adding-azure-communication-services-email

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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 crashing HTTP handlers. Use when adding email sending to a project, fixing ACS Bicep deployment errors, or migrating off SendGrid/Mailgun.

SKILL.md

6.6 KB, as published. Nobody here has run it

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

NeedUse
Account verification, password reset, transactional notificationsACS Email (this skill)
Marketing campaigns, newsletters, A/B testingA 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 valueWhere 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@<random-hash>.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<{ ok: true; messageId: string } | { ok: false; error: string }> {
  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 [email protected], 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

Templates

FilePurpose
templates/acs.bicepEmail service + Azure-managed domain + ACS resource

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.