AWS SES (Email)

SES v2 used for outbound email and domain identity management. Event notifications arrive via SNS.

Client

src/lib/providers/email.ts:

import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
const ses = new SESv2Client({
  region: process.env.AWS_REGION || "us-east-1",
  credentials: { accessKeyId: ..., secretAccessKey: ... },
});

Same client singleton reused in src/lib/providers/domain.ts for identity management.

Sending

sendEmail() issues SendEmailCommand with:

FromEmailAddress: params.from,
Destination: { ToAddresses: params.to },
Content: { Simple: { Subject, Body: { Html?, Text? } } }

Returns { providerMessageId, providerResponse } with MessageId and SDK requestId.

Domain identity

src/lib/providers/domain.ts:

Function SES command Purpose
createDomainIdentity(domain) CreateEmailIdentityCommand Returns DKIM tokens
checkDomainStatus(domain) GetEmailIdentityCommand Reads VerifiedForSendingStatus + DKIM status
deleteDomainIdentity(domain) DeleteEmailIdentityCommand Removes identity

Inbound event notifications

SES publishes events (Delivery, Bounce, Complaint, Reject) to a configured SNS topic. Configure that SNS topic to HTTP-subscribe to https://<host>/api/webhooks/ses.

Handler: src/app/api/webhooks/ses/route.ts.

Flow:

  1. SNS sends SubscriptionConfirmation — handler fetches SubscribeURL to confirm.
  2. SNS sends Notification with Message = JSON-stringified SES event.
  3. Extract mail.messageId — match to messages.provider_message_id.
  4. Update status accordingly, fan out to customer webhooks.

Setup (AWS side)

Outside this repo:

  1. Create SES configuration set with event destination → SNS.
  2. Configure SNS topic with HTTPS subscription to /api/webhooks/ses.
  3. Move SES account out of sandbox for production (AWS request).

Security gap

The SES webhook route does not verify SNS signatures — an attacker posting crafted JSON could mutate message statuses. Before production, implement SNSMessageValidator (see AWS docs).