SMTP Configuration
1. Overview
Outbound email is sent by admin-service (registration confirmations, membership cards, password resets, receipts) and occasionally by portals (logout confirmations, system alerts). All services use Spring’s JavaMailSender via the stock spring-boot-starter-mail. Configuration is environment-specific — prod uses Microsoft 365 / Exchange Online via myriadevents-co-za.mail.protection.outlook.com; dev uses GreenMail.
This page documents the config surface, the production SMTP provider, and the dev fallback.
See also: Docker Compose for Developers, GreenMail Email Server.
2. Configuration Surface
Every service exposes the same spring.mail. tree. Values flow from Helm → ConfigMap + Secret → env vars → application-.yml.
spring:
mail:
host: ${MAIL_HOST}
port: ${MAIL_PORT:25}
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
protocol: smtp
properties:
mail:
smtp:
auth: true
starttls:
enable: true
ssl:
trust: ${MAIL_HOST}
test-connection: false
Service-specific extras:
application:
mail:
from: ${MAIL_FROM:noreply@localhost}
base-url: https://${MAIL_BASE_URL} # used in email templates for absolute links
application.mail.from is the From: header on every outbound message; must be a deliverable address at the provider (Exchange rejects senders not owned by the tenant).
3. Production: Microsoft 365 / Exchange Online
Provider: myriadevents-co-za.mail.protection.outlook.com:25 with STARTTLS and basic auth.
From ArgoCD event-admin-service-prod.yml:
mail:
host: myriadevents-co-za.mail.protection.outlook.com
port: 25
username: [email protected]
# password: via existingsecret.mailpassword
protocol: smtp
tls: true
properties.mail.smtp:
auth: true
ssl.trust: myriadevents-co-za.mail.protection.outlook.com
starttls.enable: true
Notes:
-
Port 25 — standard SMTP; the Exchange connector accepts STARTTLS on 25. Port 587 (submission) also works if 25 is blocked by network egress policy.
-
From address —
[email protected]. Must match the authenticated identity or Exchange rejects with5.7.1 Client does not have permissions to send as this sender. -
SPF / DKIM — configured at the
myriadevents.co.zaDNS level. Any outbound email not coming through this connector will fail SPF at the recipient; keep that in mind if a future integration tries to send directly via a third-party mail provider. -
Rate limits — Exchange Online has per-connector and per-sender throttles (typically hundreds per hour for regular accounts, thousands for connector-style). Large batch emails (membership card annual mail-out) must be throttled on the application side; the service’s
CommunicationLogtracks sends and is the enforcement point.
3.1. Per-tenant From addresses
Multi-tenant portals may want to send as [email protected] for each tenant. Two approaches:
-
Single shared connector, rewrite From at the application layer. Exchange permits you to send as
[email protected]only if themyriadevents.co.zatenant has been delegated that domain (unlikely for most tenants). -
Per-tenant SMTP config. Each Organisation has its own
mail.host/port/username/password/from. Service picks the right config per sending context. Scales better for genuine multi-tenancy but adds config complexity.
Currently all tenants share the myriadevents.co.za sender; scale to per-tenant only when a tenant specifically asks for branded From addresses. Capture the pattern in a design-journal thread when it becomes concrete.
4. Dev: GreenMail
Provider: local GreenMail container (image: greenmail/standalone:2.1.0).
# admin-service/src/main/resources/config/application-dev.yml (relevant part)
spring:
mail:
host: localhost
port: 3025
properties:
mail:
smtp:
auth: false # GreenMail accepts anything
starttls:
enable: false
See GreenMail Email Server for the full dev setup.
Mail browsing:
-
Web UI: http://localhost:8080 — shows inboxes per address
-
IMAP: port 3143 (automated test consumption)
-
Username/password:
[email protected]/dev(configurable viaGREENMAIL_OPTS)
4.1. dev-to-stage SMTP for bigger testing
If dev needs realistic mail flow (e.g. testing that a customer actually receives a registration confirmation), switch application-dev.yml to the Exchange connector — but point application.mail.from at a test address and be careful about volume. Typically this is only needed briefly; revert after.
5. Stage
Stage uses the same Exchange connector as prod. Reason: we explicitly want stage to send real mail during UAT, to a small known list of test addresses ([email protected], etc.). Do not point stage at dev GreenMail — it silences the very thing we’re testing.
ArgoCD event-admin-service-stage.yml mirrors prod mail config; the only deltas are database URL and image version.
6. Template Rendering
Templates live in admin-service as Thymeleaf files under src/main/resources/templates/mails/. Variables resolved against the service’s MailService; email is assembled and sent synchronously.
Advanced: the "email implementation layer" design (see design-journal/2026-03/email-implementation.adoc) introduces DB-backed org-scoped templates via MJML + Thymeleaf. When that lands, the mail provider configuration stays identical; only the template-rendering layer changes.
7. Observability
Sent email is logged at INFO:
INFO ... MailService : Email sent to '[email protected]' with subject 'Registration confirmation' in 187ms
Failures log at ERROR with the full exception chain. For production debugging, CommunicationLog records every attempt with delivery status.
No email body or PII is logged. Subject is considered low-sensitivity and is logged.
8. Security
-
Credentials — always via Kubernetes Secret. Never in
values.yaml, never in git, never inapplication-prod.yml. -
TLS — mandatory via
starttls.enable: truefor any non-dev environment. Exchange rejects plaintext after the TLS handshake requirement is active. -
Rate-limit — application-level throttling for bulk sends; Exchange enforces its own throttle and will
4.7.0responses if exceeded. Handle the retryable SMTP error inMailService.
9. WordPress sites
The WordPress event sites send through this same connector, but they cannot send as the customer’s own domain, and it is worth being explicit about why.
SPF is evaluated against the domain in the From address, not against the relay. Mail sent as
@customer-domain.co.za through the myriadevents.co.za connector fails SPF at the recipient,
because the connector is not authorised in that domain’s DNS. Exchange enforces the same rule
from the other side, refusing any sender the tenant does not own.
The sites therefore pin the envelope sender to a tenant-owned address rather than letting WooCommerce set a per-message one. That is a deliberate constraint, not a default — see FluentSMTP for the setting that enforces it.
If a customer ever needs mail to originate from their own domain, that needs a sending identity for that domain — records published in the customer’s DNS — not a change to this connector. Each new customer domain is its own piece of work.
9.1. The three routes
A WordPress site picks one of three, and the choice follows from whose domain the mail is sent as and what the receiving platform allows:
| Route | When |
|---|---|
Generic SMTP |
Any authenticated SMTP server. The default, and what our own sites use to reach the 365 connector above — SMTP AUTH is simpler to operate than Graph and needs no consent step. |
Microsoft 365 (Graph) |
Our own corporate tenants, and the fallback if a tenant disables SMTP AUTH. OAuth, so it carries a one-time consent granted in the admin and a refresh token held in the database. |
Amazon SES |
Volume, and the only one of the three that can send as a customer’s own domain — because the sending identity is verified per domain rather than inherited from our tenant. |
The mechanics, and the constants each route reads its credentials from, are on
FluentSMTP. EMS services themselves use the
generic SMTP route only; the spring.mail.* surface above has no equivalent of the other two.
|
The base WordPress image ships no MTA at all, so a site without a mailer plugin fails every
|
10. New Service Onboarding
When admin-portal eventually needs to send email (unlikely at launch — admin-portal typically triggers emails via admin-service, not directly):
-
Copy
spring.mail.*tree from registration-portal’sapplication-prod.yml. -
Add
mailsection tovalues.yamlwith the same structure. -
Point at
event-admin-portalSecret formailpassword. -
No new Exchange connector config needed — reuse the tenant-level connector.
Preferred: do not add mail to admin-portal. Route all outbound email through admin-service where CommunicationLog captures the send. Portals should request emails (via admin-service API) rather than send directly.
11. Reference
| File | Role |
|---|---|
|
Base |
|
Dev override pointing at GreenMail |
|
Thymeleaf email templates |
|
Production Exchange connector config |
|
Service-level sender |