Repository navigation
How AstroBaaS sends email: choosing a transport, the environment variables, Reply-To, form and order notifications, editing the wording, newsletters, and testing mail after a deploy. It is for operators setting up or debugging a mail channel.
Every message AstroBaaS sends goes through one transport configured from the environment. That covers password resets, magic links, contact-form notifications, order confirmations and newsletter batches. Moving to another mail server means editing .env and restarting; no code changes. The operator reference is docs/EMAIL.md.
| Transport | How to select it | Use for |
|---|---|---|
| Console |
EMAIL_TRANSPORT=console (the default) |
Local development. Messages are printed to the server log and nothing is sent. |
| SMTP | EMAIL_TRANSPORT=smtp |
Any submission server: your host's, your own, or a provider's SMTP relay. |
| Webhook | EMAIL_TRANSPORT=webhook |
POSTs each message as JSON to a URL you run, which can bridge to any provider. |
| SMTP2GO | Activate the bundled smtp2go plugin |
SMTP2GO's HTTP API. Useful where outbound port 587 is blocked. |
With the default console transport, password resets, magic links and 2FA recovery end up in the server log. The deep health check warns about this.
If EMAIL_TRANSPORT is set to smtp or webhook but the configuration is incomplete, AstroBaaS logs why at startup and falls back to console. The same reason appears in /api/health/deep.
EMAIL_TRANSPORT=smtp
SMTP_HOST=mail.example.com
SMTP_PORT=587 # 587 = STARTTLS; or 465 with SMTP_SECURE=1
SMTP_USER=noreply@example.com
SMTP_PASS='the-password' # always single-quoted; set on the server, never committed
EMAIL_FROM="Example <noreply@example.com>"
EMAIL_REPLY_TO=info@example.com # where replies go, if EMAIL_FROM is unmonitored| Variable | Required | Default | Notes |
|---|---|---|---|
EMAIL_TRANSPORT |
Yes | console |
smtp, webhook or console. |
SMTP_HOST |
Yes | none | The submission server. |
EMAIL_FROM |
Yes | none | Must be an address the account may send as. |
SMTP_PORT |
No | none |
587 for STARTTLS, 465 with SMTP_SECURE=1. |
SMTP_SECURE |
No | off | Implicit TLS (port 465). |
SMTP_USER / SMTP_PASS
|
No | none | If you set a user you need a password too. |
EMAIL_REPLY_TO |
No | none | Reply-To on every message, for any transport. |
SMTP_TIMEOUT_MS |
No | 10000 |
Each network wait. One attempt is capped at 3× this. |
SMTP_DATA_TIMEOUT_MS |
No | 600000 |
Wait for the reply after the message is sent (1 s to 30 min). |
SMTP_RETRIES |
No | 2 |
0–2, transient failures only. |
SMTP_HELO_NAME |
No | Machine name or EMAIL_FROM domain |
The name given in EHLO. Never SMTP_HOST. |
SMTP_ALLOW_SELF_SIGNED |
No | off | Only for a relay on your own network. |
EMAIL_CAMPAIGNS |
No | on |
0 refuses newsletter campaigns. |
SMTP_PASS is read from the environment only. It is never stored in the database, never logged and never returned by an endpoint. Always single-quote it in the env file. Unquoted, systemd and Node's env-file parser disagree, and each one damages some passwords (a # or \ is enough). The symptom is a puzzling 535. A password containing a single quote cannot be written this way, so choose another.
- Retries are on by default. A
4xx, a dropped connection or a timeout before the message is sent is retried after about 2 s, then about 6 s. - A
5xx, a failed login (535) and a certificate that does not verify are never retried. - If the whole message was sent but no reply came, the outcome is unknown and the message is not sent again. It shows as "outcome unknown" in the log, on Admin → Background jobs (
/admin/operations) and in the health check. Check the inbox before resending.
Sends from a request are handed off, so the response never waits on the mail server. Scheduler mail (reminders, back-in-stock notices, newsletter batches) waits at most one minute for the final reply.
EMAIL_TRANSPORT=webhook
EMAIL_WEBHOOK_URL=https://mail-bridge.example.com/send
EMAIL_WEBHOOK_SECRET=... # optionalEach message is POSTed as JSON (to, subject, text, html, Reply-To and headers). With a secret set, the request carries X-AstroBaaS-Signature: sha256=<HMAC-SHA256 of the body>. Any non-2xx response counts as a failed send.
SMTP2GO_API_KEY=api-XXXXXXXXXXXX
SMTP2GO_SENDER="My Shop <no-reply@example.com>"Activate the smtp2go plugin in Admin → Plugins. If a variable is missing, activation logs what is needed and leaves the previous transport in place. Deactivating it returns to whatever EMAIL_TRANSPORT says. The key is sent in a header, never in the request body. A 200 response that reports failed recipients is treated as a failure.
-
EMAIL_REPLY_TOgoes on every message, whatever the transport. Set it wheneverEMAIL_FROMis a no-reply box. - Form notifications use the submitter's address instead. Pressing Reply answers the person who wrote. A submitted value that is not a usable address is dropped, and the default is used instead.
- If
EMAIL_REPLY_TOitself is unusable, mail still goes out without a Reply-To. The startup log and/api/health/deepboth say so. The reasoning is that losing a password reset is worse than losing the Reply-To.
The built-in contact form and every content type with writable: 'public' send the same notification to the same recipient.
The recipient is the first usable address among these settings:
-
contact_notify_email: Admin → Settings → "Send form submissions to" -
contact_email: the address shown on the public contact page -
admin_email: the operator's address
If none is set, nothing is sent. Submissions are still stored in the admin inbox (Messages).
The notification is plain text only, because a submission is text from a stranger. Each field is cut to 500 characters; the full text is in the admin.
| Recipient | Default | |
|---|---|---|
| Order confirmation | Buyer | On (order_confirmation_enabled). At most five per recipient per hour. |
| Unpaid-order reminder | Buyer | Sent once, before an unpaid order is cancelled. |
| Shipped notice | Buyer | Sent the first time tracking is recorded. |
| Back in stock | Subscriber | Sent once, when a sold-out product returns. |
| Sale notification | Staff | Off until sale_notify_enabled is on and sale_notify_recipients is set (up to 10). |
A mail failure never fails a checkout. See Commerce.
Admin → Email wording (admin-only, /admin/email-templates) lets you change the subject and body of each email without a deploy:
| Template id | When it is sent |
|---|---|
order_confirmation |
To the customer, as soon as an order is placed. |
stock_back |
Once, to somebody who asked to be told when a sold-out product returned. |
order_recovery |
Once, to a customer whose order is still unpaid, before it is cancelled. |
order_shipped |
To the customer, the first time tracking is recorded. |
password_reset |
When somebody asks to reset their password. |
magic_link |
When somebody signs in with a link instead of a password. |
newsletter_confirm |
To a new subscriber, to confirm the subscription. |
submission_notice |
To the site owner, when somebody sends a form. |
The rules:
- Templates control wording, not delivery. Who gets the email, when, and whether it is sent at all stay in code.
- Each template has required placeholders (a password reset needs its link). A body missing one is refused when you save it.
- If a stored template later fails validation, the built-in default is sent instead.
- Templates are plain text. There is no HTML editing.
- For the order confirmation, only the subject is templatable; the body is fixed.
The same data is available at /api/email-templates.
-
Double opt-in.
POST /api/newsletterstores nothing. It sends a signed confirmation link, and the address is added only when the person clicks it. -
Campaigns are written and sent from Admin → Messages ("Send a newsletter"), or via
/api/newsletter/campaigns. - Campaigns go out in batches of 25 per scheduler tick and pick up where they left off after a restart, so a newsletter is never sent twice.
- Every campaign message carries
List-UnsubscribeandList-Unsubscribe-Post: List-Unsubscribe=One-Click. Unsubscribing goes through/api/newsletter/unsubscribe. - This is not a full email service. There is no bounce handling, no complaint feedback loop and no suppression list. Sending a large list through your own mail server can hurt delivery of your order confirmations. For more than a few hundred addresses, use a dedicated provider. Subscriber events are sent as webhooks so you can sync one.
-
Transactional-only mailboxes. Set
EMAIL_CAMPAIGNS=0(orfalse,no,off) when the mailbox must not carry bulk mail. Pressing Send is then refused with the reason, and a queued campaign waits without marking anyone as failed. Password resets and order mail are not affected.
Do not skip this step. In a checkout:
npm run mail:test -- --env-file=/var/www/<site>/shared/.env you@example.comOn a built release (no scripts/ directory), use the precompiled copy:
node dist/mail-test.mjs you@example.com
docker compose exec app node dist/mail-test.mjs you@example.comTo test with exactly the environment the systemd service sees:
sudo systemd-run --pipe --wait --quiet --uid=www-data --gid=www-data \
-p EnvironmentFile=/var/www/<site>/shared/.env \
-p WorkingDirectory=/var/www/<site>/current \
/usr/bin/node dist/mail-test.mjs you@example.comThe test sends one message through the app's own mail code. It prints the configuration (the password only as "set" or "not set"), the SMTP conversation with the AUTH line masked, and the server's final reply. It exits 0 when the server accepts the message, so a deploy script can check it.
- A variable already exported in your shell wins over
--env-file. - A
250means the server accepted the message, not that it arrived. Check the inbox and spam folder. - Afterwards,
/api/health/deepreports the channel underemail_channel: the transport, Reply-To, whether campaigns are on, and the last send.
The email log is on Admin → Background jobs (/admin/operations). It keeps each server reply, so the queue id is there when a customer says a message never arrived. The server log has one line per message and never includes the subject, body or credentials.
| You see | It means |
|---|---|
mail is not being sent: EMAIL_TRANSPORT=smtp but … |
The configuration was rejected before connecting. The message names the variable. |
SMTP authentication failed: 535 |
Wrong user or password, or an unquoted SMTP_PASS. |
SMTP MAIL FROM failed: 553 / 550 … not owned by user
|
EMAIL_FROM is not an address this account may send as. |
SMTP RCPT TO failed: 550 |
The recipient was refused. |
4xx … (after 3 attempts) |
A temporary refusal outlasted the retries, often a rate limit. |
self-signed certificate / CERT_HAS_EXPIRED
|
Fix the certificate, or trust an internal CA with NODE_EXTRA_CA_CERTS. |
Refusing to send credentials over an unencrypted connection |
No STARTTLS on this port. Use 465 with SMTP_SECURE=1. |
SMTP EHLO failed: 5xx |
Set SMTP_HELO_NAME to this machine's public host name. |
SMTP outcome unknown |
The message may have been delivered and was not resent. |
- Configuration: all environment variables
-
Deployment: where
shared/.envlives - Commerce: order emails
- FAQ-and-Troubleshooting: "emails are not sent"
- docs/EMAIL.md
AstroBaaS is free software under GPL-3.0-or-later. This wiki describes the latest release; the repository and its CHANGELOG are the final word. Found a mistake here? Open an issue.
Getting started
Using the CMS
Building with it
Extending
Commerce and email
Operating
Project