Whitespace + import-grouping fixups in the 9 SDK modules touched by F-Q-002. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
132 lines
4.8 KiB
Rust
132 lines
4.8 KiB
Rust
//! `email::` Rhai bridge — outbound email (v1.1.7).
|
|
//!
|
|
//! ```rhai
|
|
//! email::send(#{
|
|
//! to: "alice@example.com", // String or Array of String
|
|
//! from: "alerts@myapp.com",
|
|
//! subject: "Build complete",
|
|
//! text: "Your deploy finished."
|
|
//! });
|
|
//!
|
|
//! email::send_html(#{
|
|
//! to: ["alice@x.com", "bob@y.com"],
|
|
//! cc: ["dave@z.com"],
|
|
//! bcc: ["audit@myapp.com"],
|
|
//! from: "alerts@myapp.com",
|
|
//! reply_to: "support@myapp.com", // optional; defaults to `from`
|
|
//! subject: "Build complete",
|
|
//! text: "Your deploy finished.", // plain-text fallback
|
|
//! html: "<p>Your deploy <b>finished</b>.</p>"
|
|
//! });
|
|
//! ```
|
|
//!
|
|
//! Both map onto `EmailService::send`. `email::send` forces a text-only
|
|
//! message (any `html` key is ignored); `email::send_html` requires an
|
|
//! `html` part. `app_id` is derived from `cx.app_id` in the service.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use super::bridge::block_on;
|
|
use picloud_shared::{OutboundEmail, SdkCallCx, Services};
|
|
use rhai::{Array, Engine as RhaiEngine, EvalAltResult, Map, Module};
|
|
|
|
pub(super) fn register(engine: &mut RhaiEngine, services: &Services, cx: Arc<SdkCallCx>) {
|
|
let svc = services.email.clone();
|
|
let mut module = Module::new();
|
|
|
|
// email::send(#{...}) — plain text (html ignored).
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn("send", move |opts: Map| -> Result<(), Box<EvalAltResult>> {
|
|
let mut email = parse_email(&opts)?;
|
|
email.html = None; // text-only path
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("email", async move { svc.send(&cx, email).await })
|
|
});
|
|
}
|
|
|
|
// email::send_html(#{...}) — multipart text + html (html required).
|
|
{
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
module.set_native_fn(
|
|
"send_html",
|
|
move |opts: Map| -> Result<(), Box<EvalAltResult>> {
|
|
let email = parse_email(&opts)?;
|
|
if email.html.as_ref().is_none_or(String::is_empty) {
|
|
return Err(runtime_err(
|
|
"email::send_html: an 'html' field is required (use email::send for text-only)",
|
|
));
|
|
}
|
|
let svc = svc.clone();
|
|
let cx = cx.clone();
|
|
block_on("email", async move { svc.send(&cx, email).await })
|
|
},
|
|
);
|
|
}
|
|
|
|
engine.register_static_module("email", module.into());
|
|
}
|
|
|
|
/// Parse the Rhai options map into an [`OutboundEmail`]. Field-level
|
|
/// validation (required fields, address shape) happens in the service;
|
|
/// here we only do type coercion (String/Array → Vec<String>).
|
|
fn parse_email(opts: &Map) -> Result<OutboundEmail, Box<EvalAltResult>> {
|
|
Ok(OutboundEmail {
|
|
to: addresses(opts, "to")?,
|
|
cc: addresses(opts, "cc")?,
|
|
bcc: addresses(opts, "bcc")?,
|
|
from: string_field(opts, "from").unwrap_or_default(),
|
|
reply_to: string_field(opts, "reply_to"),
|
|
subject: string_field(opts, "subject").unwrap_or_default(),
|
|
text: string_field(opts, "text"),
|
|
html: string_field(opts, "html"),
|
|
})
|
|
}
|
|
|
|
/// Read a string field. Missing or `()` → `None`.
|
|
fn string_field(opts: &Map, key: &str) -> Option<String> {
|
|
match opts.get(key) {
|
|
None => None,
|
|
Some(d) if d.is_unit() => None,
|
|
Some(d) if d.is_string() => Some(d.clone().into_string().unwrap_or_default()),
|
|
// Coerce non-string scalars via display (numbers, etc.).
|
|
Some(d) => Some(d.to_string()),
|
|
}
|
|
}
|
|
|
|
/// Read an address list: a String becomes a one-element list; an Array
|
|
/// of Strings becomes a list; missing/`()` is empty.
|
|
fn addresses(opts: &Map, key: &str) -> Result<Vec<String>, Box<EvalAltResult>> {
|
|
match opts.get(key) {
|
|
None => Ok(Vec::new()),
|
|
Some(d) if d.is_unit() => Ok(Vec::new()),
|
|
Some(d) if d.is_string() => Ok(vec![d.clone().into_string().unwrap_or_default()]),
|
|
Some(d) => {
|
|
if let Some(arr) = d.clone().try_cast::<Array>() {
|
|
let mut out = Vec::with_capacity(arr.len());
|
|
for el in arr {
|
|
if !el.is_string() {
|
|
return Err(runtime_err(&format!(
|
|
"email: '{key}' array must contain only strings"
|
|
)));
|
|
}
|
|
out.push(el.into_string().unwrap_or_default());
|
|
}
|
|
Ok(out)
|
|
} else {
|
|
Err(runtime_err(&format!(
|
|
"email: '{key}' must be a string or an array of strings"
|
|
)))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::unnecessary_box_returns)]
|
|
fn runtime_err(msg: &str) -> Box<EvalAltResult> {
|
|
EvalAltResult::ErrorRuntime(msg.into(), rhai::Position::NONE).into()
|
|
}
|