Skip to main content
The @ooneex/mailer component sends transactional emails. The built-in mailer renders a React node to HTML server-side so your email bodies are plain components. Every sender implements the same IMailer interface — a single send method — so callers stay identical no matter how mail is delivered. A MailerLayout component gives you a styled header/body/footer shell to build templates on.

Why this component

  • One interface, container-managed. Every mailer implements IMailer (send) and is registered with a decorator, so you resolve and inject it like any other service.
  • React templates. Email bodies are React nodes rendered to HTML with renderToString — compose them like UI, not string concatenation.
  • Layout system. MailerLayout ships with Header, Body, and Footer subcomponents and sensible email-safe defaults.
  • Environment configuration. Credentials and the default sender are read from environment variables; per-send overrides are supported.

How it works

You resolve a mailer from the container and call send. The mailer renders the content React node to HTML, resolves the sender (from the call or the environment), and delivers the message.
MemberPurpose
send(config)Render content and send the email. Resolves to void.
The config object passed to send:
FieldTypePurpose
tostring[]Recipient email addresses.
subjectstringEmail subject line.
contentReact.ReactNodeBody rendered to HTML server-side.
from{ name: string; address: string }Optional sender override; falls back to the environment defaults.
The sender is resolved per call: config.from wins, otherwise MAILER_SENDER_NAME and MAILER_SENDER_ADDRESS are used. If neither yields a name and address, send throws.

Environment variables

VariableRequiredPurpose
MAILER_SENDER_NAMENoDefault sender name. Required at send time unless from.name is provided.
MAILER_SENDER_ADDRESSNoDefault sender address. Required at send time unless from.address is provided.
MAILER_SENDER_NAME=Acme
MAILER_SENDER_ADDRESS=hello@acme.com

Decorator and usage

@decorator.mailer()

Registers a mailer class with the container. It accepts an optional scope (defaults to singleton). The built-in mailer is already decorated under the mailer token; use the decorator on your own mailers so they can be resolved and injected. Resolve the mailer from the container and send an email — the body is just a React node:
import { container } from "@ooneex/container";
import { MailerLayout } from "@ooneex/mailer";
import type { IMailer } from "@ooneex/mailer";

const mailer = container.get<IMailer>("mailer");

await mailer.send({
  to: ["user@example.com"],
  subject: "Welcome to Acme!",
  content: (
    <MailerLayout>
      <MailerLayout.Header />
      <MailerLayout.Body>
        <h1>Welcome aboard</h1>
        <p>Thanks for signing up.</p>
      </MailerLayout.Body>
      <MailerLayout.Footer />
    </MailerLayout>
  ),
});
Override the sender per call when a message should come from a specific address:
await mailer.send({
  to: ["user@example.com"],
  subject: "Important update",
  content: <UpdateEmail />,
  from: { name: "Support Team", address: "support@acme.com" },
});
A typical mailer wraps a template and delegates to the injected IMailer. Register it with the decorator and inject the underlying mailer under the mailer token:
import { inject } from "@ooneex/container";
import { decorator } from "@ooneex/mailer";
import type { IMailer } from "@ooneex/mailer";
import { type WelcomeMailerPropsType, WelcomeMailerTemplate } from "./WelcomeMailerTemplate";

@decorator.mailer()
export class WelcomeMailer implements IMailer {
  constructor(
    @inject("mailer")
    private readonly mailer: IMailer,
  ) {}

  public async send(config: {
    to: string[];
    subject: string;
    from?: { name: string; address: string };
    data?: WelcomeMailerPropsType;
  }): Promise<void> {
    const { data, ...rest } = config;

    await this.mailer.send({
      ...rest,
      content: WelcomeMailerTemplate(data),
    });
  }
}

Exceptions

The component throws MailerException when it is misconfigured or a send cannot complete. It carries a machine-readable key, a human-readable message, and a data object.
KeyWhen
EMAIL_SEND_FAILEDsend is called with no resolvable sender name (no from.name and no MAILER_SENDER_NAME).
EMAIL_OPERATION_FAILEDsend is called with no resolvable sender address (no from.address and no MAILER_SENDER_ADDRESS).
import { MailerException } from "@ooneex/mailer";
import { container } from "@ooneex/container";
import type { IMailer } from "@ooneex/mailer";

const mailer = container.get<IMailer>("mailer");

try {
  await mailer.send({ to: ["user@example.com"], subject: "Hi", content: <Email /> });
} catch (error) {
  if (error instanceof MailerException) {
    logger.error(`Mailer error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • Set the default sender in the environment. Configure MAILER_SENDER_NAME and MAILER_SENDER_ADDRESS once so every send has a fallback, and override from only when a message needs a different address.
  • Build bodies on MailerLayout. Compose with Header, Body, and Footer for email-safe, consistent markup instead of raw HTML strings.
  • Keep templates as props-driven functions. Pass data in, return a React node — easy to render in tests without sending anything.
  • Wrap a template per email type. A dedicated mailer (e.g. WelcomeMailer) that delegates to the injected IMailer keeps subjects, templates, and props in one place.
  • Catch MailerException at send sites. Inspect error.key to distinguish configuration problems from delivery problems, and never leak credentials.
  • Resolve through the container. Inject mailers rather than constructing them so the credentials and defaults are wired consistently.

CLI command

Scaffold a mailer, its JSX template, and their tests with the generator. It writes the class under modules/<module>/src/mailers/<Name>Mailer.ts, the template alongside it, and installs @ooneex/mailer if it is missing.
# Interactive: prompts for the name
ooneex mailer:create

# Provide the name
ooneex mailer:create --name=Welcome

# Target a module and overwrite
ooneex mailer:create --name=Welcome --module=auth --override
OptionDescriptionDefault
--nameMailer class name. The Mailer suffix is appended automatically.Prompted if omitted
--moduleTarget module the files are generated into.shared
--overrideOverwrite an existing mailer without prompting.false
The generated mailer wraps its template and delegates to the injected IMailer, ready for you to refine the subject and props:
import { inject } from "@ooneex/container";
import { decorator } from "@ooneex/mailer";
import type { IMailer } from "@ooneex/mailer";
import { type WelcomeMailerPropsType, WelcomeMailerTemplate } from "./WelcomeMailerTemplate";

@decorator.mailer()
export class WelcomeMailer implements IMailer {
  constructor(
    @inject("mailer")
    private readonly mailer: IMailer,
  ) {}

  public async send(config: {
    to: string[];
    subject: string;
    from?: { name: string; address: string };
    data?: WelcomeMailerPropsType;
  }): Promise<void> {
    const { data, ...rest } = config;

    await this.mailer.send({
      ...rest,
      content: WelcomeMailerTemplate(data),
    });
  }
}
The companion template is a props-driven function built on MailerLayout:
import { MailerLayout } from "@ooneex/mailer";

export type WelcomeMailerPropsType = {
  link: string;
};

export const WelcomeMailerTemplate = (props?: WelcomeMailerPropsType) => (
  <MailerLayout>
    <MailerLayout.Header />
    <MailerLayout.Body>
      <a href={props?.link}>Login</a>
    </MailerLayout.Body>
    <MailerLayout.Footer />
  </MailerLayout>
);
See mailer:create for the full command reference.

Use with Claude and Codex

The generator ships a matching mailer:create skill. It runs the scaffold and then guides your AI agent through completing the mailer — refining the send config, filling in the template props, and wiring up its tests. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a mailer that sends a welcome email after sign-up.
For example, the prompt above maps to mailer:create --name=Welcome, then builds the welcome template and completes the send method.