Files
Keysat fee037a630 Apply review polish to the digest send path (post-v0.1.0:76)
Non-blocking items from the v76 reviewer pass. No redeploy needed — the box runs
v76 and its happy path is unaffected; these ride the next build:

- digest_mailer.send_digest: when Gmail is enabled but no sender resolves
  (CRM_DIGEST_SENDER unset and no admin email), raise NoTransport so the caller
  returns a clear 400 instead of a generic 502.
- gmail_send.send_via_gmail: wrap OSError/URLError (timeout/DNS) as a RuntimeError
  ("Gmail API unreachable: ...") to match the HTTPError handling; include the
  sender in the HTTPError message for debuggability.
- credentials.py: correct the now-stale GMAIL_COMPOSE_SCOPE comment (the digest
  mailer sends with this scope; only outreach drafts never send).
- test_gmail_send.py: add the HTTPError->RuntimeError branch, default_sender DB
  fallback (+None case + env override), and the send_digest SMTP-tag path.

19/19 backend tests green.
2026-06-15 20:37:49 -05:00

70 lines
2.6 KiB
Python

"""Transport selection for CRM-originated email (daily digest, admin test sends).
Prefers Gmail-over-DWD — it reuses the service account that already powers email
capture (the grant includes gmail.compose, which can send), so there's no extra
credential to manage — and falls back to SMTP (`smtp_send`) when DWD isn't
available. One entry point so the digest and the admin test endpoint share the
same routing. Stdlib only.
"""
import os
class NoTransport(Exception):
"""Neither Gmail DWD nor SMTP is configured."""
def transport():
"""Return the active transport: 'gmail-dwd', 'smtp', or None."""
try:
from email_integration import gmail_send
if gmail_send.gmail_available():
return "gmail-dwd"
except Exception:
pass
try:
import smtp_send
if smtp_send.smtp_configured():
return "smtp"
except Exception:
pass
return None
def default_sender(conn):
"""Domain user to send as for the DWD path. `CRM_DIGEST_SENDER` if set, else
the first active admin's email."""
s = os.environ.get("CRM_DIGEST_SENDER", "").strip()
if s:
return s
if conn is None:
return None
row = conn.execute(
"SELECT email FROM users WHERE role='admin' AND is_active=1 "
"AND email IS NOT NULL AND TRIM(email)!='' ORDER BY created_at LIMIT 1"
).fetchone()
return row["email"].strip() if row and row["email"] else None
def send_digest(conn, to_addrs, subject, body, sender_email=None):
"""Send via the active transport. Returns the transport's result dict with a
'transport' key added; raises NoTransport if neither is configured."""
t = transport()
if t == "gmail-dwd":
from email_integration import gmail_send
sender = sender_email or default_sender(conn)
if not sender:
# Gmail IS available but we have nobody to send as — a config gap, not a
# send failure. Surface it as NoTransport so the caller returns a clear 400.
raise NoTransport("Gmail is enabled but no sender address is set: "
"set CRM_DIGEST_SENDER or give an active admin an email.")
result = gmail_send.send_via_gmail(sender, to_addrs, subject, body, conn=conn)
result["transport"] = "gmail-dwd"
return result
if t == "smtp":
import smtp_send
result = smtp_send.send_email(to_addrs, subject, body)
result["transport"] = "smtp"
return result
raise NoTransport("No email transport configured: enable Gmail (DWD) or set "
"SMTP via the 'Configure Digest SMTP' action.")