""" MeshBay Hub — email sending via localhost Postfix. Postfix listens on loopback only (inet_interfaces = loopback-only), so no authentication is needed. See docs/MAIL-SERVER.md for the full setup. """ import logging import smtplib from email.message import EmailMessage log = logging.getLogger(__name__) _hub_domain: str = "meshbay.org" _hub_url: str = "https://meshbay.org" def configure(hub_id: str) -> None: global _hub_domain, _hub_url _hub_domain = hub_id _hub_url = f"https://{hub_id}" def _send(msg: EmailMessage) -> bool: try: with smtplib.SMTP("localhost", 25, timeout=10) as s: s.send_message(msg) return True except Exception: log.exception("Failed to send email to %s", msg["To"]) return False def send_verification_code(to: str, code: str, recovery_key: str | None = None) -> None: """ Registration verification e-mail. When `recovery_key` is given it is appended to the body so the recipient's mailbox becomes the backup for it (docs/auth-confirm.md §4.4). `recovery_key` is a **pass-through**: it is generated on the client, never stored anywhere on the hub, and never logged — only whether one was present. """ body = ( f"Your verification code is: {code}\n" "\n" "Enter this code to verify your email address.\n" "This code expires in 24 hours.\n" ) if recovery_key: body += ( "\n" "---- Account recovery key ----\n" "\n" "Keep this message. If you ever forget your passphrase, this key is\n" "what restores your access to your groups. It is not stored on the\n" f"server and nobody at {_hub_domain} can recover it for you.\n" "\n" f" {recovery_key}\n" ) body += ( "\n" "If you did not create a MeshBay account, ignore this email.\n" "\n" f"{_hub_url}\n" ) msg = EmailMessage() msg["From"] = f"noreply@{_hub_domain}" msg["To"] = to msg["Subject"] = f"MeshBay — Your verification code: {code}" msg.set_content(body) _send(msg) log.info("Verification code sent to %s (recovery_key=%s)", _mask_email(to), bool(recovery_key)) def send_email_change_code(to: str, code: str) -> None: msg = EmailMessage() msg["From"] = f"noreply@{_hub_domain}" msg["To"] = to msg["Subject"] = f"MeshBay — Confirm your new email: {code}" msg.set_content( f"Your verification code is: {code}\n" "\n" "Enter this code to confirm your new email address.\n" "This code expires in 24 hours.\n" "\n" "If you did not request this change, ignore this email.\n" "\n" f"{_hub_url}\n" ) _send(msg) log.info("Email change code sent to %s", _mask_email(to)) def send_password_reset_code(to: str, code: str) -> None: """ Passphrase-reset code (docs/auth-confirm.md §4.2). This only re-opens hub login; it recovers no group content — that needs the recovery key. """ msg = EmailMessage() msg["From"] = f"noreply@{_hub_domain}" msg["To"] = to msg["Subject"] = f"MeshBay — Passphrase reset code: {code}" msg.set_content( f"Your passphrase reset code is: {code}\n" "\n" "Enter it to set a new passphrase. This code expires in 1 hour.\n" "\n" "This restores your sign-in only. If you also have your recovery key,\n" "you can restore access to your groups in the same step.\n" "\n" "If you did not request this, ignore this email — your account is\n" "unchanged.\n" "\n" f"{_hub_url}\n" ) _send(msg) log.info("Passphrase reset code sent to %s", _mask_email(to)) def send_invite_notification( to: str, code: str, inviter: str, group_name: str, ) -> None: msg = EmailMessage() msg["From"] = f"noreply@{_hub_domain}" msg["To"] = to msg["Subject"] = f"MeshBay — {inviter} invited you to {group_name}" msg.set_content( f"{inviter} invited you to the group \"{group_name}\" on MeshBay.\n" "\n" f"Your one-time code is: {code}\n" "\n" "Open the group and enter this code when prompted.\n" "The code works once and expires in 7 days.\n" "\n" f"{_hub_url}\n" ) _send(msg) log.info("Invite notification sent to %s", _mask_email(to)) def _mask_email(email: str) -> str: local, _, domain = email.partition("@") if len(local) <= 2: return f"{'*' * len(local)}@{domain}" return f"{local[0]}{'*' * (len(local) - 2)}{local[-1]}@{domain}"