minco-plugin-notifications
Provider-neutral notification delivery plus an explicit rich outbound-mail contract for Minco applications.
The existing Notification, NotificationSink, and NotificationService APIs
remain available for email-shaped notices, webhooks, in-app alerts, and
developer feedback. New email-specific work should use MailMessage and
MailService for CC/BCC, reply-to, text and HTML alternatives, attachments,
inline content, safe headers, provider tags, acceptance receipts, deterministic
capture, fallback policy, and submission observation.
Compose a message
use ;
let message = builder
.to
.cc
.bcc
.reply_to
.text
.html
.attachment
.tag
.build?;
Validation occurs before transport I/O. The message is bounded to 50 envelope recipients, one text and/or HTML body, 32 attachments, 25 MiB of raw attachment bytes, safe application headers, bounded tags and metadata, and a rendered MIME message below Minco's provider boundary. BCC recipients stay in the transport envelope and are never rendered into MIME headers.
Unicode display names and subjects are encoded and folded. Custom application header values are printable ASCII, Minco/SES control headers are reserved, and every physical header line is limited to 998 bytes. Tests parse the result with an independent RFC 5322/MIME parser as well as checking the transmitted bytes.
Attachment bytes are runtime values and deliberately do not implement Serde. Durable mail intent should use an application-owned schema containing object references, digests, lengths, and media types instead of serializing raw bytes into an outbox or queue record.
Rendering is intentionally in-memory and may transiently retain raw attachment bytes, Base64 output, and the final MIME buffer together. Use an access-controlled object-storage link for large files and size the runtime from measured peak memory rather than treating the 25 MiB raw limit as a recommended payload size.
Send and observe
use ;
use Arc;
let mail = single?;
let receipt = mail.send.await?;
MailReceipt means that the selected transport accepted the submission and
returned a provider identifier. It does not mean that the recipient mailbox has
received the message.
Submission observation includes prepared, attempting, failed, and accepted states. The tracing observer emits the Minco message UUID, stable topic, transport, attempt, coarse error class, and duration. It excludes recipients, display names, subject, body, attachments, metadata values, and provider message IDs.
Observers have independent bounded execution windows and run concurrently, so a slow observer cannot prevent a later observer from receiving the same event or hold the submission path indefinitely. Invalid acceptance receipts emit an ambiguous failed-attempt observation before returning an error.
Failure and fallback semantics
MailErrorKind separates invalid configuration, authentication, rejection,
throttling, unavailability, protocol violations, and ambiguous outcomes.
MailService advances to a fallback only after an explicitly retry-safe
throttled or unavailable result. It stops immediately after an ambiguous result
because the first provider may already have accepted the message.
A stable message UUID can be combined with Minco's idempotency and transactional outbox capabilities when business state and mail intent must be committed together. No exactly-once email-delivery guarantee is claimed.
Deterministic tests
use ;
use Arc;
let transport = new;
let observer = new;
let mail = single?;
mail.send.await?;
transport.assert_sent_count.await;
transport.assert_sent_to.await;
assert_eq!;
MemoryMailTransport captures the complete message without network access,
credentials, sleeps, or provider state.
The package distribution describes the plain notifications constructor and
therefore advertises only notifications.send. A runtime
NotificationsPlugin descriptor adds mail.send only when constructed with an
explicit MailService; graph tests reject one-sided rich-mail selection.
Browser inbox with Mailpit
From the Minco repository root:
Use MailpitTransport::default() to submit to 127.0.0.1:1025, then open
http://127.0.0.1:8025. The adapter refuses non-loopback plaintext SMTP
endpoints, implements command timeouts and multiline responses, dot-stuffs DATA,
and treats connection loss after DATA as an ambiguous result. The pinned
container uses its native health command, explicit CPU/memory/PID limits, and no
automatic restart policy. The host smoke verifies rich SMTP capture through the
Mailpit API. Mailpit reconstructs BCC from envelope metadata in its raw-message
API, so the byte-exact SMTP test remains the authority that Minco's transmitted
MIME omitted the BCC header. Mailpit's CSS/font preview control does not block
remote images or tracking pixels; do not open untrusted HTML without separate
browser/network isolation.
Add -v only when the captured local inbox should also be deleted.
Final delivery evidence
MailDeliveryEvent represents submission, delivery, permanent/transient/
undetermined bounce, complaint, reject, delay, rendering failure, and optional
open/click/subscription evidence.
MemoryMailDeliveryEventSink provides deterministic source-event
deduplication. TracingMailDeliveryEventSink also deduplicates and emits a
digest of the source event ID plus bounded privacy-safe fields. The memory sink
is test-scoped; the tracing sink caps its in-process window at 4,096 source IDs.
Neither persists across restarts, so durable replay protection is
application-owned. The tracing sink deliberately omits raw source IDs, provider
message IDs, and customer-provided values.
Provider adapters normalize their own event envelopes before passing events to these sinks.