""" 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 (Zoho defaults) SMTP_CONFIG = { "host": os.getenv("SMTP_HOST", "smtp.zoho.eu"), "port": int(os.getenv("SMTP_PORT", "465")), "username": os.getenv("SMTP_USER", "hello@pounce.ch"), "password": os.getenv("SMTP_PASSWORD"), "from_email": os.getenv("SMTP_FROM_EMAIL", "hello@pounce.ch"), "from_name": os.getenv("SMTP_FROM_NAME", "pounce"), "use_tls": os.getenv("SMTP_USE_TLS", "false").lower() == "true", "use_ssl": os.getenv("SMTP_USE_SSL", "true").lower() == "true", } # Contact email - where contact form submissions are sent CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "hello@pounce.ch") # Base email wrapper template BASE_TEMPLATE = """
{{ content }}
""" # Email Templates (content only, wrapped in BASE_TEMPLATE) TEMPLATES = { "domain_available": """

Time to pounce.

A domain you're tracking just dropped:

{{ domain }}

It's available right now. Move fastβ€”others are watching too.

Grab It Now β†’

You're tracking this domain on POUNCE.

""", "price_alert": """

.{{ tld }} just moved.

{% if change_percent < 0 %} ↓ Down {{ change_percent|abs }}% {% else %} ↑ Up {{ change_percent }}% {% endif %}

Was: ${{ old_price }}

Now: ${{ new_price }}

Cheapest at: {{ registrar }}

See Details β†’

You set an alert for .{{ tld }} on POUNCE.

""", "subscription_confirmed": """

You're in. {{ plan_name }} unlocked.

Your hunting arsenal just upgraded:

Start Hunting β†’

Questions? Just reply to this email.

""", "weekly_digest": """

Your week in domains.

Here's what moved while you were busy:

Domains Tracked {{ total_domains }}
Status Changes {{ status_changes }}
Price Moves {{ price_alerts }}
{% if available_domains %}

Dropped This Week

{% for domain in available_domains %}

{{ domain }}

{% endfor %}
{% endif %} View Dashboard β†’ """, "password_reset": """

Reset your password.

Hey {{ user_name }},

Someone requested a password reset. If that was you, click below:

Reset Password β†’

Or copy this link:

{{ reset_url }}

Link expires in 1 hour.

Didn't request this? Ignore it. Nothing changes.

""", "email_verification": """

One click to start hunting.

Hey {{ user_name }},

Welcome to POUNCE. Verify your email to activate your account:

Verify & Start β†’

Or copy this link:

{{ verification_url }}

Link expires in 24 hours.

Didn't sign up? Just ignore this.

""", "contact_form": """

New message from the wild.

From: {{ name }} <{{ email }}>

Subject: {{ subject }}

Date: {{ timestamp }}

Message

{{ message }}

Reply β†’

""", "contact_confirmation": """

Got it.

Hey {{ name }},

Your message landed. We'll get back to you soon.

Subject: {{ subject }}

Your message:

{{ message }}

Expect a reply within 24-48 hours.

Back to POUNCE β†’ """, "newsletter_welcome": """

You're on the list.

Welcome to POUNCE Insights.

Here's what you'll get:

1-2 emails per month. No spam. Ever.

Start Exploring β†’

Unsubscribe anytime with one click.

""", } 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 # Zoho uses SSL on port 465 (not STARTTLS on 587) if SMTP_CONFIG["use_ssl"]: # SSL connection (port 465) async with aiosmtplib.SMTP( hostname=SMTP_CONFIG["host"], port=SMTP_CONFIG["port"], use_tls=True, # Start with TLS/SSL start_tls=False, # Don't upgrade, already encrypted ) as smtp: await smtp.login(SMTP_CONFIG["username"], SMTP_CONFIG["password"]) await smtp.send_message(msg) else: # STARTTLS connection (port 587) async with aiosmtplib.SMTP( hostname=SMTP_CONFIG["host"], port=SMTP_CONFIG["port"], start_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"πŸ† POUNCE NOW: {domain} just dropped", html_content=html, text_content=f"{domain} is available! Grab it now: {register_url}", ) @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 = "down" if change_percent < 0 else "up" return await EmailService.send_email( to_email=to_email, subject=f"πŸ’° .{tld} moved {direction} {abs(change_percent)}%", html_content=html, text_content=f".{tld} is now ${new_price:.2f} (was ${old_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"🎯 {plan_name} unlocked. Let's hunt.", html_content=html, text_content=f"Your {plan_name} plan is active. Start hunting: https://pounce.ch/dashboard", ) # ============== 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 week in domains", 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"Reset your password: {reset_url} (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 email. Start hunting.", html_content=html, text_content=f"Verify your POUNCE account: {verification_url}", ) # ============== 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="Got your message. We'll be in touch.", html_content=confirm_html, text_content=f"Hey {name}, we received your message. Expect a reply within 24-48 hours.", ) 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="You're on the list. Welcome to POUNCE.", html_content=html, text_content="Welcome to POUNCE Insights. Expect market moves, strategies, and feature drops. No spam.", ) # Global instance email_service = EmailService()