ligbox-ops-platform/projects/ops-desk/api/app/email_relay.py
Ligbox Spec Hub b03bb2c37c feat(desk): UI/API Spec 039-041 + deploy atómico e smoke GREEN
Commita governance, user-wizard, operational-feed e catálogo RBAC;
adiciona deploy-desk-full.sh, smoke-desk.sh e regra anti-deploy parcial;
documenta credencial VM112 @betinplace.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 13:32:15 +00:00

123 lines
3.9 KiB
Python

"""Email relay VM122 → VM112 — status, config e teste (Spec 004 / Infra CODE)."""
from __future__ import annotations
import json
import os
import smtplib
from datetime import datetime, timezone
from email.message import EmailMessage
from pathlib import Path
from typing import Any
from app import mail_notify
CONFIG_PATH = Path(os.getenv("EMAIL_RELAY_CONFIG_PATH", "/data/email_relay_config.json"))
RELAY_HOST = os.getenv("DESK_RELAY_HOST", "10.10.10.112")
RELAY_PORT = int(os.getenv("DESK_RELAY_PORT", "25"))
SMTP_HOST = os.getenv("DESK_SMTP_HOST", "10.10.10.122")
SMTP_PORT = int(os.getenv("DESK_SMTP_PORT", "25"))
MAIL_FROM = os.getenv("DESK_MAIL_FROM", "ligbox-ops@ligbox.com.br")
DEFAULT_CONFIG: dict[str, Any] = {
"vm": "122",
"vm_label": "VM122 · Ops Desk",
"service_id": "vm122-email-relay",
"title": "Email Relay (Postfix)",
"relayhost": RELAY_HOST,
"relayport": RELAY_PORT,
"smtp_host": SMTP_HOST,
"smtp_port": SMTP_PORT,
"mail_from": MAIL_FROM,
"myorigin": "ligbox.com.br",
"transport_local": {
"ligbox.com.br": "LMTP [10.10.10.112]:7025",
"ibytera.com": "LMTP [10.10.10.112]:7025",
"dratcoin.com": "LMTP [10.10.10.112]:7025",
},
"external_route": "relayhost → mail.ligbox.com.br (DKIM/SPF)",
"docs": "docs/postfix-vm122.md",
}
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def load_config() -> dict[str, Any]:
cfg = dict(DEFAULT_CONFIG)
cfg["updated_at"] = _now()
if CONFIG_PATH.is_file():
try:
stored = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
if isinstance(stored, dict):
cfg.update(stored)
except Exception:
pass
return cfg
def save_config(patch: dict[str, Any]) -> dict[str, Any]:
cfg = load_config()
cfg.update(patch)
cfg["updated_at"] = _now()
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
CONFIG_PATH.write_text(json.dumps(cfg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return cfg
def _probe_smtp(host: str, port: int, timeout: float = 8.0) -> dict[str, Any]:
try:
with smtplib.SMTP(host, port, timeout=timeout) as smtp:
code, _ = smtp.ehlo()
smtp.noop()
ok = 200 <= code < 400
return {"ok": ok, "status": "online" if ok else "check", "detail": f"SMTP {host}:{port} EHLO {code}"}
except Exception as exc:
return {"ok": False, "status": "down", "detail": f"SMTP {host}:{port}{exc}"}
def build_status() -> dict[str, Any]:
cfg = load_config()
local = _probe_smtp(cfg["smtp_host"], int(cfg["smtp_port"]))
relay = _probe_smtp(cfg["relayhost"], int(cfg["relayport"]))
ok = bool(local.get("ok") and relay.get("ok"))
detail_parts = [
local.get("detail", ""),
f"relay {cfg['relayhost']}:{cfg['relayport']}{relay.get('detail', '')}",
]
return {
"ok": ok,
"status": "online" if ok else ("check" if local.get("ok") else "down"),
"detail": " · ".join(p for p in detail_parts if p),
"config": cfg,
"checks": {
"smtp_local": local,
"smtp_relay": relay,
},
"generated_at": _now(),
}
def probe_stack() -> dict[str, Any]:
st = build_status()
return {"ok": st["ok"], "status": st["status"], "detail": st["detail"]}
def send_test_email(to: str, subject: str | None = None) -> dict[str, Any]:
to = (to or "").strip()
if not to:
return {"ok": False, "detail": "destinatário vazio"}
subj = subject or "[Ligbox Ops] Teste Email Relay VM122"
body = (
"Teste do relay Postfix VM122 → VM112 (mail.ligbox.com.br).\n\n"
f"Gerado em: {_now()}\n"
)
ok = mail_notify.send_email(to, subj, body)
return {
"ok": ok,
"detail": "enviado via SMTP local" if ok else "falha SMTP — verificar Postfix/relay",
"to": to,
"from": MAIL_FROM,
"at": _now(),
}