ClassLens / chatkit /backend /app /email_service.py
chih.yikuan
🔧 Fix OAuth: auto-detect redirect_uri + sync all ClassLens updates
e5c2788
"""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)"
}