Spaces:
Runtime error
Runtime error
File size: 11,577 Bytes
00a4363 1e1198f 00a4363 2823b28 00a4363 34b0226 811ee7a 00a4363 b74c6ae 00a4363 1e1198f 00a4363 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | """EmailService over a provider interface (Resend in prod, console locally).
Rule: no em dashes in any copy. Broadcast bodies are plain text and get HTML escaped.
"""
import hashlib
import hmac
import html
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from app.config import settings
log = logging.getLogger(__name__)
@dataclass
class OutgoingEmail:
to: str
subject: str
html: str
text: str
headers: dict[str, str] | None = None
class BaseEmailProvider(ABC):
@abstractmethod
def send(self, email: OutgoingEmail) -> str | None: ...
class ResendEmailProvider(BaseEmailProvider):
def __init__(self, api_key: str):
import resend
resend.api_key = api_key
self._resend = resend
def send(self, email: OutgoingEmail) -> str | None:
params = {
"from": settings.from_email,
"to": [email.to],
"subject": email.subject,
"html": email.html,
"text": email.text,
}
if email.headers:
params["headers"] = email.headers
result = self._resend.Emails.send(params)
return result.get("id") if isinstance(result, dict) else None
class ConsoleEmailProvider(BaseEmailProvider):
sent: list[OutgoingEmail] = []
def send(self, email: OutgoingEmail) -> str | None:
ConsoleEmailProvider.sent.append(email)
# Warning, not info: locally this is the only place a sign up or reset code shows up.
log.warning("[email] to=%s subject=%s\n%s", email.to, email.subject, email.text)
return None
def make_unsubscribe_token(user_id: str) -> str:
sig = hmac.new(settings.unsubscribe_secret.encode(), user_id.encode(), hashlib.sha256).hexdigest()[:32]
return f"{user_id}.{sig}"
def verify_unsubscribe_token(token: str) -> str | None:
user_id, _, _sig = token.partition(".")
if user_id and hmac.compare_digest(make_unsubscribe_token(user_id), token):
return user_id
return None
def _layout(title: str, body_html: str, footer_html: str = "") -> str:
# Inline styles only; email clients ignore <style>. Black, #217cff, white.
return f"""<!doctype html><html><body style="margin:0;background:#000000;padding:32px 16px;
font-family:Inter,Arial,sans-serif;color:#ffffff">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"><tr><td align="center">
<table role="presentation" width="560" cellpadding="0" cellspacing="0" style="max-width:560px;width:100%;
border:1px solid rgba(33,124,255,0.35);border-radius:16px;padding:32px;background:#000000">
<tr><td style="font-size:13px;letter-spacing:0.2em;text-transform:uppercase;color:#217cff">Notestack</td></tr>
<tr><td style="padding-top:16px;font-size:22px;font-weight:600;color:#ffffff">{html.escape(title)}</td></tr>
<tr><td style="padding-top:16px;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.85)">{body_html}</td></tr>
<tr><td style="padding-top:28px;font-size:12px;color:rgba(255,255,255,0.5)">{footer_html}</td></tr>
</table></td></tr></table></body></html>"""
def _button(label: str, url: str) -> str:
return (
f'<p style="padding-top:12px"><a href="{html.escape(url)}" style="display:inline-block;'
f"background:#217cff;color:#ffffff;text-decoration:none;padding:12px 20px;border-radius:999px;"
f'font-weight:600">{html.escape(label)}</a></p>'
)
def _code_block(code: str) -> str:
return (
f'<p style="font-family:JetBrains Mono,monospace;font-size:32px;letter-spacing:0.3em;'
f'color:#ffffff;padding:8px 0">{html.escape(code)}</p>'
)
class EmailService:
def __init__(self, provider: BaseEmailProvider):
self.provider = provider
def _send(self, email: OutgoingEmail) -> bool:
try:
self.provider.send(email)
return True
except Exception: # never let email failures break the request
log.exception("email send failed to=%s subject=%s", email.to, email.subject)
return False
# Transactional
def send_verification_code(self, to: str, code: str) -> bool:
body = f"Enter this code to finish creating your account:{_code_block(code)}It expires in 10 minutes."
return self._send(
OutgoingEmail(
to=to,
subject=f"{code} is your Notestack code",
html=_layout("Confirm your email", body, "If you did not sign up, ignore this email."),
text=f"Your Notestack code is {code}. It expires in 10 minutes.",
)
)
def send_password_reset_code(self, to: str, code: str) -> bool:
body = f"Use this code to reset your password:{_code_block(code)}It expires in 10 minutes."
return self._send(
OutgoingEmail(
to=to,
subject="Reset your Notestack password",
html=_layout("Password reset", body, "If you did not ask for this, you can ignore it."),
text=f"Your Notestack password reset code is {code}. It expires in 10 minutes.",
)
)
def send_welcome(self, to: str, name: str | None, user_id: str) -> bool:
first = (name or "there").split(" ")[0]
url = f"{settings.frontend_url}/app"
body = (
f"Hi {html.escape(first)},<br><br>Your knowledge is about to come into orbit. Connect any blog, newsletter "
"or site, or upload your markdown files, and Notestack will index your posts, learn your voice and get "
"your first audio overview ready."
+ _button("Open Mission Control", url)
)
return self._send(
OutgoingEmail(
to=to,
subject="Welcome to Notestack",
html=_layout("Welcome aboard", body, self._unsubscribe_footer(user_id)),
text=f"Hi {first}, welcome to Notestack. Open Mission Control: {url}",
headers=self._list_unsubscribe(user_id),
)
)
def send_job_ready(self, to: str, what: str, url: str) -> bool:
body = f"Your {html.escape(what)} is ready." + _button("View it", url)
return self._send(
OutgoingEmail(
to=to,
subject=f"Your {what} is ready",
html=_layout("Launch complete", body),
text=f"Your {what} is ready: {url}",
)
)
def send_post_reminder(self, to: str, platform: str, content: str, url: str) -> bool:
body = (
f"Your {html.escape(platform)} post is scheduled for now. Copy it below and post it."
f"<pre style='white-space:pre-wrap;background:rgba(255,255,255,0.06);padding:16px;border-radius:8px'>"
f"{html.escape(content)}</pre>" + _button("Open Launchpad", url)
)
return self._send(
OutgoingEmail(
to=to,
subject=f"Time to post on {platform}",
html=_layout("Launch window open", body),
text=f"Time to post on {platform}:\n\n{content}\n\nLaunchpad: {url}",
)
)
# Billing
def send_payment_action(self, to: str, kind: str, url: str) -> bool:
"""kind: 'confirm' (the bank wants 3D Secure for a renewal) or 'failed' (card declined; Stripe retries)."""
if kind == "confirm":
title, subject = "Confirm your payment", "Your bank needs you to confirm your Notestack payment"
body = ("Your bank asked for a quick confirmation (3D Secure) before your Notestack renewal can go "
"through. It takes a few seconds, and your plan stays active meanwhile.")
cta = "Confirm payment"
else:
title, subject = "Your payment did not go through", "Your Notestack payment failed"
body = ("We could not charge your card for Notestack. We will retry automatically over the next few "
"days; update your card to keep your plan without interruption.")
cta = "Update payment"
return self._send(
OutgoingEmail(
to=to,
subject=subject,
html=_layout(title, html.escape(body) + _button(cta, url)),
text=f"{body}\n\n{cta}: {url}",
)
)
# Internal alerts
def send_alert(self, subject: str, text: str) -> bool:
return self._send(
OutgoingEmail(
to=settings.alerts_email,
subject=f"[Notestack alert] {subject}",
html=_layout(subject, f"<pre style='white-space:pre-wrap'>{html.escape(text)}</pre>"),
text=text,
)
)
def send_support_escalation(self, *, from_email: str, name: str | None, plan: str | None, reason: str,
concern: str, page: str | None, transcript: list[tuple[str, str]]) -> bool:
lines = [f"From: {name or 'unknown'} <{from_email}>", f"Plan: {plan or 'unknown'}", f"Reason: {reason}",
f"Page: {page or 'unknown'}", "", "Concern:", concern]
if transcript:
lines += ["", "Recent chat:"] + [f"{role}: {text}" for role, text in transcript]
text = "\n".join(lines)
return self._send(
OutgoingEmail(
to=settings.alerts_email,
subject=f"[Notestack help] {reason}: {concern[:60]}",
html=_layout("Help request", f"<pre style='white-space:pre-wrap'>{html.escape(text)}</pre>"),
text=text,
headers={"Reply-To": from_email},
)
)
# Broadcast
def send_blast_email(self, user_id: str, user_email: str, user_name: str | None, subject: str, body: str) -> bool:
first = (user_name or "there").split(" ")[0]
body_html = f"Hi {html.escape(first)},<br><br>" + html.escape(body).replace("\n", "<br>")
return self._send(
OutgoingEmail(
to=user_email,
subject=subject,
html=_layout(subject, body_html, self._unsubscribe_footer(user_id)),
text=f"Hi {first},\n\n{body}\n\nUnsubscribe: {self._unsubscribe_url(user_id)}",
headers=self._list_unsubscribe(user_id),
)
)
def _unsubscribe_url(self, user_id: str) -> str:
return f"{settings.api_url}/api/unsubscribe?token={make_unsubscribe_token(user_id)}"
def _unsubscribe_footer(self, user_id: str) -> str:
return f'<a href="{self._unsubscribe_url(user_id)}" style="color:rgba(255,255,255,0.5)">Unsubscribe</a>'
def _list_unsubscribe(self, user_id: str) -> dict[str, str]:
return {
"List-Unsubscribe": f"<{self._unsubscribe_url(user_id)}>",
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
}
def _build() -> EmailService:
if settings.email_provider == "resend" and settings.resend_api_key:
return EmailService(ResendEmailProvider(settings.resend_api_key))
if settings.email_provider == "resend":
log.warning("EMAIL_PROVIDER=resend but RESEND_API_KEY is empty: emails are only logged, not sent")
elif settings.email_provider != "console":
log.warning("Unknown EMAIL_PROVIDER=%r (use resend or console): emails are only logged, not sent",
settings.email_provider)
else:
log.warning("EMAIL_PROVIDER=console: emails are only logged, not sent")
return EmailService(ConsoleEmailProvider())
email_service = _build()
|