Skip to content

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 bulkEmail block 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 → MailManage 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 from address 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 (ufw defaults 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 return Connection 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

  1. Get the real error. Don't trust the UI's generic toast. Tail Ghost's log:
ghost log -f

Trigger the action again in another terminal. The error will include a Nodemailer code — that code identifies the failing layer.

  1. Isolate Ghost from the relay with swaks (see section 3.2). If swaks succeeds and Ghost still fails, the problem is in config.production.json. If swaks reproduces the failure, the problem is upstream — credentials, provider config, or network.

  2. Fall back to openssl only if swaks isn'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

sudo apt install swaks

Basic connectivity test (no auth)

Confirms you can reach the relay and negotiate TLS:

swaks --server smtp.gmail.com --port 587 --tls --quit-after 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

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


EAUTH535-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