import logging import re from datetime import datetime from fastapi_mail import FastMail, MessageSchema, MessageType, ConnectionConfig from app.config import settings logger = logging.getLogger(__name__) def get_mail_config() -> ConnectionConfig: return ConnectionConfig( MAIL_USERNAME=settings.MAIL_USERNAME, MAIL_PASSWORD=settings.MAIL_PASSWORD, MAIL_FROM=settings.MAIL_FROM, MAIL_FROM_NAME="PedQuiz", MAIL_PORT=settings.MAIL_PORT, MAIL_SERVER=settings.MAIL_SERVER, MAIL_STARTTLS=settings.MAIL_STARTTLS, MAIL_SSL_TLS=settings.MAIL_SSL_TLS, USE_CREDENTIALS=bool(settings.MAIL_USERNAME and settings.MAIL_PASSWORD), ) def _render(md: str) -> str: """ Minimal markdown โ†’ inline HTML for email. Resend-style: white background, dark text, clean sans-serif, one column. """ lines, out = md.strip().split("\n"), [] for line in lines: s = line.strip() # Bold inline s_html = re.sub(r'\*\*(.+?)\*\*', r'\1', s) if s.startswith("# "): out.append(f'

{s[2:]}

') elif s.startswith("## "): out.append(f'

{s[3:]}

') elif s.startswith("> "): out.append(f'
{s_html[2:]}
') elif s.startswith("---"): out.append('
') elif s.startswith("[button:"): m = re.match(r'\[button:(.+?)\]\((.+?)\)', s) if m: label, url = m.group(1), m.group(2) out.append(f'

{label} โ†’

') elif s.startswith("[link:"): m = re.match(r'\[link:(.+?)\]\((.+?)\)', s) if m: label, url = m.group(1), m.group(2) out.append(f'

Or copy: {url}

') elif s == "": out.append('
') else: out.append(f'

{s_html}

') return "\n".join(out) def _wrap(subject: str, body_md: str) -> str: year = datetime.utcnow().year body_html = _render(body_md) return f""" {subject}
๐Ÿฉบ PedQuiz
{body_html}

PedQuiz ยท Pediatric Knowledge Platform
{settings.APP_URL}

Didn't expect this email? You can safely ignore it.

""" async def _send(to_email: str, subject: str, html: str): if not settings.MAIL_USERNAME or not settings.MAIL_FROM: logger.info(f"[EMAIL not configured] To:{to_email} Subject:{subject}") return try: fm = FastMail(get_mail_config()) await fm.send_message(MessageSchema(subject=subject, recipients=[to_email], body=html, subtype=MessageType.html)) logger.info(f"Email sent โ†’ {to_email}: {subject}") except Exception as e: logger.error(f"Email failed โ†’ {to_email}: {e}") async def send_verification_email(to_email: str, name: str, token: str): url = f"{settings.APP_URL}/verify-email?token={token}" subject = "Verify your PedQuiz email" md = f"""# Verify your email Hi **{name}**, Welcome to PedQuiz. Click below to verify your email address and activate your account. [button:Verify Email Address]({url}) > This link expires in **24 hours**. --- [link:Or copy this link]({url}) """ await _send(to_email, subject, _wrap(subject, md)) async def send_password_reset_email(to_email: str, name: str, token: str): url = f"{settings.APP_URL}/reset-password?token={token}" subject = "Reset your PedQuiz password" md = f"""# Reset your password Hi **{name}**, We received a request to reset your password. Click below to choose a new one. [button:Reset Password]({url}) > Expires in **1 hour** ยท Single use only. If you didn't request this, ignore this email โ€” your account is safe. --- [link:Or copy this link]({url}) """ await _send(to_email, subject, _wrap(subject, md)) async def send_reminder_email(email: str, user_name: str, quiz_title: str, score: float, next_date: str): subject = f"Time to review: {quiz_title}" note = "Great work โ€” keep the streak going! ๐ŸŒŸ" if score >= 75 else "A bit more practice will get you there. ๐Ÿ“š" md = f"""# Time to review Hi **{user_name}**, Your spaced repetition schedule says it's time to revisit **{quiz_title}**. ## Last score **{score:.0f}%** โ€” {note} Reviewing at spaced intervals is one of the most effective ways to retain medical knowledge long-term. Each session strengthens the memory trace. [button:Take Quiz Now]({settings.APP_URL}/quizzes) --- You're receiving this because you have active quiz reminders. To stop, disable reminders in your account settings. """ await _send(email, subject, _wrap(subject, md))