SES + Lambda: a personal email forwarder that costs nothing
I bought a domain. I needed hello@my-domain.com to forward to my actual Gmail inbox. The sensible options:
- Google Workspace: $7/user/month. Real Gmail at your domain. Overkill.
- Fastmail: $5/user/month. Excellent. Still overkill for forwarding.
- Domain registrar's free forwarding (GoDaddy, Namecheap, Cloudflare): free, but the forwarding is unreliable, sometimes mangles SPF/DKIM, and Gmail flags forwarded mail as spam.
- AWS SES + a tiny Lambda: free at this volume. Reliable. Mail lands in Gmail's primary inbox.
I picked the last one. Here's the whole pattern. It took me about 30 minutes.
The shape
Sender → MX (Route 53) → SES inbound → S3 PutObject
↓
Lambda trigger
↓
SES outbound → your real inbox
Five components. One DNS record set. Twenty lines of Lambda code. The whole pipeline runs on free tier.
Step by step
1. Verify the domain in SES. AWS Console → SES → Verified identities → Create. Pick "Domain." It generates DKIM tokens and a verification token. Drop them into Route 53 as the CNAMEs and TXT record SES tells you to. SES auto-detects when DNS propagates and flips the domain to "Verified."
2. Verify your destination email (the one you want forwarded mail to land in). This time you pick "Email address" instead of "Domain." SES sends you a one-click verification link. Click it.
3. Add MX + SPF to Route 53. Two records:
- MX record on the apex:
10 inbound-smtp.us-east-1.amazonaws.com(or whatever region you're in) - TXT record on the apex:
v=spf1 include:amazonses.com ~all
4. Create an S3 bucket for inbound mail (e.g., my-domain-mail-inbound). Block all public access. Add a lifecycle rule that deletes objects after 30 days so old mail doesn't accumulate forever.
Add a bucket policy allowing the SES service principal to write to a inbox/ prefix:
{
"Effect": "Allow",
"Principal": { "Service": "ses.amazonaws.com" },
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-domain-mail-inbound/inbox/*",
"Condition": { "StringEquals": { "AWS:SourceAccount": "<your-account-id>" } }
}
5. Create the Lambda with an IAM role that can read from the bucket and call ses:SendRawEmail. The function code, in Node.js with mailparser and nodemailer:
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
import { simpleParser } from 'mailparser';
import nodemailer from 'nodemailer';
const FROM_ADDR = 'forwarder@my-domain.com';
const TO = 'me@gmail.com';
export const handler = async (event) => {
const s3 = new S3Client({});
const ses = new SESv2Client({});
for (const record of event.Records) {
const obj = await s3.send(new GetObjectCommand({
Bucket: record.s3.bucket.name,
Key: decodeURIComponent(record.s3.object.key),
}));
const raw = Buffer.concat(await obj.Body.toArray());
const parsed = await simpleParser(raw);
const transporter = nodemailer.createTransport({ streamTransport: true, buffer: true });
const info = await transporter.sendMail({
from: { name: parsed.from?.text ?? 'Forwarder', address: FROM_ADDR },
to: TO,
replyTo: parsed.from?.value?.[0]?.address,
subject: parsed.subject ?? '(no subject)',
text: parsed.text,
html: parsed.html,
attachments: parsed.attachments,
});
await ses.send(new SendEmailCommand({
FromEmailAddress: FROM_ADDR,
Destination: { ToAddresses: [TO] },
Content: { Raw: { Data: info.message } },
}));
}
return { statusCode: 200 };
};
6. Wire S3 → Lambda. In the bucket's properties, add an event notification: s3:ObjectCreated:*, prefix inbox/, target = your Lambda. Give Lambda invoke permission to S3.
7. Create a SES receipt rule set. One rule with one recipient (hello@my-domain.com) and one action: write to S3 inbox/. Activate the rule set.
8. Test it. Send an email from any address to hello@my-domain.com. Within a few seconds it lands in your real inbox, with the original sender in the Reply-To so you can reply naturally.
That's the entire setup.
Why use mailparser + nodemailer
I tried the naive version first — read the raw RFC 822 message, do header rewrites with regex, ship it back through SES. It worked, but Gmail rendered the body badly because my hand-rolled MIME boundary handling was wrong. Multipart bodies leaked their headers into the visible text.
The fix is mailparser (parses incoming) plus nodemailer (composes outgoing) — both battle-tested and exactly what you want for this. The Lambda code stays under 50 lines and Gmail renders the forwarded mail as cleanly as the original.
The cost math
- SES inbound: free for the first 1,000 emails per month, then $0.10 per 1,000.
- S3 storage: pennies. Inbound mail is small and the lifecycle rule auto-deletes after 30 days.
- Lambda: free tier is 1M invocations + 400,000 GB-seconds per month. A personal email forwarder might see a few hundred invocations a month.
- SES outbound: free for emails sent from EC2 (and Lambda counts), $0.10 per 1,000 otherwise. At personal-mail volume, free.
Total: $0/month in practice. If your email volume hits 5,000 forwarded messages a month, you'd be at maybe $0.40.
Why this beats the registrar's free forwarding
GoDaddy and Cloudflare offer "free email forwarding" that works fine for your dad sending you a forward. For anything important, two failure modes show up:
SPF/DKIM mismatch on rewrites. Forwarders that don't re-sign the outgoing mail get flagged as spam by Gmail because the From header doesn't match the SPF record of the actual sending server. SES lets you DKIM-sign outbound, which keeps you out of spam.
Reliability. Free forwarders deprioritize your traffic when their systems are under load. SES has the same SLA as the rest of AWS — i.e., you'll never notice.
For a domain you actually use professionally, the AWS pattern is worth the extra 30 minutes.
When to upgrade
If you grow into needing real email send capability for a product (transactional mail, marketing, password reset flows), use Resend instead — it's friendlier than SES outbound for app-driven email, has a great dev experience, and is what the Vibe Coder's Guide skills recommend by default.
But for personal forwarding to your inbox, SES + Lambda is the answer. Free, reliable, and you own every component.