47dfd110a0
The box's existing service-account domain-wide-delegation grant already includes gmail.compose, which authorizes users.messages.send — verified 2026-06-15 by a token-mint probe and a live messages.send to grant. So CRM-originated mail can send through the account that already powers email capture: no SMTP account, no app password, no admin change. - backend/email_integration/gmail_send.py: send_via_gmail() impersonates a domain user and POSTs users.messages.send (reuses credentials.py + the compose scope; mirrors compose.py's REST pattern). - backend/digest_mailer.py: send_digest() prefers Gmail DWD when enabled, falls back to smtp_send otherwise. Sender = CRM_DIGEST_SENDER else first active admin. - server.py: the admin test endpoint now routes through digest_mailer (so the Settings button sends via DWD on the box with zero SMTP config). Recipient restriction to the admin set and no-leak error handling preserved. - test_gmail_send.py: build/send + transport routing (provider + urlopen faked). 19/19 backend green; s9pk typechecks. SMTP (v75) stays as the fallback transport. Send-path decision + scope finding recorded in ROADMAP.md and AGENTS.md.
65 lines
2.2 KiB
Python
65 lines
2.2 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)
|
|
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.")
|