diff --git a/config.py b/config.py index fb8dc16..f1541e0 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,7 @@ DATA_PATH = BASE_DIR / "data" / "entries.csv" DEFAULT_DATABASE_URL = f"sqlite:///{BASE_DIR / 'data' / 'boker-dev.sqlite3'}" ELIGIBLE_MIN_SESSIONS = 3 -APP_VERSION = "2.5.4" +APP_VERSION = "2.5.5" def load_local_env(env_path: Path) -> None: @@ -44,9 +44,14 @@ class Config: MAIL_SERVER: str = os.getenv("MAIL_SERVER", "") MAIL_PORT: int = int(os.getenv("MAIL_PORT", "587")) MAIL_USE_TLS: bool = os.getenv("MAIL_USE_TLS", "true").lower() in ("true", "1", "yes") + MAIL_USE_SSL: bool = os.getenv( + "MAIL_USE_SSL", + "true" if MAIL_PORT == 465 else "false", + ).lower() in ("true", "1", "yes") MAIL_USERNAME: str | None = os.getenv("MAIL_USERNAME") or None MAIL_PASSWORD: str | None = os.getenv("MAIL_PASSWORD") or None MAIL_DEFAULT_SENDER: str = os.getenv("MAIL_DEFAULT_SENDER", "noreply@myboker.org") + MAIL_TIMEOUT: float = float(os.getenv("MAIL_TIMEOUT", "5")) class ProductionConfig(Config): diff --git a/emails.py b/emails.py index 9f15ce6..b8de782 100644 --- a/emails.py +++ b/emails.py @@ -1,12 +1,31 @@ #!/usr/bin/env python3 from __future__ import annotations +import socket + from flask import current_app from flask_mail import Message from extensions import mail +def _mail_send_suppressed() -> bool: + state = current_app.extensions.get("mail") + return bool(getattr(state, "suppress", False)) + + +def _send_message(msg: Message) -> None: + if not _mail_send_suppressed() and not current_app.config.get("MAIL_SERVER"): + raise RuntimeError("Mail server is not configured.") + + previous_timeout = socket.getdefaulttimeout() + socket.setdefaulttimeout(float(current_app.config.get("MAIL_TIMEOUT", 5))) + try: + mail.send(msg) + finally: + socket.setdefaulttimeout(previous_timeout) + + def send_password_reset(to_email: str, reset_url: str) -> None: msg = Message( subject="Reset your myboker.org password", @@ -19,19 +38,20 @@ def send_password_reset(to_email: str, reset_url: str) -> None: ), sender=current_app.config.get("MAIL_DEFAULT_SENDER"), ) - mail.send(msg) + _send_message(msg) -def send_league_invite(to_email: str, league_name: str, invite_url: str, invited_by_email: str) -> None: +def send_league_invite(to_email: str, league_name: str, invite_url: str, invited_by_email: str, role: str) -> None: + role_label = role.strip().lower() if role else "member" msg = Message( - subject=f"You've been invited to manage {league_name} on myboker.org", + subject=f"You've been invited to join {league_name} on myboker.org", recipients=[to_email], body=( - f"{invited_by_email} has invited you to join {league_name} as a manager on myboker.org.\n\n" + f"{invited_by_email} has invited you to join {league_name} as a {role_label} on myboker.org.\n\n" f"Accept your invitation:\n\n" f"{invite_url}\n\n" f"This link expires in 7 days. You'll need a myboker.org account with this email address to accept." ), sender=current_app.config.get("MAIL_DEFAULT_SENDER"), ) - mail.send(msg) + _send_message(msg) diff --git a/routes/leagues.py b/routes/leagues.py index b95340c..c1a83cd 100644 --- a/routes/leagues.py +++ b/routes/leagues.py @@ -1204,6 +1204,7 @@ def invite_member(league_ref: str): from auth import generate_invite_token from emails import send_league_invite from flask import current_app + from db_models import User from league_repositories import find_membership, find_user_by_email league = require_league(league_ref, {"owner"}) @@ -1228,10 +1229,12 @@ def invite_member(league_ref: str): token = generate_invite_token(league.id, email, role, current_user_id() or "") base_url = current_app.config.get("APP_BASE_URL", "").rstrip("/") invite_url = f"{base_url}{url_for('account.accept_invite', token=token)}" + inviter = db.session.get(User, current_user_id() or "") + inviter_email = inviter.email if inviter is not None else "A league owner" try: - send_league_invite(email, league.name, invite_url, current_user_id() or "") - flash(f"Invitation sent to {email}.", "success") + send_league_invite(email, league.name, invite_url, inviter_email, role) + flash(f"{role.title()} invitation sent to {email}.", "success") except Exception: flash("Failed to send invitation email. Check your mail configuration.", "error") diff --git a/templates/league_settings.html b/templates/league_settings.html index bc85561..4cb4985 100644 --- a/templates/league_settings.html +++ b/templates/league_settings.html @@ -71,7 +71,7 @@ {% endfor %} -
+

Invite by email

@@ -177,6 +177,17 @@ document.querySelectorAll('.modal-confirm-input').forEach(inp => { inp.addEventListener('input', () => checkConfirmInput(inp)); }); + + const inviteForm = document.getElementById('invite-member-form'); + if (inviteForm) { + inviteForm.addEventListener('submit', () => { + const btn = inviteForm.querySelector('button[type="submit"]'); + if (!btn) return; + btn.disabled = true; + btn.textContent = 'Sending...'; + inviteForm.setAttribute('aria-busy', 'true'); + }); + } })();