""" 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") # Minimalistic Professional Email Template BASE_TEMPLATE = """

pounce

{{ content }}

pounce — Domain Intelligence Platform
pounce.ch

""" # Email Templates (content only, wrapped in BASE_TEMPLATE) TEMPLATES = { "domain_available": """

Domain available

A domain you're monitoring is now available:

{{ domain }}

Register Domain
""", "price_alert": """

Price alert: .{{ tld }}

The price for .{{ tld }} has changed:

Previous Price

\${{ old_price }}

New Price

\${{ new_price }}

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

Cheapest at: {{ registrar }}

View Details
""", "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

Hi {{ user_name }},

We received a request to reset your password. Click the button below to create a new password.

Reset Password

This link expires in 1 hour.

If you didn't request this, you can safely ignore this email.

""", "email_verification": """

Verify your email

Hi {{ user_name }},

Thanks for signing up. Click the button below to verify your email and activate your account.

Verify Email

This link expires in 24 hours.

If you didn't sign up, you can safely ignore this email.

""", "contact_form": """

New Contact Form Submission

From

{{ name }} <{{ email }}>

Subject

{{ subject }}

Message

{{ message }}

Reply

Sent: {{ timestamp }}

""", "contact_confirmation": """

Message received

Hi {{ name }},

Thanks for reaching out. We've received your message and will get back to you within 24–48 hours.

Your message

{{ message }}

""", "newsletter_welcome": """

Welcome to pounce insights

You'll receive updates about TLD market trends, domain investment strategies, and new features. 1–2 emails per month. No spam.

Visit pounce.ch
""", "listing_inquiry": """

New inquiry for {{ domain }}

Someone is interested in your domain listing:

From

{{ name }} <{{ email }}>

{% if company %}

{{ company }}

{% endif %} {% if offer_amount %}

Offer

${{ offer_amount }}

{% endif %}

Message

{{ message }}

Reply to Buyer

Manage your listings →

""", } 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.", ) # ============== Listing Inquiries ============== @staticmethod async def send_listing_inquiry( to_email: str, domain: str, name: str, email: str, message: str, company: Optional[str] = None, offer_amount: Optional[float] = None, ) -> bool: """Send notification to seller when they receive an inquiry.""" html = EmailService._render_email( "listing_inquiry", domain=domain, name=name, email=email, message=message, company=company, offer_amount=f"{offer_amount:,.0f}" if offer_amount else None, ) subject = f"💰 New inquiry for {domain}" if offer_amount: subject = f"💰 ${offer_amount:,.0f} offer for {domain}" return await EmailService.send_email( to_email=to_email, subject=subject, html_content=html, text_content=f"New inquiry from {name} ({email}) for {domain}. Message: {message}", ) # Global instance email_service = EmailService()