feat(v1.1.7-email-outbound): SMTP send/send_html

Outbound email reachable from scripts as email::send(#{...}) (plain
text) and email::send_html(#{...}) (multipart text + HTML). Backed by a
lettre SMTP relay configured from PICLOUD_SMTP_HOST/PORT/USER/PASSWORD/
TLS/TIMEOUT_SECS; if HOST/USER/PASSWORD aren't all set the service runs
in disabled mode (every send throws NotConfigured, warned at startup).

- EmailService trait + OutboundEmail DTO (picloud-shared);
  EmailServiceImpl + EmailTransport seam + lettre transport
  (manager-core), wired into the Services bundle and Rhai engine.
- Capability::AppEmailSend (→ script:write); seven-scope commitment held.
- Required-field + RFC5322-ish address validation; 25 MB per-message cap
  (PICLOUD_EMAIL_MAX_MESSAGE_BYTES). reply_to defaults to from.
- Per-call connection (pooling deferred to v1.2); no per-app from
  validation (operator's SMTP/SPF/DKIM concern).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-04 21:47:46 +02:00
parent 2d11090d1a
commit 8f2d2bc721
21 changed files with 1120 additions and 13 deletions

View File

@@ -20,10 +20,10 @@
use std::sync::Arc;
use crate::{
DeadLetterService, DocsService, FilesService, HttpService, KvService, ModuleSource,
NoopDeadLetterService, NoopDocsService, NoopEventEmitter, NoopFilesService, NoopHttpService,
NoopKvService, NoopModuleSource, NoopPubsubService, NoopSecretsService, PubsubService,
SecretsService, ServiceEventEmitter,
DeadLetterService, DocsService, EmailService, FilesService, HttpService, KvService,
ModuleSource, NoopDeadLetterService, NoopDocsService, NoopEmailService, NoopEventEmitter,
NoopFilesService, NoopHttpService, NoopKvService, NoopModuleSource, NoopPubsubService,
NoopSecretsService, PubsubService, SecretsService, ServiceEventEmitter,
};
/// SDK service bundle. See module docs for the lifecycle and the v1.1.x
@@ -80,6 +80,12 @@ pub struct Services {
/// AES-256-GCM-at-rest Postgres repo in the picloud binary;
/// `NoopSecretsService` in tests that don't touch secrets.
pub secrets: Arc<dyn SecretsService>,
/// Outbound email (v1.1.7). Scripts get `email::{send,send_html}`.
/// Backed by an SMTP relay (lettre) in the picloud binary;
/// `NoopEmailService` (always `NotConfigured`) in tests that don't
/// send mail.
pub email: Arc<dyn EmailService>,
}
impl Services {
@@ -98,6 +104,7 @@ impl Services {
files: Arc<dyn FilesService>,
pubsub: Arc<dyn PubsubService>,
secrets: Arc<dyn SecretsService>,
email: Arc<dyn EmailService>,
) -> Self {
Self {
kv,
@@ -109,6 +116,7 @@ impl Services {
files,
pubsub,
secrets,
email,
}
}
@@ -129,6 +137,7 @@ impl Services {
Arc::new(NoopFilesService),
Arc::new(NoopPubsubService),
Arc::new(NoopSecretsService),
Arc::new(NoopEmailService),
)
}
}