Spaces:
Running
Running
| """ | |
| Email Service - Send notification emails | |
| Uses Resend API (can be swapped for SendGrid, AWS SES, etc.) | |
| """ | |
| import os | |
| import httpx | |
| from typing import Optional | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # Email configuration | |
| RESEND_API_KEY = os.getenv("RESEND_API_KEY") | |
| # FROM_EMAIL must be from a verified domain in Resend | |
| # User has verified: ai20insights.tech | |
| FROM_EMAIL = os.getenv("FROM_EMAIL", "insights@ai20insights.tech") | |
| APP_URL = os.getenv("APP_URL", "https://datavision-ai-datavision.hf.space") | |
| async def send_insight_email( | |
| to_email: str, | |
| title: str, | |
| body: str, | |
| chart_payload: Optional[dict] = None, | |
| workspace_id: str = None | |
| ): | |
| """Send insight notification email via Resend API or SMTP fallback""" | |
| html_content = render_insight_email_template(title, body, chart_payload, workspace_id) | |
| # 1. Try Resend API first if RESEND_API_KEY is configured | |
| if RESEND_API_KEY: | |
| logger.info(f"π§ Sending email via Resend API from {FROM_EMAIL} to {to_email}") | |
| try: | |
| async with httpx.AsyncClient(timeout=15.0) as client: | |
| payload = { | |
| "from": f"DataVision <{FROM_EMAIL}>", | |
| "to": [to_email], | |
| "subject": f"π DataVision: {title}", | |
| "html": html_content, | |
| "text": render_plain_text_email(title, body), | |
| } | |
| response = await client.post( | |
| "https://api.resend.com/emails", | |
| headers={ | |
| "Authorization": f"Bearer {RESEND_API_KEY}", | |
| "Content-Type": "application/json" | |
| }, | |
| json=payload | |
| ) | |
| if response.status_code == 200: | |
| result = response.json() | |
| logger.info(f"β Email sent via Resend API! ID: {result.get('id', 'unknown')}") | |
| return result | |
| else: | |
| logger.warning(f"π§ Resend API status {response.status_code}: {response.text}") | |
| except Exception as e: | |
| logger.warning(f"π§ Resend API attempt failed: {e}") | |
| # 2. Try SMTP fallback if SMTP env vars are present | |
| smtp_host = os.getenv("SMTP_HOST") | |
| smtp_user = os.getenv("SMTP_USER") | |
| smtp_pass = os.getenv("SMTP_PASSWORD") | |
| smtp_port = int(os.getenv("SMTP_PORT", "587")) | |
| if smtp_host and smtp_user and smtp_pass: | |
| logger.info(f"π§ Attempting SMTP send via {smtp_host}:{smtp_port} to {to_email}") | |
| try: | |
| import smtplib | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| msg = MIMEMultipart("alternative") | |
| msg["Subject"] = f"π DataVision: {title}" | |
| msg["From"] = f"DataVision <{smtp_user}>" | |
| msg["To"] = to_email | |
| part1 = MIMEText(render_plain_text_email(title, body), "plain") | |
| part2 = MIMEText(html_content, "html") | |
| msg.attach(part1) | |
| msg.attach(part2) | |
| with smtplib.SMTP(smtp_host, smtp_port, timeout=10) as server: | |
| server.starttls() | |
| server.login(smtp_user, smtp_pass) | |
| server.sendmail(smtp_user, [to_email], msg.as_string()) | |
| logger.info(f"β Email sent via SMTP to {to_email}!") | |
| return {"status": "sent", "provider": "smtp"} | |
| except Exception as smtp_err: | |
| logger.error(f"β SMTP send failed: {smtp_err}") | |
| logger.warning("π§ No active email provider (Resend API key or SMTP config). Simulating email send for development/testing.") | |
| # Log the email content to console so developers can still see what would have been sent | |
| logger.info(f"[SIMULATED EMAIL TO {to_email}]: {title}\n{body[:100]}...") | |
| return {"status": "simulated", "provider": "none"} | |
| def render_insight_email_template( | |
| title: str, | |
| body: str, | |
| chart_payload: Optional[dict], | |
| workspace_id: str | |
| ) -> str: | |
| """Render ultra-premium HTML email template for insight notification (DataVision Enterprise)""" | |
| # Modern Enterprise Gradient (Teal/Emerald) | |
| button_gradient = "linear-gradient(135deg, #0d9488 0%, #059669 100%)" | |
| # Check if we should link to a specific chat context or general dashboard | |
| cta_text = "View Dashboard Insights" | |
| if not chart_payload: | |
| cta_text = "Discuss with AI Analyst" | |
| return f''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta name="color-scheme" content="light dark"> | |
| <meta name="supported-color-schemes" content="light dark"> | |
| <title>{title}</title> | |
| <!--[if mso]> | |
| <style> | |
| table {{border-collapse:collapse;border:0;padding:0;margin:0;}} | |
| div, td {{font-family: Arial, sans-serif !important;}} | |
| </style> | |
| <![endif]--> | |
| </head> | |
| <body style="margin: 0; padding: 0; background-color: #f4f4f5; font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;"> | |
| <table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f4f4f5; padding: 40px 20px;"> | |
| <tr> | |
| <td align="center"> | |
| <!-- Main Container --> | |
| <table border="0" cellpadding="0" cellspacing="0" width="600" style="background-color: #ffffff; border-radius: 16px; border: 1px solid #e4e4e7; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05); overflow: hidden;"> | |
| <!-- Header --> | |
| <tr> | |
| <td style="padding: 40px 48px; background: #fafafa; border-bottom: 1px solid #f4f4f5;"> | |
| <table border="0" cellpadding="0" cellspacing="0" width="100%"> | |
| <tr> | |
| <td> | |
| <div style="display: inline-block; padding: 6px 12px; background: rgba(13, 148, 136, 0.1); border-radius: 20px; border: 1px solid rgba(13, 148, 136, 0.2); margin-bottom: 24px;"> | |
| <span style="color: #0d9488; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;">β¨ AI Intelligence Digest</span> | |
| </div> | |
| <h1 style="margin: 0 0 12px 0; font-size: 32px; font-weight: 700; color: #18181b; letter-spacing: -0.02em; line-height: 1.2;"> | |
| {title} | |
| </h1> | |
| <p style="margin: 0; font-size: 15px; color: #71717a; font-weight: 400;"> | |
| Powered by Datavision AI. | |
| </p> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| <!-- Content Body --> | |
| <tr> | |
| <td style="padding: 48px;"> | |
| <!-- LLM Generated Content --> | |
| <div style="font-size: 16px; color: #3f3f46; line-height: 1.7; margin-bottom: 40px; font-weight: 400;"> | |
| {body.replace(chr(10), '<br/>')} | |
| </div> | |
| <!-- Call to Action --> | |
| <table cellpadding="0" cellspacing="0" width="100%"> | |
| <tr> | |
| <td align="center" style="padding-top: 16px; border-top: 1px solid #f4f4f5;"> | |
| <table cellpadding="0" cellspacing="0" style="margin: 24px auto 0 auto;"> | |
| <tr> | |
| <td style="border-radius: 8px; background: {button_gradient};"> | |
| <a href="{APP_URL}/chat" | |
| style="display: inline-block; padding: 14px 32px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; box-shadow: 0 4px 14px 0 rgba(13, 148, 136, 0.39);"> | |
| {cta_text} → | |
| </a> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| <!-- Footer --> | |
| <tr> | |
| <td style="padding: 32px 48px; background-color: #fafafa; border-top: 1px solid #f4f4f5; text-align: center;"> | |
| <table border="0" cellpadding="0" cellspacing="0" width="100%"> | |
| <tr> | |
| <td align="center"> | |
| <h2 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 800; letter-spacing: -0.02em;"> | |
| <span style="color: #0d9488;">Data</span><span style="color: #18181b;">Vision</span> | |
| </h2> | |
| <p style="margin: 0 0 16px 0; font-size: 13px; color: #71717a; line-height: 1.5;"> | |
| This report was autonomously generated by your DataVision agents.<br/> | |
| If you wish to change your notification preferences, you can do so in your <a href="{APP_URL}/settings" style="color: #0d9488; text-decoration: underline;">account settings</a>. | |
| </p> | |
| <p style="margin: 0; font-size: 12px; color: #a1a1aa;"> | |
| © 2026 DataVision AI Analytics. All rights reserved. | |
| </p> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| </table> | |
| </body> | |
| </html> | |
| ''' | |
| # Alternative: Plain text version for email clients that don't support HTML | |
| def render_plain_text_email(title: str, body: str) -> str: | |
| """Render plain text version of email""" | |
| return f""" | |
| AI INSIGHT DETECTED | |
| ================== | |
| {title} | |
| {body} | |
| View in dashboard: {APP_URL}/dashboard | |
| --- | |
| DataVision - Universal Data Intelligence | |
| Manage your notification preferences: {APP_URL}/settings/notifications | |
| """ | |
| async def send_password_reset_email(to_email: str, reset_link: str) -> dict: | |
| """Send a branded password reset email""" | |
| if not RESEND_API_KEY: | |
| error_msg = "Email service not configured. Please add RESEND_API_KEY to your .env file." | |
| logger.warning(error_msg) | |
| raise Exception(error_msg) | |
| html_content = render_password_reset_template(reset_link) | |
| plain_text = f""" | |
| DataVision Password Reset | |
| ======================== | |
| We received a request to reset your password for your DataVision account. | |
| Click the link below to reset your password: | |
| {reset_link} | |
| This link will expire in 1 hour. | |
| If you didn't request this, you can safely ignore this email. | |
| --- | |
| DataVision AI Analytics | |
| {APP_URL} | |
| """ | |
| logger.info(f"π§ Sending password reset email to {to_email}") | |
| try: | |
| async with httpx.AsyncClient(timeout=30.0) as client: | |
| payload = { | |
| "from": f"DataVision <{FROM_EMAIL}>", | |
| "to": [to_email], | |
| "subject": "π Reset Your DataVision Password", | |
| "html": html_content, | |
| "text": plain_text, | |
| } | |
| response = await client.post( | |
| "https://api.resend.com/emails", | |
| headers={ | |
| "Authorization": f"Bearer {RESEND_API_KEY}", | |
| "Content-Type": "application/json" | |
| }, | |
| json=payload | |
| ) | |
| logger.info(f"π§ Resend response status: {response.status_code}") | |
| if response.status_code != 200: | |
| error_detail = response.text | |
| logger.error(f"π§ Resend API error: {error_detail}") | |
| raise Exception(f"Resend API error ({response.status_code}): {error_detail}") | |
| result = response.json() | |
| logger.info(f"β Password reset email sent! ID: {result.get('id', 'unknown')}") | |
| return result | |
| except httpx.TimeoutException: | |
| error_msg = "Email request timed out" | |
| logger.error(error_msg) | |
| raise Exception(error_msg) | |
| except Exception as e: | |
| logger.error(f"β Failed to send password reset email: {str(e)}") | |
| raise | |
| def render_password_reset_template(reset_link: str) -> str: | |
| """Render branded HTML password reset email template""" | |
| return f''' | |
| <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
| <html xmlns="http://www.w3.org/1999/xhtml"> | |
| <head> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | |
| <title>Reset Your Password - DataVision</title> | |
| <style type="text/css"> | |
| body, td, div, p, a, input {{ font-family: 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; }} | |
| </style> | |
| </head> | |
| <body style="margin: 0; padding: 0; background-color: #0a0a0b; color: #e2e8f0;"> | |
| <table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #0a0a0b;"> | |
| <tr> | |
| <td align="center" style="padding: 40px 10px;"> | |
| <table border="0" cellpadding="0" cellspacing="0" width="600" style="background-color: #141414; border-radius: 16px; overflow: hidden; border: 1px solid #262626; box-shadow: 0 4px 24px rgba(0,0,0,0.5); max-width: 600px;"> | |
| <!-- Header --> | |
| <tr> | |
| <td style="background-color: #1a1a1a; padding: 40px 30px; border-bottom: 1px solid #262626; text-align: center;"> | |
| <div style="display: inline-block; padding: 6px 12px; border-radius: 50px; background-color: rgba(20, 184, 166, 0.1); border: 1px solid rgba(20, 184, 166, 0.2); margin-bottom: 16px;"> | |
| <span style="color: #2dd4bf; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px;">π Password Reset</span> | |
| </div> | |
| <h1 style="margin: 0; font-size: 28px; font-weight: 800; letter-spacing: -0.5px; color: #ffffff;"> | |
| <span style="color: #2dd4bf;">Data</span><span style="color: #f59e0b;">Vision</span> | |
| </h1> | |
| </td> | |
| </tr> | |
| <!-- Content Body --> | |
| <tr> | |
| <td style="padding: 40px 30px; background-color: #141414;"> | |
| <h2 style="margin: 0 0 16px 0; font-size: 22px; font-weight: 700; color: #ffffff;"> | |
| Reset Your Password | |
| </h2> | |
| <div style="color: #cccccc; font-size: 16px; line-height: 1.6; margin-bottom: 24px;"> | |
| <p style="margin: 0 0 16px 0;">Hi there! π</p> | |
| <p style="margin: 0 0 16px 0;">We received a request to reset your password for your DataVision account.</p> | |
| <p style="margin: 0 0 16px 0;">Click the button below to create a new password:</p> | |
| </div> | |
| <!-- Reset Button --> | |
| <table cellpadding="0" cellspacing="0" style="margin: 25px 0;"> | |
| <tr> | |
| <td style="border-radius: 12px; background: linear-gradient(135deg, #0d9488 0%, #14b8a6 100%); box-shadow: 0 4px 12px rgba(13, 148, 136, 0.3);"> | |
| <a href="{reset_link}" | |
| style="display: inline-block; padding: 16px 36px; color: #ffffff; text-decoration: none; font-weight: 600; font-size: 15px; letter-spacing: 0.5px;"> | |
| π Reset Password | |
| </a> | |
| </td> | |
| </tr> | |
| </table> | |
| <div style="color: #888888; font-size: 14px; line-height: 1.6; margin-top: 24px; padding-top: 20px; border-top: 1px solid #262626;"> | |
| <p style="margin: 0 0 12px 0;"><strong>β° This link expires in 1 hour.</strong></p> | |
| <p style="margin: 0 0 12px 0;">If you didn't request this password reset, you can safely ignore this email. Your password will remain unchanged.</p> | |
| <p style="margin: 0; color: #666666; font-size: 12px;"> | |
| If the button doesn't work, copy and paste this link into your browser:<br/> | |
| <span style="color: #2dd4bf; word-break: break-all;">{reset_link}</span> | |
| </p> | |
| </div> | |
| </td> | |
| </tr> | |
| <!-- Footer --> | |
| <tr> | |
| <td style="background-color: #0f0f0f; padding: 24px; text-align: center; border-top: 1px solid #262626;"> | |
| <p style="margin: 0; color: #64748b; font-size: 13px;"> | |
| © 2026 DataVision AI. All rights reserved. | |
| </p> | |
| <p style="margin: 8px 0 0 0; font-size: 12px; color: #475569;"> | |
| <a href="{APP_URL}" style="color: #2dd4bf; text-decoration: none;">Dashboard</a> • | |
| <a href="{APP_URL}/settings" style="color: #2dd4bf; text-decoration: none;">Settings</a> | |
| </p> | |
| </td> | |
| </tr> | |
| </table> | |
| </td> | |
| </tr> | |
| </table> | |
| </body> | |
| </html> | |
| ''' | |