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 registrations → New registration.
Add Delegated API permissions:
https://outlook.office.com/SMTP.Sendopenid,offline_access,email,profile(Perms evolve; follow MS docs if these change.)
Authentication → Add a platform → Web → 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=12345Copy 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:587locally to confirm before deploying. - Proxy mapping correct:
smtp.portmust 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_SECRETandSECRET_SMTP_OAUTH_REFRESH_TOKENexist in the target environment. - User mailbox: The
smtp.usermust 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.tunnelas 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_secretandrefresh_tokensecurely as Cloud Manager Secrets. - Mention the use of
oauth.flow=trueincom.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=trueincom.day.cq.mailer.DefaultMailService. - Use AEM Cloud Manager's Developer Console → Log Viewer to tail
error.logandmail.log. - Suggest adding custom
MessageGatewayServicedebug 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.