- analysis.py: deterministic claim scorer + /analyze /approve /letter /advance-tier endpoints (auto-runs on intake) - packet.py + packet_fields.json: welcome-packet templating engine (6 onboarding docs, field catalog) - letterstream.py + letters.py: certified-mail send pipeline + letter lifecycle (webhook verified) - docuseal.py: DocuSeal signing integration - staff.py/models.py/schema.sql/auth.py: approval actor from staff key, tier gate (APPROVED+ACTIVE+onboarding docs), onboarding_docs table - frontend/: dependency-free static portal (intake, magic-link login/verify, dashboard) - landing-mockups/: 4 design-stance mockups + favicons - legal/: aup/privacy/sms-terms/terms HTML - docs/: letter-queue scope, letterstream API contract, 6 welcome-packet templates - review-dre-landing-2026-08-21.md: 3-variant landing feedback sprint - compliance/DRE_Compliance_Manual.md: updated Source synced from deployed /opt/dre-portal/app/ (was 4 days ahead of git).
115 lines
4.3 KiB
Python
115 lines
4.3 KiB
Python
"""Best-effort SMTP email via the germainebrown.com relay (mail.germainebrown.com:2525, STARTTLS).
|
|
Email failure must NEVER fail the API request — wrap every send in try/except, log, continue.
|
|
Per conductor decision #3.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import smtplib
|
|
import ssl
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
logger = logging.getLogger("dre.email")
|
|
|
|
|
|
def _env(name: str, default: str = "") -> str:
|
|
return os.environ.get(name, default)
|
|
|
|
|
|
def _team_recipients() -> list[str]:
|
|
"""Fan-out list from DRE_TEAM_NOTIFY (comma-separated). Defaults to dre@."""
|
|
raw = _env("DRE_TEAM_NOTIFY", "dre@debtrecoveryexperts.com")
|
|
return [a.strip() for a in raw.split(",") if a.strip()]
|
|
|
|
|
|
def send_email(to_addr: str, subject: str, body_text: str, html: str | None = None) -> bool:
|
|
"""Send an email via the configured SMTP relay. Returns True on success, False on failure.
|
|
Never raises — caller proceeds regardless."""
|
|
host = _env("DRE_SMTP_HOST", "mail.germainebrown.com")
|
|
port = int(_env("DRE_SMTP_PORT", "2525"))
|
|
user = _env("DRE_SMTP_USER", "")
|
|
pw = _env("DRE_SMTP_PASS", "")
|
|
sender = _env("DRE_SMTP_FROM", "dre@debtrecoveryexperts.com")
|
|
try:
|
|
msg = MIMEMultipart("alternative")
|
|
msg["From"] = sender
|
|
msg["To"] = to_addr
|
|
msg["Subject"] = subject
|
|
msg.attach(MIMEText(body_text, "plain", "utf-8"))
|
|
if html:
|
|
msg.attach(MIMEText(html, "html", "utf-8"))
|
|
with smtplib.SMTP(host, port, timeout=15) as server:
|
|
server.starttls(context=ssl.create_default_context())
|
|
if user and pw:
|
|
server.login(user, pw)
|
|
server.sendmail(sender, [to_addr], msg.as_string())
|
|
return True
|
|
except Exception as exc: # noqa: BLE001 — best-effort
|
|
logger.error("email send failed to=%s subject=%s err=%s", to_addr, subject, exc)
|
|
return False
|
|
|
|
|
|
def notify_team_intake(claim_number: str, client_number: str, company_name: str,
|
|
amount_cents: int, debtor_name: str) -> bool:
|
|
team = _team_recipients()
|
|
base = _env("DRE_BASE_URL", "https://my.debtrecoveryexperts.com")
|
|
dollars = amount_cents / 100.0
|
|
body = (
|
|
f"New claim submitted via portal.\n\n"
|
|
f"Claim: {claim_number}\n"
|
|
f"Client: {client_number} — {company_name}\n"
|
|
f"Debtor: {debtor_name}\n"
|
|
f"Amount: ${dollars:,.2f}\n\n"
|
|
f"Review at: {base}/\n"
|
|
)
|
|
html = (
|
|
f"<h2>New claim submitted</h2>"
|
|
f"<p><b>Claim:</b> {claim_number}<br>"
|
|
f"<b>Client:</b> {client_number} — {company_name}<br>"
|
|
f"<b>Debtor:</b> {debtor_name}<br>"
|
|
f"<b>Amount:</b> ${dollars:,.2f}</p>"
|
|
f"<p><a href=\"{base}/\">Review in portal</a></p>"
|
|
)
|
|
ok = True
|
|
for addr in team:
|
|
ok = send_email(addr, f"New DRE Claim: {claim_number}", body, html) and ok
|
|
return ok
|
|
|
|
|
|
def send_magic_link(to_addr: str, raw_token: str, client_number: str) -> bool:
|
|
base = _env("DRE_BASE_URL", "https://my.debtrecoveryexperts.com")
|
|
link = f"{base}/verify?token={raw_token}"
|
|
body = (
|
|
f"Hello,\n\n"
|
|
f"Click the link below to log in to your DRE client portal. "
|
|
f"This link expires in 15 minutes and can only be used once.\n\n"
|
|
f"{link}\n\n"
|
|
f"If you did not request this link, you can ignore this email.\n"
|
|
)
|
|
html = (
|
|
f"<p>Hello,</p>"
|
|
f"<p>Click the button below to log in to your DRE client portal. "
|
|
f"This link expires in 15 minutes and can only be used once.</p>"
|
|
f"<p><a href=\"{link}\" style=\"...\">Log In</a></p>"
|
|
f"<p>If the button doesn't work, copy this link: {link}</p>"
|
|
f"<p>If you did not request this link, you can ignore this email.</p>"
|
|
)
|
|
return send_email(to_addr, "Your DRE Portal Login Link", body, html)
|
|
|
|
|
|
def notify_team_message(claim_number: str, subject: str, content: str,
|
|
author: str) -> bool:
|
|
team = _team_recipients()
|
|
body = (
|
|
f"New client message on claim {claim_number}.\n\n"
|
|
f"From: {author}\n"
|
|
f"Subject: {subject}\n\n"
|
|
f"{content}\n"
|
|
)
|
|
ok = True
|
|
for addr in team:
|
|
ok = send_email(addr, f"Client message on {claim_number}: {subject}", body) and ok
|
|
return ok
|