Jul 28 - Update the demo request button, and add demo registration page
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Minimal SMTP sender for owner/customer notifications.
|
||||
|
||||
Deliberately stdlib-only (smtplib + email) — no new dependency, no queue, no
|
||||
broker. Mail is sent on a daemon thread so a slow or dead SMTP host never makes
|
||||
the visitor wait, and a failure is logged rather than raised: the demo request
|
||||
is already committed to the database by the time we get here, so the owner can
|
||||
always see it in /admin/demos even if mail is misconfigured.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
import threading
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr, parseaddr
|
||||
|
||||
mail_log = logging.getLogger("jqc.mail")
|
||||
|
||||
|
||||
def mail_enabled(config):
|
||||
"""Mail can only go out with a host and a From address configured."""
|
||||
return bool(config.get("SMTP_HOST") and config.get("MAIL_FROM"))
|
||||
|
||||
|
||||
def _clean_header(value):
|
||||
"""Strip CR/LF so a user-supplied name can't inject extra headers."""
|
||||
return " ".join(str(value or "").split())[:200]
|
||||
|
||||
|
||||
def send_email(config, to, subject, body, reply_to=None, reply_name=None):
|
||||
"""Send one plain-text mail. Returns True on success, False otherwise.
|
||||
Never raises — callers treat mail as best-effort."""
|
||||
if not mail_enabled(config) or not to:
|
||||
mail_log.info("Mail skipped (SMTP not configured): %s", _clean_header(subject))
|
||||
return False
|
||||
|
||||
prefix = config.get("MAIL_SUBJECT_PREFIX", "")
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = _clean_header(f"{prefix} {subject}".strip())
|
||||
msg["From"] = config["MAIL_FROM"]
|
||||
msg["To"] = to
|
||||
if reply_to:
|
||||
addr = parseaddr(_clean_header(reply_to))[1]
|
||||
if addr:
|
||||
msg["Reply-To"] = formataddr((_clean_header(reply_name), addr))
|
||||
msg.set_content(body)
|
||||
|
||||
host = config["SMTP_HOST"]
|
||||
port = config.get("SMTP_PORT", 587)
|
||||
timeout = config.get("SMTP_TIMEOUT", 20)
|
||||
try:
|
||||
if config.get("SMTP_USE_SSL"):
|
||||
server = smtplib.SMTP_SSL(host, port, timeout=timeout)
|
||||
else:
|
||||
server = smtplib.SMTP(host, port, timeout=timeout)
|
||||
with server:
|
||||
server.ehlo()
|
||||
if config.get("SMTP_USE_TLS") and not config.get("SMTP_USE_SSL"):
|
||||
server.starttls()
|
||||
server.ehlo()
|
||||
if config.get("SMTP_USER"):
|
||||
server.login(config["SMTP_USER"], config.get("SMTP_PASSWORD", ""))
|
||||
server.send_message(msg)
|
||||
mail_log.info("Mail sent to %s: %s", to, msg["Subject"])
|
||||
return True
|
||||
except Exception as exc: # noqa: BLE001 - mail must never break the request
|
||||
mail_log.error("Mail to %s failed: %s", to, exc)
|
||||
return False
|
||||
|
||||
|
||||
def send_email_async(config, to, subject, body, reply_to=None, reply_name=None):
|
||||
"""Fire-and-forget wrapper. `config` is copied to a plain dict first so the
|
||||
thread never touches the Flask app/request context."""
|
||||
cfg = {k: config.get(k) for k in (
|
||||
"SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD", "SMTP_USE_TLS",
|
||||
"SMTP_USE_SSL", "SMTP_TIMEOUT", "MAIL_FROM", "MAIL_SUBJECT_PREFIX",
|
||||
)}
|
||||
thread = threading.Thread(
|
||||
target=send_email,
|
||||
args=(cfg, to, subject, body),
|
||||
kwargs={"reply_to": reply_to, "reply_name": reply_name},
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
return thread
|
||||
Reference in New Issue
Block a user