"""Email service for order confirmations and shipping notifications."""
import logging
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import aiosmtplib
from app.config import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
class EmailService:
"""Async email sender via SMTP (Mailcow)."""
@property
def enabled(self) -> bool:
return bool(settings.smtp_user and settings.smtp_password)
async def send_order_confirmation(
self,
*,
to_email: str,
to_name: str | None,
order_id: str,
items: list[dict],
total: float,
currency: str = "USD",
):
"""Send order confirmation email after successful payment."""
if not self.enabled:
logger.info("SMTP not configured, skipping order confirmation email")
return
subject = f"Order Confirmed — {settings.app_name} #{order_id[:8]}"
html = self._render_confirmation_html(
to_name=to_name,
order_id=order_id,
items=items,
total=total,
currency=currency,
)
await self._send(to_email=to_email, subject=subject, html=html)
async def send_shipping_notification(
self,
*,
to_email: str,
to_name: str | None,
order_id: str,
tracking_number: str | None = None,
tracking_url: str | None = None,
):
"""Send shipping notification when POD provider ships the order."""
if not self.enabled:
return
subject = f"Your Order Has Shipped — {settings.app_name}"
html = self._render_shipping_html(
to_name=to_name,
order_id=order_id,
tracking_number=tracking_number,
tracking_url=tracking_url,
)
await self._send(to_email=to_email, subject=subject, html=html)
async def _send(self, *, to_email: str, subject: str, html: str):
"""Send an HTML email via SMTP."""
msg = MIMEMultipart("alternative")
msg["From"] = f"{settings.smtp_from_name} <{settings.smtp_from_email}>"
msg["To"] = to_email
msg["Subject"] = subject
# Plain-text fallback
plain = html.replace("
", "\n").replace("
{greeting}
Thank you for your order! Your items are being prepared for production. Print-on-demand means each piece is made just for you at the nearest fulfillment center.
| Item | Qty | Price |
| Total | {currency_symbol}{total:.2f} | |
Order #{order_id[:8]}
{settings.app_name} — Community merch, on demand.
Part of the rStack ecosystem.
{greeting}
Great news — your order is on its way! It was printed at the nearest fulfillment center and is now heading to you.
{tracking_html}Order #{order_id[:8]}
{settings.app_name} — Community merch, on demand.