youtube-summarizer/backend/services/email_service.py

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>&copy; 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>&copy; 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>&copy; 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)
)