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.

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 with 5.7.1 Client does not have permissions to send as this sender.

  • SPF / DKIM — configured at the myriadevents.co.za DNS 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 CommunicationLog tracks 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:

  1. Single shared connector, rewrite From at the application layer. Exchange permits you to send as [email protected] only if the myriadevents.co.za tenant has been delegated that domain (unlikely for most tenants).

  2. 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:

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 in application-prod.yml.

  • TLS — mandatory via starttls.enable: true for 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.0 responses if exceeded. Handle the retryable SMTP error in MailService.

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 wp_mail() call silently — order emails, invoices and password resets included.

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):

  1. Copy spring.mail.* tree from registration-portal’s application-prod.yml.

  2. Add mail section to values.yaml with the same structure.

  3. Point at event-admin-portal Secret for mailpassword.

  4. 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

admin-service/src/main/resources/config/application.yml

Base spring.mail.* structure

admin-service/src/main/resources/config/application-dev.yml

Dev override pointing at GreenMail

admin-service/src/main/resources/templates/mails/

Thymeleaf email templates

~/dev/idl-xnl-jhb-rc01/argocd/event-admin-service-prod.yml

Production Exchange connector config

admin-service/src/main/java/…​/service/mail/MailService.java

Service-level sender

13. Change History

Date Change

2026-04-24

Initial draft. Grounded in ArgoCD prod manifests.