Wednesday, September 9, 2026

Testing an Azure Communication Services SMTP + XOAUTH2 connection

I spent a while debugging an SMTP XOAUTH2 setup against Azure Communication Services that kept failing inside another application, without much visibility into what was actually happening on the wire. Pulling the same credentials out into a small standalone script made the problem obvious pretty quickly. Sharing it here in case it saves someone else the same loop.

Before you start, you need:

  • An Azure Communication Services Email resource with a linked, verified domain
  • A Microsoft Entra app registration granted the Communication and Email Service Owner role (or a narrower custom role) on the Communication Service resource
  • An SMTP Username resource created under that Communication Service, linking the Entra app to a chosen username (az communication smtp-username create, or the Portal's "SMTP Usernames" blade)
USERNAME and FROM are not the same thing. SMTP_USERNAME is the identity the SMTP session authenticates as — the value you set with az communication smtp-username create, linked to your Entra app. FROM_ADDRESS is the actual sender address on the message, and Azure only accepts addresses from a pre-approved list per domain, regardless of what SMTP_USERNAME is set to. To stay safe, use DoNotReply@<your-domain> for FROM_ADDRESS unless you've explicitly added another sender username in the Portal under Email → Sender Usernames. See this Microsoft Q&A thread for the full explanation and also from https://davebarr.dev/550-5.3.5-email-senders-username-is-invalid/.

Fill in the CONFIGURE block near the top, then run it with the client secret passed in as an environment variable rather than pasted into the file:

import base64
import json
import os
import smtplib
import urllib.parse
import urllib.request

# --- CONFIGURE ---
TENANT_ID = "<your-entra-tenant-id>"
CLIENT_ID = "<your-entra-app-client-id>"
CLIENT_SECRET = os.environ.get("ACS_CLIENT_SECRET", "")

# The literal value you set as --username when creating the SMTP Username resource.
SMTP_USERNAME = "<your-smtp-username-configured-in-azure>"

# Must be a pre-approved sender address for the domain - see the note above.
FROM_ADDRESS = "DoNotReply@<your-domain>"

TO_ADDRESS = "<recipient-address>"
# --- END CONFIGURE ---

SCOPE = "https://communication.azure.com/.default"
SMTP_HOST = "smtp.azurecomm.net"
SMTP_PORT = 587


def get_access_token() -> str:
    if not CLIENT_SECRET:
        raise SystemExit("Set the ACS_CLIENT_SECRET environment variable before running this script.")

    token_url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
    data = urllib.parse.urlencode(
        {
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "scope": SCOPE,
            "grant_type": "client_credentials",
        }
    ).encode()

    request = urllib.request.Request(token_url, data=data, method="POST")
    request.add_header("Content-Type", "application/x-www-form-urlencoded")

    with urllib.request.urlopen(request) as response:
        payload = json.loads(response.read())

    return payload["access_token"]


def send_test_email(access_token: str) -> None:
    auth_string = f"user={SMTP_USERNAME}\x01auth=Bearer {access_token}\x01\x01"
    auth_b64 = base64.b64encode(auth_string.encode()).decode()

    server = smtplib.SMTP(SMTP_HOST, SMTP_PORT)
    server.set_debuglevel(1)  # prints the raw wire-level SMTP conversation
    server.ehlo()
    server.starttls()
    server.ehlo()

    code, resp = server.docmd("AUTH", "XOAUTH2 " + auth_b64)
    print("AUTH result:", code, resp)

    if code == 235:
        msg = f"From: {FROM_ADDRESS}\r\nTo: {TO_ADDRESS}\r\nSubject: XOAUTH2 test\r\n\r\nTest message."
        send_result = server.sendmail(FROM_ADDRESS, [TO_ADDRESS], msg)
        print("Send result:", send_result if send_result else "accepted, no errors")
    else:
        print("AUTH failed - stopping")

    server.quit()


if __name__ == "__main__":
    token = get_access_token()
    send_test_email(token)

A couple of things that tripped me up, in case they help:

  • Authentication succeeding does not mean the send will succeed. AUTH and MAIL FROM are checked separately, and the errors from each look completely different.
  • If you see 550 5.3.5 Email sender's username is invalid, it's almost always the FROM_ADDRESS, not SMTP_USERNAME — switch to DoNotReply@<your-domain> before assuming your DNS or domain setup is broken.

Official docs for the setup steps: Set up SMTP authentication for sending emails - Azure Communication Services