From 23e6b04f095376e01e4e98f24cfe30f70d1177d6 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 6 Mar 2019 19:07:35 +0100 Subject: [PATCH 1/7] feat(ses): add constructs for email receiving --- packages/@aws-cdk/aws-ses/README.md | 121 +++++ packages/@aws-cdk/aws-ses/lib/index.ts | 5 + .../@aws-cdk/aws-ses/lib/receipt-filter.ts | 82 ++++ .../aws-ses/lib/receipt-rule-action.ts | 421 +++++++++++++++++ .../@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 139 ++++++ packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 215 +++++++++ packages/@aws-cdk/aws-ses/package.json | 13 +- .../aws-ses/test/integ.receipt.expected.json | 399 ++++++++++++++++ .../@aws-cdk/aws-ses/test/integ.receipt.ts | 76 +++ .../aws-ses/test/test.receipt-filter.ts | 94 ++++ .../aws-ses/test/test.receipt-rule-action.ts | 431 ++++++++++++++++++ .../aws-ses/test/test.receipt-rule-set.ts | 119 +++++ .../aws-ses/test/test.receipt-rule.ts | 153 +++++++ packages/@aws-cdk/aws-ses/test/test.ses.ts | 8 - 14 files changed, 2267 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-ses/lib/receipt-filter.ts create mode 100644 packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts create mode 100644 packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts create mode 100644 packages/@aws-cdk/aws-ses/lib/receipt-rule.ts create mode 100644 packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json create mode 100644 packages/@aws-cdk/aws-ses/test/integ.receipt.ts create mode 100644 packages/@aws-cdk/aws-ses/test/test.receipt-filter.ts create mode 100644 packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts create mode 100644 packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts create mode 100644 packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts delete mode 100644 packages/@aws-cdk/aws-ses/test/test.ses.ts diff --git a/packages/@aws-cdk/aws-ses/README.md b/packages/@aws-cdk/aws-ses/README.md index 77cf9101c97ba..67c6000cf4cce 100644 --- a/packages/@aws-cdk/aws-ses/README.md +++ b/packages/@aws-cdk/aws-ses/README.md @@ -1,2 +1,123 @@ ## The CDK Construct Library for AWS Simple Email Service This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. + +### Email receiving +Create a receipt rule set with rules and actions: +```ts +const bucket = new s3.Bucket(this, 'Bucket'); + +const topic = new sns.Topic(this, 'Topic'); + +const ruleSet = new ses.ReceiptRuleSet(this, 'RuleSet', { + rules: [ + { + actions: [ + new ses.ReceiptRuleAddHeaderAction({ + name: 'X-Special-Header', + value: 'aws' + }) + new ses.ReceiptRuleS3Action({ + bucket, + objectKeyPrefix: 'emails/', + topic + }) + ], + recipients: ['hello@aws.com'], + }, + { + actions: [ + new ses.ReceiptRuleSnsAction({ + topic + }) + ] + recipients: ['aws.com'], + } + ] +}); +``` + +Alternatively, rules can be added to a rule set: +```ts +const ruleSet = new ses.ReceiptRuleSet(this, 'RuleSet'): + +const awsRule = ruleSet.addRule('Aws', { + recipients: ['aws.com'] +}); +``` + +And actions to rules: +```ts +awsRule.addAction( + new ses.ReceiptRuleSnsAction({ + topic + }); +); +``` +When using `addRule`, the new rule is added after the last added rule unless `after` is specified. + +[More actions](test/integ.receipt.ts) + +#### Drop spams +A rule to drop spam can be added by setting `dropSpam` to `true`: + +```ts +new ses.ReceiptRuleSet(this, 'RuleSet', { + dropSpam: true +}); +``` + +This will add a rule at the top of the rule set with a Lambda action that stops processing messages that have at least one spam indicator. See [Lambda Function Examples](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html). + +### Import and export receipt rule set and receipt rules +Receipt rule sets and receipt rules can be exported: + +```ts +const ruleSet = new ReceiptRuleSet(this, 'RuleSet'); +const rule = ruleSet.addRule(this, 'Rule', { + recipients: ['hello@mydomain.com'] +}); + +const ruleSetRef = ruleSet.export(); +const ruleRef = rule.export(); +``` + +And imported: +```ts +const importedRuleSet = ses.ReceiptRuleSet.import(this, 'ImportedRuleSet', ruleSetRef); + +const importedRule = ses.ReceiptRule.import(this, 'ImportedRule', ruleRef); + +const otherRule = ses.ReceiptRule.import(this, 'OtherRule', { + name: 'other-rule' +}); + +importedRuleSet.addRule('New', { // This rule as added after the imported rule + after: importedRule, + recipients: ['mydomain.com'] +}); + +importedRuleSet.addRule('Extra', { // Added after the 'New' rule + recipients: ['extra.com'] +}); +``` + +### Receipt filter +Create a receipt filter: +```ts +new ses.ReceiptFilter(this, 'Filter', { + ip: '1.2.3.4/16' // Will be blocked +}) +``` + +Without props, a block all (0.0.0.0/0) filter is created. + +A white list filter is also available: +```ts +new ses.WhiteListReceiptFilter(this, 'WhiteList', { + ips: [ + '10.0.0.0/16', + '1.2.3.4/16', + ] +}); +``` +This will first create a block all filter and then create allow filters for the listed ip addresses. diff --git a/packages/@aws-cdk/aws-ses/lib/index.ts b/packages/@aws-cdk/aws-ses/lib/index.ts index 725bd4c040640..078887862180c 100644 --- a/packages/@aws-cdk/aws-ses/lib/index.ts +++ b/packages/@aws-cdk/aws-ses/lib/index.ts @@ -1,2 +1,7 @@ +export * from './receipt-rule-set'; +export * from './receipt-rule'; +export * from './receipt-rule-action'; +export * from './receipt-filter'; + // AWS::SES CloudFormation Resources: export * from './ses.generated'; diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts new file mode 100644 index 0000000000000..cf3078c9149a6 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts @@ -0,0 +1,82 @@ +import cdk = require('@aws-cdk/cdk'); +import { CfnReceiptFilter } from './ses.generated'; + +export enum ReceiptFilterPolicy { + /** + * Allow the ip address or range. + */ + Allow = 'Allow', + + /** + * Block the ip address or range. + */ + Block = 'Block' +} + +export interface ReceiptFilterProps { + /** + * The name for the receipt filter. + * + * @default a CloudFormation generated name + */ + name?: string; + + /** + * The ip address or range to filter. + * + * @default 0.0.0.0/0 + */ + ip?: string; + + /** + * The policy for the filter. + * + * @default Block + */ + policy?: ReceiptFilterPolicy; +} + +/** + * A receipt filter. When instantiated without props, it creates a + * block all receipt filter. + */ +export class ReceiptFilter extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props?: ReceiptFilterProps) { + super(scope, id); + + new CfnReceiptFilter(this, 'Resource', { + filter: { + ipFilter: { + cidr: (props && props.ip) || '0.0.0.0/0', + policy: (props && props.policy) || ReceiptFilterPolicy.Block + }, + name: props ? props.name : undefined + } + }); + } +} + +export interface WhiteListReceiptFilterProps { + /** + * A list of ip addresses or ranges to white list. + */ + ips: string[]; +} + +/** + * A white list receipt filter. + */ +export class WhiteListReceiptFilter extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: WhiteListReceiptFilterProps) { + super(scope, id); + + new ReceiptFilter(this, 'BlockAll'); + + props.ips.forEach(ip => { + new ReceiptFilter(this, `Allow${ip.replace(/[^\d]/g, '')}`, { + ip, + policy: ReceiptFilterPolicy.Allow + }); + }); + } +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts new file mode 100644 index 0000000000000..cff4eeb9dc6c0 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts @@ -0,0 +1,421 @@ +import iam = require('@aws-cdk/aws-iam'); +import kms = require('@aws-cdk/aws-kms'); +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import { CfnReceiptRule } from './ses.generated'; + +export interface ReceiptRuleActionProps { + /** + * Adds a header to the received email. + */ + addHeaderAction?: CfnReceiptRule.AddHeaderActionProperty + + /** + * Rejects the received email by returning a bounce response to the sender and, + * optionally, publishes a notification to Amazon SNS. + */ + bounceAction?: CfnReceiptRule.BounceActionProperty; + + /** + * Calls an AWS Lambda function, and optionally, publishes a notification to + * Amazon SNS. + */ + lambdaAction?: CfnReceiptRule.LambdaActionProperty; + + /** + * Saves the received message to an Amazon S3 bucket and, optionally, publishes + * a notification to Amazon SNS. + */ + s3Action?: CfnReceiptRule.S3ActionProperty; + + /** + * Publishes the email content within a notification to Amazon SNS. + */ + snsAction?: CfnReceiptRule.SNSActionProperty; + + /** + * Terminates the evaluation of the receipt rule set and optionally publishes a + * notification to Amazon SNS. + */ + stopAction?: CfnReceiptRule.StopActionProperty; + + /** + * Calls Amazon WorkMail and, optionally, publishes a notification to Amazon SNS. + */ + workmailAction?: CfnReceiptRule.WorkmailActionProperty; +} + +/** + * An abstract action for a receipt rule. + */ +export interface IReceiptRuleAction { + /** + * Renders the action specification + */ + render(): ReceiptRuleActionProps; +} + +export interface ReceiptRuleAddHeaderActionProps { + /** + * The name of the header to add. Must be between 1 and 50 characters, + * inclusive, and consist of alphanumeric (a-z, A-Z, 0-9) characters + * and dashes only. + */ + name: string; + + /** + * The value of the header to add. Must be less than 2048 characters, + * and must not contain newline characters ("\r" or "\n"). + */ + value: string; +} + +/** + * Adds a header to the received email + */ +export class ReceiptRuleAddHeaderAction implements IReceiptRuleAction { + private readonly name: string; + private readonly value: string; + + constructor(props: ReceiptRuleAddHeaderActionProps) { + if (!/^[a-zA-Z0-9-]{1,50}$/.test(props.name)) { + // tslint:disable:max-line-length + throw new Error('Header `name` must be between 1 and 50 characters, inclusive, and consist of alphanumeric (a-z, A-Z, 0-9) characters and dashes only.'); + // tslint:enable:max-line-length + } + + if (!/^[^\n\r]{0,2047}$/.test(props.value)) { + throw new Error('Header `value` must be less than 2048 characters, and must not contain newline characters ("\r" or "\n").'); + } + + this.name = props.name; + this.value = props.value; + } + + public render(): ReceiptRuleActionProps { + return { + addHeaderAction: { + headerName: this.name, + headerValue: this.value + } + }; + } +} + +export interface ReceiptRuleBounceActionTemplateProps { + /** + * Human-readable text to include in the bounce message. + */ + message: string; + + /** + * The SMTP reply code, as defined by RFC 5321. + * + * @see https://tools.ietf.org/html/rfc5321 + */ + smtpReplyCode: string; + + /** + * The SMTP enhanced status code, as defined by RFC 3463. + * + * @see https://tools.ietf.org/html/rfc3463 + */ + statusCode?: string; +} + +/** + * A bounce action template. + */ +export class ReceiptRuleBounceActionTemplate { + public static readonly MailboxDoesNotExist = new ReceiptRuleBounceActionTemplate({ + message: 'Mailbox does not exist', + smtpReplyCode: '550', + statusCode: '5.1.1' + }); + + public static readonly MessageTooLarge = new ReceiptRuleBounceActionTemplate({ + message: 'Message too large', + smtpReplyCode: '552', + statusCode: '5.3.4' + }); + + public static readonly MailboxFull = new ReceiptRuleBounceActionTemplate({ + message: 'Mailbox full', + smtpReplyCode: '552', + statusCode: '5.2.2' + }); + + public static readonly MessageContentRejected = new ReceiptRuleBounceActionTemplate({ + message: 'Message content rejected', + smtpReplyCode: '500', + statusCode: '5.6.1' + }); + + public static readonly TemporaryFailure = new ReceiptRuleBounceActionTemplate({ + message: 'Temporary failure', + smtpReplyCode: '450', + statusCode: '4.0.0' + }); + + public readonly message: string; + public readonly smtpReplyCode: string; + public readonly statusCode?: string; + + constructor(props: ReceiptRuleBounceActionTemplateProps) { + this.message = props.message; + this.smtpReplyCode = props.smtpReplyCode; + this.statusCode = props.statusCode; + } +} + +export interface ReceiptRuleBounceActionProps { + /** + * The template containing the message, reply code and status code. + */ + template: ReceiptRuleBounceActionTemplate; + + /** + * The email address of the sender of the bounced email. This is the address + * from which the bounce message will be sent. + */ + sender: string; + + /** + * The SNS topic to notify when the bounce action is taken. + * + * @default no notification + */ + topic?: sns.ITopic; +} + +/** + * Rejects the received email by returning a bounce response to the sender and, + * optionally, publishes a notification to Amazon SNS. + */ +export class ReceiptRuleBounceAction implements IReceiptRuleAction { + constructor(private readonly props: ReceiptRuleBounceActionProps) { + } + + public render(): ReceiptRuleActionProps { + return { + bounceAction: { + sender: this.props.sender, + smtpReplyCode: this.props.template.smtpReplyCode, + message: this.props.template.message, + topicArn: this.props.topic ? this.props.topic.topicArn : undefined, + statusCode: this.props.template.statusCode + } + }; + } +} + +export enum LambdaInvocationType { + /** + * The function will be invoked asynchronously. + */ + Event = 'Event', + + /** + * The function will be invoked sychronously. Use RequestResponse only when + * you want to make a mail flow decision, such as whether to stop the receipt + * rule or the receipt rule set. + */ + RequestResponse = 'RequestResponse', +} + +export interface ReceiptRuleLambdaActionProps { + /** + * The Lambda function to invoke. + */ + function: lambda.IFunction + + /** + * The invocation type of the Lambda function. + * + * @default Event + */ + invocationType?: LambdaInvocationType; + + /** + * The SNS topic to notify when the Lambda action is taken. + * + * @default no notification + */ + topic?: sns.ITopic; +} + +/** + * Calls an AWS Lambda function, and optionally, publishes a notification to + * Amazon SNS. + */ +export class ReceiptRuleLambdaAction implements IReceiptRuleAction { + constructor(private readonly props: ReceiptRuleLambdaActionProps) { + } + + public render(): ReceiptRuleActionProps { + // Allow SES to invoke Lambda function + // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-lambda + const permissionId = 'AllowSes'; + if (!this.props.function.node.tryFindChild(permissionId)) { + this.props.function.addPermission(permissionId, { + action: 'lambda:InvokeFunction', + principal: new iam.ServicePrincipal('ses.amazonaws.com'), + sourceAccount: new cdk.ScopedAws().accountId + }); + } + + return { + lambdaAction: { + functionArn: this.props.function.functionArn, + invocationType: this.props.invocationType, + topicArn: this.props.topic ? this.props.topic.topicArn : undefined + } + }; + } +} + +export interface ReceiptRuleS3ActionProps { + /** + * The S3 bucket that incoming email will be saved to. + */ + bucket: s3.IBucket; + + /** + * The master key that SES should use to encrypt your emails before saving + * them to the S3 bucket. + * + * @default no encryption + */ + kmsKey?: kms.IEncryptionKey; + + /** + * The key prefix of the S3 bucket. + * + * @default no prefix + */ + objectKeyPrefix?: string; + + /** + * The SNS topic to notify when the S3 action is taken. + * + * @default no notification + */ + topic?: sns.ITopic; +} + +/** + * Saves the received message to an Amazon S3 bucket and, optionally, publishes + * a notification to Amazon SNS. + */ +export class ReceiptRuleS3Action implements IReceiptRuleAction { + constructor(private readonly props: ReceiptRuleS3ActionProps) { + } + + public render(): ReceiptRuleActionProps { + // Allow SES to write to S3 bucket + // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-s3 + const keyPattern = this.props.objectKeyPrefix || ''; + + const s3Statement = new iam.PolicyStatement() + .addAction('s3:PutObject') + .addServicePrincipal('ses.amazonaws.com') + .addResource(this.props.bucket.arnForObjects(`${keyPattern}*`)) + .addCondition('StringEquals', { + 'aws:Referer': new cdk.ScopedAws().accountId + }); + + this.props.bucket.addToResourcePolicy(s3Statement); + + // Allow SES to use KMS master key + // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-kms + if (this.props.kmsKey && !/alias\/aws\/ses$/.test(this.props.kmsKey.keyArn)) { + const kmsStatement = new iam.PolicyStatement() + .addActions('km:Encrypt', 'kms:GenerateDataKey') + .addServicePrincipal('ses.amazonaws.com') + .addAllResources() + .addConditions({ + Null: { + 'kms:EncryptionContext:aws:ses:rule-name': 'false', + 'kms:EncryptionContext:aws:ses:message-id': 'false' + }, + StringEquals: { + 'kms:EncryptionContext:aws:ses:source-account': new cdk.ScopedAws().accountId + } + }); + + this.props.kmsKey.addToResourcePolicy(kmsStatement); + } + + return { + s3Action: { + bucketName: this.props.bucket.bucketName, + kmsKeyArn: this.props.kmsKey ? this.props.kmsKey.keyArn : undefined, + objectKeyPrefix: this.props.objectKeyPrefix, + topicArn: this.props.topic ? this.props.topic.topicArn : undefined + } + }; + } +} + +export enum EmailEncoding { + Base64 = 'Base64', + UTF8 = 'UTF-8', +} + +export interface ReceiptRuleSnsActionProps { + /** + * The encoding to use for the email within the Amazon SNS notification. + * + * @default UTF-8 + */ + encoding?: EmailEncoding; + + /** + * The SNS topic to notify. + */ + topic: sns.ITopic; +} + +/** + * Publishes the email content within a notification to Amazon SNS. + */ +export class ReceiptRuleSnsAction implements IReceiptRuleAction { + constructor(private readonly props: ReceiptRuleSnsActionProps) { + } + + public render(): ReceiptRuleActionProps { + return { + snsAction: { + encoding: this.props.encoding, + topicArn: this.props.topic.topicArn + } + }; + } +} + +export interface ReceiptRuleStopActionProps { + /** + * The SNS topic to notify when the stop action is taken. + */ + topic?: sns.ITopic; +} + +/** + * Terminates the evaluation of the receipt rule set and optionally publishes a + * notification to Amazon SNS. + */ +export class ReceiptRuleStopAction implements IReceiptRuleAction { + constructor(private readonly props?: ReceiptRuleStopActionProps) { + } + + public render(): ReceiptRuleActionProps { + return { + stopAction: { + scope: 'RuleSet', + topicArn: this.props && this.props.topic ? this.props.topic.topicArn : undefined + } + }; + } +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts new file mode 100644 index 0000000000000..fd57e6c0af0b1 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -0,0 +1,139 @@ +import cdk = require('@aws-cdk/cdk'); +import { DropSpamReceiptRule, ReceiptRule, ReceiptRuleOptions } from './receipt-rule'; +import { CfnReceiptRuleSet } from './ses.generated'; + +export interface IReceiptRuleSet extends cdk.IConstruct { + /** + * The receipt rule set name. + */ + readonly name: string; + + /** + * Adds a new receipt rule in this rule set. The new rule is added after + * the last added rule unless `after` is specified. + */ + addRule(id: string, options?: ReceiptRuleOptions): ReceiptRule; + + /** + * Exports this receipt rule from the stack. + */ + export(): ReceiptRuleSetImportProps; +} + +export interface ReceiptRuleSetProps { + /** + * The name for the receipt rule set. + * + * @default a CloudFormation generated name + */ + name?: string; + + /** + * The list of rules to add to this rule set. Rules are added in the same + * order as they appear in the list. + */ + rules?: ReceiptRuleOptions[] + + /** + * Whether to add a first rule to stop processing messages + * that have at least one spam indicator. + * + * @default false + */ + dropSpam?: boolean; +} + +/** + * A new or imported receipt rule set. + */ +export abstract class ReceiptRuleSetBase extends cdk.Construct implements IReceiptRuleSet { + public abstract readonly name: string; + + private lastAddedRule?: ReceiptRule; + + /** + * Adds a new receipt rule in this rule set. The new rule is added after + * the last added rule unless `after` is specified. + */ + public addRule(id: string, options?: ReceiptRuleOptions): ReceiptRule { + this.lastAddedRule = new ReceiptRule(this, id, { + after: this.lastAddedRule ? this.lastAddedRule : undefined, + ruleSet: this, + ...options + }); + + return this.lastAddedRule; + } + + public abstract export(): ReceiptRuleSetImportProps; + + /** + * Adds a drop spam rule + */ + protected addDropSpamRule(): void { + const dropSpam = new DropSpamReceiptRule(this, 'DropSpam', { + ruleSet: this + }); + this.lastAddedRule = dropSpam.rule; + } +} + +export class ReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { + /** + * Import an exported receipt rule set. + */ + public static import(scope: cdk.Construct, id: string, props: ReceiptRuleSetImportProps): IReceiptRuleSet { + return new ImportedReceiptRuleSet(scope, id, props); + } + + public readonly name: string; + + constructor(scope: cdk.Construct, id: string, props?: ReceiptRuleSetProps) { + super(scope, id); + + const resource = new CfnReceiptRuleSet(this, 'Resource', { + ruleSetName: props ? props.name : undefined + }); + + this.name = resource.receiptRuleSetName; + + if (props) { + const rules = props.rules || []; + rules.forEach((ruleOption, idx) => this.addRule(`Rule${idx}`, ruleOption)); + + if (props.dropSpam) { + this.addDropSpamRule(); + } + } + } + + /** + * Exports this receipt rule set to another stack. + */ + public export(): ReceiptRuleSetImportProps { + return { + name: new cdk.Output(this, 'ReceiptRuleSetName', { value: this.name }).makeImportValue().toString() + }; + } +} + +export interface ReceiptRuleSetImportProps { + /** + * The receipt rule set name. + */ + name: string; +} + +export class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { + public readonly name: string; + + constructor(scope: cdk.Construct, id: string, private readonly props: ReceiptRuleSetImportProps) { + super(scope, id); + + this.name = props.name; + } + + public export() { + return this.props; + } +} diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts new file mode 100644 index 0000000000000..4212fc58a1ebe --- /dev/null +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -0,0 +1,215 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { IReceiptRuleAction, LambdaInvocationType, ReceiptRuleActionProps, ReceiptRuleLambdaAction } from './receipt-rule-action'; +import { IReceiptRuleSet } from './receipt-rule-set'; +import { CfnReceiptRule } from './ses.generated'; + +export interface IReceiptRule extends cdk.IConstruct { + /** + * The name of the receipt rule. + */ + readonly name: string; + + /** + * Exports this receipt rule from the stack. + */ + export(): ReceiptRuleImportProps; +} + +export enum TlsPolicy { + /** + * Do not check for TLS. + */ + Optional = 'Optional', + + /** + * Bounce emails that are not received over TLS. + */ + Require = 'Require' +} + +export interface ReceiptRuleOptions { + /** + * An ordered list of actions to perform on messages that match at least + * one of the recipient email addresses or domains specified in the + * receipt rule. + */ + actions?: IReceiptRuleAction[]; + + /** + * An existing rule after which the new rule will be placed. + * + * @default the new rule is inserted at the beginning of the rule list + */ + after?: IReceiptRule; + + /** + * Whether the rule is active. + * + * @default true + */ + enabled?: boolean; + + /** + * The name for the rule + * + * @default a CloudFormation generated name + */ + name?: string; + + /** + * The recipient domains and email addresses that the receipt rule applies to. + * + * @default match all recipients under all verified domains. + */ + recipients?: string[]; + + /** + * Wheter to scan for spam and viruses. + * + * @default false + */ + scanEnabled?: boolean; + + /** + * The TLS policy + * + * @default Optional + */ + tlsPolicy?: TlsPolicy; +} + +export interface ReceiptRuleProps extends ReceiptRuleOptions { + /** + * The name of the rule set that the receipt rule will be added to. + */ + ruleSet: IReceiptRuleSet; +} + +export class ReceiptRule extends cdk.Construct implements IReceiptRule { + /** + * Import an exported receipt rule. + */ + public static import(scope: cdk.Construct, id: string, props: ReceiptRuleImportProps): IReceiptRule { + return new ImportedReceiptRule(scope, id, props); + } + + public readonly name: string; + private readonly renderedActions = new Array(); + + constructor(scope: cdk.Construct, id: string, props: ReceiptRuleProps) { + super(scope, id); + + const resource = new CfnReceiptRule(this, 'Resource', { + after: props.after ? props.after.name : undefined, + rule: { + actions: new cdk.Token(() => this.getRenderedActions()), + enabled: props.enabled === undefined ? true : props.enabled, + name: props.name, + recipients: props.recipients, + scanEnabled: props.scanEnabled, + tlsPolicy: props.tlsPolicy + }, + ruleSetName: props.ruleSet.name + }); + + this.name = resource.receiptRuleName; + + if (props.actions) { + props.actions.forEach(action => this.addAction(action)); + } + } + + public addAction(action: IReceiptRuleAction) { + const renderedAction = action.render(); + + this.renderedActions.push(renderedAction); + } + + public export(): ReceiptRuleImportProps { + return { + name: new cdk.Output(this, 'ReceiptRuleName', { value: this.name }).makeImportValue().toString() + }; + } + + private getRenderedActions() { + if (this.renderedActions.length === 0) { + return undefined; + } + + return this.renderedActions; + } +} + +export interface ReceiptRuleImportProps { + /** + * The name of the receipt rule. + */ + name: string; +} +export class ImportedReceiptRule extends cdk.Construct implements IReceiptRule { + public readonly name: string; + + constructor(scope: cdk.Construct, id: string, private readonly props: ReceiptRuleImportProps) { + super(scope, id); + + this.name = props.name; + } + + public export() { + return this.props; + } +} + +/** + * A rule added at the top of the rule set to drop spam/virus. + * + * @see https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html + */ +export class DropSpamReceiptRule extends cdk.Construct { + public readonly rule: ReceiptRule; + + constructor(scope: cdk.Construct, id: string, props: ReceiptRuleProps) { + super(scope, id); + + const fn = new lambda.SingletonFunction(this, 'Function', { + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${dropSpamCode}`), + uuid: '224e77f9-a32e-4b4d-ac32-983477abba16' + }); + + this.rule = new ReceiptRule(this, 'Rule', { + actions: [ + new ReceiptRuleLambdaAction({ + function: fn, + invocationType: LambdaInvocationType.RequestResponse + }) + ], + scanEnabled: true, + ruleSet: props.ruleSet + }); + } +} + +// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html +// tslint:disable:no-console +function dropSpamCode(event: any, _: any, callback: any) { + console.log('Spam filter'); + + const sesNotification = event.Records[0].ses; + console.log("SES Notification:\n", JSON.stringify(sesNotification, null, 2)); + + // Check if any spam check failed + if (sesNotification.receipt.spfVerdict.status === 'FAIL' + || sesNotification.receipt.dkimVerdict.status === 'FAIL' + || sesNotification.receipt.spamVerdict.status === 'FAIL' + || sesNotification.receipt.virusVerdict.status === 'FAIL') { + console.log('Dropping spam'); + + // Stop processing rule set, dropping message + callback(null, { disposition : 'STOP_RULE_SET' }); + } else { + callback(null, null); + } +} diff --git a/packages/@aws-cdk/aws-ses/package.json b/packages/@aws-cdk/aws-ses/package.json index c6f8e2cb8106c..49969253289c7 100644 --- a/packages/@aws-cdk/aws-ses/package.json +++ b/packages/@aws-cdk/aws-ses/package.json @@ -56,17 +56,28 @@ "devDependencies": { "@aws-cdk/assert": "^0.25.1", "cdk-build-tools": "^0.25.1", + "cdk-integ-tools": "^0.25.1", "cfn2ts": "^0.25.1", "pkglint": "^0.25.1" }, "dependencies": { + "@aws-cdk/aws-iam": "^0.25.1", + "@aws-cdk/aws-kms": "^0.25.1", + "@aws-cdk/aws-lambda": "^0.25.1", + "@aws-cdk/aws-s3": "^0.25.1", + "@aws-cdk/aws-sns": "^0.25.1", "@aws-cdk/cdk": "^0.25.1" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-iam": "^0.25.1", + "@aws-cdk/aws-kms": "^0.25.1", + "@aws-cdk/aws-lambda": "^0.25.1", + "@aws-cdk/aws-s3": "^0.25.1", + "@aws-cdk/aws-sns": "^0.25.1", "@aws-cdk/cdk": "^0.25.1" }, "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json new file mode 100644 index 0000000000000..8ab2aa38f6731 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json @@ -0,0 +1,399 @@ +{ + "Resources": { + "TopicBFC7AF6E": { + "Type": "AWS::SNS::Topic" + }, + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (event) => event;" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "FunctionServiceRole675BB04A" + ] + }, + "FunctionAllowSes1829904A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function76856677" + }, + "Principal": "ses.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + } + } + }, + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:PutObject", + "Condition": { + "StringEquals": { + "aws:Referer": { + "Ref": "AWS::AccountId" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "ses.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/emails/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "km:Encrypt", + "kms:GenerateDataKey" + ], + "Condition": { + "Null": { + "kms:EncryptionContext:aws:ses:rule-name": "false", + "kms:EncryptionContext:aws:ses:message-id": "false" + }, + "StringEquals": { + "kms:EncryptionContext:aws:ses:source-account": { + "Ref": "AWS::AccountId" + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "ses.amazonaws.com" + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "DeletionPolicy": "Retain" + }, + "RuleSetE30C6C48": { + "Type": "AWS::SES::ReceiptRuleSet" + }, + "RuleSetDropSpamRule5809F51B": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Actions": [ + { + "LambdaAction": { + "FunctionArn": { + "Fn::GetAtt": [ + "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", + "Arn" + ] + }, + "InvocationType": "RequestResponse" + } + } + ], + "Enabled": true, + "ScanEnabled": true + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + } + } + }, + "RuleSetFirstRule0A27C8CC": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Actions": [ + { + "AddHeaderAction": { + "HeaderName": "X-My-Header", + "HeaderValue": "value" + } + }, + { + "LambdaAction": { + "FunctionArn": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "InvocationType": "RequestResponse", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, + { + "S3Action": { + "BucketName": { + "Ref": "Bucket83908E77" + }, + "KmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "ObjectKeyPrefix": "emails/", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, + { + "SNSAction": { + "Encoding": "Base64", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, + { + "BounceAction": { + "Message": "Message content rejected", + "Sender": "cdk-ses-receipt-test@yopmail.com", + "SmtpReplyCode": "500", + "StatusCode": "5.6.1", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + } + ], + "Enabled": true, + "Name": "FirstRule", + "Recipients": [ + "cdk-ses-receipt-test@yopmail.com" + ], + "ScanEnabled": true, + "TlsPolicy": "Require" + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + }, + "After": { + "Ref": "RuleSetDropSpamRule5809F51B" + } + } + }, + "RuleSetSecondRule03178AD4": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Actions": [ + { + "StopAction": { + "Scope": "RuleSet", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } + } + } + ], + "Enabled": true + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + }, + "After": { + "Ref": "RuleSetFirstRule0A27C8CC" + } + } + }, + "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function dropSpamCode(event, _, callback) {\n console.log('Spam filter');\n const sesNotification = event.Records[0].ses;\n console.log(\"SES Notification:\\n\", JSON.stringify(sesNotification, null, 2));\n // Check if any spam check failed\n if (sesNotification.receipt.spfVerdict.status === 'FAIL'\n || sesNotification.receipt.dkimVerdict.status === 'FAIL'\n || sesNotification.receipt.spamVerdict.status === 'FAIL'\n || sesNotification.receipt.virusVerdict.status === 'FAIL') {\n console.log('Dropping spam');\n // Stop processing rule set, dropping message\n callback(null, { disposition: 'STOP_RULE_SET' });\n }\n else {\n callback(null, null);\n }\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" + ] + }, + "SingletonLambda224e77f9a32e4b4dac32983477abba16AllowSesB42DF904": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15" + }, + "Principal": "ses.amazonaws.com", + "SourceAccount": { + "Ref": "AWS::AccountId" + } + } + }, + "WhiteListBlockAllAE2CDDFF": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "0.0.0.0/0", + "Policy": "Block" + } + } + } + }, + "WhiteListAllow1000016F396A7F2": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "10.0.0.0/16", + "Policy": "Allow" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.ts b/packages/@aws-cdk/aws-ses/test/integ.receipt.ts new file mode 100644 index 0000000000000..2994acd4e2f60 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.ts @@ -0,0 +1,76 @@ +import kms = require('@aws-cdk/aws-kms'); +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import ses = require('../lib'); + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-ses-receipt'); + +const topic = new sns.Topic(stack, 'Topic'); + +const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.inline('exports.handler = async (event) => event;'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 +}); + +const bucket = new s3.Bucket(stack, 'Bucket'); + +const kmsKey = new kms.EncryptionKey(stack, 'Key'); + +const ruleSet = new ses.ReceiptRuleSet(stack, 'RuleSet', { + dropSpam: true +}); + +const firstRule = ruleSet.addRule('FirstRule', { + actions: [ + new ses.ReceiptRuleAddHeaderAction({ + name: 'X-My-Header', + value: 'value' + }), + new ses.ReceiptRuleLambdaAction({ + function: fn, + invocationType: ses.LambdaInvocationType.RequestResponse, + topic + }), + new ses.ReceiptRuleS3Action({ + bucket, + kmsKey, + objectKeyPrefix: 'emails/', + topic + }), + new ses.ReceiptRuleSnsAction({ + encoding: ses.EmailEncoding.Base64, + topic + }) + ], + name: 'FirstRule', + recipients: ['cdk-ses-receipt-test@yopmail.com'], + scanEnabled: true, + tlsPolicy: ses.TlsPolicy.Require, +}); + +firstRule.addAction( + new ses.ReceiptRuleBounceAction({ + sender: 'cdk-ses-receipt-test@yopmail.com', + template: ses.ReceiptRuleBounceActionTemplate.MessageContentRejected, + topic + }) +); + +const secondRule = ruleSet.addRule('SecondRule'); + +secondRule.addAction(new ses.ReceiptRuleStopAction({ + topic +})); + +new ses.WhiteListReceiptFilter(stack, 'WhiteList', { + ips: [ + '10.0.0.0/16' + ] +}); + +app.run(); diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-filter.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-filter.ts new file mode 100644 index 0000000000000..b0d40052a3534 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-filter.ts @@ -0,0 +1,94 @@ +import { expect } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { ReceiptFilter, ReceiptFilterPolicy, WhiteListReceiptFilter } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'can create a receipt filter'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new ReceiptFilter(stack, 'Filter', { + ip: '1.2.3.4/16', + name: 'MyFilter', + policy: ReceiptFilterPolicy.Block + }); + + // THEN + expect(stack).toMatch({ + "Resources": { + "FilterC907D6DA": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "1.2.3.4/16", + "Policy": "Block" + }, + "Name": "MyFilter" + } + } + } + } + }); + + test.done(); + }, + + 'can create a white list filter'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new WhiteListReceiptFilter(stack, 'WhiteList', { + ips: [ + '10.0.0.0/16', + '1.2.3.4' + ] + }); + + // THEN + expect(stack).toMatch({ + "Resources": { + "WhiteListBlockAllAE2CDDFF": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "0.0.0.0/0", + "Policy": "Block" + } + } + } + }, + "WhiteListAllow1000016F396A7F2": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "10.0.0.0/16", + "Policy": "Allow" + } + } + } + }, + "WhiteListAllow1234A4DDAD4E": { + "Type": "AWS::SES::ReceiptFilter", + "Properties": { + "Filter": { + "IpFilter": { + "Cidr": "1.2.3.4", + "Policy": "Allow" + } + } + } + } + } + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts new file mode 100644 index 0000000000000..c2700ad76ccd2 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts @@ -0,0 +1,431 @@ +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import kms = require('@aws-cdk/aws-kms'); +import lambda = require('@aws-cdk/aws-lambda'); +import s3 = require('@aws-cdk/aws-s3'); +import sns = require('@aws-cdk/aws-sns'); +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +// tslint:disable:max-line-length +import { EmailEncoding, LambdaInvocationType, ReceiptRuleAddHeaderAction, ReceiptRuleBounceAction, ReceiptRuleBounceActionTemplate, ReceiptRuleLambdaAction, ReceiptRuleS3Action, ReceiptRuleSet, ReceiptRuleSnsAction, ReceiptRuleStopAction } from '../lib'; +// tslint:enable:max-line-length + +export = { + 'can add an add header action'(test: Test) { + // GIVEN + const stack = new Stack(); + + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleAddHeaderAction({ + name: 'X-My-Header', + value: 'value' + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + AddHeaderAction: { + HeaderName: 'X-My-Header', + HeaderValue: 'value' + } + } + ], + Enabled: true + } + })); + + test.done(); + }, + + 'fails when header name is invalid'(test: Test) { + const stack = new Stack(); + + test.throws(() => new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleAddHeaderAction({ + name: 'He@der', + value: 'value' + }) + ] + } + ] + }), /`name`/); + + test.done(); + }, + + 'fails when header value is invalid'(test: Test) { + const stack = new Stack(); + + const ruleSet = new ReceiptRuleSet(stack, 'RuleSet'); + + test.throws(() => ruleSet.addRule('Rule', { + actions: [ + new ReceiptRuleAddHeaderAction({ + name: 'Header', + value: `va + lu` + }) + ] + }), /`value`/); + + test.done(); + }, + + 'can add a bounce action'(test: Test) { + // GIVEN + const stack = new Stack(); + + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleBounceAction({ + sender: 'noreply@aws.com', + template: ReceiptRuleBounceActionTemplate.MessageContentRejected, + topic + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + BounceAction: { + Message: 'Message content rejected', + Sender: 'noreply@aws.com', + SmtpReplyCode: '500', + TopicArn: { + Ref: 'TopicBFC7AF6E' + }, + StatusCode: '5.6.1', + } + } + ], + Enabled: true + } + })); + + test.done(); + }, + + 'can add a lambda action'(test: Test) { + // GIVEN + const stack = new Stack(); + + const topic = new sns.Topic(stack, 'Topic'); + + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.inline(''), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleLambdaAction({ + function: fn, + invocationType: LambdaInvocationType.RequestResponse, + topic + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + LambdaAction: { + FunctionArn: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn' + ] + }, + InvocationType: 'RequestResponse', + TopicArn: { + Ref: 'TopicBFC7AF6E' + } + } + }, + ], + Enabled: true + } + })); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: { + Ref: 'Function76856677' + }, + Principal: 'ses.amazonaws.com', + SourceAccount: { + Ref: 'AWS::AccountId' + } + })); + + test.done(); + }, + + 'can add a s3 action'(test: Test) { + // GIVEN + const stack = new Stack(); + + const topic = new sns.Topic(stack, 'Topic'); + + const bucket = new s3.Bucket(stack, 'Bucket'); + + const kmsKey = new kms.EncryptionKey(stack, 'Key'); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleS3Action({ + bucket, + kmsKey, + objectKeyPrefix: 'emails/', + topic + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + S3Action: { + BucketName: { + Ref: 'Bucket83908E77' + }, + KmsKeyArn: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn' + ] + }, + TopicArn: { + Ref: 'TopicBFC7AF6E' + }, + ObjectKeyPrefix: 'emails/' + } + } + ], + Enabled: true + } + })); + + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + Bucket: { + Ref: 'Bucket83908E77' + }, + PolicyDocument: { + Statement: [ + { + Action: 's3:PutObject', + Condition: { + StringEquals: { + 'aws:Referer': { + Ref: 'AWS::AccountId' + } + } + }, + Effect: 'Allow', + Principal: { + Service: 'ses.amazonaws.com' + }, + Resource: { + 'Fn::Join': [ + '', + [ + { + 'Fn::GetAtt': [ + 'Bucket83908E77', + 'Arn' + ] + }, + '/emails/*' + ] + ] + } + } + ], + Version: '2012-10-17' + } + })); + + expect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + Action: [ + 'kms:Create*', + 'kms:Describe*', + 'kms:Enable*', + 'kms:List*', + 'kms:Put*', + 'kms:Update*', + 'kms:Revoke*', + 'kms:Disable*', + 'kms:Get*', + 'kms:Delete*', + 'kms:ScheduleKeyDeletion', + 'kms:CancelKeyDeletion' + ], + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':iam::', + { + Ref: 'AWS::AccountId' + }, + ':root' + ] + ] + } + }, + Resource: '*' + }, + { + Action: [ + 'km:Encrypt', + 'kms:GenerateDataKey' + ], + Condition: { + Null: { + 'kms:EncryptionContext:aws:ses:rule-name': 'false', + 'kms:EncryptionContext:aws:ses:message-id': 'false' + }, + StringEquals: { + 'kms:EncryptionContext:aws:ses:source-account': { + Ref: 'AWS::AccountId' + } + } + }, + Effect: 'Allow', + Principal: { + Service: 'ses.amazonaws.com' + }, + Resource: '*' + } + ], + Version: '2012-10-17' + } + })); + + test.done(); + }, + + 'can add a sns action'(test: Test) { + // GIVEN + const stack = new Stack(); + + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleSnsAction({ + encoding: EmailEncoding.Base64, + topic + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + SNSAction: { + Encoding: 'Base64', + TopicArn: { + Ref: 'TopicBFC7AF6E' + } + } + } + ], + Enabled: true + } + })); + + test.done(); + }, + + 'can add a stop action'(test: Test) { + // GIVEN + const stack = new Stack(); + + const topic = new sns.Topic(stack, 'Topic'); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + actions: [ + new ReceiptRuleStopAction({ + topic + }) + ] + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + StopAction: { + Scope: 'RuleSet', + TopicArn: { + Ref: 'TopicBFC7AF6E' + } + } + } + ], + Enabled: true + } + })); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts new file mode 100644 index 0000000000000..6ccca2d1b0be6 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-set.ts @@ -0,0 +1,119 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { ReceiptRuleSet } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'can create a receipt rule set'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + name: 'MyRuleSet' + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRuleSet', { + RuleSetName: 'MyRuleSet' + })); + + test.done(); + }, + + 'can create a receipt rule set with drop spam'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + dropSpam: true + }); + + // THEN + expect(stack).to(haveResource('AWS::SES::ReceiptRule', { + Rule: { + Actions: [ + { + LambdaAction: { + FunctionArn: { + 'Fn::GetAtt': [ + 'SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15', + 'Arn' + ] + }, + InvocationType: 'RequestResponse' + } + } + ], + Enabled: true, + ScanEnabled: true + } + })); + + expect(stack).to(haveResource('AWS::Lambda::Function')); + + test.done(); + }, + + 'export receipt rule set'(test: Test) { + // GIVEN + const stack = new Stack(); + const receiptRuleSet = new ReceiptRuleSet(stack, 'RuleSet'); + + // WHEN + receiptRuleSet.export(); + + // THEN + expect(stack).toMatch({ + "Resources": { + "RuleSetE30C6C48": { + "Type": "AWS::SES::ReceiptRuleSet" + } + }, + "Outputs": { + "RuleSetReceiptRuleSetNameBA4266DD": { + "Value": { + "Ref": "RuleSetE30C6C48" + }, + "Export": { + "Name": "RuleSetReceiptRuleSetNameBA4266DD" + } + } + } + }); + + test.done(); + }, + + 'import receipt rule set'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const receiptRuleSet = ReceiptRuleSet.import(stack, 'ImportedRuleSet', { + name: 'MyRuleSet' + }); + + receiptRuleSet.addRule('MyRule'); + + // THEN + expect(stack).toMatch({ + "Resources": { + "ImportedRuleSetMyRule53EE2F7F": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Enabled": true + }, + "RuleSetName": "MyRuleSet" + } + } + }, + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts new file mode 100644 index 0000000000000..dbd369a16be08 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule.ts @@ -0,0 +1,153 @@ +import { expect } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { ReceiptRule, ReceiptRuleSet, TlsPolicy } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +export = { + 'can create receipt rules with second after first'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + name: 'FirstRule', + }, + { + enabled: false, + name: 'SecondRule', + recipients: ['hello@aws.com'], + scanEnabled: true, + tlsPolicy: TlsPolicy.Require + } + ] + }); + + // THEN + expect(stack).toMatch({ + "Resources": { + "RuleSetE30C6C48": { + "Type": "AWS::SES::ReceiptRuleSet" + }, + "RuleSetRule023C3B8E1": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Name": "FirstRule", + "Enabled": true + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + } + } + }, + "RuleSetRule117041B57": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Enabled": false, + "Name": "SecondRule", + "Recipients": [ + "hello@aws.com" + ], + "ScanEnabled": true, + "TlsPolicy": "Require" + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + }, + "After": { + "Ref": "RuleSetRule023C3B8E1" + } + } + } + } + }); + + test.done(); + }, + + 'export receipt rule'(test: Test) { + // GIVEN + const stack = new Stack(); + const receiptRuleSet = new ReceiptRuleSet(stack, 'RuleSet'); + const receiptRule = receiptRuleSet.addRule('Rule'); + + // WHEN + receiptRule.export(); + + // THEN + expect(stack).toMatch({ + "Resources": { + "RuleSetE30C6C48": { + "Type": "AWS::SES::ReceiptRuleSet" + }, + "RuleSetRule0B1D6BCA": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Enabled": true + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + } + } + } + }, + "Outputs": { + "RuleSetRuleReceiptRuleName5620D98F": { + "Value": { + "Ref": "RuleSetRule0B1D6BCA" + }, + "Export": { + "Name": "RuleSetRuleReceiptRuleName5620D98F" + } + } + } + }); + + test.done(); + }, + + 'import receipt rule'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const receiptRule = ReceiptRule.import(stack, 'ImportedRule', { + name: 'MyRule' + }); + + const receiptRuleSet = new ReceiptRuleSet(stack, 'RuleSet'); + + receiptRuleSet.addRule('MyRule', { + after: receiptRule + }); + + // THEN + expect(stack).toMatch({ + "Resources": { + "RuleSetE30C6C48": { + "Type": "AWS::SES::ReceiptRuleSet" + }, + "RuleSetMyRule60B1D107": { + "Type": "AWS::SES::ReceiptRule", + "Properties": { + "Rule": { + "Enabled": true + }, + "RuleSetName": { + "Ref": "RuleSetE30C6C48" + }, + "After": "MyRule" + } + } + }, + }); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-ses/test/test.ses.ts b/packages/@aws-cdk/aws-ses/test/test.ses.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-ses/test/test.ses.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); From a0c175d462458bdab746e6a3f0b406fe193d9a18 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 14 Mar 2019 16:39:44 +0100 Subject: [PATCH 2/7] Do not export Imported classes --- packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 2 +- packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index fd57e6c0af0b1..71ef0b4ed00ea 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -124,7 +124,7 @@ export interface ReceiptRuleSetImportProps { name: string; } -export class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { +class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { public readonly name: string; constructor(scope: cdk.Construct, id: string, private readonly props: ReceiptRuleSetImportProps) { diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 4212fc58a1ebe..53906275d2bf5 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -147,7 +147,7 @@ export interface ReceiptRuleImportProps { */ name: string; } -export class ImportedReceiptRule extends cdk.Construct implements IReceiptRule { +class ImportedReceiptRule extends cdk.Construct implements IReceiptRule { public readonly name: string; constructor(scope: cdk.Construct, id: string, private readonly props: ReceiptRuleImportProps) { From 926bb92f6f5e17733d3ca70d7c8d022b370671ce Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 14 Mar 2019 16:46:32 +0100 Subject: [PATCH 3/7] Fix tests with AWS::URLSuffix --- .../aws-ses/test/integ.receipt.expected.json | 48 +++++++++++++++++-- .../aws-ses/test/test.receipt-rule-action.ts | 24 +++++++++- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json index 8ab2aa38f6731..e74ddbee8d0b3 100644 --- a/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json +++ b/packages/@aws-cdk/aws-ses/test/integ.receipt.expected.json @@ -12,7 +12,17 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": "lambda.amazonaws.com" + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } } } ], @@ -89,7 +99,17 @@ }, "Effect": "Allow", "Principal": { - "Service": "ses.amazonaws.com" + "Service": { + "Fn::Join": [ + "", + [ + "ses.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } }, "Resource": { "Fn::Join": [ @@ -170,7 +190,17 @@ }, "Effect": "Allow", "Principal": { - "Service": "ses.amazonaws.com" + "Service": { + "Fn::Join": [ + "", + [ + "ses.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } }, "Resource": "*" } @@ -319,7 +349,17 @@ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { - "Service": "lambda.amazonaws.com" + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } } } ], diff --git a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts index c2700ad76ccd2..4ac0e349169cb 100644 --- a/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts +++ b/packages/@aws-cdk/aws-ses/test/test.receipt-rule-action.ts @@ -259,7 +259,17 @@ export = { }, Effect: 'Allow', Principal: { - Service: 'ses.amazonaws.com' + Service: { + 'Fn::Join': [ + '', + [ + 'ses.', + { + Ref: 'AWS::URLSuffix' + } + ] + ] + } }, Resource: { 'Fn::Join': [ @@ -338,7 +348,17 @@ export = { }, Effect: 'Allow', Principal: { - Service: 'ses.amazonaws.com' + Service: { + 'Fn::Join': [ + '', + [ + 'ses.', + { + Ref: 'AWS::URLSuffix' + } + ] + ] + } }, Resource: '*' } From 3a017dfcf6d0c9261f2a00fa27d6fbc18c3ea134 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 15 Mar 2019 14:39:51 +0100 Subject: [PATCH 4/7] Improve README and JSDoc --- packages/@aws-cdk/aws-ses/README.md | 37 +----------------- .../@aws-cdk/aws-ses/lib/receipt-filter.ts | 3 ++ .../aws-ses/lib/receipt-rule-action.ts | 13 +++++++ .../@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 6 +++ packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 10 +++++ .../aws-ses/test/example.receiving.lit.ts | 38 +++++++++++++++++++ 6 files changed, 72 insertions(+), 35 deletions(-) create mode 100644 packages/@aws-cdk/aws-ses/test/example.receiving.lit.ts diff --git a/packages/@aws-cdk/aws-ses/README.md b/packages/@aws-cdk/aws-ses/README.md index 67c6000cf4cce..7dc46f9b29c8c 100644 --- a/packages/@aws-cdk/aws-ses/README.md +++ b/packages/@aws-cdk/aws-ses/README.md @@ -3,38 +3,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/awslab ### Email receiving Create a receipt rule set with rules and actions: -```ts -const bucket = new s3.Bucket(this, 'Bucket'); - -const topic = new sns.Topic(this, 'Topic'); - -const ruleSet = new ses.ReceiptRuleSet(this, 'RuleSet', { - rules: [ - { - actions: [ - new ses.ReceiptRuleAddHeaderAction({ - name: 'X-Special-Header', - value: 'aws' - }) - new ses.ReceiptRuleS3Action({ - bucket, - objectKeyPrefix: 'emails/', - topic - }) - ], - recipients: ['hello@aws.com'], - }, - { - actions: [ - new ses.ReceiptRuleSnsAction({ - topic - }) - ] - recipients: ['aws.com'], - } - ] -}); -``` +[example of setting up a receipt rule set](test/example.receiving.lit.ts) Alternatively, rules can be added to a rule set: ```ts @@ -91,7 +60,7 @@ const otherRule = ses.ReceiptRule.import(this, 'OtherRule', { name: 'other-rule' }); -importedRuleSet.addRule('New', { // This rule as added after the imported rule +importedRuleSet.addRule('New', { // This rule is added after the imported rule after: importedRule, recipients: ['mydomain.com'] }); @@ -109,8 +78,6 @@ new ses.ReceiptFilter(this, 'Filter', { }) ``` -Without props, a block all (0.0.0.0/0) filter is created. - A white list filter is also available: ```ts new ses.WhiteListReceiptFilter(this, 'WhiteList', { diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts index cf3078c9149a6..19b2f76b3645e 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts @@ -1,6 +1,9 @@ import cdk = require('@aws-cdk/cdk'); import { CfnReceiptFilter } from './ses.generated'; +/** + * The policy for the receipt filter. + */ export enum ReceiptFilterPolicy { /** * Allow the ip address or range. diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts index cff4eeb9dc6c0..62b24eed55256 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts @@ -211,6 +211,9 @@ export class ReceiptRuleBounceAction implements IReceiptRuleAction { } } +/** + * The type of invocation to use for a Lambda Action. + */ export enum LambdaInvocationType { /** * The function will be invoked asynchronously. @@ -359,8 +362,18 @@ export class ReceiptRuleS3Action implements IReceiptRuleAction { } } +/** + * The type of email encoding to use for a SNS action. + */ export enum EmailEncoding { + /** + * Base 64 + */ Base64 = 'Base64', + + /** + * UTF-8 + */ UTF8 = 'UTF-8', } diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index 71ef0b4ed00ea..e22bfd1f1262a 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -78,6 +78,9 @@ export abstract class ReceiptRuleSetBase extends cdk.Construct implements IRecei } } +/** + * A new receipt rule set. + */ export class ReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { /** * Import an exported receipt rule set. @@ -124,6 +127,9 @@ export interface ReceiptRuleSetImportProps { name: string; } +/** + * An imported receipt rule set. + */ class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSet { public readonly name: string; diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 53906275d2bf5..c41535f58a392 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -16,6 +16,9 @@ export interface IReceiptRule extends cdk.IConstruct { export(): ReceiptRuleImportProps; } +/** + * The type of TLS policy for a receipt rule. + */ export enum TlsPolicy { /** * Do not check for TLS. @@ -86,6 +89,9 @@ export interface ReceiptRuleProps extends ReceiptRuleOptions { ruleSet: IReceiptRuleSet; } +/** + * A new receipt rule. + */ export class ReceiptRule extends cdk.Construct implements IReceiptRule { /** * Import an exported receipt rule. @@ -147,6 +153,10 @@ export interface ReceiptRuleImportProps { */ name: string; } + +/** + * An imported receipt rule. + */ class ImportedReceiptRule extends cdk.Construct implements IReceiptRule { public readonly name: string; diff --git a/packages/@aws-cdk/aws-ses/test/example.receiving.lit.ts b/packages/@aws-cdk/aws-ses/test/example.receiving.lit.ts new file mode 100644 index 0000000000000..f14cfa46d10b6 --- /dev/null +++ b/packages/@aws-cdk/aws-ses/test/example.receiving.lit.ts @@ -0,0 +1,38 @@ +import s3 = require('@aws-cdk/aws-s3'); +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import ses = require('../lib'); + +const stack = new cdk.Stack(); + +/// !show +const bucket = new s3.Bucket(stack, 'Bucket'); +const topic = new sns.Topic(stack, 'Topic'); + +new ses.ReceiptRuleSet(stack, 'RuleSet', { + rules: [ + { + recipients: ['hello@aws.com'], + actions: [ + new ses.ReceiptRuleAddHeaderAction({ + name: 'X-Special-Header', + value: 'aws' + }), + new ses.ReceiptRuleS3Action({ + bucket, + objectKeyPrefix: 'emails/', + topic + }) + ], + }, + { + recipients: ['aws.com'], + actions: [ + new ses.ReceiptRuleSnsAction({ + topic + }) + ] + } + ] +}); +/// !hide From 72dc416062e915fc2dd19464ce8d1ca986692208 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 15 Mar 2019 15:07:45 +0100 Subject: [PATCH 5/7] Use CfnOutput --- packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 2 +- packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index e22bfd1f1262a..e2f49780cdf48 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -115,7 +115,7 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSe */ public export(): ReceiptRuleSetImportProps { return { - name: new cdk.Output(this, 'ReceiptRuleSetName', { value: this.name }).makeImportValue().toString() + name: new cdk.CfnOutput(this, 'ReceiptRuleSetName', { value: this.name }).makeImportValue().toString() }; } } diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index c41535f58a392..519503c1a3d6c 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -134,7 +134,7 @@ export class ReceiptRule extends cdk.Construct implements IReceiptRule { public export(): ReceiptRuleImportProps { return { - name: new cdk.Output(this, 'ReceiptRuleName', { value: this.name }).makeImportValue().toString() + name: new cdk.CfnOutput(this, 'ReceiptRuleName', { value: this.name }).makeImportValue().toString() }; } From b0b37b1c043dd953c25227ecf9ae7e1313950645 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 15 Mar 2019 17:56:58 +0100 Subject: [PATCH 6/7] Add JSDoc --- .../@aws-cdk/aws-ses/lib/receipt-filter.ts | 6 +++++ .../aws-ses/lib/receipt-rule-action.ts | 24 +++++++++++++++++++ .../@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 9 +++++++ packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 15 ++++++++++++ 4 files changed, 54 insertions(+) diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts index 19b2f76b3645e..f715454906ae1 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-filter.ts @@ -16,6 +16,9 @@ export enum ReceiptFilterPolicy { Block = 'Block' } +/** + * Construction properties for a ReceiptFilter. + */ export interface ReceiptFilterProps { /** * The name for the receipt filter. @@ -59,6 +62,9 @@ export class ReceiptFilter extends cdk.Construct { } } +/** + * Construction properties for a WhiteListReceiptFilter. + */ export interface WhiteListReceiptFilterProps { /** * A list of ip addresses or ranges to white list. diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts index 62b24eed55256..36c8a7892e105 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-action.ts @@ -6,6 +6,9 @@ import sns = require('@aws-cdk/aws-sns'); import cdk = require('@aws-cdk/cdk'); import { CfnReceiptRule } from './ses.generated'; +/** + * Properties for a receipt rule action. + */ export interface ReceiptRuleActionProps { /** * Adds a header to the received email. @@ -57,6 +60,9 @@ export interface IReceiptRuleAction { render(): ReceiptRuleActionProps; } +/** + * Construction properties for a ReceiptRuleAddHeaderAction. + */ export interface ReceiptRuleAddHeaderActionProps { /** * The name of the header to add. Must be between 1 and 50 characters, @@ -104,6 +110,9 @@ export class ReceiptRuleAddHeaderAction implements IReceiptRuleAction { } } +/** + * Construction properties for a ReceiptRuleBounceActionTemplate. + */ export interface ReceiptRuleBounceActionTemplateProps { /** * Human-readable text to include in the bounce message. @@ -170,6 +179,9 @@ export class ReceiptRuleBounceActionTemplate { } } +/** + * Construction properties for a ReceiptRuleBounceAction. + */ export interface ReceiptRuleBounceActionProps { /** * The template containing the message, reply code and status code. @@ -228,6 +240,9 @@ export enum LambdaInvocationType { RequestResponse = 'RequestResponse', } +/** + * Construction properties for a ReceiptRuleLambdaAction. + */ export interface ReceiptRuleLambdaActionProps { /** * The Lambda function to invoke. @@ -279,6 +294,9 @@ export class ReceiptRuleLambdaAction implements IReceiptRuleAction { } } +/** + * Construction properties for a ReceiptRuleS3Action. + */ export interface ReceiptRuleS3ActionProps { /** * The S3 bucket that incoming email will be saved to. @@ -377,6 +395,9 @@ export enum EmailEncoding { UTF8 = 'UTF-8', } +/** + * Construction properties for a ReceiptRuleSnsAction. + */ export interface ReceiptRuleSnsActionProps { /** * The encoding to use for the email within the Amazon SNS notification. @@ -408,6 +429,9 @@ export class ReceiptRuleSnsAction implements IReceiptRuleAction { } } +/** + * Construction properties for a ReceiptRuleStopAction. + */ export interface ReceiptRuleStopActionProps { /** * The SNS topic to notify when the stop action is taken. diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index e2f49780cdf48..60b5530a5ca71 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -2,6 +2,9 @@ import cdk = require('@aws-cdk/cdk'); import { DropSpamReceiptRule, ReceiptRule, ReceiptRuleOptions } from './receipt-rule'; import { CfnReceiptRuleSet } from './ses.generated'; +/** + * A receipt rule set. + */ export interface IReceiptRuleSet extends cdk.IConstruct { /** * The receipt rule set name. @@ -20,6 +23,9 @@ export interface IReceiptRuleSet extends cdk.IConstruct { export(): ReceiptRuleSetImportProps; } +/** + * Construction properties for a ReceiptRuleSet. + */ export interface ReceiptRuleSetProps { /** * The name for the receipt rule set. @@ -120,6 +126,9 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSe } } +/** + * Construction properties for an ImportedReceiptRuleSet. + */ export interface ReceiptRuleSetImportProps { /** * The receipt rule set name. diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index 519503c1a3d6c..b969da14cf44a 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -4,6 +4,9 @@ import { IReceiptRuleAction, LambdaInvocationType, ReceiptRuleActionProps, Recei import { IReceiptRuleSet } from './receipt-rule-set'; import { CfnReceiptRule } from './ses.generated'; +/** + * A receipt rule. + */ export interface IReceiptRule extends cdk.IConstruct { /** * The name of the receipt rule. @@ -31,6 +34,9 @@ export enum TlsPolicy { Require = 'Require' } +/** + * Options to add a receipt rule to a receipt rule set. + */ export interface ReceiptRuleOptions { /** * An ordered list of actions to perform on messages that match at least @@ -82,6 +88,9 @@ export interface ReceiptRuleOptions { tlsPolicy?: TlsPolicy; } +/** + * Construction properties for a ReceiptRule. + */ export interface ReceiptRuleProps extends ReceiptRuleOptions { /** * The name of the rule set that the receipt rule will be added to. @@ -126,12 +135,18 @@ export class ReceiptRule extends cdk.Construct implements IReceiptRule { } } + /** + * Adds an action to this receipt rule. + */ public addAction(action: IReceiptRuleAction) { const renderedAction = action.render(); this.renderedActions.push(renderedAction); } + /** + * Exports this receipt rule from the stack. + */ public export(): ReceiptRuleImportProps { return { name: new cdk.CfnOutput(this, 'ReceiptRuleName', { value: this.name }).makeImportValue().toString() From b7d495d20218688293a0313020b96ecfd380a97c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 15 Mar 2019 18:15:43 +0100 Subject: [PATCH 7/7] Add JSDoc --- packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts | 7 +++++-- packages/@aws-cdk/aws-ses/lib/receipt-rule.ts | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts index 60b5530a5ca71..54122a7a2ed62 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule-set.ts @@ -18,7 +18,7 @@ export interface IReceiptRuleSet extends cdk.IConstruct { addRule(id: string, options?: ReceiptRuleOptions): ReceiptRule; /** - * Exports this receipt rule from the stack. + * Exports this receipt rule set from the stack. */ export(): ReceiptRuleSetImportProps; } @@ -117,7 +117,7 @@ export class ReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleSe } /** - * Exports this receipt rule set to another stack. + * Exports this receipt rule set from the stack. */ public export(): ReceiptRuleSetImportProps { return { @@ -148,6 +148,9 @@ class ImportedReceiptRuleSet extends ReceiptRuleSetBase implements IReceiptRuleS this.name = props.name; } + /** + * Exports this receipt rule set from the stack. + */ public export() { return this.props; } diff --git a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts index b969da14cf44a..05956125bcc56 100644 --- a/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts +++ b/packages/@aws-cdk/aws-ses/lib/receipt-rule.ts @@ -181,6 +181,9 @@ class ImportedReceiptRule extends cdk.Construct implements IReceiptRule { this.name = props.name; } + /** + * Exports this receipt rule from the stack. + */ public export() { return this.props; }