Use SES and Lambda on AWS to forward custom domain email
I'll start by noting that this solution won't have you sending out email from your custom domain, but it will allow you to receive emails on it and forward them on to your personal account on Gmail or similar, which works for me! Since this uses Lambda you're going to be paying for minimal compute, so you're saving a big chunk of the £5 a month that Google Workspace will cost you.
All the infrastructure for this is managed with CloudFormation, but you should be able to follow along and recreate things in the AWS console if that's your bag.
Prerequisites
I've already got my custom domain set up as a hosted zone inside Route 53 and so I'm not going to cover that here.
SES
The SES side of this is pretty simple for now. Before setting up our Lambda, all we need to do is create our identity and add the MX record to Route 53.
EmailIdentity:
Type: AWS::SES::EmailIdentity
Properties:
EmailIdentity: {YOUR DOMAIN}
MXRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: {HOSTED ZONE ID}
Name: {YOUR DOMAIN}
Type: MX
TTL: 3600
ResourceRecords:
- '10 inbound-smtp.us-east-1.amazonaws.com'
Be sure to replace {YOUR DOMAIN}
with (funnily enough) your domain, and {HOSTED ZONE ID}
with the ID of your Route 53 hosted zone. The other thing to note is that there's a region in the MX ResourceRecords
field, you'll need to change this if you're not using SES in us-east-1
like me.
Once you've deployed this, you'll need to verify the identity in SES. You should see a button in SES which will update Route 53 for you with the records needed for the verification.
S3 Bucket
ProxyLambdaScriptBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: {BUCKET NAME}
ProxyLambdaScriptBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ProxyLambdaScriptBucket
PolicyDocument:
Statement:
- Action: s3:PutObject
Effect: Allow
Principal:
Service: ses.amazonaws.com
Resource: !Sub 'arn:aws:s3:::${ProxyLambdaScriptBucket}/*'
Principal: '*'
Condition:
StringEquals:
'aws:Referer': {AWS ACCOUNT ID}
What we're doing here is creating an S3 bucket and then attaching a policy to it that allows SES to write to it. Later on we'll be adding a receipt rule to SES which drops the email contents into S3, and this supports that.
Be sure to replace {BUCKET NAME}
with whatever you want the bucket to be called, and swap in your AWS account ID too.
At this point, you'll want to upload the Lambda code to S3 inside a zip file (be sure to include the node_modules
!). Here's the code
const aws = require('aws-sdk');
const simpleParser = require('mailparser').simpleParser;
exports.handler = function (event, context, callback) {
const s3 = new aws.S3();
s3.getObject({ Bucket: process.env.s3Bucketname, Key: event.Records[0].ses.mail.messageId }, async (err, data) => {
if (err) {
return callback(err);
}
const contents = data.Body.toString();
const parsedEmail = await simpleParser(contents);
const replyTo = parsedEmail.from.value.map((value) => value.address);
const subject = parsedEmail.subject;
const text = parsedEmail.text;
const html = parsedEmail.html;
const params = {
Source: ... // An email on your custom domain,
Destination: {
ToAddresses: [...] // The email you want the message to be sent on to,
},
Message: {
Subject: {
Data: subject,
},
Body: {
Html: {
Data: html,
},
Text: {
Data: text,
},
},
},
ReplyToAddresses: replyTo,
};
const ses = new aws.SES();
const sendEmailPromise = ses.sendEmail(params).promise();
sendEmailPromise.then(() => callback());
});
};
You'll want to install mailparser
to support this, which is a great little library that handles parsing MIME stuff. The high level summary of this script is that it grabs an object from S3 which contains the email contents (annoyingly SES doesn't pass them on inside the event which is why we have S3 inbetween), then parses it and forwards it on to whatever email you want.
The two things you'll need to add to the above are an email on your custom domain to send the email from (I went for proxy@rhysc.me
), and your personal email you'd like the message to be forwarded to.
Once you've uploaded that to your new S3 bucket, we can get on with adding the Lambda.
Lambda
ProxyLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: email-proxy-s3
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource:
- !Sub "arn:aws:s3:::${ProxyLambdaScriptBucket}/*"
- Effect: Allow
Action:
- ses:SendEmail
Resource:
- !Sub 'arn:aws:ses:us-east-1:{AWS ACCOUNT ID}:identity/${EmailIdentity}'
ProxyLambda:
Type: AWS::Lambda::Function
Properties:
Role: !GetAtt ProxyLambdaRole.Arn
FunctionName: EmailProxy
Runtime: nodejs14.x
Handler: index.handler
Environment:
Variables:
s3Bucketname: !Ref ProxyLambdaScriptBucket
Code:
S3Bucket: !Ref ProxyLambdaScriptBucket
S3Key: {ZIP FILE NAME}
ProxyLambdaSESPermission:
Type: AWS::Lambda::Permission
Properties:
Action: 'lambda:InvokeFunction'
FunctionName: !GetAtt ProxyLambda.Arn
Principal: ses.amazonaws.com
SourceAccount: {AWS ACCOUNT ID}
There is a pretty verbose definition for an IAM role for the Lambda above. Basically it gives the Lambda the basic execution role, which is an AWS managed policy that gives access to CloudWatch logs. We then give it access to get objects from our new S3 bucket, and to send emails from our SES identity. Be sure to swap in your account ID, and change the region if you're not in us-east-1
.
We then add the definition for our Lambda function itself, be sure to change the S3Key
to whatever you called your zip file, for example email-proxy.zip
.
Finally, we give SES permission to invoke the Lambda script.
SES part 2
We're very nearly there! All that remains now is to wire up SES to call the Lambda.
EmailReceiptRuleSet:
Type: AWS::SES::ReceiptRuleSet
Properties:
RuleSetName: proxy-ruleset
EmailReceiptRule:
Type: AWS::SES::ReceiptRule
Properties:
RuleSetName: !Ref EmailReceiptRuleSet
Rule:
Enabled: True
Name: ForwardProxy
Recipients:
- {YOUR CUSTOM EMAIL}
Actions:
- S3Action:
BucketName: !Ref ProxyLambdaScriptBucket
- LambdaAction:
FunctionArn: !GetAtt ProxyLambda.Arn
This creates a receipt rule set and then adds a single rule to it. The rule drops the email contents in S3 and then runs our Lambda. Replace {YOUR CUSTOM EMAIL}
with whatever you want, (mine is hi@rhysc.me
!). The receipt rule will drop any emails you're sent into our S3 bucket, and then run the Lambda function.
You'll need to set the new receipt rule as active in the SES console.
Wrapping up
You should be able to do this while your account is still in the SES sandbox, provided you've verified your personal email in SES.