"""
Email Service for pounce.ch
Sends transactional emails using SMTP.
Email Types:
- Domain availability alerts
- Price change notifications
- Subscription confirmations
- Password reset
- 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
import asyncio
from typing import Optional, List, Dict, Any
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",
}
# Email Templates
TEMPLATES = {
"domain_available": """
🐆 pounce
Domain Available!
Great news! A domain you're monitoring is now available for registration:
{{ domain }}
This domain was previously registered but has now expired or been deleted. Act fast before someone else registers it!
Register Now →
""",
"price_alert": """
🐆 pounce
Price Alert: .{{ tld }}
{% 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 }}
View Details →
""",
"subscription_confirmed": """
🐆 pounce
Welcome to {{ plan_name }}!
Your subscription is now active. Here's what you can do:
{% for feature in features %}
- {{ feature }}
{% endfor %}
Go to Dashboard →
""",
"weekly_digest": """
🐆 pounce
Your Weekly Digest
Here's what happened with your monitored domains this week:
Domains Monitored
{{ total_domains }}
Status Changes
{{ status_changes }}
Price Alerts
{{ price_alerts }}
{% if available_domains %}
Domains Now Available
{% for domain in available_domains %}
{{ domain }}
{% endfor %}
{% endif %}
View Dashboard →
""",
}
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
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, for email clients that don't support HTML)
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
@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 = f"https://pounce.ch/dashboard"
template = Template(TEMPLATES["domain_available"])
html = template.render(
domain=domain,
register_url=register_url,
year=datetime.utcnow().year,
)
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)
template = Template(TEMPLATES["price_alert"])
html = template.render(
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}",
year=datetime.utcnow().year,
)
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}.",
)
@staticmethod
async def send_subscription_confirmed(
to_email: str,
plan_name: str,
features: List[str],
) -> bool:
"""Send subscription confirmation email."""
template = Template(TEMPLATES["subscription_confirmed"])
html = template.render(
plan_name=plan_name,
features=features,
dashboard_url="https://pounce.ch/dashboard",
year=datetime.utcnow().year,
)
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.",
)
@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."""
template = Template(TEMPLATES["weekly_digest"])
html = template.render(
total_domains=total_domains,
status_changes=status_changes,
price_alerts=price_alerts,
available_domains=available_domains,
dashboard_url="https://pounce.ch/dashboard",
year=datetime.utcnow().year,
)
return await EmailService.send_email(
to_email=to_email,
subject=f"📬 Your pounce Weekly Digest",
html_content=html,
text_content=f"This week: {total_domains} domains monitored, {status_changes} status changes, {price_alerts} price alerts.",
)
# Global instance
email_service = EmailService()