//! `EmailService` — the v1.1.7 outbound email contract. //! //! Scripts get `email::send(#{...})` (plain text) and //! `email::send_html(#{...})` (multipart text + HTML). Both route to the //! single `send` trait method with an [`OutboundEmail`]; the bridge sets //! `html` only for `send_html`. //! //! Lives in `picloud-shared` (not `manager-core`) so the Rhai bridge and //! the impl share one trait. The impl (an SMTP relay over `lettre`) //! lives in `manager-core::email_service`; `picloud-shared` stays free //! of the `lettre` dependency. //! //! `app_id` is derived from `cx.app_id` (authz only — there is no //! per-app `from` validation in v1.1.7; deliverability is the operator's //! SMTP-relay concern). use async_trait::async_trait; use thiserror::Error; use crate::SdkCallCx; /// A single outbound message. `to`/`cc`/`bcc` are address lists (the /// bridge accepts a String or an Array of Strings). At least one of /// `text` / `html` must be present. #[derive(Debug, Clone, Default)] pub struct OutboundEmail { pub to: Vec, pub cc: Vec, pub bcc: Vec, pub from: String, /// Defaults to `from` when absent. pub reply_to: Option, pub subject: String, pub text: Option, pub html: Option, } #[async_trait] pub trait EmailService: Send + Sync { /// Validate, build, and send the message. Returns `Ok(())` once the /// SMTP relay has accepted it for delivery (not on actual delivery — /// that's the relay's job). async fn send(&self, cx: &SdkCallCx, email: OutboundEmail) -> Result<(), EmailError>; } /// Failure modes surfaced to the Rhai bridge. #[derive(Debug, Error)] pub enum EmailError { /// Caller principal lacked `AppEmailSend`. Only raised when /// `cx.principal.is_some()` (script-as-gate semantics). #[error("forbidden")] Forbidden, /// A required field (`to`, `from`, `subject`, or one of `text`/`html`) /// was missing or empty. #[error("missing required email field: {0}")] MissingField(String), /// An address failed basic RFC 5322-ish validation. #[error("invalid email address: {0}")] InvalidAddress(String), /// The assembled message exceeded the per-message size cap. #[error("email too large: {actual} bytes exceeds the {limit}-byte limit")] TooLarge { limit: usize, actual: usize }, /// No SMTP relay is configured (HOST/USER/PASSWORD unset). Every /// `send` fails until the operator configures one. #[error( "email is not configured: set PICLOUD_SMTP_HOST/USER/PASSWORD to enable outbound email" )] NotConfigured, /// The SMTP relay rejected the message or the connection failed. #[error("email transport error: {0}")] Transport(String), } /// Stub used by test harnesses that build a `Services` bundle without an /// SMTP relay. Every call returns `EmailError::NotConfigured`. #[derive(Debug, Default, Clone, Copy)] pub struct NoopEmailService; #[async_trait] impl EmailService for NoopEmailService { async fn send(&self, _cx: &SdkCallCx, _email: OutboundEmail) -> Result<(), EmailError> { Err(EmailError::NotConfigured) } }