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.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user