Brevo SMTP → HTTPS bridge: when DigitalOcean blocks port 587
You spin up a fresh DigitalOcean droplet. You install Ghost. You wire it to Brevo SMTP. You click "Send test email". Ghost logs:
Error sending email: Failed to send email.
Reason: Connection timeout. Please check your email settings.Welcome to the club. DigitalOcean blocks outbound TCP on ports 25, 465, and 587 by default on new accounts. It's an anti-spam policy. They'll unblock it if you ask politely and your account is more than a month old. Otherwise, you wait or work around it.
Verify the block
timeout 5 bash -c '</dev/tcp/smtp-relay.brevo.com/587' && echo OK || echo BLOCKED
# BLOCKED
timeout 5 bash -c '</dev/tcp/api.brevo.com/443' && echo OK || echo BLOCKED
# OKPort 443 (HTTPS) is open. Brevo offers the same email functionality on HTTPS via their REST API — but Ghost only knows how to speak SMTP. So we bridge.
The architecture
Ghost ─ SMTP ─► brevo-relay ─ HTTPS ─► api.brevo.com
(in Docker) (sidecar) (the real world)The sidecar listens on TCP 587 inside the Docker network. Ghost connects to brevo-relay:587 by service name — no public exposure, no DigitalOcean port block in the way. Each accepted message gets parsed and re-shaped into Brevo's JSON API payload, which we POST over HTTPS.
The Python relay
Standard library + aiosmtpd. About 120 lines total. The interesting bit is the handler:
from aiosmtpd.controller import Controller
from email import policy
from email.utils import parseaddr
import email, json, os, urllib.request
BREVO_KEY = os.environ["BREVO_API_KEY"]
BREVO_URL = "https://api.brevo.com/v3/smtp/email"
class BrevoHandler:
async def handle_DATA(self, server, session, envelope):
msg = email.message_from_bytes(envelope.content, policy=policy.default)
from_name, from_email = parseaddr(msg.get("From") or envelope.mail_from)
to_list = [{"email": parseaddr(r)[1]} for r in envelope.rcpt_tos]
html_body, text_body = extract_bodies(msg) # walks multipart
payload = {
"sender": {"email": from_email, "name": from_name or from_email},
"to": to_list,
"subject": msg.get("Subject") or "(no subject)",
"htmlContent": html_body or text_body,
}
req = urllib.request.Request(
BREVO_URL,
data=json.dumps(payload).encode(),
headers={"api-key": BREVO_KEY, "Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=20) as r:
return "250 Message accepted by Brevo"
except Exception as e:
return f"451 Relay error: {e}"
Controller(BrevoHandler(), hostname="0.0.0.0", port=587).start()A few gotchas worth knowing:
- Use
email.utils.parseaddr, not regex. I tried a custom regex first and it choppeddelwerhossain006@gmail.comdown to6@gmail.com. parseaddr handles RFC 5322 properly. - Walk multipart messages. Ghost sends both
text/htmlandtext/plainparts. Brevo accepts both. - Return SMTP-correct status codes.
250on success,451on transient failure,550on permanent. Ghost decides whether to retry based on these.
Wire it as a Docker sidecar
In the Ghost compose file:
services:
ghost:
image: ghost:6-alpine
environment:
mail__transport: SMTP
mail__options__host: brevo-relay # ← service name, not Brevo
mail__options__port: 587
mail__options__secure: "false"
mail__options__ignoreTLS: "true"
mail__from: "Akik <you@yourdomain.com>"
depends_on:
- brevo-relay
brevo-relay:
image: python:3.12-alpine
environment:
BREVO_API_KEY: ${BREVO_API_KEY}
RELAY_SCRIPT_B64: ${RELAY_SCRIPT_B64}
command:
- sh
- -c
- |
pip install --quiet aiosmtpd
echo "$RELAY_SCRIPT_B64" | base64 -d > /relay.py
exec python /relay.pyThe base64-encoded script trick lets you ship the Python source as an env var without baking a custom image or mounting a volume. Tidy for one-droplet deployments.
What this unlocks
- Ghost staff invites + member emails + password resets. All flow through Brevo, all stay within Brevo's free 300/day limit.
- Any other Dockerized app on the droplet can point at
brevo-relay:587. n8n, Strapi, Plausible — anything with an SMTP config. - Drop the DO support ticket. You no longer need outbound SMTP unblocked.
Total work: maybe 30 minutes including a wrong-regex detour. The full script and compose snippet live in my notes repo — clone it and swap your Brevo key in.
Topics:
Want to Implement These Strategies?
I can help you apply these insights to your business. Book a free consultation today.
Book Your Free Consultation