Mail Sink
Neither event-dev nor event-stage can send email to the outside world. Every Java service in those namespaces relays through an in-cluster GreenMail server that has no route off the cluster, and a SnappyMail webmail client renders what GreenMail captured.
Why a sink rather than a recipient override
event-stage is refreshed from production nightly by the event-clone-prod-to-stage CronJob, so its database holds real customer email addresses. A recipient override would redirect mail; a sink makes escape impossible, because there is no path to an external relay at all.
The sink also exposes an HTTP API. That turns "did the message go out, and to whom" from a log inspection into an assertion a test can make, which is what allows recipient-selection logic to be exercised against production-shaped data.
Before this was in place, stage pointed config.mail.host at myriadevents-co-za.mail.protection.outlook.com — a live Microsoft 365 relay — with working credentials in the manifest. The only thing preventing a live send was an application feature flag.
Deployments
| Application | Namespace | Path | Webmail |
|---|---|---|---|
greenmail-dev |
|
|
— |
snappymail-dev |
|
|
|
greenmail-stage |
|
|
— |
snappymail-stage |
|
|
The stage manifests are copies of the dev ones. Only the ArgoCD destination namespace, the GreenMail service hostname in the SnappyMail configmap, and the ingress hostname differ. All four use automated sync with prune and self-heal.
GreenMail
Image |
|
Service |
|
SMTP |
25 |
POP3 |
110 |
IMAP |
143 |
API |
8080 |
Authentication |
Disabled ( |
Probes |
|
Storage |
In-memory — messages do not survive a pod restart |
Accounts are created implicitly. Any address that receives a message becomes a mailbox, and any passphrase authenticates it.
Asserting delivery through the API
# Every mailbox that has received something
curl -s http://greenmail:8080/api/user
# The messages for one recipient: subject, recipient, full MIME
curl -s http://greenmail:8080/api/user/[email protected]/messages
|
There is no |
SnappyMail
Image |
|
Service |
|
Storage |
256Mi Longhorn PVC ( |
TLS |
cert-manager, |
Runtime user |
uid 82 |
Sign in with any address, on any domain, and any passphrase. Every mailbox GreenMail has captured can be opened, and each one can send.
Configuration is generated at container start by init-config.sh, delivered through the snappymail-config ConfigMap and invoked from the deployment’s postStart hook. The script owns the domain directory and rebuilds it on every start, so a domain added through the admin panel does not survive a restart.
Any domain
The script writes a single domains/default.ini pointing IMAP and SMTP at the in-cluster GreenMail service. SnappyMail stores the * wildcard domain under the name default, so every address whose domain has no file of its own resolves to it. event-stage holds real customer addresses on arbitrary domains, which a fixed domain list cannot cover.
Two things SnappyMail seeds on first start would defeat this, and the script removes both:
domains/default.json-
Points at
localhost:143. SnappyMail reads a.jsonahead of an.iniof the same name, so while it exists every unlisted domain fails withCan’t connect to host "tcp://localhost:143". domains/disabled-
Refuses
gmail.com,hotmail.com,outlook.com,yahoo.comandqq.combefore any lookup — where most customer mail lands. The script empties it. The file must still exist: SnappyMail re-seeds both files only while it is absent.
System folders
GreenMail creates a mailbox holding only INBOX, loses every folder when it restarts, and advertises no SPECIAL-USE. SnappyMail refuses to send until Sent, Drafts, Spam, Trash and Archive are mapped, so a fresh mailbox stops at Select system folders with nothing to select.
The greenmail-folders plugin, installed and enabled by the script, handles this on every login: it creates Sent, Drafts, Junk, Trash and Archive where they are missing and maps them for the account. A message sent from SnappyMail is then kept in the sender’s Sent.
|
After GreenMail restarts, sign out and back in. The folders are recreated at login, and a session that began before the restart has nowhere to save sent mail. The restart has emptied every mailbox in any case. |
Mail the EMS services send is not copied to the sender’s Sent. They submit over plain SMTP, which has no concept of a sent folder, and GreenMail delivers only to the envelope recipients. To see what a service sent, open the recipient’s mailbox.
|
The The |
Service wiring
Every Java service in these namespaces points at the sink with authentication and TLS off:
config:
mail:
host: greenmail.event-stage.svc.cluster.local
port: 25
username: ""
password: ""
protocol: smtp
tls: false
properties.mail.smtp:
auth: false
starttls.enable: false
Set in event-admin-service-stage.yml, registration-portal-stage.yml and membership-ui-stage.yml, and in the corresponding -dev manifests. admin-portal and ems-mcp-server send no mail and configure none.
|
Changing
|
Health reporting
The mail health indicator is not available. The service charts hardcode management.health.mail.enabled: false into the rendered application.yaml, and no manifest setting can override it:
-
config.extraPropertiesmerges at the document root, so a secondmanagement:key collides and Spring Boot rejects the duplicate. -
extraEnvloses too. These services load the ConfigMap through Spring Cloud Kubernetes in bootstrap mode, whereoverrideSystemPropertiesdefaults totrueand the bootstrap composite is inserted ahead ofsystemEnvironment— so the ConfigMap outranks container environment variables.
Until the chart templates the value, reachability is established through the GreenMail API rather than /management/health.
Verifying the sink
# GreenMail is up
kubectl -n event-stage run probe --rm -i --restart=Never \
--image=curlimages/curl -- \
curl -s http://greenmail:8080/api/service/readiness
# No service still references an external relay
kubectl -n event-stage get cm -o yaml | grep -c mail.protection.outlook.com # expect 0
Then send a message and confirm it appears both in the API and in the webmail client.