Files
membership-be/email_service.py
2025-12-16 20:03:50 +07:00

453 lines
20 KiB
Python

import os
import ssl
import smtplib
import asyncio
from pathlib import Path
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import logging
from dotenv import load_dotenv
logger = logging.getLogger(__name__)
# Load .env file
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# SMTP Configuration - supports both new (SMTP_USER/SMTP_PASS) and old (SMTP_USERNAME/SMTP_PASSWORD) variable names
SMTP_HOST = os.environ.get('SMTP_HOST', 'smtp.gmail.com')
SMTP_PORT = int(os.environ.get('SMTP_PORT', 587))
SMTP_SECURE = os.environ.get('SMTP_SECURE', 'false').lower() == 'true'
SMTP_VERIFY_CERT = os.environ.get('SMTP_VERIFY_CERT', 'true').lower() == 'true'
SMTP_AUTH_METHOD = os.environ.get('SMTP_AUTH_METHOD', None) # PLAIN, LOGIN, or CRAM-MD5
# Log configuration at startup
logger.info(f"📧 SMTP Configuration Loaded:")
logger.info(f" Host: {SMTP_HOST}:{SMTP_PORT}")
logger.info(f" Secure (SSL from start): {SMTP_SECURE}")
logger.info(f" Verify Certificate: {SMTP_VERIFY_CERT}")
logger.info(f" Auth Method: {SMTP_AUTH_METHOD or 'auto-detect'}")
# Support both SMTP_USER and SMTP_USERNAME (new and old)
SMTP_USER = os.environ.get('SMTP_USER') or os.environ.get('SMTP_USERNAME', '')
SMTP_PASS = os.environ.get('SMTP_PASS') or os.environ.get('SMTP_PASSWORD', '')
# Support both SMTP_FROM and SMTP_FROM_EMAIL (new and old)
SMTP_FROM = os.environ.get('SMTP_FROM') or os.environ.get('SMTP_FROM_EMAIL', 'noreply@membership.com')
SMTP_FROM_NAME = os.environ.get('SMTP_FROM_NAME', 'LOAF Membership')
FRONTEND_URL = os.environ.get('FRONTEND_URL', 'http://localhost:3000')
def _send_email_sync(message: MIMEMultipart, to_email: str):
"""
Synchronous email sending (will be wrapped in asyncio.to_thread)
Matches the working pattern from previous implementation
"""
# Create SSL context for certificate verification control
ssl_context = None
if not SMTP_VERIFY_CERT:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
logger.warning(f"⚠️ SSL certificate verification DISABLED for {SMTP_HOST}")
# Determine connection method based on configuration
if not SMTP_SECURE:
# Plain SMTP without encryption (some providers require this)
logger.info(f"🔌 Using plain SMTP connection (SMTP_SECURE=false) to {SMTP_HOST}:{SMTP_PORT}")
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
logger.info(f" ✅ SMTP connection established")
server.login(SMTP_USER, SMTP_PASS)
logger.info(f" ✅ Login successful")
server.send_message(message)
logger.info(f" ✅ Message sent")
elif SMTP_PORT == 465:
# Use SSL/TLS from start (standard for port 465)
logger.info(f"🔌 Using SMTP_SSL connection to {SMTP_HOST}:{SMTP_PORT}")
with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ssl_context) as server:
logger.info(f" ✅ SSL connection established")
server.login(SMTP_USER, SMTP_PASS)
logger.info(f" ✅ Login successful")
server.send_message(message)
else:
# Use STARTTLS (standard for port 587)
logger.info(f"🔌 Using SMTP with STARTTLS to {SMTP_HOST}:{SMTP_PORT}")
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls(context=ssl_context)
logger.info(f" ✅ STARTTLS enabled")
server.login(SMTP_USER, SMTP_PASS)
logger.info(f" ✅ Login successful")
server.send_message(message)
async def send_email(to_email: str, subject: str, html_content: str):
"""
Send an email using SMTP
Configuration via environment variables:
- SMTP_SECURE=false: Plain SMTP without encryption
- SMTP_SECURE=true + Port 465: SSL/TLS from start
- SMTP_SECURE=true + Port 587: STARTTLS
"""
try:
message = MIMEMultipart('alternative')
message['From'] = f"{SMTP_FROM_NAME} <{SMTP_FROM}>"
message['To'] = to_email
message['Subject'] = subject
html_part = MIMEText(html_content, 'html')
message.attach(html_part)
# For development/testing, just log the email
if not SMTP_USER or not SMTP_PASS:
logger.info(f"[EMAIL] To: {to_email}")
logger.info(f"[EMAIL] Subject: {subject}")
logger.info(f"[EMAIL] Content: {html_content[:200]}...")
return True
logger.info(f"📧 Sending email to {to_email} via {SMTP_HOST}:{SMTP_PORT}")
# Run synchronous SMTP in thread pool to avoid blocking
await asyncio.to_thread(_send_email_sync, message, to_email)
logger.info(f"✓ Email sent successfully to {to_email}")
return True
except smtplib.SMTPAuthenticationError as e:
logger.error(f"❌ SMTP Authentication Error for {to_email}: {str(e)}")
logger.error(f" Check SMTP_USER and SMTP_PASS")
return False
except smtplib.SMTPException as e:
logger.error(f"❌ SMTP Error sending to {to_email}: {str(e)}")
logger.exception(e)
return False
except Exception as e:
logger.error(f"✗ Failed to send email to {to_email}: {str(e)}")
logger.exception(e)
return False
async def send_verification_email(to_email: str, token: str):
"""Send email verification link"""
verification_url = f"{FRONTEND_URL}/verify-email?token={token}"
subject = "Verify Your Email Address"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #DDD8EB; color: #422268; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #FFFFFF; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to Our Community!</h1>
</div>
<div class="content">
<p>Thank you for registering with us. We're excited to have you join our community.</p>
<p>Please click the button below to verify your email address:</p>
<p style="text-align: center;">
<a href="{verification_url}" class="button">Verify Email</a>
</p>
<p>Or copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #664fa3;">{verification_url}</p>
<p>This link will expire in 24 hours.</p>
<p>If you didn't create an account, please ignore this email.</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)
async def send_approval_notification(to_email: str, first_name: str):
"""Send notification when user is approved"""
login_url = f"{FRONTEND_URL}/login"
subject = "Your Membership Application Has Been Approved!"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #DDD8EB; color: #422268; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #FFFFFF; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Congratulations, {first_name}!</h1>
</div>
<div class="content">
<p>Great news! Your membership application has been approved.</p>
<p>You now have full access to all member features and events.</p>
<p style="text-align: center;">
<a href="{login_url}" class="button">Login to Your Account</a>
</p>
<p>We look forward to seeing you at our events!</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)
async def send_payment_prompt_email(to_email: str, first_name: str):
"""Send payment prompt email after admin approval"""
payment_url = f"{FRONTEND_URL}/plans"
subject = "Complete Your LOAF Membership - Payment Required"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #DDD8EB; color: #422268; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #FFFFFF; }}
.benefits {{ background: #f1eef9; padding: 20px; border-radius: 8px; margin: 20px 0; border: 2px solid #ddd8eb; }}
.benefits ul {{ list-style: none; padding: 0; }}
.benefits li {{ padding: 8px 0; padding-left: 25px; position: relative; }}
.benefits li:before {{ content: ""; position: absolute; left: 0; color: #ff9e77; font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 You're Approved, {first_name}!</h1>
</div>
<div class="content">
<p><strong>Great news!</strong> Your LOAF membership application has been approved by our admin team.</p>
<p>To activate your membership and gain full access, please complete your annual membership payment:</p>
<p style="text-align: center;">
<a href="{payment_url}" class="button">Complete Payment →</a>
</p>
<div class="benefits">
<p><strong>Once payment is processed, you'll have immediate access to:</strong></p>
<ul>
<li>Members-only events and gatherings</li>
<li>Community directory and networking</li>
<li>Exclusive member benefits and discounts</li>
<li>LOAF newsletter and updates</li>
</ul>
</div>
<p>We're excited to have you join the LOAF community!</p>
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd8eb; color: #664fa3; font-size: 14px;">
Questions? Contact us at support@loaf.org
</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)
async def send_password_reset_email(to_email: str, first_name: str, reset_url: str):
"""Send password reset link email"""
subject = "Reset Your Password - LOAF Membership"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #DDD8EB; color: #422268; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #FFFFFF; }}
.note {{ background: #f1eef9; border-left: 4px solid #ff9e77; padding: 15px; margin: 20px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Reset Your Password</h1>
</div>
<div class="content">
<p>Hi {first_name},</p>
<p>You requested to reset your password. Click the button below to create a new password:</p>
<p style="text-align: center;">
<a href="{reset_url}" class="button">Reset Password</a>
</p>
<div class="note">
<p style="margin: 0; font-size: 14px;"><strong>⏰ This link will expire in 1 hour.</strong></p>
<p style="margin: 5px 0 0 0; font-size: 14px;">If you didn't request this, please ignore this email.</p>
</div>
<p style="margin-top: 20px; color: #664fa3; font-size: 14px;">
Or copy and paste this link into your browser:<br>
<span style="word-break: break-all;">{reset_url}</span>
</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)
async def send_admin_password_reset_email(
to_email: str,
first_name: str,
temp_password: str,
force_change: bool
):
"""Send temporary password when admin resets user password"""
subject = "Your Password Has Been Reset - LOAF Membership"
force_change_text = (
"""
<div class="note" style="background: #FFEBEE; border-left: 4px solid #ff9e77;">
<p style="margin: 0; font-weight: bold; color: #ff9e77;">⚠️ You will be required to change this password when you log in.</p>
</div>
"""
) if force_change else ""
login_url = f"{FRONTEND_URL}/login"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #DDD8EB; color: #422268; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #FFFFFF; }}
.password-box {{ background: #f1eef9; padding: 20px; margin: 20px 0; border-left: 4px solid #ff9e77; font-family: 'Courier New', monospace; font-size: 18px; font-weight: bold; word-break: break-all; }}
.note {{ background: #f1eef9; border-left: 4px solid #ff9e77; padding: 15px; margin: 20px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Password Reset by Administrator</h1>
</div>
<div class="content">
<p>Hi {first_name},</p>
<p>An administrator has reset your password. Here is your temporary password:</p>
<div class="password-box">
{temp_password}
</div>
{force_change_text}
<p>Please log in and change your password to something memorable.</p>
<p style="text-align: center;">
<a href="{login_url}" class="button">Go to Login</a>
</p>
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd8eb; color: #664fa3; font-size: 14px;">
Questions? Contact us at support@loaf.org
</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)
async def send_invitation_email(
to_email: str,
inviter_name: str,
invitation_url: str,
role: str
):
"""Send invitation email to new user"""
subject = f"You've Been Invited to Join LOAF - {role.capitalize()} Access"
role_descriptions = {
"member": "full member access to our community",
"admin": "administrative access to manage the platform",
"superadmin": "full administrative access with system-wide permissions"
}
role_description = role_descriptions.get(role.lower(), "access to our platform")
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Nunito Sans', Arial, sans-serif; line-height: 1.6; color: #422268; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #644c9f 0%, #48286e 100%); padding: 30px; text-align: center; border-radius: 10px 10px 0 0; }}
.header h1 {{ color: white; margin: 0; font-family: 'Inter', sans-serif; }}
.content {{ background: #FFFFFF; padding: 30px; border-radius: 0 0 10px 10px; }}
.button {{ display: inline-block; background: #ff9e77; color: #FFFFFF; padding: 15px 40px; text-decoration: none; border-radius: 50px; font-weight: 600; margin: 20px 0; }}
.button:hover {{ background: #e88d66; }}
.info-box {{ background: #f1eef9; padding: 20px; border-radius: 8px; margin: 20px 0; border: 2px solid #ddd8eb; }}
.note {{ background: #FFEBEE; border-left: 4px solid #ff9e77; padding: 15px; margin: 20px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎉 You're Invited!</h1>
</div>
<div class="content">
<p><strong>{inviter_name}</strong> has invited you to join the LOAF community with <strong>{role_description}</strong>.</p>
<div class="info-box">
<p style="margin: 0;"><strong>Your Role:</strong> {role.capitalize()}</p>
<p style="margin: 10px 0 0 0;"><strong>Invited By:</strong> {inviter_name}</p>
</div>
<p>Click the button below to accept your invitation and create your account:</p>
<p style="text-align: center;">
<a href="{invitation_url}" class="button">Accept Invitation</a>
</p>
<div class="note">
<p style="margin: 0; font-size: 14px;"><strong>⏰ This invitation expires in 7 days.</strong></p>
<p style="margin: 5px 0 0 0; font-size: 14px;">If you didn't expect this invitation, you can safely ignore this email.</p>
</div>
<p style="margin-top: 20px; color: #664fa3; font-size: 14px;">
Or copy and paste this link into your browser:<br>
<span style="word-break: break-all;">{invitation_url}</span>
</p>
<p style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd8eb; color: #664fa3; font-size: 14px;">
Questions? Contact us at support@loaf.org
</p>
</div>
</div>
</body>
</html>
"""
return await send_email(to_email, subject, html_content)