319218ce21
Test backoffice (smb-crm) / test (push) Has been cancelled
Postgres is now the sole source of truth: delete sheets.py and import_from_sheets.py, strip mirror_entity/mirror_async/_mirror_worker and POST /api/sync from app.py, drop the tab/mirror keys from db.py's TABLES. Re-point n8n/renewal-reminder.json at the CRM's own HTTP API (GET /api/clients, POST /api/activity_log) instead of the Sheets nodes, and drop SHEET_ID/GOOGLE_SA_JSON from deploy env/compose and requests from requirements.txt (PyJWT stays — still used by booking_api.py). Updates docs/README/playbooks accordingly and closes the old #5 (atomic mirror) as moot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""Best-effort SMTP email sending (#18).
|
|
|
|
A single background worker thread drains a queue, and a send failure is
|
|
logged, never raised back to the caller -- booking creation must succeed even
|
|
if the mail relay is down.
|
|
"""
|
|
import os
|
|
import queue
|
|
import smtplib
|
|
import threading
|
|
import traceback
|
|
from email.message import EmailMessage
|
|
|
|
SMTP_HOST = os.environ.get("SMTP_HOST", "")
|
|
SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
|
|
SMTP_USERNAME = os.environ.get("SMTP_USERNAME", "")
|
|
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
|
|
# Used when a client has no domain configured -- see booking_mail.py.
|
|
MAIL_FALLBACK_FROM = os.environ.get("MAIL_FALLBACK_FROM", "noreply@mivanchenko.de")
|
|
|
|
_queue = queue.Queue()
|
|
|
|
|
|
def _send_now(msg):
|
|
if not SMTP_HOST:
|
|
print(f"[mailer] SMTP_HOST not configured, skipping send to {msg['To']}", flush=True)
|
|
return
|
|
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as smtp:
|
|
smtp.starttls()
|
|
if SMTP_USERNAME:
|
|
smtp.login(SMTP_USERNAME, SMTP_PASSWORD)
|
|
smtp.send_message(msg)
|
|
|
|
|
|
def _worker():
|
|
while True:
|
|
msg = _queue.get()
|
|
try:
|
|
_send_now(msg)
|
|
except Exception: # noqa: BLE001
|
|
print(f"[mailer] send to {msg['To']} failed:", flush=True)
|
|
traceback.print_exc()
|
|
finally:
|
|
_queue.task_done()
|
|
|
|
|
|
threading.Thread(target=_worker, daemon=True).start()
|
|
|
|
|
|
def send_email(to_addr, subject, html_body, from_addr=None):
|
|
"""Queue an HTML email for best-effort async delivery. Never raises and
|
|
never blocks the caller on the network -- the actual SMTP conversation
|
|
happens on the background worker thread."""
|
|
msg = EmailMessage()
|
|
msg["Subject"] = subject
|
|
msg["From"] = from_addr or MAIL_FALLBACK_FROM
|
|
msg["To"] = to_addr
|
|
msg.set_content(html_body, subtype="html")
|
|
_queue.put(msg)
|