RECORDGOD/app/mailer.py
type-two 5351cfb9c2 feat(pos): rebuild Sales — 3 tabs, smooth checkout + tender/change, print+email receipts
7 tabs → Register / History / Settings. Register: search/scan + cart + checkout flow
(cash tender→change, split tenders, layby deposit, notes) → completes → receipt auto-pops.
Receipts now server-rendered (app/receipts.py, one template) with per-item discounts, GST
breakdown, split lines, tendered/change, store header/footer; Print opens a clean window
(no visibility hack); Email via app/mailer.py (stdlib SMTP, creds in vault, admin-only) with
a Settings 'send test' button. History merges past sales + laybys (reprint/collect). Settings
holds currency/GST/discount + receipt header/footer + promotions. sales gains amount_tendered
+ notes; SaleIn carries tendered/notes; /sales/{id}/receipt + /{id}/email + /email-test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:29:42 +10:00

61 lines
2.0 KiB
Python

import asyncio
import smtplib
import ssl
from email.message import EmailMessage
from . import vault
# Receipt email. SMTP creds live in the vault (admin-only). stdlib smtplib in a thread so the
# event loop isn't blocked — no new dependency. ponytail: one sender, swap to API mail if needed.
class MailUnconfigured(Exception):
pass
async def _creds(db):
host = await vault.get_secret(db, "smtp_host")
if not host:
raise MailUnconfigured("SMTP not configured — set smtp_* in admin → Connections")
user = await vault.get_secret(db, "smtp_user") or ""
return {
"host": host.strip(),
"port": int((await vault.get_secret(db, "smtp_port") or "587").strip() or 587),
"user": user,
"pass": await vault.get_secret(db, "smtp_pass") or "",
"from": (await vault.get_secret(db, "smtp_from") or user).strip(),
"from_name": await vault.get_secret(db, "smtp_from_name") or "RecordGod",
}
def _send_sync(c, to, subject, html):
msg = EmailMessage()
msg["Subject"] = subject
msg["From"] = f'{c["from_name"]} <{c["from"]}>' if c["from_name"] else c["from"]
msg["To"] = to
msg.set_content("Your receipt is below — view this email in an HTML-capable client.")
msg.add_alternative(html, subtype="html")
ctx = ssl.create_default_context()
if c["port"] == 465:
with smtplib.SMTP_SSL(c["host"], c["port"], context=ctx, timeout=20) as s:
if c["user"]:
s.login(c["user"], c["pass"])
s.send_message(msg)
else:
with smtplib.SMTP(c["host"], c["port"], timeout=20) as s:
s.ehlo()
try:
s.starttls(context=ctx)
s.ehlo()
except smtplib.SMTPNotSupportedError:
pass
if c["user"]:
s.login(c["user"], c["pass"])
s.send_message(msg)
async def send_mail(db, to, subject, html):
c = await _creds(db)
await asyncio.to_thread(_send_sync, c, to, subject, html)
return c["from"]