"""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("

", "\n") # Strip remaining tags import re plain = re.sub(r"<[^>]+>", "", plain) msg.attach(MIMEText(plain, "plain")) msg.attach(MIMEText(html, "html")) tls_context = ssl.create_default_context() tls_context.check_hostname = False tls_context.verify_mode = ssl.CERT_NONE # self-signed cert on Mailcow try: await aiosmtplib.send( msg, hostname=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_user, password=settings.smtp_password, start_tls=True, tls_context=tls_context, ) logger.info(f"Sent email to {to_email}: {subject}") except Exception as e: logger.error(f"Failed to send email to {to_email}: {e}") def _render_confirmation_html( self, *, to_name: str | None, order_id: str, items: list[dict], total: float, currency: str, ) -> str: greeting = f"Hi {to_name}," if to_name else "Hi there," order_url = f"{settings.public_url}/checkout/success?order_id={order_id}" currency_symbol = "$" if currency == "USD" else currency + " " items_html = "" for item in items: qty = item.get("quantity", 1) name = item.get("product_name", "Item") variant = item.get("variant", "") price = item.get("unit_price", 0) variant_str = f" ({variant})" if variant else "" items_html += f""" {name}{variant_str} {qty} {currency_symbol}{price:.2f} """ return f"""
rSw

Order Confirmed

{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.

Order Summary
{items_html}
Item Qty Price
Total {currency_symbol}{total:.2f}
View Order Status
What Happens Next
  1. Your design is sent to the nearest print facility
  2. Each item is printed on demand — just for you
  3. You'll get a shipping email with tracking info
  4. Revenue from your purchase supports the community

Order #{order_id[:8]}

{settings.app_name} — Community merch, on demand.

Part of the rStack ecosystem.

""" def _render_shipping_html( self, *, to_name: str | None, order_id: str, tracking_number: str | None, tracking_url: str | None, ) -> str: greeting = f"Hi {to_name}," if to_name else "Hi there," order_url = f"{settings.public_url}/checkout/success?order_id={order_id}" tracking_html = "" if tracking_number: track_link = tracking_url or "#" tracking_html = f"""
Tracking Number
{tracking_number}
""" return f"""
rSw

Your Order Has Shipped!

{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}
View Order

Order #{order_id[:8]}

{settings.app_name} — Community merch, on demand.

"""