"""Anti-corruption layer: mail.send (capabilities.yaml), never a vendor.
A signing link, a reminder and the executed copy reach people through the mail.send capability. With NO provider in
the composition (the first demo) the platform hands back a ManualTask (rule 8): contracts keeps it as a delivery row
for the owner with the exact recipient and message — WITHOUT the link, because a link is shown once and never stored;
the owner's "copy the signing link" reissues it (CT-52). A provider wired later makes the same delivery automatic:
nothing in a template or a type changes.
idempotency_key = contract_id|signer_no|<kind>|<seq> (CT-52): a retried tick sends one mail.
"""
from dataclasses import dataclass
from typing import Optional
from erp_platform import capabilities as caps
MAIL = caps.CapabilitySeam("mail.send")
@dataclass
class Sent:
status: str # sent | manual | retry | failed
provider_ref: Optional[str] = None
message: str = ""
plan: str = ""
def provider() -> Optional[str]:
return MAIL.provider()
def send(key: str, *, from_email: str, to: str, subject: str, body: str, actor: str = "") -> Sent:
data = {"from_email": from_email or "", "to": [to], "cc": [], "subject": subject, "body_text": body,
"body_html": None}
try:
r = MAIL.call(key, data, actor=actor)
except caps.InvalidCapabilityInput as e:
return Sent("manual", message=f"cannot be mailed as it stands ({str(e)[:200]}); send it by hand")
except caps.CapabilityRefused as e:
return Sent("failed", message=f"mail.send refused the call: {e.reason[:200]}")
if isinstance(r, caps.ManualTask):
return Sent("manual", message=r.reason, plan=r.plan)
if r.status == "done":
return Sent("sent", r.provider_ref or (r.output or {}).get("message_id"), r.message)
if r.status in ("in_progress",) or (r.status == "failed" and r.retryable):
return Sent("retry", message=r.message)
return Sent("failed", message=r.message)
__all__ = ["MAIL", "send", "provider", "Sent"]