Ghost SMTP Relay Configuration (Google Workspace / Microsoft 365)¶
Setup guide and troubleshooting notes for configuring transactional SMTP on a self-hosted Ghost install (Ghost-CLI on Ubuntu Server). Covers Google Workspace and Microsoft 365 as the relay.
Scope: This configures transactional email only — admin invites, member magic links, password resets, staff notifications. Bulk newsletter sending is a separate
bulkEmailblock that uses the Mailgun API, not SMTP, and is not covered here.
1. Prerequisites¶
Google Workspace¶
- 2-Step Verification enabled on the sending account.
- A 16-character App Password generated at https://myaccount.google.com/apppasswords. Your normal Google password will not work.
- The sending address must be a real mailbox or a verified "Send mail as" alias.
Microsoft 365¶
- Authenticated SMTP enabled on the mailbox: Exchange admin center → Users → the account → Mail → Manage email apps → enable Authenticated SMTP. It is disabled by default on all M365 mailboxes since 2022.
- If Security Defaults / MFA are enforced, generate an app password or exempt the account via a Conditional Access policy.
- The
fromaddress must equal the authenticated user or a valid alias.
Network¶
- Outbound TCP 587 (or 465) must be open from the Ghost VPS. Two places to check on Ubuntu:
- Local firewall:
sudo ufw status. If active, ensure outbound isn't restricted (ufwdefaults to allow outbound, but custom rulesets sometimes deny it). - Cloud provider egress: AWS security groups, GCP firewall rules, Oracle Cloud security lists, DigitalOcean cloud firewalls, and Hetzner often block outbound 25/465/587 by default regardless of
ufw. Check the provider console. - Quick egress test from the VPS:
nc -vz smtp.gmail.com 587— should returnConnection to smtp.gmail.com 587 port [tcp/submission] succeeded!If it hangs, egress is blocked.
2. Configuration¶
Edit config.production.json in your Ghost install directory (typically /var/www/<ghost-dir>/).
Google Workspace — port 587 (STARTTLS)¶
"mail": {
"transport": "SMTP",
"from": "Your Site Name <you@yourdomain.com>",
"options": {
"host": "smtp.gmail.com",
"port": 587,
"secure": false,
"requireTLS": true,
"auth": {
"user": "you@yourdomain.com",
"pass": "your-16-char-app-password"
}
}
}
Google Workspace — port 465 (implicit TLS, alternative)¶
"mail": {
"transport": "SMTP",
"from": "Your Site Name <you@yourdomain.com>",
"options": {
"host": "smtp.gmail.com",
"port": 465,
"secure": true,
"auth": {
"user": "you@yourdomain.com",
"pass": "your-16-char-app-password"
}
}
}
Microsoft 365 — port 587 (STARTTLS, only supported option)¶
"mail": {
"transport": "SMTP",
"from": "Your Site Name <you@yourdomain.com>",
"options": {
"host": "smtp.office365.com",
"port": 587,
"secure": false,
"requireTLS": true,
"auth": {
"user": "you@yourdomain.com",
"pass": "your-password-or-app-password"
}
}
}
M365 does not support implicit TLS on 465. Use 587 + STARTTLS.
Key rules¶
| Field | Rule |
|---|---|
from | Sibling of transport and options, not inside options. Must match auth.user or a verified alias. |
secure | true only for port 465. false for 587. Mismatching this is the #1 cause of failures. |
requireTLS | true on port 587 to force STARTTLS. Prevents silent plaintext downgrade. |
"service": "Gmail" | Omit it. It overrides host/port with Nodemailer presets and causes confusion. |
auth.pass | App password for Workspace and MFA-enabled M365. Regular passwords will fail with EAUTH. |
Apply changes¶
Edit the config as the ghost user (or restore ownership after editing as root):
cd /var/www/<ghost-dir>
sudo -u ghost nano config.production.json
# or, if you edited as another user:
sudo chown ghost:ghost config.production.json
ghost restart
ghost log -f
Then trigger a transactional email (Settings → Staff → invite a user) and watch the log.
3. Troubleshooting¶
3.1 Diagnostic workflow¶
- Get the real error. Don't trust the UI's generic toast. Tail Ghost's log:
Trigger the action again in another terminal. The error will include a Nodemailer code — that code identifies the failing layer.
-
Isolate Ghost from the relay with
swaks(see section 3.2). Ifswakssucceeds and Ghost still fails, the problem is inconfig.production.json. Ifswaksreproduces the failure, the problem is upstream — credentials, provider config, or network. -
Fall back to
opensslonly ifswaksisn't available (e.g., minimal container):
openssl s_client -starttls smtp -crlf -connect smtp.gmail.com:587
# or
openssl s_client -starttls smtp -crlf -connect smtp.office365.com:587
This proves TLS negotiation but requires manual base64 encoding of credentials for auth testing — use swaks when you can.
3.2 Testing SMTP with swaks¶
swaks (Swiss Army Knife for SMTP) is the fastest way to prove whether the relay itself works, independent of Ghost. If swaks succeeds and Ghost fails, the problem is in config.production.json. If swaks fails with the same error Ghost showed, the problem is upstream.
Install¶
Basic connectivity test (no auth)¶
Confirms you can reach the relay and negotiate TLS:
--tls= STARTTLS (use for port 587)--tls-on-connect= implicit TLS (use for port 465)--quit-after TLS= disconnect right after the handshake; no auth, no message sent
Success looks like <~ 220 2.0.0 Ready to start TLS followed by a clean QUIT. Failure here means network egress or TLS mismatch — not a Ghost problem.
Full auth + send test — Google Workspace (port 587)¶
swaks \
--server smtp.gmail.com \
--port 587 \
--tls \
--auth LOGIN \
--auth-user you@yourdomain.com \
--auth-password 'your-16-char-app-password' \
--from you@yourdomain.com \
--to test-recipient@example.com \
--header "Subject: swaks test from Ghost VPS" \
--body "If you got this, SMTP relay works."
Full auth + send test — Google Workspace (port 465, implicit TLS)¶
swaks \
--server smtp.gmail.com \
--port 465 \
--tls-on-connect \
--auth LOGIN \
--auth-user you@yourdomain.com \
--auth-password 'your-16-char-app-password' \
--from you@yourdomain.com \
--to test-recipient@example.com
Full auth + send test — Microsoft 365¶
swaks \
--server smtp.office365.com \
--port 587 \
--tls \
--auth LOGIN \
--auth-user you@yourdomain.com \
--auth-password 'your-password-or-app-password' \
--from you@yourdomain.com \
--to test-recipient@example.com \
--header "Subject: swaks test from Ghost VPS" \
--body "If you got this, SMTP relay works."
Reading swaks output¶
Every SMTP command and response is prefixed:
| Prefix | Meaning |
|---|---|
-> | Client (swaks) sending to server |
<- | Server response |
<~ | Server response, TLS-decrypted |
~> | Client sending, TLS-encrypted |
*** | swaks informational message |
The response code on the last <- line before QUIT tells you the result:
| Code | Meaning | Action |
|---|---|---|
250 2.0.0 OK (or 250 2.6.0 ... Queued) | Message accepted for delivery | Relay works. If Ghost still fails, fix config.production.json. |
235 2.7.0 Accepted | Auth succeeded (seen mid-conversation before DATA) | Credentials are correct. |
535 5.7.8 Username and Password not accepted | Auth rejected | Wrong password, missing app password, or SMTP AUTH disabled (M365). |
530 5.7.0 Authentication required | Server wants auth before it accepts mail | You omitted --auth flags, or auth negotiation failed silently. |
550 5.7.1 ... not allowed | Sender or recipient policy rejection | from doesn't match authenticated user, or relay refuses external recipient. |
| No response / hang on connect | Network egress blocked | Confirm outbound 587/465 open from the VPS. |
Verbose mode for deeper debugging¶
Add --show-raw-text to see the exact bytes on the wire, or --tls-verify to fail on certificate issues (useful when debugging self-hosted relays with self-signed certs).
swaks --server smtp.gmail.com --port 587 --tls --auth LOGIN \
--auth-user you@yourdomain.com --auth-password 'app-pw' \
--from you@yourdomain.com --to test@example.com \
--show-raw-text
Quick reference: mapping swaks results to Ghost errors¶
| swaks result | Ghost error you'd see | Root cause |
|---|---|---|
| Hangs on connect | ECONNECTION / timeout | Egress port blocked |
| TLS handshake fails | ESOCKET / wrong version number | Wrong secure/tls-on-connect for port |
535 5.7.8 | EAUTH | Wrong password / no app password |
530 5.7.0 | EAUTH | SMTP AUTH disabled on mailbox |
250 OK but Ghost fails | (varies) | Ghost config.production.json is wrong — compare to working swaks flags |
3.3 Error reference¶
ESOCKET — SSL routines:tls_validate_record_header:wrong version number¶
Meaning: Nodemailer opened a TLS socket immediately, but the server responded with plaintext SMTP. OpenSSL tried to parse plaintext as a TLS record and failed.
Cause: secure: true on port 587 (or any STARTTLS port). The client wrapped the socket in TLS before the server offered STARTTLS.
Fix: Set secure: false and requireTLS: true on port 587. Or switch to port 465 with secure: true.
EAUTH — 535-5.7.8 Username and Password not accepted (Google)¶
Meaning: Authentication rejected.
Causes: - Using a regular Google password instead of an App Password. - 2-Step Verification not enabled (App Passwords require it). - Account is a personal Gmail without app-password support enabled.
Fix: Generate an App Password at https://myaccount.google.com/apppasswords and paste the 16 characters (no spaces) into auth.pass.
EAUTH on Microsoft 365¶
Meaning: Authentication rejected.
Causes (in order of likelihood): 1. SMTP AUTH is disabled on the mailbox (default since 2022). 2. Security Defaults / MFA is blocking basic auth. 3. Wrong password, or password not an app password when MFA is required.
Fix: 1. Exchange admin center → user → Mail → Manage email apps → enable Authenticated SMTP. 2. Either exempt the user via Conditional Access, or generate an app password.
ECONNECTION / connection timeout¶
Meaning: TCP connection to the SMTP host never completed.
Causes: Outbound port blocked (many cloud/VPS providers block 25/465/587 by default), DNS failure, or firewall rules on the host.
Fix: Verify egress with nc -vz smtp.gmail.com 587 or openssl s_client -connect host:port. If it hangs, check ufw locally and your cloud provider's egress rules. Open a support ticket with the provider or use an alternative port they allow.
WARN Missing mail.from config, falling back to a generated email address¶
Meaning: No from set in the mail block. Ghost synthesizes noreply@<your-ghost-url-host>, which almost always fails SPF/DKIM alignment and lands in spam.
Fix: Add "from": "Your Site Name <you@yourdomain.com>" inside the mail object as a sibling of transport and options. The address must match auth.user or a verified alias.
Emails send successfully but land in spam¶
Meaning: SMTP layer is working; deliverability layer is failing.
Causes and fixes: - from doesn't match auth.user — align them, or add the address as a verified send-as alias on the provider side. - Missing SPF record — publish an SPF TXT record for your domain that includes the relay (include:_spf.google.com or include:spf.protection.outlook.com). - Missing DKIM — enable DKIM signing in the Workspace / M365 admin console and publish the CNAME/TXT records they generate. - Missing DMARC — publish at least v=DMARC1; p=none; rua=mailto:you@domain.
4. Reference¶
- Ghost mail config docs: https://ghost.org/docs/config/#mail
- Google App Passwords: https://myaccount.google.com/apppasswords
- Microsoft SMTP AUTH docs: https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission
- Nodemailer SMTP options (what Ghost passes through): https://nodemailer.com/smtp/