Files
gw-svelte/mail-api/main.py
T
ponzischeme89 b7ea05f150 Initial commit
2026-05-02 08:26:18 +12:00

460 lines
20 KiB
Python

import os
import logging
from datetime import datetime
import resend
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
resend.api_key = os.environ["RESEND_API_KEY"]
OWNER_EMAIL = os.environ["OWNER_EMAIL"]
FROM_EMAIL = os.environ.get("FROM_EMAIL", "GoodWalk <bookings@goodwalk.co.nz>")
REPLY_TO = os.environ.get("REPLY_TO", "aless@goodwalk.co.nz")
LOGO_URL = "https://www.goodwalk.co.nz/wp-content/uploads/2022/06/logo-v6.png"
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["POST"],
allow_headers=["*"],
)
class BookingSubmission(BaseModel):
fullName: str
email: EmailStr
phone: str
petName: str
location: str
message: str = ""
services: list[str] = []
referrer: str = ""
page: str = ""
# ── Helpers ──────────────────────────────────────────────────────────────────
def _get_ip(request: Request) -> str:
forwarded = request.headers.get("x-forwarded-for")
if forwarded:
return forwarded.split(",")[0].strip()
return request.client.host if request.client else "unknown"
def _parse_ua(ua: str) -> str:
if not ua:
return "Unknown"
browsers = [("Edg/", "Edge"), ("OPR/", "Opera"), ("Chrome/", "Chrome"),
("Firefox/", "Firefox"), ("Safari/", "Safari")]
systems = [("Windows NT 10", "Windows 10/11"), ("Windows NT 6", "Windows 8"),
("Mac OS X", "macOS"), ("iPhone", "iPhone"), ("iPad", "iPad"),
("Android", "Android"), ("Linux", "Linux")]
browser = next((n for p, n in browsers if p in ua), "Unknown browser")
system = next((n for p, n in systems if p in ua), "Unknown OS")
return f"{browser} on {system}"
def _detail_row(label: str, value: str) -> str:
if not value:
return ""
return f"""
<tr>
<td style="padding:8px 0;color:#888;font-size:13px;white-space:nowrap;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;width:130px;">{label}</td>
<td style="padding:8px 0 8px 16px;color:#213021;font-size:14px;font-weight:500;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">{value}</td>
</tr>"""
def _meta_row(label: str, value: str) -> str:
if not value:
return ""
return f"""
<tr>
<td style="padding:5px 0;color:#aaa;font-size:12px;white-space:nowrap;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;width:100px;">{label}</td>
<td style="padding:5px 0 5px 16px;color:#666;font-size:12px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;word-break:break-all;">{value}</td>
</tr>"""
# ── Email templates ──────────────────────────────────────────────────────────
def _logo_header(badge_html: str = "", subtitle: str = "") -> str:
badge = f'<div style="margin-top:20px;">{badge_html}</div>' if badge_html else ""
sub = f"""<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:13px;color:#7aaa7a;letter-spacing:0.04em;margin-top:8px;">
{subtitle}</div>""" if subtitle else ""
return f"""
<tr>
<td style="background:#213021;padding:36px 48px 32px;text-align:center;">
<img src="{LOGO_URL}" width="161" height="32" alt="GoodWalk"
style="display:inline-block;max-width:161px;height:auto;border:0;">
{sub}
{badge}
</td>
</tr>"""
def client_email(data: BookingSubmission) -> str:
services_text = ", ".join(data.services) if data.services else "Not specified"
message_row = _detail_row("About the dog", data.message) if data.message else ""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>We received your enquiry</title>
</head>
<body style="margin:0;padding:0;background:#f2f2f0;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f2f2f0;padding:40px 16px;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:600px;width:100%;border-radius:16px;overflow:hidden;
box-shadow:0 4px 24px rgba(0,0,0,0.08);">
{_logo_header(subtitle="Auckland&rsquo;s favourite dog walking service")}
<!-- Body -->
<tr>
<td style="background:#ffffff;padding:48px 48px 40px;">
<h1 style="margin:0 0 8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:26px;font-weight:700;color:#213021;line-height:1.2;">
Thanks, {data.fullName.split()[0]}! &#x1F43E;
</h1>
<p style="margin:0 0 32px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:16px;color:#555;line-height:1.65;">
We&rsquo;ve received your enquiry and Aless will be in touch shortly to arrange
a <strong style="color:#213021;">Meet &amp; Greet</strong> with you and
{data.petName}.
</p>
<!-- Details card -->
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f8f7f4;border-radius:12px;margin-bottom:36px;">
<tr>
<td style="padding:28px 32px;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.1em;
color:#888;text-transform:uppercase;margin-bottom:20px;">
Your enquiry summary
</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
{_detail_row("Your name", data.fullName)}
{_detail_row("Email", data.email)}
{_detail_row("Phone", data.phone)}
{_detail_row("Dog&rsquo;s name", data.petName)}
{_detail_row("Location", data.location)}
{_detail_row("Services", services_text)}
{message_row}
</table>
</td>
</tr>
</table>
<!-- What's next -->
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="border-left:3px solid #FFD100;margin-bottom:36px;">
<tr>
<td style="padding:4px 0 4px 20px;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:13px;font-weight:700;color:#213021;margin-bottom:6px;">
What happens next?
</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#666;line-height:1.6;">
Aless will review your details and reach out within 1&ndash;2 business days
to schedule a free Meet &amp; Greet. No commitment required &mdash; just a
chance for {data.petName} to make a new best friend.
</div>
</td>
</tr>
</table>
<p style="margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#888;line-height:1.6;">
Questions? Just reply to this email or reach us at
<a href="mailto:{REPLY_TO}" style="color:#213021;font-weight:600;
text-decoration:none;">{REPLY_TO}</a>.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background:#213021;padding:24px 48px;text-align:center;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:12px;color:#5a8a5a;line-height:1.6;">
GoodWalk &middot; Auckland, New Zealand<br>
<a href="https://www.goodwalk.co.nz" style="color:#7aaa7a;text-decoration:none;">
goodwalk.co.nz
</a>
</div>
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>"""
def owner_email(data: BookingSubmission, ip: str, browser: str) -> str:
services_text = ", ".join(data.services) if data.services else ""
now = datetime.now()
submitted_at = now.strftime("%d %b %Y at %I:%M %p").lstrip("0")
message_block = f"""
<tr>
<td colspan="2" style="padding:16px 0 0;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.08em;color:#888;
text-transform:uppercase;margin-bottom:8px;">About the dog</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;color:#444;line-height:1.6;background:#f0efe9;
border-radius:8px;padding:14px 16px;">{data.message}</div>
</td>
</tr>""" if data.message else ""
badge = """<div style="display:inline-block;background:#FFD100;border-radius:100px;
padding:10px 28px;">
<span style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:18px;font-weight:700;color:#213021;">
&#x1F4E9;&nbsp; New lead!
</span>
</div>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:12px;color:#5a8a5a;margin-top:12px;">
Submitted {submitted_at}
</div>""".format(submitted_at=submitted_at)
referrer_row = _meta_row("Came from", data.referrer) if data.referrer else _meta_row("Came from", "Direct / bookmark")
page_row = _meta_row("Page", data.page) if data.page else ""
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>New GoodWalk Lead</title>
</head>
<body style="margin:0;padding:0;background:#f2f2f0;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f2f2f0;padding:40px 16px;">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" role="presentation"
style="max-width:600px;width:100%;border-radius:16px;overflow:hidden;
box-shadow:0 4px 24px rgba(0,0,0,0.08);">
{_logo_header(badge_html=badge)}
<!-- Body -->
<tr>
<td style="background:#ffffff;padding:40px 48px 36px;">
<!-- Owner details -->
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.1em;color:#888;
text-transform:uppercase;margin-bottom:16px;">Owner details</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f8f7f4;border-radius:12px;margin-bottom:28px;">
<tr><td style="padding:24px 28px;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;width:80px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Name</td>
<td style="padding:6px 0 6px 16px;font-size:15px;font-weight:600;
color:#213021;font-family:-apple-system,BlinkMacSystemFont,
'Segoe UI',sans-serif;vertical-align:top;">{data.fullName}</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Email</td>
<td style="padding:6px 0 6px 16px;font-size:14px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">
<a href="mailto:{data.email}" style="color:#213021;font-weight:500;
text-decoration:none;">{data.email}</a>
</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Phone</td>
<td style="padding:6px 0 6px 16px;font-size:14px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">
<a href="tel:{data.phone}" style="color:#213021;font-weight:500;
text-decoration:none;">{data.phone}</a>
</td>
</tr>
</table>
</td></tr>
</table>
<!-- Dog & service details -->
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.1em;color:#888;
text-transform:uppercase;margin-bottom:16px;">Dog &amp; services</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="background:#f8f7f4;border-radius:12px;margin-bottom:28px;">
<tr><td style="padding:24px 28px;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;width:80px;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Dog</td>
<td style="padding:6px 0 6px 16px;font-size:15px;font-weight:600;
color:#213021;font-family:-apple-system,BlinkMacSystemFont,
'Segoe UI',sans-serif;vertical-align:top;">{data.petName}</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Location</td>
<td style="padding:6px 0 6px 16px;font-size:14px;font-weight:500;color:#213021;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">{data.location}</td>
</tr>
<tr>
<td style="padding:6px 0;font-size:13px;color:#888;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">Services</td>
<td style="padding:6px 0 6px 16px;font-size:14px;color:#444;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
vertical-align:top;">{services_text}</td>
</tr>
{message_block}
</table>
</td></tr>
</table>
<!-- CTA buttons -->
<table cellpadding="0" cellspacing="0" role="presentation" style="margin-bottom:32px;">
<tr>
<td style="padding-right:12px;">
<a href="mailto:{data.email}"
style="display:inline-block;background:#213021;color:#FFD100;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;font-weight:600;text-decoration:none;
border-radius:8px;padding:12px 24px;">
Reply to {data.fullName.split()[0]}
</a>
</td>
<td>
<a href="tel:{data.phone}"
style="display:inline-block;background:#f8f7f4;color:#213021;
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:14px;font-weight:600;text-decoration:none;
border-radius:8px;padding:12px 24px;border:1px solid #e0e0d8;">
Call {data.phone}
</a>
</td>
</tr>
</table>
<!-- Session info -->
<table width="100%" cellpadding="0" cellspacing="0" role="presentation"
style="border-top:1px solid #eeeee8;padding-top:20px;">
<tr><td>
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:11px;font-weight:700;letter-spacing:0.08em;color:#ccc;
text-transform:uppercase;margin-bottom:12px;">Session info</div>
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
{_meta_row("IP address", ip)}
{_meta_row("Browser", browser)}
{referrer_row}
{page_row}
</table>
</td></tr>
</table>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background:#f8f7f4;padding:18px 48px;text-align:center;
border-top:1px solid #e8e8e4;">
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
font-size:12px;color:#bbb;">
Sent automatically by GoodWalk booking form
</div>
</td>
</tr>
</table>
</td></tr>
</table>
</body>
</html>"""
# ── Route ────────────────────────────────────────────────────────────────────
@app.post("/submit")
async def submit_booking(data: BookingSubmission, request: Request):
ip = _get_ip(request)
browser = _parse_ua(request.headers.get("user-agent", ""))
logger.info("Booking from %s (%s, %s) for dog %s", data.email, ip, browser, data.petName)
errors = []
try:
resend.Emails.send({
"from": FROM_EMAIL,
"to": [data.email],
"reply_to": REPLY_TO,
"subject": f"We received your enquiry, {data.fullName.split()[0]}! 🐾",
"html": client_email(data),
})
logger.info("Client confirmation sent to %s", data.email)
except Exception as exc:
logger.error("Failed to send client email: %s", exc)
errors.append("client_email")
try:
resend.Emails.send({
"from": FROM_EMAIL,
"to": [OWNER_EMAIL],
"reply_to": data.email,
"subject": f"New GoodWalk lead — {data.fullName} ({data.petName})",
"html": owner_email(data, ip, browser),
})
logger.info("Owner notification sent to %s", OWNER_EMAIL)
except Exception as exc:
logger.error("Failed to send owner email: %s", exc)
errors.append("owner_email")
if "client_email" in errors and "owner_email" in errors:
raise HTTPException(status_code=500, detail="Failed to send emails. Please try again.")
return {"ok": True}