Spaces:
Runtime error
Runtime error
| """Email service for sending exam reports to teachers.""" | |
| from __future__ import annotations | |
| import os | |
| import smtplib | |
| import ssl | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| from typing import Optional | |
| import markdown | |
| from .config import get_settings | |
| def markdown_to_html(md_content: str) -> str: | |
| """Convert markdown to styled HTML for email.""" | |
| # Convert markdown to HTML | |
| html_body = markdown.markdown( | |
| md_content, | |
| extensions=['tables', 'fenced_code', 'nl2br'] | |
| ) | |
| # Wrap in email template | |
| return f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| body {{ | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| }} | |
| .container {{ | |
| background-color: white; | |
| padding: 30px; | |
| border-radius: 10px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| }} | |
| h1 {{ | |
| color: #2563eb; | |
| border-bottom: 2px solid #2563eb; | |
| padding-bottom: 10px; | |
| }} | |
| h2 {{ | |
| color: #1e40af; | |
| margin-top: 30px; | |
| }} | |
| h3 {{ | |
| color: #3b82f6; | |
| }} | |
| table {{ | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 20px 0; | |
| }} | |
| th, td {{ | |
| border: 1px solid #ddd; | |
| padding: 12px; | |
| text-align: left; | |
| }} | |
| th {{ | |
| background-color: #2563eb; | |
| color: white; | |
| }} | |
| tr:nth-child(even) {{ | |
| background-color: #f8fafc; | |
| }} | |
| code {{ | |
| background-color: #f1f5f9; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-family: monospace; | |
| }} | |
| .footer {{ | |
| margin-top: 40px; | |
| padding-top: 20px; | |
| border-top: 1px solid #ddd; | |
| text-align: center; | |
| color: #666; | |
| font-size: 0.9em; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| {html_body} | |
| <div class="footer"> | |
| <p>Generated by ClassLens • AI-Powered Exam Analysis for Teachers</p> | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| async def send_email_via_gmail( | |
| to_email: str, | |
| subject: str, | |
| body_markdown: str, | |
| gmail_user: str, | |
| gmail_app_password: str | |
| ) -> dict: | |
| """ | |
| Send email using Gmail SMTP. | |
| Requires: | |
| - Gmail account | |
| - App Password (not your regular password) | |
| To get an App Password: | |
| 1. Enable 2-Step Verification on your Google account | |
| 2. Go to https://myaccount.google.com/apppasswords | |
| 3. Generate an app password for "Mail" | |
| """ | |
| try: | |
| html_content = markdown_to_html(body_markdown) | |
| # Create message | |
| message = MIMEMultipart("alternative") | |
| message["Subject"] = subject | |
| message["From"] = gmail_user | |
| message["To"] = to_email | |
| # Add plain text and HTML versions | |
| part1 = MIMEText(body_markdown, "plain") | |
| part2 = MIMEText(html_content, "html") | |
| message.attach(part1) | |
| message.attach(part2) | |
| # Create secure connection and send | |
| context = ssl.create_default_context() | |
| with smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) as server: | |
| server.login(gmail_user, gmail_app_password) | |
| server.sendmail(gmail_user, to_email, message.as_string()) | |
| return {"status": "ok", "message": "Email sent via Gmail"} | |
| except smtplib.SMTPAuthenticationError: | |
| return { | |
| "status": "error", | |
| "message": "Gmail authentication failed. Check your email and app password." | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "error", | |
| "message": f"Failed to send email: {str(e)}" | |
| } | |
| async def send_email_via_sendgrid( | |
| to_email: str, | |
| subject: str, | |
| body_markdown: str | |
| ) -> dict: | |
| """Send email using SendGrid API.""" | |
| settings = get_settings() | |
| try: | |
| from sendgrid import SendGridAPIClient | |
| from sendgrid.helpers.mail import Mail, Email, To, Content, HtmlContent | |
| html_content = markdown_to_html(body_markdown) | |
| message = Mail( | |
| from_email=Email(settings.sendgrid_from_email, "ClassLens"), | |
| to_emails=To(to_email), | |
| subject=subject, | |
| html_content=HtmlContent(html_content) | |
| ) | |
| message.add_content(Content("text/plain", body_markdown)) | |
| sg = SendGridAPIClient(settings.sendgrid_api_key) | |
| response = sg.send(message) | |
| if response.status_code in (200, 201, 202): | |
| return {"status": "ok", "message": "Email sent via SendGrid"} | |
| else: | |
| return { | |
| "status": "error", | |
| "message": f"SendGrid returned status {response.status_code}" | |
| } | |
| except Exception as e: | |
| return { | |
| "status": "error", | |
| "message": str(e) | |
| } | |
| async def send_email_report( | |
| email: str, | |
| subject: str, | |
| body_markdown: str | |
| ) -> dict: | |
| """ | |
| Send an email report to a teacher. | |
| Tries in order: | |
| 1. Gmail SMTP (if configured) | |
| 2. SendGrid (if configured) | |
| 3. Logs to console (fallback) | |
| Returns: | |
| {"status": "ok"} on success | |
| {"status": "error", "message": str} on failure | |
| """ | |
| # Try Gmail SMTP first | |
| gmail_user = os.getenv("GMAIL_USER", "") | |
| gmail_app_password = os.getenv("GMAIL_APP_PASSWORD", "") | |
| if gmail_user and gmail_app_password: | |
| result = await send_email_via_gmail(email, subject, body_markdown, gmail_user, gmail_app_password) | |
| if result["status"] == "ok": | |
| print(f"📧 Email sent to {email} via Gmail") | |
| return result | |
| else: | |
| print(f"⚠️ Gmail failed: {result['message']}, trying fallback...") | |
| # Try SendGrid | |
| settings = get_settings() | |
| if settings.sendgrid_api_key: | |
| result = await send_email_via_sendgrid(email, subject, body_markdown) | |
| if result["status"] == "ok": | |
| print(f"📧 Email sent to {email} via SendGrid") | |
| return result | |
| else: | |
| print(f"⚠️ SendGrid failed: {result['message']}") | |
| # Fallback: print to console | |
| print(f"\n{'='*60}") | |
| print(f"📧 EMAIL (not sent - no email service configured)") | |
| print(f"To: {email}") | |
| print(f"Subject: {subject}") | |
| print(f"{'='*60}") | |
| print(body_markdown[:500] + "..." if len(body_markdown) > 500 else body_markdown) | |
| print(f"{'='*60}\n") | |
| return { | |
| "status": "ok", | |
| "message": "Email logged to console (configure GMAIL_USER/GMAIL_APP_PASSWORD or SENDGRID_API_KEY to send real emails)" | |
| } | |