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"""

Welcome to Our Community!

Thank you for registering with us. We're excited to have you join our community.

Please click the button below to verify your email address:

Verify Email

Or copy and paste this link into your browser:

{verification_url}

This link will expire in 24 hours.

If you didn't create an account, please ignore this email.

""" 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"""

Congratulations, {first_name}!

Great news! Your membership application has been approved.

You now have full access to all member features and events.

Login to Your Account

We look forward to seeing you at our events!

""" 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"""

🎉 You're Approved, {first_name}!

Great news! Your LOAF membership application has been approved by our admin team.

To activate your membership and gain full access, please complete your annual membership payment:

Complete Payment →

Once payment is processed, you'll have immediate access to:

  • Members-only events and gatherings
  • Community directory and networking
  • Exclusive member benefits and discounts
  • LOAF newsletter and updates

We're excited to have you join the LOAF community!

Questions? Contact us at support@loaf.org

""" 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"""

Reset Your Password

Hi {first_name},

You requested to reset your password. Click the button below to create a new password:

Reset Password

⏰ This link will expire in 1 hour.

If you didn't request this, please ignore this email.

Or copy and paste this link into your browser:
{reset_url}

""" 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 = ( """

⚠️ You will be required to change this password when you log in.

""" ) if force_change else "" login_url = f"{FRONTEND_URL}/login" html_content = f"""

Password Reset by Administrator

Hi {first_name},

An administrator has reset your password. Here is your temporary password:

{temp_password}
{force_change_text}

Please log in and change your password to something memorable.

Go to Login

Questions? Contact us at support@loaf.org

""" return await send_email(to_email, subject, html_content)