433 lines
15 KiB
Python
433 lines
15 KiB
Python
"""Email service for sending verification and notification emails."""
|
|
|
|
import asyncio
|
|
from typing import Optional, Dict, Any
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import aiosmtplib
|
|
import logging
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from core.config import settings
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailService:
|
|
"""Service for sending emails."""
|
|
|
|
@staticmethod
|
|
async def send_email(
|
|
to_email: str,
|
|
subject: str,
|
|
html_content: str,
|
|
text_content: Optional[str] = None
|
|
) -> bool:
|
|
"""
|
|
Send an email asynchronously.
|
|
|
|
Args:
|
|
to_email: Recipient email address
|
|
subject: Email subject
|
|
html_content: HTML email content
|
|
text_content: Plain text content (optional)
|
|
|
|
Returns:
|
|
True if sent successfully, False otherwise
|
|
"""
|
|
try:
|
|
# Create message
|
|
message = MIMEMultipart("alternative")
|
|
message["From"] = settings.SMTP_FROM_EMAIL
|
|
message["To"] = to_email
|
|
message["Subject"] = subject
|
|
|
|
# Add text and HTML parts
|
|
if text_content:
|
|
text_part = MIMEText(text_content, "plain")
|
|
message.attach(text_part)
|
|
|
|
html_part = MIMEText(html_content, "html")
|
|
message.attach(html_part)
|
|
|
|
# Send email
|
|
if settings.ENVIRONMENT == "development":
|
|
# In development, just log the email
|
|
logger.info(f"Email would be sent to {to_email}: {subject}")
|
|
logger.debug(f"Email content: {html_content}")
|
|
return True
|
|
|
|
# Send via SMTP
|
|
async with aiosmtplib.SMTP(
|
|
hostname=settings.SMTP_HOST,
|
|
port=settings.SMTP_PORT,
|
|
use_tls=settings.SMTP_TLS,
|
|
start_tls=settings.SMTP_SSL
|
|
) as smtp:
|
|
if settings.SMTP_USER and settings.SMTP_PASSWORD:
|
|
await smtp.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
|
|
|
|
await smtp.send_message(message)
|
|
|
|
logger.info(f"Email sent successfully to {to_email}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email to {to_email}: {e}")
|
|
return False
|
|
|
|
@staticmethod
|
|
def send_verification_email(email: str, token: str) -> None:
|
|
"""
|
|
Send email verification link.
|
|
|
|
Args:
|
|
email: User email address
|
|
token: Verification token
|
|
"""
|
|
# Create verification URL
|
|
if settings.ENVIRONMENT == "development":
|
|
base_url = "http://localhost:3000"
|
|
else:
|
|
base_url = "https://youtube-summarizer.com" # Replace with actual domain
|
|
|
|
verification_url = f"{base_url}/auth/verify-email?token={token}"
|
|
|
|
# Email content
|
|
subject = f"Verify your email - {settings.APP_NAME}"
|
|
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
}}
|
|
.container {{
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}}
|
|
.header {{
|
|
background-color: #0066FF;
|
|
color: white;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-radius: 5px 5px 0 0;
|
|
}}
|
|
.content {{
|
|
background-color: #f9f9f9;
|
|
padding: 30px;
|
|
border-radius: 0 0 5px 5px;
|
|
}}
|
|
.button {{
|
|
display: inline-block;
|
|
padding: 12px 30px;
|
|
background-color: #0066FF;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
}}
|
|
.footer {{
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 14px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>{settings.APP_NAME}</h1>
|
|
</div>
|
|
<div class="content">
|
|
<h2>Verify Your Email Address</h2>
|
|
<p>Thank you for signing up! Please click the button below to verify your email address:</p>
|
|
|
|
<a href="{verification_url}" class="button">Verify Email</a>
|
|
|
|
<p>Or copy and paste this link in your browser:</p>
|
|
<p style="word-break: break-all; color: #0066FF;">{verification_url}</p>
|
|
|
|
<p>This link will expire in {settings.EMAIL_VERIFICATION_EXPIRE_HOURS} hours.</p>
|
|
|
|
<div class="footer">
|
|
<p>If you didn't create an account, you can safely ignore this email.</p>
|
|
<p>© 2024 {settings.APP_NAME}. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
text_content = f"""
|
|
Verify Your Email Address
|
|
|
|
Thank you for signing up for {settings.APP_NAME}!
|
|
|
|
Please verify your email address by clicking the link below:
|
|
{verification_url}
|
|
|
|
This link will expire in {settings.EMAIL_VERIFICATION_EXPIRE_HOURS} hours.
|
|
|
|
If you didn't create an account, you can safely ignore this email.
|
|
"""
|
|
|
|
# Send email asynchronously in background (for production, use a proper task queue)
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
loop.create_task(
|
|
EmailService.send_email(email, subject, html_content, text_content)
|
|
)
|
|
except RuntimeError:
|
|
# No event loop running, log the email content instead (development mode)
|
|
logger.info(f"Email verification would be sent to {email} with token: {token}")
|
|
logger.info(f"Verification URL: {settings.FRONTEND_URL}/verify-email?token={token}")
|
|
|
|
@staticmethod
|
|
def send_password_reset_email(email: str, token: str) -> None:
|
|
"""
|
|
Send password reset email.
|
|
|
|
Args:
|
|
email: User email address
|
|
token: Password reset token
|
|
"""
|
|
# Create reset URL
|
|
if settings.ENVIRONMENT == "development":
|
|
base_url = "http://localhost:3000"
|
|
else:
|
|
base_url = "https://youtube-summarizer.com" # Replace with actual domain
|
|
|
|
reset_url = f"{base_url}/auth/reset-password?token={token}"
|
|
|
|
# Email content
|
|
subject = f"Password Reset - {settings.APP_NAME}"
|
|
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
}}
|
|
.container {{
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}}
|
|
.header {{
|
|
background-color: #F59E0B;
|
|
color: white;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-radius: 5px 5px 0 0;
|
|
}}
|
|
.content {{
|
|
background-color: #f9f9f9;
|
|
padding: 30px;
|
|
border-radius: 0 0 5px 5px;
|
|
}}
|
|
.button {{
|
|
display: inline-block;
|
|
padding: 12px 30px;
|
|
background-color: #F59E0B;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
}}
|
|
.warning {{
|
|
background-color: #FEF3C7;
|
|
border-left: 4px solid #F59E0B;
|
|
padding: 10px;
|
|
margin: 20px 0;
|
|
}}
|
|
.footer {{
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 14px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Password Reset Request</h1>
|
|
</div>
|
|
<div class="content">
|
|
<h2>Reset Your Password</h2>
|
|
<p>We received a request to reset your password for your {settings.APP_NAME} account.</p>
|
|
|
|
<a href="{reset_url}" class="button">Reset Password</a>
|
|
|
|
<p>Or copy and paste this link in your browser:</p>
|
|
<p style="word-break: break-all; color: #F59E0B;">{reset_url}</p>
|
|
|
|
<div class="warning">
|
|
<strong>Security Note:</strong> This link will expire in {settings.PASSWORD_RESET_EXPIRE_MINUTES} minutes.
|
|
After resetting your password, all existing sessions will be logged out for security.
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>If you didn't request a password reset, please ignore this email or contact support if you have concerns.</p>
|
|
<p>© 2024 {settings.APP_NAME}. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
text_content = f"""
|
|
Password Reset Request
|
|
|
|
We received a request to reset your password for your {settings.APP_NAME} account.
|
|
|
|
Reset your password by clicking the link below:
|
|
{reset_url}
|
|
|
|
This link will expire in {settings.PASSWORD_RESET_EXPIRE_MINUTES} minutes.
|
|
|
|
Security Note: After resetting your password, all existing sessions will be logged out for security.
|
|
|
|
If you didn't request a password reset, please ignore this email or contact support if you have concerns.
|
|
"""
|
|
|
|
# Send email asynchronously in background (for production, use a proper task queue)
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
loop.create_task(
|
|
EmailService.send_email(email, subject, html_content, text_content)
|
|
)
|
|
except RuntimeError:
|
|
# No event loop running, log the email content instead (development mode)
|
|
logger.info(f"Password reset email would be sent to {email} with token: {token}")
|
|
logger.info(f"Reset URL: {settings.FRONTEND_URL}/reset-password?token={token}")
|
|
|
|
@staticmethod
|
|
def send_welcome_email(email: str, username: Optional[str] = None) -> None:
|
|
"""
|
|
Send welcome email to new user.
|
|
|
|
Args:
|
|
email: User email address
|
|
username: Optional username
|
|
"""
|
|
subject = f"Welcome to {settings.APP_NAME}!"
|
|
|
|
display_name = username or email.split('@')[0]
|
|
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {{
|
|
font-family: Arial, sans-serif;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
}}
|
|
.container {{
|
|
max-width: 600px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}}
|
|
.header {{
|
|
background-color: #22C55E;
|
|
color: white;
|
|
padding: 20px;
|
|
text-align: center;
|
|
border-radius: 5px 5px 0 0;
|
|
}}
|
|
.content {{
|
|
background-color: #f9f9f9;
|
|
padding: 30px;
|
|
border-radius: 0 0 5px 5px;
|
|
}}
|
|
.feature {{
|
|
margin: 15px 0;
|
|
padding-left: 30px;
|
|
}}
|
|
.button {{
|
|
display: inline-block;
|
|
padding: 12px 30px;
|
|
background-color: #22C55E;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 5px;
|
|
margin: 20px 0;
|
|
}}
|
|
.footer {{
|
|
margin-top: 30px;
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 14px;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Welcome to {settings.APP_NAME}!</h1>
|
|
</div>
|
|
<div class="content">
|
|
<h2>Hi {display_name}!</h2>
|
|
<p>Your account has been successfully created and verified. You can now enjoy all the features of {settings.APP_NAME}:</p>
|
|
|
|
<div class="feature">✅ Summarize YouTube videos with AI</div>
|
|
<div class="feature">✅ Save and organize your summaries</div>
|
|
<div class="feature">✅ Export summaries in multiple formats</div>
|
|
<div class="feature">✅ Access your summary history</div>
|
|
|
|
<a href="http://localhost:3000/dashboard" class="button">Go to Dashboard</a>
|
|
|
|
<p>Need help getting started? Check out our <a href="http://localhost:3000/help">Help Center</a> or reply to this email.</p>
|
|
|
|
<div class="footer">
|
|
<p>Happy summarizing!</p>
|
|
<p>© 2024 {settings.APP_NAME}. All rights reserved.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
text_content = f"""
|
|
Welcome to {settings.APP_NAME}!
|
|
|
|
Hi {display_name}!
|
|
|
|
Your account has been successfully created and verified. You can now enjoy all the features:
|
|
|
|
✅ Summarize YouTube videos with AI
|
|
✅ Save and organize your summaries
|
|
✅ Export summaries in multiple formats
|
|
✅ Access your summary history
|
|
|
|
Get started: http://localhost:3000/dashboard
|
|
|
|
Need help? Visit http://localhost:3000/help or reply to this email.
|
|
|
|
Happy summarizing!
|
|
"""
|
|
|
|
# Send email asynchronously
|
|
asyncio.create_task(
|
|
EmailService.send_email(email, subject, html_content, text_content)
|
|
) |