>_EXECUTIVE SUMMARY
stryke-email is an opt-in connector package in the stryke ecosystem — transactional and campaign email through your own authenticated SMTP. It exposes single send, personalized mass mailing, {{merge}} templates, List-Unsubscribe headers, suppression lists, and rate limiting as the Email package.
Sending is built on lettre's blocking transport over rustls — no async runtime, no OpenSSL, no SaaS email API. The dependency set is small, durable, and vendorable, and ships once on demand rather than being linked into stryke core.
~FFI MODEL
Each public symbol is a #[no_mangle] extern "C" fn email__* that takes a JSON string and returns a JSON string — one function per namespace method. The .stk wrapper (lib/Email.stk) builds the args dict, calls the symbol via the dlopened cdylib, and decodes the return. A return carrying an error key is converted into a die "Email::<fn>: <reason>"; otherwise the success payload is unwrapped to its meaningful field (value / ok / raw / groups / etc.). Connection params are merged into the request hash by a shared helper, with $SMTP_HOST / $SMTP_USER / $SMTP_PASS as the env fallback when host is absent. The 27 exports are pinned in stryke.toml's [ffi].exports and mirror the 27 fn Email:: wrappers one-to-one.
~TRANSPORT
Sending uses lettre's blocking SmtpTransport, which carries its own connection pool, over rustls TLS — no async runtime, no OpenSSL. One transport is cached per (host, port, tls, user) tuple, so a mass mailing reuses a single authenticated, pooled connection instead of reconnecting per message. The TLS mode (starttls / tls / none) selects the relay builder and the default port (587 / 465 / 25); an explicit port overrides the TLS-implied default. none uses builder_dangerous — intended for a local catch-all like MailHog, not for the open internet.
~THE CAMPAIGN LOOP
send_bulk iterates the recipient list once. Per recipient it validates the address, skips it if it is on the suppression set, merges the subject/text/html template with the recipient's vars (plus email and name), injects the List-Unsubscribe header (whose URL may itself be per-recipient via {{email}}), sends, and records { email, ok, error? }. The aggregate return is { sent, failed, skipped, results }. Throttling reads rate.delay_ms directly, or derives a millisecond delay from rate.per_minute; the sleep runs between sends, not after the last one. The per-recipient result set lets the caller retry failures and update its own bounce/opt-out state.
$TEMPLATING
The merge engine is a deliberate mustache-lite: {{ key }} substitution only, no conditionals or loops, unknown keys render empty, key names trimmed. String values insert verbatim; other JSON values insert their string form. The scan is byte-wise and UTF-8 safe. Keeping it logic-free avoids turning user-supplied template data into a template-injection surface. It is a pure function, unit-tested in the crate and exercised by the surface test with no SMTP server. render applies it to each of { subject, text, html }; merge_many applies one template across many var dicts.
%ADDRESS & VALIDATION
Address checks are conservative shape checks, not deliverability lookups (no MX/DNS): one @, a non-empty local part, a dotted domain with no leading/trailing dot and no whitespace. parse / split_addresses split "Name <user@host>" forms, honoring commas inside quotes and angle brackets. normalize drops the display name, trims, and lowercases only the domain (RFC 5321: the local part stays case-sensitive) — this is the canonical key used by dedupe and suppression matching. validate_message pre-flights a full message dict, returning { ok, errors } that flag missing/invalid from, no recipients across to/cc/bcc, all-invalid recipients, and an empty body.
&COMPLIANCE AS CODE
The package is scoped to legitimate sending — your own opted-in recipients, your own SMTP. List-Unsubscribe (RFC 2369 / 8058), suppression filtering, rate limiting, and address validation are first-class.
| Mechanism | Implementation |
|---|---|
| List-Unsubscribe | RFC 2369 / 8058 one-click header auto-added to every send_bulk message; URL templated per recipient |
| Suppression | suppress_filter + send_bulk honor opt-outs and bounces before send |
| Rate limiting | per_minute / delay_ms throttle for deliverability and provider limits |
| Validation | Address checks before a message is queued |
None of it removes the sender's legal obligation to obtain consent and honor unsubscribes (CAN-SPAM, GDPR); the package provides the mechanisms, not a way around them.
=FUNCTION SURFACE
27 public Email:: functions, grouped by role. Send functions take trailing SMTP %opts; the rest are network-free and CI-tested with no server. Full per-function signatures are in the documentation reference.
| Group | Functions |
|---|---|
| Send | send, send_raw, send_bulk, send_template, build_message, verify_connection |
| Templates | render, merge, merge_many |
| Addresses | validate, validate_list, validate_message, parse, split_addresses, format_address, domain, normalize |
| Lists | unsubscribe_header, suppress_filter, dedupe, chunk, group_by_domain, mailto |
| URL | parse_url, build_url, redact_url |
| Meta | version |
/DEPENDENCIES
lettre 0.11 with default-features = false (SMTP transport + message builder over rustls, hostname), serde / serde_json (the latter with preserve_order) for the FFI payloads, once_cell + parking_lot for the transport cache. A small, durable, vendorable set — no tokio, no OpenSSL, no SaaS email API client. The cdylib is publish = false: it ships through stryke's package channel, not crates.io.
#PROJECT METADATA
| Item | Value |
|---|---|
| Version | 0.3.0 |
| License | MIT |
| Crate type | cdylib (libstryke_email, publish = false) |
| Public functions | 27 (Email namespace) |
| Repository | github.com/MenkeTechnologies/stryke-email |
| Parent language | strykelang |
| Meta umbrella | MenkeTechnologiesMeta |
| Issues | github.com/MenkeTechnologies/stryke-email/issues |