Skip to Content
ProvidersAWS SES (Simple Email Service)

AWS SES (Simple Email Service)

Category: Notification — Email
Package: @leadmetrics/provider-emailSesProvider
External SDK: @aws-sdk/client-ses


Purpose

AWS SES is the email option for tenants who are already on AWS. They use their own AWS account and SES configuration — the platform never touches their AWS credentials for anything other than sending email on their behalf. Emails arrive from their verified SES domain with full SPF/DKIM, giving excellent deliverability.

There is no platform SES default — the platform uses SendGrid. SES is tenant-only and only activates when configured and verified.


Config Structure

Tenant config (stored in notification_providers.config, encrypted)

interface SesConfig { accessKeyId: string; // AWS IAM access key — must have ses:SendRawEmail permission secretAccessKey: string; // AWS IAM secret key region: string; // AWS region where SES is set up, e.g. "ap-south-1", "us-east-1" fromAddress: string; // Must be verified in the tenant's SES account fromName: string; // Display name shown in email client }

Required IAM permissions

The IAM user/role used must have only the minimum required permissions:

{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["ses:SendRawEmail"], "Resource": "*" } ] }

SendRawEmail is used (rather than SendEmail) to support attachments as raw MIME parts.


Integration Pattern

Provider class (packages/provider-email/src/providers/ses.ts)

import { SESClient, SendRawEmailCommand } from '@aws-sdk/client-ses'; import * as nodemailer from 'nodemailer'; class SesProvider implements EmailProvider { readonly name = 'ses'; private client: SESClient; private transporter: nodemailer.Transporter; constructor(private cfg: SesConfig) { this.client = new SESClient({ region: cfg.region, credentials: { accessKeyId: cfg.accessKeyId, secretAccessKey: cfg.secretAccessKey, }, }); // Use nodemailer with SES transport for MIME construction this.transporter = nodemailer.createTransport({ SES: { ses: this.client, aws: { SendRawEmailCommand } }, }); } async send(message: EmailMessage): Promise<EmailSendResult> { const info = await this.transporter.sendMail({ to: message.to.map(r => `"${r.name}" <${r.email}>`).join(', '), from: `"${this.cfg.fromName}" <${this.cfg.fromAddress}>`, replyTo: message.replyTo ? `"${message.replyTo.name}" <${message.replyTo.email}>` : undefined, subject: message.subject, html: message.html, text: message.text, attachments: message.attachments?.map(a => ({ filename: a.filename, content: a.content, contentType: a.contentType, })), }); return { messageId: info.messageId, provider: 'ses', }; } async verify(): Promise<void> { // SES doesn't have a "ping" endpoint — send a real test email to the from-address await this.transporter.sendMail({ to: this.cfg.fromAddress, from: `"${this.cfg.fromName}" <${this.cfg.fromAddress}>`, subject: 'Leadmetrics — SES connection test', text: 'This is a test email from Leadmetrics to verify your AWS SES configuration.', }); } }

Why SendRawEmail via nodemailer-SES transport

The AWS SDK SendEmailCommand does not support attachments as multipart MIME. By routing through nodemailer with the SES transport, we get proper MIME construction with attachment boundaries, while still using the tenant’s IAM credentials via the @aws-sdk/client-ses client.


SES Sandbox vs Production

New AWS SES accounts start in sandbox mode: they can only send to verified email addresses. Tenants must request production access from AWS before using SES to send to their end users. The platform UI should surface a warning when first verifying an SES provider:

“AWS SES may be in sandbox mode. Verify that production access has been granted in your AWS console before using this for client notifications.”


Test Cases

Unit tests (packages/provider-email/src/providers/ses.test.ts)

TestApproach
send() calls nodemailer sendMail with correct MIME structureMock nodemailer.createTransport; spy on sendMail; assert fields
send() passes attachment Buffer as multipartAssert attachment content field intact
send() returns messageIdMock response includes message ID
send() throws on invalid credentials (SES AuthFailure)Mock sendMail throws AWS SDK error; assert propagated
send() throws when from-address not verified in SESMock 400 MessageRejected; assert propagated
verify() sends self-addressed test emailAssert to === fromAddress

Integration tests

TestApproach
Sends against SES in us-east-1 sandboxUse test IAM credentials; send to verified test address; check SES send statistics
Attachment arrives with correct MIME typeAssert multipart/mixed content-type in raw SES message
Error on unverified from-addressConfigure with unverified sender; assert MessageRejected error

Local development

AWS SES cannot be easily mocked locally. Options:

  1. Use SMTP in dev — configure the dev tenant with Mailpit SMTP instead of SES
  2. LocalStack — run localstack/localstack with SERVICES=ses; point baseUrl to http://localhost:4566
  3. Real SES sandbox — use a dedicated AWS dev account with a verified test domain

© 2026 Leadmetrics — Internal use only