Personalize an email
Render recipient variables locally or deliberately choose a native grouped audience.
Use sendPersonalized for an explicit template and audience. Templates omit recipient fields; audience entries supply to, variables and optional keys.
import { Effect } from "effect";
import { Email, renderEmailPersonalizedMessage } from "@corven/email-sdk";
import { recordingEmailLayer } from "@corven/email-sdk/testing";
const template = {
body: { html: "<p>Hello %recipient.name%</p>", text: "Hello %recipient.name%" },
from: { address: "sender@example.test", name: "Synthetic Sender" },
subject: "Hello %recipient.name%",
} as const;
export const personalizedPreview = renderEmailPersonalizedMessage(template, {
to: "first@example.test",
variables: { name: "Fictional & synthetic" },
});
export const recordedPersonalization = Effect.gen(function* recordPersonalizedNotifications() {
return yield* (yield* Email).sendPersonalized(
{
message: template,
recipients: [
{
idempotencyKey: "synthetic/first",
to: "first@example.test",
variables: { name: "First" },
},
{
idempotencyKey: "synthetic/second",
to: "second@example.test",
variables: { name: "Second" },
},
],
},
{ concurrency: 2 },
);
}).pipe(Effect.provide(recordingEmailLayer));
The preview helper needs no sender. The recording program expands two individual messages without contacting a provider.
Template expansion
Variables use %recipient.name% syntax. Missing variables and invalid expanded messages remain failures. HTML expansion escapes text markup, but does not validate URL/CSS contexts or sanitize arbitrary HTML. Keep templates trusted and validate links in application code.
Every independent recipient operation has its own key. Shared options carry concurrency and execution context, not one key for the audience.
Native grouped requests
Use sendgridPersonalizedEmailLayer or mailgunPersonalizedEmailLayer only when the provider’s grouped operation matches your intent:
import { Effect, Layer } from "effect";
import type { Redacted } from "effect";
import { Email } from "@corven/email-sdk";
import { sendgridPersonalizedEmailLayer } from "@corven/email-sdk/sendgrid";
import { mailgunPersonalizedEmailLayer } from "@corven/email-sdk/mailgun";
import { emailHttpLayer } from "./sending.js";
export const sendgridNativeAccount = (apiKey: Redacted.Redacted) =>
sendgridPersonalizedEmailLayer({ apiKey, region: "Europe" }).pipe(Layer.provide(emailHttpLayer));
export const mailgunNativeAccount = (apiKey: Redacted.Redacted, domain: string) =>
mailgunPersonalizedEmailLayer({ apiKey, domain, region: "Europe" }).pipe(
Layer.provide(emailHttpLayer),
);
export const submitNativeAudience = Effect.gen(function* submitNativeAudience() {
const email = yield* Email;
return yield* email.sendPersonalized({
message: {
body: { html: "<p>Hello %recipient.name%</p>", text: "Hello %recipient.name%" },
from: "sender@example.test",
subject: "Hello %recipient.name%",
},
recipients: [
{ to: "first@example.test", variables: { name: "First" } },
{ to: "second@example.test", variables: { name: "Second" } },
],
});
});
Provide one of these account Layers to submitNativeAudience. Inspect personalization: "NativeAudience" on the acquired service. A valid subset can be submitted as one request with shared acknowledgment, deadline and correlation.
Common defaults, transformations and ordinary routing refuse native-audience Layers. They do not silently rerender or split the request. A per-audience Result is not proof of a separate wire send or independent recipient acceptance.
See sending modes and batching and routing for ordering and failure semantics.