Your own SES client

Send through an SESv2Client your app already has, with sesMailer.

sesMailer sends with SES v2 SendEmail through a client you already have, so the mailer uses the same credentials, region and middleware as the rest of your app.

It is for apps that already own an SESv2Client. If you’d let WrapsEmail build the client, or your dev sink reads the SES v1 / WrapsEmail shape, use wrapsMailer: sesMailer gains you nothing there.

terminal
npm install @byo-oss/mailer @aws-sdk/client-sesv2

@aws-sdk/client-sesv2 is an optional peer dependency, ^3.527.0: the first version whose SendEmail types include custom Headers. It is loaded on the first send, so apps that use another adapter never need it.

lib/mailer.ts
import { SESv2Client } from "@aws-sdk/client-sesv2";
import { sesMailer } from "@byo-oss/mailer/ses";

export const mailer = sesMailer({
  from: "Acme Support <help@acme.com>",
  client: new SESv2Client({ region: "us-east-1" }),
});

If what you have is an SES v1 SESClient, use wrapsMailer with client: new WrapsEmail({ client: sesClient }) instead.

Options

Option Default
from required Sender address, verified in SES: "Acme <help@acme.com>".
client required Your new SESv2Client({ ... }), or any { send(command), config? } with the same shape (a dev sink, a fake in tests). Its region (config.region) is read after each send, for messageId.

Errors

SendEmail throws Result
BadRequestException, or MessageRejected for the message itself. PermanentMailError
MessageRejected for an address or domain that isn’t verified, or a sandboxed account. Rethrown, so it retries once you fix the account
Anything else: throttling, LimitExceededException, SendingPausedException, AccountSuspendedException, MailFromDomainNotVerifiedException, NotFoundException, network errors, missing credentials. Rethrown, so it retries

Headers SES owns (From, To, Subject, Reply-To, Message-ID, Date, …) are rejected with a PermanentMailError, and so are malformed headers. Use replyTo for the reply-to address. A non-ASCII display name in from is sent RFC 2047 encoded. See Delivery semantics.

Result

sesMailer returns { id, messageId }. id is the SES message id. messageId is <id@email.amazonses.com> when the client’s region is us-east-1, the one region checked with real sends (see Wraps for why).

messageId is left out in other regions, with a custom endpoint, and for a client without a readable config.region (most dev sinks). If SES returns no MessageId, id is "ses:<uuid>" rather than a throw, which would send the message again.

It is also left out when AWS_ENDPOINT_URL or AWS_ENDPOINT_URL_SESV2 is set: the AWS SDK sends there, and the client’s config doesn’t show it. An endpoint set only in an AWS profile isn’t read.

Setting AWS_IGNORE_CONFIGURED_ENDPOINT_URLS to exactly true makes the AWS SDK ignore AWS_ENDPOINT_URL and AWS_ENDPOINT_URL_SESV2, so those variables no longer leave messageId out. The AWS SDK for JavaScript reads this variable only from @smithy/core 3.29.6 (July 2026) on. An older SDK still sends to the endpoint variables, and the mailer would then return a messageId SES never wrote, so update the SDK before you set it. Any other value, such as 1 or TRUE, doesn’t count, and is ignored without an error. A custom endpoint on the client still leaves it out, and so does the client’s own ignoreConfiguredEndpointUrls option, which isn’t read. A profile’s ignore_configured_endpoint_urls isn’t read, so an endpoint variable still leaves it out in that case.

Confirmed with a real SES v2 simple send through sesMailer in us-east-1: the delivered Message-ID was the messageId it returned, and In-Reply-To and References arrived unchanged.

Edit this page on GitHub ↗@byo-oss/mailer 0.2.0