Documentation for agents: an index at /llms.txt, every docs page in one file at /llms-full.txt, and Markdown for any docs page by adding .md to its URL.

Effect KitEmails

Custom providers

Wrap any delivery service in an EmailAdapter and get the same validation, capability checks, receipts and spans as the built-in providers.

To support a provider the package does not ship, write an EmailAdapter and pass it to Email.make. Email.make returns an EmailService, and Layer.succeed(Email, service) turns it into a layer.

Example (An Adapter for a Hypothetical HTTP API)

import { ,  } from "effect";
import { , ,  } from "@effect-kit/emails";
import type { EmailAdapter } from "@effect-kit/emails";

const : EmailAdapter = {
  : "acme",
  : {
    : true,
    : true,
    : true,
    : true,
    : false,
    : false,
    : "none",
    : false,
    : false,
    : false,
    : { : "unsupported" },
  },
  : () =>
    .({
      : async () => {
        const  = await ("https://api.acme.test/send", {
          : "POST",
          : .({
            : ..,
            : ..(() => .),
            : .,
            : .,
            : .,
          }),
        });
        const  = (await .()) as { : string };
        return {
          : "acme",
          : "accepted" as ,
          : [{ : "providerMessageId" as , : . }],
        };
      },
      : () =>
        new ({
          : "acme",
          : "send",
          : "unknown",
          : new (),
        }),
    }),
};

export const  = .(, .());

Code that uses Email does not change. Provide AcmeLayer where you would provide a built-in provider layer.

What Email.make adds

The adapter only submits. Email.make wraps it so that every send, in this order:

  1. Validates the message and rejects unknown keys with InvalidMessageError.
  2. Checks the message against your capabilities and fails with UnsupportedFeatureError.
  3. Calls your send with a NormalizedEmailMessage and the decoded SendOptions.
  4. Validates the receipt you return.
  5. Records the Email.send span described in Observability.

The adapter never sees a message that failed steps 1 or 2.

Adapter input

message is a NormalizedEmailMessage. Compared with what callers pass, from is always a Mailbox object, and to, cc, bcc and replyTo are always arrays of Mailbox. Attachment bytes are private copies.

capabilities must describe what your adapter honors, because step 2 trusts it. The schema also requires text or html to be true, requires text and html both true when htmlAndText is, and requires attachments when inlineAttachments is.

Failing and returning receipts

Return failures as EmailError and set acceptance truthfully. The rules in Errors and retries apply: use notSubmitted only when nothing left your process, rejected when the provider refused, and unknown otherwise.

Email.make fails a send with UnexpectedResponseError and acceptance: "unknown" if the receipt:

  • does not decode as a SendReceipt,
  • names a different provider than the adapter,
  • lists a recipient index that is out of range for its role.

A SendReceipt with status partiallyAccepted must list at least one accepted and one rejected recipient. A receipt with status accepted must list none rejected.

Invalid configuration throws

Email.make throws a TypeError synchronously if provider or capabilities is invalid. A provider name starts with a letter, digit or @ and contains only letters, digits and @ . _ / -, up to 128 characters.