""" Email Service for pounce.ch Sends transactional emails using SMTP. Email Types: - Domain availability alerts - Price change notifications - Subscription confirmations - Password reset - Email verification - Contact form messages - Weekly digests Environment Variables Required: - SMTP_HOST: SMTP server hostname - SMTP_PORT: SMTP port (usually 587 for TLS, 465 for SSL) - SMTP_USER: SMTP username - SMTP_PASSWORD: SMTP password - SMTP_FROM_EMAIL: Sender email address - SMTP_FROM_NAME: Sender name (default: pounce) """ import logging import os from typing import Optional, List from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime import aiosmtplib from jinja2 import Template logger = logging.getLogger(__name__) # SMTP Configuration from environment SMTP_CONFIG = { "host": os.getenv("SMTP_HOST"), "port": int(os.getenv("SMTP_PORT", "587")), "username": os.getenv("SMTP_USER"), "password": os.getenv("SMTP_PASSWORD"), "from_email": os.getenv("SMTP_FROM_EMAIL", "noreply@pounce.ch"), "from_name": os.getenv("SMTP_FROM_NAME", "pounce"), "use_tls": os.getenv("SMTP_USE_TLS", "true").lower() == "true", } # Contact email - where contact form submissions are sent CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "support@pounce.ch") # Base email wrapper template BASE_TEMPLATE = """
Great news! A domain you're monitoring is now available for registration:
This domain was previously registered but has now expired or been deleted. Act fast before someone else registers it!
Register Now โYou're receiving this because you're monitoring this domain on pounce.
""", "price_alert": """{% if change_percent < 0 %} โ Price dropped {{ change_percent|abs }}% {% else %} โ Price increased {{ change_percent }}% {% endif %}
Old price: ${{ old_price }}
New price: ${{ new_price }}
Cheapest registrar: {{ registrar }}
You're receiving this because you set a price alert for .{{ tld }} on pounce.
""", "subscription_confirmed": """Your subscription is now active. Here's what you can do:
Questions? Reply to this email or contact support@pounce.ch
""", "weekly_digest": """Here's what happened with your monitored domains this week:
{{ domain }}
{% endfor %}Hi {{ user_name }},
We received a request to reset your password. Click the button below to set a new password:
Reset Password โOr copy and paste this link into your browser:
{{ reset_url }}
โ ๏ธ This link expires in 1 hour.
If you didn't request a password reset, you can safely ignore this email. Your password won't be changed.
""", "email_verification": """Hi {{ user_name }},
Welcome to pounce! Please verify your email address to activate your account:
Verify Email โOr copy and paste this link into your browser:
{{ verification_url }}
This link expires in 24 hours.
If you didn't create an account on pounce, you can safely ignore this email.
""", "contact_form": """From: {{ name }} <{{ email }}>
Subject: {{ subject }}
Date: {{ timestamp }}
{{ message }}
Hi {{ name }},
Thank you for contacting pounce! We've received your message and will get back to you as soon as possible.
Subject: {{ subject }}
Your message:
{{ message }}
We typically respond within 24-48 hours during business days.
Back to pounce โ """, "newsletter_welcome": """Hi there,
You're now subscribed to our newsletter. Here's what you can expect:
We typically send 1-2 emails per month. No spam, ever.
Explore pounce โYou can unsubscribe at any time by clicking the link at the bottom of any email.
""", } class EmailService: """ Async email service using SMTP. All emails use HTML templates with the pounce branding. """ @staticmethod def is_configured() -> bool: """Check if SMTP is properly configured.""" return bool( SMTP_CONFIG["host"] and SMTP_CONFIG["username"] and SMTP_CONFIG["password"] ) @staticmethod def _render_email(template_name: str, **kwargs) -> str: """Render email with base template wrapper.""" content_template = Template(TEMPLATES.get(template_name, "")) content = content_template.render(**kwargs) base_template = Template(BASE_TEMPLATE) return base_template.render( content=content, year=datetime.utcnow().year, ) @staticmethod async def send_email( to_email: str, subject: str, html_content: str, text_content: Optional[str] = None, ) -> bool: """ Send an email via SMTP. Args: to_email: Recipient email address subject: Email subject html_content: HTML body text_content: Plain text body (optional) Returns: True if sent successfully, False otherwise """ if not EmailService.is_configured(): logger.warning(f"SMTP not configured. Would send to {to_email}: {subject}") return False try: # Create message msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = f"{SMTP_CONFIG['from_name']} <{SMTP_CONFIG['from_email']}>" msg["To"] = to_email # Add text part (fallback) if text_content: msg.attach(MIMEText(text_content, "plain")) # Add HTML part msg.attach(MIMEText(html_content, "html")) # Send via SMTP async with aiosmtplib.SMTP( hostname=SMTP_CONFIG["host"], port=SMTP_CONFIG["port"], use_tls=SMTP_CONFIG["use_tls"], ) as smtp: await smtp.login(SMTP_CONFIG["username"], SMTP_CONFIG["password"]) await smtp.send_message(msg) logger.info(f"Email sent to {to_email}: {subject}") return True except Exception as e: logger.error(f"Failed to send email to {to_email}: {e}") return False # ============== Domain Alerts ============== @staticmethod async def send_domain_available( to_email: str, domain: str, register_url: Optional[str] = None, ) -> bool: """Send domain available notification.""" if not register_url: register_url = "https://pounce.ch/dashboard" html = EmailService._render_email( "domain_available", domain=domain, register_url=register_url, ) return await EmailService.send_email( to_email=to_email, subject=f"๐ Domain Available: {domain}", html_content=html, text_content=f"Great news! {domain} is now available for registration. Visit {register_url} to register.", ) @staticmethod async def send_price_alert( to_email: str, tld: str, old_price: float, new_price: float, registrar: str, ) -> bool: """Send TLD price change alert.""" change_percent = round(((new_price - old_price) / old_price) * 100, 1) html = EmailService._render_email( "price_alert", tld=tld, old_price=f"{old_price:.2f}", new_price=f"{new_price:.2f}", change_percent=change_percent, registrar=registrar, tld_url=f"https://pounce.ch/tld-pricing/{tld}", ) direction = "dropped" if change_percent < 0 else "increased" return await EmailService.send_email( to_email=to_email, subject=f"๐ Price Alert: .{tld} {direction} {abs(change_percent)}%", html_content=html, text_content=f".{tld} price {direction} from ${old_price:.2f} to ${new_price:.2f} at {registrar}.", ) # ============== Subscription ============== @staticmethod async def send_subscription_confirmed( to_email: str, plan_name: str, features: List[str], ) -> bool: """Send subscription confirmation email.""" html = EmailService._render_email( "subscription_confirmed", plan_name=plan_name, features=features, dashboard_url="https://pounce.ch/dashboard", ) return await EmailService.send_email( to_email=to_email, subject=f"โ Welcome to pounce {plan_name}!", html_content=html, text_content=f"Your {plan_name} subscription is now active. Visit https://pounce.ch/dashboard to get started.", ) # ============== Digest ============== @staticmethod async def send_weekly_digest( to_email: str, total_domains: int, status_changes: int, price_alerts: int, available_domains: List[str], ) -> bool: """Send weekly summary digest.""" html = EmailService._render_email( "weekly_digest", total_domains=total_domains, status_changes=status_changes, price_alerts=price_alerts, available_domains=available_domains, dashboard_url="https://pounce.ch/dashboard", ) return await EmailService.send_email( to_email=to_email, subject="๐ฌ Your pounce Weekly Digest", html_content=html, text_content=f"This week: {total_domains} domains monitored, {status_changes} status changes, {price_alerts} price alerts.", ) # ============== Authentication ============== @staticmethod async def send_password_reset( to_email: str, user_name: str, reset_url: str, ) -> bool: """Send password reset email.""" html = EmailService._render_email( "password_reset", user_name=user_name, reset_url=reset_url, ) return await EmailService.send_email( to_email=to_email, subject="๐ Reset Your pounce Password", html_content=html, text_content=f"Hi {user_name}, reset your password by visiting: {reset_url}. This link expires in 1 hour.", ) @staticmethod async def send_email_verification( to_email: str, user_name: str, verification_url: str, ) -> bool: """Send email verification email.""" html = EmailService._render_email( "email_verification", user_name=user_name, verification_url=verification_url, ) return await EmailService.send_email( to_email=to_email, subject="โ๏ธ Verify Your pounce Email", html_content=html, text_content=f"Hi {user_name}, verify your email by visiting: {verification_url}. This link expires in 24 hours.", ) # ============== Contact Form ============== @staticmethod async def send_contact_form( name: str, email: str, subject: str, message: str, ) -> bool: """ Send contact form submission to support. Also sends confirmation to the user. """ timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC") # Send to support support_html = EmailService._render_email( "contact_form", name=name, email=email, subject=subject, message=message, timestamp=timestamp, ) support_sent = await EmailService.send_email( to_email=CONTACT_EMAIL, subject=f"[Contact] {subject} - from {name}", html_content=support_html, text_content=f"From: {name} <{email}>\nSubject: {subject}\n\n{message}", ) # Send confirmation to user confirm_html = EmailService._render_email( "contact_confirmation", name=name, subject=subject, message=message, ) confirm_sent = await EmailService.send_email( to_email=email, subject="We've received your message - pounce", html_content=confirm_html, text_content=f"Hi {name}, we've received your message and will get back to you soon.", ) return support_sent # Return whether support email was sent # ============== Newsletter ============== @staticmethod async def send_newsletter_welcome( to_email: str, ) -> bool: """Send newsletter subscription welcome email.""" html = EmailService._render_email("newsletter_welcome") return await EmailService.send_email( to_email=to_email, subject="๐ Welcome to pounce Insights!", html_content=html, text_content="Welcome to pounce Insights! You'll receive TLD market trends, domain investing tips, and feature announcements.", ) # Global instance email_service = EmailService()