SES

Lightweight, dependency-free fake of AWS SES (Simple Email Service, the classic v1 API) that speaks the real SES AWS Query wire protocol (form-encoded requests, XML responses, API version 2010-12-01), so application code using @aws-sdk/client-ses can run against it with zero cost and zero side effects.

KeyValue
Port4570
ProtocolAWS Query (application/x-www-form-urlencoded request, XML response) over HTTP
API version2010-12-01
Compatible client@aws-sdk/client-ses (v3)
Size~80 KB
Startup< 100ms
StateIn-memory, ephemeral, resettable

Quick Start

Start the server:

import { SesServer } from "./services/ses/src/server.js";

const server = new SesServer(4570);
await server.start();
// ... use it ...
await server.stop();

Connect with the real AWS SDK client:

import {
  SESClient,
  VerifyEmailIdentityCommand,
  SendEmailCommand,
} from "@aws-sdk/client-ses";

const ses = new SESClient({
  region: "us-east-1",
  endpoint: "http://127.0.0.1:4570",
  credentials: { accessKeyId: "parlel", secretAccessKey: "parlel" },
});

// Verify a sender (auto-verified in the fake)
await ses.send(new VerifyEmailIdentityCommand({ EmailAddress: "sender@example.com" }));

// Send an email
const { MessageId } = await ses.send(
  new SendEmailCommand({
    Source: "sender@example.com",
    Destination: { ToAddresses: ["dest@example.com"] },
    Message: {
      Subject: { Data: "Hello" },
      Body: { Text: { Data: "World" } },
    },
  }),
);

Implemented operations

All 71 operations exposed by @aws-sdk/client-ses are implemented.

Identities & verification

Identity policies

Sending

Account & statistics

Email templates

Custom verification email templates

Configuration sets

Configuration set event destinations

Configuration set tracking options

Receipt rule sets & rules

Receipt filters

Internal (non-SES) endpoints

MethodPathPurpose
GET/_parlel/healthHealth probe — returns { status, service, identities, templates, configurationSets }
POST/_parlel/resetClears all in-memory state
GET/_parlel/sentReturns captured sent messages (for test assertions)

server.reset() resets all state in-process as well.

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureSupportedNotes
Full SES v1 API surface (71 operations)Every @aws-sdk/client-ses command
AWS Query protocol (form request / XML response)API version 2010-12-01
Identity verificationEmails & domains auto-verify; DKIM tokens are deterministic
Identity policies, MAIL FROM, notificationsStored in-memory per identity
Email / raw / templated / bulk / custom verification sendingSender verification enforced
Template rendering ({{placeholder}})Basic mustache-style substitution
Configuration sets, event destinations, tracking, delivery optionsStored & described
Receipt rule sets, rules, filtersFull CRUD + ordering + active set
Send quota / statisticsQuota fixed at 200/day, rate 1/s; counter tracks sends
Real email delivery⟳ Roadmap
Real DKIM/SPF/DMARC validation✓ By design — Intentional for a local, zero-cost test emulator
Actual event/notification publishing to SNS/Firehose/CloudWatch✓ By design — Intentional for a local, zero-cost test emulator
Sandbox/production-access enforcement⟳ Roadmap
IAM / SigV4 signature verification✓ By design — Structurally faithful tokens; cryptographic verification is skipped for local use
SESv2 API (@aws-sdk/client-sesv2)⟳ Roadmap

Error codes & shapes

Errors are returned as standard AWS Query XML with a non-2xx status:

<?xml version="1.0"?>
<ErrorResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
  <Error>
    <Type>Sender</Type>
    <Code>MessageRejected</Code>
    <Message>Email address is not verified. ...</Message>
  </Error>
  <RequestId>...</RequestId>
</ErrorResponse>

Type is Sender for 4xx (client fault) and Receiver for 5xx (server fault). Common codes:

CodeStatusWhen
InvalidParameterValue400Bad / missing parameters, invalid email
MessageRejected400Sending from an unverified identity
AccountSendingPausedException400Account sending disabled
TemplateDoesNotExistException400Template not found
AlreadyExistsException400Duplicate template / rule / filter / rule set
ConfigurationSetDoesNotExistException400Config set not found
ConfigurationSetAlreadyExistsException400Duplicate config set
EventDestinationAlreadyExistsException400Duplicate event destination
EventDestinationDoesNotExistException400Event destination not found
TrackingOptionsAlreadyExistsException400Tracking options already set
TrackingOptionsDoesNotExistException400Tracking options not set
CustomVerificationEmailTemplateAlreadyExistsException400Duplicate custom template
CustomVerificationEmailTemplateDoesNotExistException400Custom template not found
RuleSetDoesNotExistException400Receipt rule set not found
RuleDoesNotExistException400Receipt rule not found
CannotDeleteException400Deleting the active receipt rule set
MissingRenderingAttributeException400TestRenderTemplate data missing a placeholder
InvalidAction400Unknown Action
InternalError500Unexpected server fault
<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

AWS_ACCESS_KEY_ID=parlel
AWS_SECRET_ACCESS_KEY=parlel
AWS_REGION=us-east-1
AWS_ENDPOINT_URL_SES=http://parlel-bridge:4570
AWS_ENDPOINT_URL=http://parlel-bridge:4570
<!-- parlel:testenv:end -->