AEM as a Cloud Service can send notifications over SMTP using either Basic auth or OAuth 2.0 (the modern, recommended way). Below are practical steps covering networking, OSGi configs, OAuth for Microsoft 365 (and notes for Gmail), secrets, and a tiny code sample to send a message.

1) Prepare networking (egress) in AEM Cloud

SMTP is not HTTP(S), so in AEMaaCS traffic must exit via Advanced Networking (typically Flexible Port Egress). Configure a portForwards rule that maps an internal proxy port to your real SMTP host:port (example: 30465 → smtp.sendgrid.com:465). In your OSGi config you'll point to the proxy host and the forwarded port, not the real SMTP port.

Example Cloud Manager advanced networking (conceptual):

"portForwards": [{
  "name": "smtp.office365.com",
  "portDest": 587,
  "portOrig": 30465
}]

And then in OSGi use smtp.host="$[env:AEM_PROXY_HOST;default=proxy.tunnel]" and smtp.port=30465. Do not set AEM_PROXY_HOST yourself; it's reserved by AEM.

2) Manage secrets & environment variables

Don't hard-code credentials. Use Cloud Manager Environment Variables/Secrets (or AIO CLI) to provide things like usernames, client secrets, and refresh tokens per environment.

Example (AIO CLI):

aio cloudmanager:set-environment-variables --programId=<PROGRAM_ID> <ENVIRONMENT_ID> \
  --secret EMAIL_PASSWORD "p@ssw0rd" \
  --secret SECRET_SMTP_OAUTH_CLIENT_SECRET "<client-secret>" \
  --secret SECRET_SMTP_OAUTH_REFRESH_TOKEN "<refresh-token>"

3) Configure the Mail Service OSGi

AEM uses Day CQ Mail Service (com.day.cq.mailer.DefaultMailService). The shape differs slightly for Basic vs OAuth.

3.a) If using Basic (only if your SMTP provider still permits it)

// /apps/<project>/osgiconfig/config/com.day.cq.mailer.DefaultMailService.cfg.json
{
  "smtp.host": "$[env:AEM_PROXY_HOST;default=proxy.tunnel]",
  "smtp.port": 30465,                // forwarded port (see §1)
  "smtp.user": "$[env:EMAIL_USERNAME;default=apikey-or-user]",
  "smtp.password": "$[secret:EMAIL_PASSWORD]",
  "from.address": "noreply@example.com",
  "smtp.ssl": true,                  // or false + starttls=true for 587
  "smtp.starttls": false,
  "smtp.requiretls": false,
  "debug.email": false,
  "oauth.flow": false
}

Note: Microsoft is deprecating Basic Auth for Exchange Online SMTP; plan for OAuth2 before Oct 1, 2025.

3.b) OAuth 2.0 with Microsoft 365 (recommended)

AEM has first-party OAuth2 support for its Mail Service. You'll set up an Azure AD app, generate tokens, then add an OAuth provider OSGi config + enable oauth.flow on the Mail Service. experienceleague.adobe.com

Step 1 - Register Azure AD App

Azure Portal → Azure Active Directory → App registrationsNew registration.

Add Delegated API permissions:

Authentication → Add a platformWeb → add redirect URIs: http://localhost and http://localhost/. experienceleague.adobe.com

Certificates & secrets → create a Client secret; note the Client ID and Tenant ID.

Step 2 - Get an authorization code, then refresh token Open this (replace tenantID/clientId), sign in and consent:

https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/authorize
  ?client_id=<clientId>
  &response_type=code
  &redirect_uri=http://localhost
  &response_mode=query
  &scope=https://outlook.office.com/SMTP.Send%20email%20openid%20profile%20offline_access
  &state=12345

Copy the code from the redirected http://localhost/?code=... URL. Exchange it for tokens:

curl -X POST "https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'client_id=<clientId>' \
  --data-urlencode 'client_secret=<clientSecret>' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'redirect_uri=http://localhost' \
  --data-urlencode 'scope=https://outlook.office.com/SMTP.Send email openid profile offline_access' \
  --data-urlencode 'code=<code>'

Store the refresh_token & (optionally) validate by exchanging it for an access token using grant_type=refresh_token.

Step 3 - Configure AEM's OAuth provider OSGi Create /apps/<project>/osgiconfig/config/com.day.cq.mailer.oauth.impl.OAuthConfigurationProviderImpl.cfg.json:

{
  "authUrl":   "https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/authorize",
  "tokenUrl":  "https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token",
  "clientId":  "<clientId>",
  "clientSecret": "$[secret:SECRET_SMTP_OAUTH_CLIENT_SECRET]",
  "scopes": [
    "https://outlook.office.com/SMTP.Send",
    "openid", "offline_access", "email", "profile"
  ],
  "authCodeRedirectUrl": "http://localhost",
  "refreshUrl": "https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token",
  "refreshToken": "$[secret:SECRET_SMTP_OAUTH_REFRESH_TOKEN]"
}

Then update the Mail Service to turn on OAuth and set the proxy host/forwarded port:

// /apps/<project>/osgiconfig/config/com.day.cq.mailer.DefaultMailService.cfg.json
{
  "smtp.host": "$[env:AEM_PROXY_HOST;default=proxy.tunnel]",
  "smtp.port": 30465,
  "smtp.user": "<the mailbox UPN you consented with>",
  "smtp.password": "value not used",
  "from.address": "noreply@yourdomain.com",
  "smtp.ssl": false,
  "smtp.starttls": true,
  "smtp.requiretls": true,
  "debug.email": false,
  "oauth.flow": true
}

Finally, set SECRET_SMTP_OAUTH_CLIENT_SECRET and SECRET_SMTP_OAUTH_REFRESH_TOKEN as Cloud Manager secrets (per §2). experienceleague.adobe.com

Microsoft scopes & endpoints reference (for SMTP XOAUTH2): Microsoft Learn

Notes for Gmail (XOAUTH2)

If you use Gmail instead of Microsoft 365, the classic SMTP XOAUTH2 scope is https://mail.google.com/ and you'll generate tokens against Google's OAuth endpoints. (Gmail also offers the Gmail API as an alternative to SMTP)

4) Minimal code to send a notification

Use AEM's MailService (available via OSGi) and keep the message simple/UTF-8.

import com.day.cq.mailer.MailService;
import com.day.cq.mailer.MessageGateway;
import com.day.cq.mailer.MessageGatewayService;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.MultiPartEmail;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(service = Runnable.class, immediate = true)
public class DemoEmailSender implements Runnable {
  @Reference private MessageGatewayService gatewayService;

  @Override public void run() {
    try {
      MultiPartEmail email = new MultiPartEmail();
      email.setCharset("UTF-8");
      email.addTo("recipient@example.com");
      email.setFrom("noreply@yourdomain.com", "AEM Notifications");
      email.setSubject("Test from AEM");
      email.setMsg("Hello from AEM via SMTP OAuth2!");

      MessageGateway<MultiPartEmail> gateway = gatewayService.getGateway(MultiPartEmail.class);
      gateway.send(email);
    } catch (EmailException e) {
      // log and alert
    }
  }
}

(You can also enable built-in workflow notifications once the Mail Service is configured)

5) Testing checklist

  • Token works: Use your refresh token to fetch an access token and attempt SMTP AUTH XOAUTH2 to smtp.office365.com:587 locally to confirm before deploying.
  • Proxy mapping correct: smtp.port must equal your forwarded port (e.g., 30465), not 587/465.
  • TLS settings: For port 587: starttls=true, ssl=false. For 465: ssl=true, starttls=false. Match your provider.
  • Secrets present in env: Verify SECRET_SMTP_OAUTH_CLIENT_SECRET and SECRET_SMTP_OAUTH_REFRESH_TOKEN exist in the target environment.
  • User mailbox: The smtp.user must be the mailbox principal you consented for and that's allowed to send. For high-volume or service-principal scenarios, review Microsoft's SMTP OAuth guidance.

Below is a structured list of key challenges you can include in your blog, grouped by category, along with how to explain or resolve them to make your article stand out.

Networking and Egress Challenges

Challenge: AEM Cloud doesn't allow direct external SMTP connections (since it's a fully managed environment without open ports).

Impact: Developers can't directly connect to smtp.office365.com or smtp.gmail.com from AEM Cloud instances.

Solution to Highlight:

  • Use Advanced Networking (Flexible Port Egress) to map internal proxy ports to external SMTP ports.
  • Configure your OSGi SMTP settings to use proxy.tunnel as the host and the forwarded internal port (e.g., 30465 → 587).
  • Emphasize that this is mandatory for AEM Cloud Service — traditional SMTP configs will fail without this.

Tip for readers: Add a JSON snippet showing the portForwards configuration in Cloud Manager.

OAuth 2.0 Authentication Complexity

Challenge: Setting up OAuth 2.0 for SMTP (especially with Microsoft 365) is not straightforward - multiple endpoints, scopes, and token flows are involved.

Impact: Hard to understand difference between auth code and refresh token flows.

  • Tokens can expire if not configured properly.
  • AEM's OAuth provider requires precise configuration, or emails fail silently.

Solution to Highlight:

  • Step-by-step guide for registering Azure AD App (with permissions like SMTP.Send, offline_access, etc.).
  • Show how to use cURL to exchange the authorization code for a refresh token.
  • Store client_secret and refresh_token securely as Cloud Manager Secrets.
  • Mention the use of oauth.flow=true in com.day.cq.mailer.DefaultMailService.

Bonus Insight: Explain that AEM doesn't fetch or renew OAuth tokens automatically - tokens need to be valid and refreshed externally when required.

Environment Variables and Secret Management

Challenge: You can't store sensitive credentials (like client secrets or refresh tokens) in OSGi configs directly - they'll end up in Git and violate security policies.

Impact: Mismanagement can expose secrets in repositories or pipelines.

Solution:

  • Store secrets using Cloud Manager environment variables.
  • Reference them with $[secret:VAR_NAME] syntax in OSGi configs.
  • Differentiate env vs secret variables in your example JSON.

Example:

"smtp.password": "$[secret:EMAIL_PASSWORD]",
"clientSecret": "$[secret:SECRET_SMTP_OAUTH_CLIENT_SECRET]"

TLS / SSL Port Confusion

Challenge: Different SMTP providers require different TLS settings (e.g., starttls=true vs. ssl=true).

Impact: Emails fail to send silently, often with vague "Connection refused" or "Authentication failed" errors.

Solution:

  • For port 587 → starttls=true, ssl=false.
  • For port 465 → ssl=true, starttls=false.
  • Emphasize testing both locally and on Cloud instance.

Pro Tip: Add a "Testing Checklist" section in your blog with verification steps.

Debugging and Logging Issues

Challenge: AEM Cloud limits direct log access - debugging SMTP/OAuth issues isn't as simple as in on-prem deployments.

Impact: Difficult to trace whether the issue is with OAuth, networking, or TLS.

Solution:

  • Enable debug.email=true in com.day.cq.mailer.DefaultMailService.
  • Use AEM Cloud Manager's Developer Console → Log Viewer to tail error.log and mail.log.
  • Suggest adding custom MessageGatewayService debug output in test components.

Organizational and Policy Challenges

Challenge: Large organizations (like Avalara or others using Microsoft 365) have strict IT/security controls.

Impact:

  • SMTP may be restricted by tenant policies.
  • Admin consent might be required for OAuth scopes.
  • Multi-tenant restrictions can block token generation.

Solution:

  • Coordinate with IT/Security early to get admin consent for the app registration.
  • Document this dependency clearly in your AEM deployment runbook.

Token Lifecycle and Expiry Management

Challenge: Refresh tokens can expire (or be revoked) if unused or regenerated incorrectly.

Impact: Emails suddenly stop working after a few weeks or months.

Solution:

  • Implement a token rotation process (manual or automated through Cloud Manager API).
  • Keep monitoring email failure logs.
  • Add health checks to ensure SMTP connection validity.

Testing in Cloud vs Local

Challenge: What works locally (on SDK or Dispatcher Dev setup) may fail in Cloud due to egress and environment isolation.

Solution:

  • Clarify that SMTP works differently on AEM SDK vs Cloud.
  • Always replicate the Cloud configuration in your lower environment (Stage) for validation.