Files
upbeatBytes/goodnews/email_send.py
T
thejayman77 84fd61bf3f In-site feedback reply (plain-text v1)
Reply to a reader from the admin inbox instead of a mailto. Per Codex: keep v1
plain text (no rich editor — defers the user's bold/bullets ask as a fast-follow).

* DB: feedback_replies table (feedback_id, user_id, message, sent_to, sent_at),
  created on the live DB.
* email_send.send_feedback_reply: plain-text "Re: Your Upbeat Bytes feedback"
  with a quoted context block, no analytics/account details.
* API: POST /api/admin/feedback/{id}/reply — admin-gated, requires the feedback
  exists (404) and has a contact_email (400), trims+caps the message; sends via
  SMTP and only records the reply on success (502 on send failure so the UI keeps
  the draft); marks the item read. Feedback list now includes each item's replies.
* Frontend: inline composer (Send/Cancel, sending state, error keeps draft) +
  reply thread under the message; Reply only shows when there's an address,
  else "No reply address".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:23:56 -04:00

103 lines
3.7 KiB
Python

"""Minimal transactional email over SMTP (magic-link sign-in).
Config comes from the environment (GOODNEWS_SMTP_*), supplied to the API
container via its env_file. STARTTLS submission; plain-text with a simple HTML
alternative. No third-party email API — just the configured relay.
"""
from __future__ import annotations
import os
import smtplib
import ssl
from email.message import EmailMessage
from html import escape
def smtp_configured() -> bool:
return bool(os.environ.get("GOODNEWS_SMTP_HOST"))
def _cfg() -> dict:
return {
"host": os.environ.get("GOODNEWS_SMTP_HOST", ""),
"port": int(os.environ.get("GOODNEWS_SMTP_PORT", "587")),
"user": os.environ.get("GOODNEWS_SMTP_USER", ""),
"password": os.environ.get("GOODNEWS_SMTP_PASSWORD", ""),
"sender": os.environ.get("GOODNEWS_SMTP_FROM", "Upbeat Bytes <hello@upbeatbytes.com>"),
}
def send_email(to: str, subject: str, text: str, html: str | None = None) -> None:
"""Send one message. Raises on failure (caller decides how loud to be)."""
cfg = _cfg()
if not cfg["host"]:
raise RuntimeError("SMTP not configured (set GOODNEWS_SMTP_HOST)")
msg = EmailMessage()
msg["From"] = cfg["sender"]
msg["To"] = to
msg["Subject"] = subject
msg.set_content(text)
if html:
msg.add_alternative(html, subtype="html")
context = ssl.create_default_context()
with smtplib.SMTP(cfg["host"], cfg["port"], timeout=20) as server:
server.ehlo()
server.starttls(context=context)
server.ehlo()
if cfg["user"]:
server.login(cfg["user"], cfg["password"])
server.send_message(msg)
def send_feedback(to: str, category: str, message: str, contact: str | None, who: str) -> None:
"""Notify the admin of new user feedback (plain text is plenty here)."""
subject = f"Upbeat Bytes feedback · {category}"
reply = contact or "(none given)"
text = (
f"New feedback ({category})\n"
f"From: {who}\n"
f"Reply to: {reply}\n\n"
f"{message}\n"
)
send_email(to, subject, text)
def send_feedback_reply(to: str, reply_message: str, original_message: str) -> None:
"""Reply to a reader's feedback from the admin inbox (plain text). Quotes
their original note for context; exposes no analytics/account details."""
subject = "Re: Your Upbeat Bytes feedback"
quoted = "\n".join("> " + line for line in (original_message or "").splitlines())
text = (
f"{reply_message}\n\n"
"\n"
"In reply to your note to Upbeat Bytes:\n"
f"{quoted}\n\n"
"Thanks for reaching out.\n— Upbeat Bytes\n"
)
send_email(to, subject, text)
def send_magic_link(to: str, link: str) -> None:
"""Send a calm, single-purpose sign-in email."""
subject = "Your Upbeat Bytes sign-in link"
text = (
"Welcome back to Upbeat Bytes.\n\n"
f"Tap to sign in:\n{link}\n\n"
"This link works once and expires in 15 minutes.\n"
"If you didn't request it, you can safely ignore this email."
)
safe = escape(link, quote=True)
html = (
'<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;'
'color:#16263a;line-height:1.6">'
"<p>Welcome back to <strong>Upbeat Bytes</strong>.</p>"
f'<p><a href="{safe}" style="display:inline-block;background:#0083ad;color:#fff;'
'text-decoration:none;padding:11px 20px;border-radius:999px;font-weight:600">'
"Sign in</a></p>"
'<p style="color:#5d6b78;font-size:14px">This link works once and expires in 15 minutes. '
"If you didn't request it, you can safely ignore this email.</p>"
"</div>"
)
send_email(to, subject, text, html)