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="PedsHub", 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("[code:"): # A thing to read off the screen and type, so it is set large, # spaced and monospaced rather than left to look like a heading. m = re.match(r'\[code:(.+?)\]', s) if m: out.append(f'

{m.group(1)}

') 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}
๐Ÿฅ PedsHub
{body_html}

PedsHub ยท 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 PedsHub email" md = f"""# Verify your email Hi **{name}**, Welcome to PedsHub. 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 PedsHub 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_login_code_email(to_email: str, name: str, code: str): #: The code arrives here already grouped for reading; any shape it is typed #: back in is normalised before it is compared. subject = "Your PedsHub sign-in code" md = f"""# Your sign-in code Hi **{name}**, Enter this code on the sign-in page you just came from. [code:{code}] > Expires in **15 minutes** ยท Works once. If you asked more than once, only the newest code works. Nobody from PedsHub will ever ask you to read this code out, on the phone or anywhere else. If somebody has, they are not us. If you didn't ask for this code, ignore this email โ€” nobody can get in without it. """ await _send(to_email, subject, _wrap(subject, md))