Files
wmj c0b8981629 Initial commit: HTTP wrapper around the existing falah-ibaas email connector
Gives Supabase Edge Functions (and anything else outside the VPS) a real,
authenticated endpoint onto the VPS's already-working internal Postfix
relay (the same one Ghost uses in production), via the falah-ibaas
EmailConnector that was fully built but never actually deployed.

Deployed live at https://nfmailrelay.falahos.my as a Docker Swarm service
on the existing Traefik network, TLS via Let's Encrypt, shared-secret
bearer auth. Built for Nur Falah's heir notification emails.
2026-08-14 08:00:28 +08:00

80 lines
2.9 KiB
Python

"""
Nur Falah mail relay — tiny HTTP wrapper around the existing, working
falah-ibaas EmailConnector (SMTP via the local Postfix relay Ghost already
uses successfully on this VPS). Exists because Supabase Edge Functions run
on Supabase's cloud, not on this VPS, and can't reach localhost:25 directly
— this gives them a real internet-reachable endpoint that forwards to it.
Auth: a single shared-secret bearer token (X-Relay-Secret header), generated
locally and never committed to the repo — set as a Supabase project secret
(NF_RELAY_SECRET) and matched here via the RELAY_SECRET env var.
"""
import os
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
sys.path.insert(0, "/opt/falah-ibaas/connectors/email")
from handler import EmailConnector
RELAY_SECRET = os.environ["RELAY_SECRET"]
connector = EmailConnector(
smtp_host=os.environ.get("SMTP_HOST", "localhost"),
smtp_port=int(os.environ.get("SMTP_PORT", "25")),
smtp_user=os.environ.get("SMTP_USER", ""),
smtp_pass=os.environ.get("SMTP_PASS", ""),
from_address=os.environ.get("SMTP_FROM", "notifications@falahos.my"),
from_name="Nur Falah",
)
class Handler(BaseHTTPRequestHandler):
def _send(self, code, body):
payload = json.dumps(body).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def do_GET(self):
if self.path == "/health":
return self._send(200, {"status": "ok", "metrics": connector.metrics()})
return self._send(404, {"error": "not_found"})
def do_POST(self):
if self.path != "/send":
return self._send(404, {"error": "not_found"})
if self.headers.get("X-Relay-Secret") != RELAY_SECRET:
return self._send(401, {"error": "unauthorized"})
length = int(self.headers.get("Content-Length", 0))
try:
data = json.loads(self.rfile.read(length))
except Exception:
return self._send(400, {"error": "invalid_json"})
to = data.get("to")
subject = data.get("subject")
if not to or not subject:
return self._send(400, {"error": "to and subject are required"})
result = connector.send_email(
to=to, subject=subject,
body_text=data.get("body_text"), body_html=data.get("body_html"),
cc=data.get("cc"), bcc=data.get("bcc"),
)
code = 200 if result.get("status") == "sent" else 502
return self._send(code, result)
def log_message(self, fmt, *args):
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
if __name__ == "__main__":
port = int(os.environ.get("PORT", "8091"))
server = HTTPServer(("0.0.0.0", port), Handler)
print(f"nf-mail-relay listening on :{port}")
server.serve_forever()