"""Author RAG — Notifications Service (SMTP Email). Sends OTP login codes and system alerts via Gmail SMTP. Per implementation plan feature #2: Author login OTP via email. """ import smtplib import ssl from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import structlog from app.config import get_settings logger = structlog.get_logger(__name__) cfg = get_settings() async def send_login_otp(to_email: str, otp_code: str, author_name: str = "") -> bool: """Send a 6-digit OTP code to the author's email for login. Args: to_email: Recipient email address. otp_code: 6-digit TOTP code. author_name: Author's display name. Returns: True if sent successfully, False on failure. """ subject = f"Your {cfg.APP_NAME} Login Code: {otp_code}" body = f""" Hi {author_name or "there"}, Your login verification code is: {otp_code} This code expires in 30 seconds. Do not share it with anyone. If you didn't request this, please ignore this email. — {cfg.APP_NAME} Team """ return await _send_email(to_email, subject, body) async def send_token_warning(to_email: str, author_name: str, usage_pct: float) -> bool: """Send a token budget warning when usage crosses a threshold.""" subject = f"[{cfg.APP_NAME}] Token Budget Warning — {usage_pct:.0%} Used" body = f""" Hi {author_name}, Your chatbot has used {usage_pct:.0%} of your monthly token budget. Log in to your dashboard to view usage details or contact support to upgrade. — {cfg.APP_NAME} Team """ return await _send_email(to_email, subject, body) async def _send_email(to: str, subject: str, body: str) -> bool: """Send a plain-text email via SMTP. Args: to: Recipient email. subject: Email subject. body: Plain text body. Returns: True on success. """ if not cfg.SMTP_USERNAME or not cfg.SMTP_PASSWORD: logger.warning("SMTP not configured — email not sent", to=to, subject=subject) return False msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = f"{cfg.EMAIL_FROM_NAME} <{cfg.EMAIL_FROM_ADDRESS or cfg.SMTP_USERNAME}>" msg["To"] = to msg.attach(MIMEText(body, "plain")) try: context = ssl.create_default_context() with smtplib.SMTP_SSL(cfg.SMTP_HOST, cfg.SMTP_PORT, context=context) as server: server.login(str(cfg.SMTP_USERNAME), cfg.SMTP_PASSWORD) server.sendmail(str(cfg.SMTP_USERNAME), to, msg.as_string()) logger.info("Email sent", to=to, subject=subject) return True except Exception as e: logger.error("Failed to send email", to=to, error=str(e)) return False