Skip to content

Commit

Permalink
Code formatting + add enableRuleSet property + documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
seeebiii committed Jan 16, 2021
1 parent 2897c24 commit cdec7ac
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 70 deletions.
89 changes: 81 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,87 @@
# @seeebiii/ses-email-forwarding

This AWS CDK construct allows you to setup email forwarding mappings to receive emails from your domain and forward them to another email address.
This [AWS CDK](https://aws.amazon.com/cdk/) construct allows you to setup email forwarding mappings in [AWS SES](https://aws.amazon.com/ses/) to receive emails from your domain and forward them to another email address.
All of this is possible without hosting your own email server, you just need a domain.

For example, if you own a domain `example.org` and want to receive emails for `hello@example.org` and `privacy@example.org`, you can forward emails to `example@gmail.com`.
For example, if you own a domain `example.org` and want to receive emails for `hello@example.org` and `privacy@example.org`, you can forward emails to `whatever@provider.com`.
This is achieved by using a Lambda function that forwards the emails using [aws-lambda-ses-forwarder](https://github.com/arithmetric/aws-lambda-ses-forwarder).

This construct is creating quite a few resources under the hood and can also automatically verify your domain and email addresses in SES.
Consider reading the [Architecture](#architecture) section below if you want to know more about the details.

## Examples

Forward all emails received under `hello@example.org` to `whatever+hello@provider.com`:

```javascript
new EmailForwardingRuleSet(this, 'EmailForwardingRuleSet', {
// make the underlying rule set the active one
enableRuleSet: true,
// define how emails are being forwarded
emailForwardingProps: [{
// your domain name you want to use for receiving and sending emails
domainName: 'example.org',
// a prefix that is used for the From email address to forward your mails
fromPrefix: 'noreply',
// a list of mappings between a prefix and target email address
emailMappings: [{
// the prefix matching the receiver address as <prefix>@<domainName>
receivePrefix: 'hello',
targetEmails: ['jane+hello-example@gmail.com']
// the target email address(es) that you want to forward emails to
targetEmails: ['whatever+hello@provider.com']
}]
}]
});
```

Forward all emails to `hello@example.org` to `whatever+hello@provider.com` and verify the domain `example.org` in SES:

```javascript
new EmailForwardingRuleSet(this, 'EmailForwardingRuleSet', {
emailForwardingProps: [{
domainName: 'example.org',
// let the construct automatically verify your domain
verifyDomain: true,
fromPrefix: 'noreply',
emailMappings: [{
receivePrefix: 'hello',
targetEmails: ['whatever+hello@provider.com']
}]
}]
});
```

If you don't want to verify your domain in SES or you are in the SES sandbox, you can still send emails to verified email addresses.
Use the property `verifyTargetEmailAddresses` in this case and set it to `true`.

For a full & up-to-date reference of the available options, please look at the source code of [`EmailForwardingRuleSet`](lib/email-forwarding-rule-set.ts) and [`EmailForwardingRule`](lib/email-forwarding-rule.ts).

#### Note

Since the verification of domains requires to lookup the Route53 domains in your account, you need to define your AWS account and region.
You can do it like this in your CDK stack:

```javascript
const app = new cdk.App();

class EmailForwardingSetupStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

new EmailForwardingRuleSet(this, 'EmailForwardingRuleSet', {
// define your config here
});
}
}

new EmailForwardingSetupStack(app, 'EmailForwardingSetupStack', {
env: {
account: '<account-id>',
region: '<region>'
}
});
```

## Use Cases

- Build a landing page on AWS and offer an email address to contact you.
Expand All @@ -38,25 +97,39 @@ At the moment only TypeScript/JavaScript is supported.

Install it as a dev dependency in your project:

```
```shell script
npm i -D @seeebiii/ses-email-forwarding
```

Take a look at [package.json](./package.json) to make sure you're installing the correct version compatible with your current AWS CDK version.

## Usage

TODO
This package provides two constructs: [`EmailForwardingRuleSet`](lib/email-forwarding-rule-set.ts) and [`EmailForwardingRule`](lib/email-forwarding-rule.ts).
The `EmailForwardingRuleSet` is a wrapper around `ReceiptRuleSet` but adds a bit more magic to e.g. verify a domain or target email address.
Similarly, `EmailForwardingRule` is a wrapper around `ReceiptRule` but adds two SES rule actions to forward the email addresses appropriately.

This means if you want the full flexibility, you can use the `EmailForwardingRule` construct in your stack.

## Architecture

TODO
The `EmailForwardingRuleSet` creates a `EmailForwardingRule` for each forward mapping.
Each rule contains an `S3Action` to store the incoming emails and a Lambda Function to forward the emails to the target email addresses.
The Lambda function is just a thin wrapper around the [aws-lambda-ses-forwarder](https://github.com/arithmetric/aws-lambda-ses-forwarder) library.
Since this library expects a JSON config with the email mappings, the `EmailForwardingRule` will create an SSM parameter to store the config.
(Note: this is not ideal because an SSM parameter is limited in the size and hence, this might be changed later)
The Lambda function receives a reference to this parameter as an environment variable (and a bit more) and forwards everything to the library.

In order to verify a domain or email address, the `EmailForwardingRuleSet` construct is using the package [@seeebiii/ses-verify-identities](https://www.npmjs.com/package/@seeebiii/ses-verify-identities).
It provides constructs to verify the SES identities.
For domains, it creates appropriate Route53 records like MX, TXT and Cname (for DKIM).
For email addresses, it calls the AWS API to initiate email address verification.

## TODO

- Encrypt email files on S3 bucket by either using S3 bucket encryption (server side) or enable
- Encrypt email files on S3 bucket by either using S3 bucket encryption (server side) or enable client encryption using SES actions
- Write tests
- Extend this README
- Document options/JSDoc in Readme or separate HTML

## Contributing

Expand Down
20 changes: 11 additions & 9 deletions esbuild.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
require('esbuild').build({
entryPoints: ['lambda/index.ts'],
bundle: true,
outdir: './build',
target: 'node12',
platform: 'node',
external: ['aws-sdk'],
minify: true
}).catch(() => process.exit(1));
require('esbuild')
.build({
entryPoints: ['lambda/index.ts'],
bundle: true,
outdir: './build',
target: 'node12',
platform: 'node',
external: ['aws-sdk'],
minify: true
})
.catch(() => process.exit(1));
78 changes: 50 additions & 28 deletions lib/email-forwarding-rule-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export interface EmailForwardingProps {
*/
domainName: string;
/**
* Optional: true if you want to verify the domain identity in SES, false otherwise. Default: `true`.
* Optional: true if you want to verify the domain identity in SES, false otherwise.
*
* @default `false`
*/
verifyDomain?: boolean;
/**
Expand All @@ -27,26 +29,36 @@ export interface EmailForwardingProps {
*/
emailMappings: EmailMapping[];
/**
* Optional: true if you want to initiate the verification of your target email addresses, false otherwise. Default: `true`.
* Optional: true if you want to initiate the verification of your target email addresses, false otherwise.
*
* If `true`, a verification email is sent out to all target email addresses. Then, the owner of an email address needs to verify it by clicking the link in the verification email.
* Please note that it's required to verify your email addresses in order to send mails to them.
* Please note in case you don't verify your sender domain, it's required to verify your target email addresses in order to send mails to them.
*
* @default `false`
*/
verifyTargetEmailAddresses?: boolean;
/**
* Optional: an S3 bucket to store the received emails. If none is provided, a new one will be created.
*
* @default A new bucket.
*/
bucket?: Bucket;
/**
* Optional: a prefix for the email files that are stored on the S3 bucket. Default: `inbox/`.
* Optional: a prefix for the email files that are stored on the S3 bucket.
*
* @default `inbox/`
*/
bucketPrefix?: string;
/**
* Optional: an SNS topic to receive notifications about sending events like bounces or complaints. The events are defined by `notificationTypes` using {@link NotificationType}. If no topic is defined, a new one will be created.
*
* @default A new SNS topic.
*/
notificationTopic?: Topic;
/**
* Optional: a list of {@link NotificationType}s to define which sending events should be subscribed. Default: `['Bounce', 'Complaint']`.
* Optional: a list of {@link NotificationType}s to define which sending events should be subscribed.
*
* @default `['Bounce', 'Complaint']`
*/
notificationTypes?: NotificationType[];
}
Expand All @@ -57,9 +69,17 @@ export interface EmailForwardingRuleSetProps {
*/
ruleSet?: ReceiptRuleSet;
/**
* Optional: provide a name for the receipt rule set that this construct creates if you don't provide one. Default: 'custom-rule-set'.
* Optional: provide a name for the receipt rule set that this construct creates if you don't provide one.
*
* @default `custom-rule-set`
*/
ruleSetName?: string;
/**
* Optional: whether to enable the rule set or not.
*
* @default `true`
*/
enableRuleSet?: boolean;
/**
* A list of mapping options to define how emails should be forwarded.
*/
Expand Down Expand Up @@ -87,7 +107,7 @@ export class EmailForwardingRuleSet extends Construct {

this.ruleSet = this.createRuleSetOrUseExisting(props);
this.setupEmailForwardingMappings(props, this.ruleSet);
this.enableRuleSet(this.ruleSet);
this.enableRuleSet(props, this.ruleSet);
}

private createRuleSetOrUseExisting(props: EmailForwardingRuleSetProps) {
Expand Down Expand Up @@ -152,28 +172,30 @@ export class EmailForwardingRuleSet extends Construct {
}
}

private enableRuleSet(ruleSet: ReceiptRuleSet) {
const enableRuleSet = new AwsCustomResource(this, 'EnableRuleSet', {
logRetention: RetentionDays.ONE_DAY,
installLatestAwsSdk: false,
onCreate: {
service: 'SES',
action: 'setActiveReceiptRuleSet',
parameters: {
RuleSetName: ruleSet.receiptRuleSetName
private enableRuleSet(props: EmailForwardingRuleSetProps, ruleSet: ReceiptRuleSet) {
if (props.enableRuleSet === undefined || props.enableRuleSet) {
const enableRuleSet = new AwsCustomResource(this, 'EnableRuleSet', {
logRetention: RetentionDays.ONE_DAY,
installLatestAwsSdk: false,
onCreate: {
service: 'SES',
action: 'setActiveReceiptRuleSet',
parameters: {
RuleSetName: ruleSet.receiptRuleSetName
},
physicalResourceId: PhysicalResourceId.of('enable-rule-set-on-create')
},
physicalResourceId: PhysicalResourceId.of('enable-rule-set-on-create')
},
onDelete: {
service: 'SES',
action: 'setActiveReceiptRuleSet',
// providing no parameters (especially no RuleSetName) means we're disabling the currently active rule set
parameters: {},
physicalResourceId: PhysicalResourceId.of('enable-rule-set-on-delete')
},
policy: generateSesPolicyForCustomResource('SetActiveReceiptRuleSet')
});
onDelete: {
service: 'SES',
action: 'setActiveReceiptRuleSet',
// providing no parameters (especially no RuleSetName) means we're disabling the currently active rule set
parameters: {},
physicalResourceId: PhysicalResourceId.of('enable-rule-set-on-delete')
},
policy: generateSesPolicyForCustomResource('SetActiveReceiptRuleSet')
});

enableRuleSet.node.addDependency(ruleSet);
enableRuleSet.node.addDependency(ruleSet);
}
}
}
35 changes: 10 additions & 25 deletions lib/email-forwarding-rule.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import * as path from 'path';
import * as fs from 'fs';
import { Construct, Duration, RemovalPolicy } from '@aws-cdk/core';
import { ReceiptRule, ReceiptRuleSet, TlsPolicy } from '@aws-cdk/aws-ses';
import { Bucket, BucketEncryption } from '@aws-cdk/aws-s3';
import { Bucket } from '@aws-cdk/aws-s3';
import * as actions from '@aws-cdk/aws-ses-actions';
import { LambdaInvocationType } from '@aws-cdk/aws-ses-actions';
import { AssetCode, Function, Runtime } from '@aws-cdk/aws-lambda';
import { Code, Function, Runtime } from '@aws-cdk/aws-lambda';
import { StringParameter } from '@aws-cdk/aws-ssm';
import { PolicyStatement } from '@aws-cdk/aws-iam';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';

export interface EmailMapping {
/**
Expand Down Expand Up @@ -48,7 +46,7 @@ export interface EmailForwardingRuleProps {
*/
domainName: string;
/**
* A prefix that is used as the sender address of the forwarded mail, e.g. 'noreply'.
* A prefix that is used as the sender address of the forwarded mail, e.g. `noreply`.
*/
fromPrefix: string;
/**
Expand All @@ -61,7 +59,9 @@ export interface EmailForwardingRuleProps {
*/
bucket?: Bucket;
/**
* A prefix for the email files that are saved to the bucket. Default: 'inbox'.
* A prefix for the email files that are saved to the bucket.
*
* @default `inbox/`
*/
bucketPrefix?: string;
}
Expand Down Expand Up @@ -181,23 +181,10 @@ export class EmailForwardingRule extends Construct {
bucket: Bucket,
bucketPrefix: string
) {
// const forwarderFunction = new Function(this, 'EmailForwardingFunction', {
// runtime: Runtime.NODEJS_12_X,
// handler: 'index.handler',
// code: AssetCode.fromAsset(`${path.resolve(__dirname)}/../build`),
// timeout: Duration.seconds(30),
// environment: {
// ENABLE_LOGGING: 'true',
// EMAIL_MAPPING_SSM_KEY: forwardMappingParameter.parameterName,
// FROM_EMAIL: (props.fromPrefix ?? 'noreply') + '@' + props.domainName,
// BUCKET_NAME: bucket.bucketName,
// BUCKET_PREFIX: bucketPrefix
// }
// });

const entryBasePath = `${path.resolve(__dirname)}/../build/index`;
const forwarderFunction2 = new NodejsFunction(this, 'EmailForwardingFunction2', {
entry: entryBasePath + (fs.existsSync(entryBasePath + '.ts') ? '.ts' : '.js'),
return new Function(this, 'EmailForwardingFunction', {
runtime: Runtime.NODEJS_12_X,
handler: 'index.handler',
code: Code.fromAsset(`${path.resolve(__dirname)}/../build`),
timeout: Duration.seconds(30),
environment: {
ENABLE_LOGGING: 'true',
Expand All @@ -207,8 +194,6 @@ export class EmailForwardingRule extends Construct {
BUCKET_PREFIX: bucketPrefix
}
});

return forwarderFunction2;
}
}

Expand Down

0 comments on commit cdec7ac

Please sign in to comment.