#!/usr/bin/env python3 # mailer.py — Mock e-mail sender for dev/demo (R6 #3) # v1.0 dradulic@outlook.com / damir@rinet.one — 2026-05-04 """ In dev/demo mode, e-mails are appended to: - /tmp/pgz_mailbox/-.eml (raw .eml file) - /tmp/pgz_mailbox/INDEX.jsonl (one JSON line per send) plus printed to stdout (visible in journalctl). Set PGZ_SMTP_HOST=... to switch to real SMTP (skipped here — out of scope for R6 demo). The function ALWAYS returns a result; never raises. """ import os, json, time, smtplib from email.message import EmailMessage from datetime import datetime from pathlib import Path from typing import Dict, Optional MAILBOX_DIR = Path(os.environ.get("PGZ_MAILBOX_DIR", "/tmp/pgz_mailbox")) MAILBOX_DIR.mkdir(parents=True, exist_ok=True) INDEX_FILE = MAILBOX_DIR / "INDEX.jsonl" DEFAULT_FROM = os.environ.get("PGZ_MAIL_FROM", "no-reply@pgz.hr") DEFAULT_SENDER = os.environ.get("PGZ_MAIL_SENDER", "PGŽ Sport platforma") def send_email(to: str, subject: str, body: str, html: Optional[str] = None, from_addr: Optional[str] = None, metadata: Optional[Dict] = None) -> Dict: """Send (or mock-send) an email. Returns a dict with status + storage info.""" sender = from_addr or DEFAULT_FROM msg = EmailMessage() msg["From"] = f"{DEFAULT_SENDER} <{sender}>" msg["To"] = to msg["Subject"] = subject msg["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") msg.set_content(body) if html: msg.add_alternative(html, subtype="html") smtp_host = os.environ.get("PGZ_SMTP_HOST") use_real = bool(smtp_host) sent_at = int(time.time()) fname = f"{sent_at}-{to.replace('@','_at_')}.eml" fpath = MAILBOX_DIR / fname try: with open(fpath, "wb") as f: f.write(bytes(msg)) except Exception as e: print(f"[MAILER WARN] cannot write {fpath}: {e}") rec = { "ts": sent_at, "to": to, "from": sender, "subject": subject, "file": str(fpath), "real_send": use_real, "metadata": metadata or {}, } try: with open(INDEX_FILE, "a") as f: f.write(json.dumps(rec, ensure_ascii=False) + "\n") except Exception: pass if use_real: try: port = int(os.environ.get("PGZ_SMTP_PORT", "587")) user = os.environ.get("PGZ_SMTP_USER") pwd = os.environ.get("PGZ_SMTP_PASS") with smtplib.SMTP(smtp_host, port, timeout=10) as s: s.starttls() if user: s.login(user, pwd or "") s.send_message(msg) rec["sent"] = True except Exception as e: rec["sent"] = False rec["error"] = str(e) print(f"[MAILER ERROR] {e}") else: # demo / dev mode — print preview to stdout print(f"[MOCK-MAIL] → {to} | {subject}") print(f"[MOCK-MAIL] stored at {fpath}") rec["sent"] = True rec["mock"] = True return rec # ─────────────────────────── Convenience helpers ─────────────────────────── def send_password_reset(email: str, reset_link: str, expires_in_seconds: int) -> Dict: hours = expires_in_seconds / 3600 body = ( f"Pozdrav,\n\n" f"Zatraženo je resetiranje lozinke za vaš račun na PGŽ Sport platformi.\n\n" f"Otvorite ovaj link za postavljanje nove lozinke (vrijedi {hours:.0f} h):\n" f"{reset_link}\n\n" f"Ako niste vi tražili promjenu, ignorirajte ovu poruku.\n\n" f"— PGŽ Sport platforma" ) html = ( f'

Pozdrav,

' f'

Zatraženo je resetiranje lozinke za vaš račun na PGŽ Sport platformi.

' f'

Postavi novu lozinku

' f'

Link vrijedi {hours:.0f} h.

' ) return send_email(email, "Resetiranje lozinke — PGŽ Sport", body, html=html, metadata={"kind": "password_reset"}) def send_invite(email: str, invite_link: str, expires_in_seconds: int, inviter: Optional[str] = None, role: Optional[str] = None) -> Dict: days = expires_in_seconds / 86400 by = f" od {inviter}" if inviter else "" body = ( f"Pozdrav,\n\n" f"Pozvani ste{by} u PGŽ Sport platformu" + (f" kao {role}" if role else "") + ".\n\n" f"Otvorite ovaj link i postavite svoju lozinku (vrijedi {days:.0f} dana):\n" f"{invite_link}\n\n" f"— PGŽ Sport platforma" ) html = ( f'

Pozdrav,

' f'

Pozvani ste{by} u PGŽ Sport platformu' + (f' kao {role}' if role else '') + '.

' f'

Postavi lozinku i prijavi se

' f'

Pozivnica vrijedi {days:.0f} dana.

' ) return send_email(email, "Pozivnica — PGŽ Sport", body, html=html, metadata={"kind": "invite", "role": role})