Vibe Coder's Guide

SES + Lambda: a personal email forwarder that costs nothing

April 18, 2026 · awsemailinfrastructure

I bought a domain. I needed hello@my-domain.com to forward to my actual Gmail inbox. The sensible options:

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:

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

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:

  1. 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.

  2. 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.


← All posts