From 771eeff283644e766af67f7a86da772736dbeac2 Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Tue, 19 Nov 2024 08:42:07 +0900 Subject: [PATCH] feat(ses): maximum delivery time for emails (#32102) ### Issue # (if applicable) None ### Reason for this change Amazon Simple Email Service (SES) offers a [new delivery option that allows us to set a custom maximum delivery time for our emails at Oct 15, 2024](https://aws.amazon.com/about-aws/whats-new/2024/10/amazon-ses-configurability-maximum-delivery-time-emails/?nc1=h_ls) Cfn documentation: ### Description of changes Add `maxDeliveryDuration` to `ConfigurationSetProps`. ### Description of how you validated changes Add both unit and integ tests. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- ...efaultTestDeployAssert9B6AD46A.assets.json | 2 +- ...dk-ses-configuration-set-integ.assets.json | 6 +- ...-ses-configuration-set-integ.template.json | 7 +- .../cdk.out | 2 +- .../integ.json | 2 +- .../manifest.json | 8 +- .../tree.json | 78 ++++++++++--------- .../aws-ses/test/integ.configuration-set.ts | 8 +- packages/aws-cdk-lib/aws-ses/README.md | 6 ++ .../aws-ses/lib/configuration-set.ts | 21 ++++- .../aws-ses/test/configuration-set.test.ts | 22 +++++- 11 files changed, 110 insertions(+), 52 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/ConfigurationSetIntegDefaultTestDeployAssert9B6AD46A.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/ConfigurationSetIntegDefaultTestDeployAssert9B6AD46A.assets.json index 4ef37de9e9372..317f7f4f5f19d 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/ConfigurationSetIntegDefaultTestDeployAssert9B6AD46A.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/ConfigurationSetIntegDefaultTestDeployAssert9B6AD46A.assets.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "38.0.1", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.assets.json index 0d6eb0203c7d3..5db5ccf8914e8 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.assets.json @@ -1,7 +1,7 @@ { - "version": "31.0.0", + "version": "38.0.1", "files": { - "bd889c61b6018acea6fdd262c97b73b9af12855427e235914757eaeea73396ba": { + "294964f715c1c7d130e5bf44115a08af6bb3a7f9885cf8512a4bcb1a196a1c3a": { "source": { "path": "cdk-ses-configuration-set-integ.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "bd889c61b6018acea6fdd262c97b73b9af12855427e235914757eaeea73396ba.json", + "objectKey": "294964f715c1c7d130e5bf44115a08af6bb3a7f9885cf8512a4bcb1a196a1c3a.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.template.json index cf164e9fede59..a3833fb9105c3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk-ses-configuration-set-integ.template.json @@ -1,7 +1,12 @@ { "Resources": { "ConfigurationSet3DD38186": { - "Type": "AWS::SES::ConfigurationSet" + "Type": "AWS::SES::ConfigurationSet", + "Properties": { + "DeliveryOptions": { + "MaxDeliverySeconds": 600 + } + } }, "ConfigurationSetSns63B38980": { "Type": "AWS::SES::ConfigurationSetEventDestination", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk.out index 7925065efbcc4..c6e612584e352 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"31.0.0"} \ No newline at end of file +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/integ.json index 2c2d955ad91b8..06b732eeb546f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "38.0.1", "testCases": { "ConfigurationSetInteg/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/manifest.json index ca2489ecf8928..d82a5a1c83324 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "31.0.0", + "version": "38.0.1", "artifacts": { "cdk-ses-configuration-set-integ.assets": { "type": "cdk:asset-manifest", @@ -14,10 +14,12 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "cdk-ses-configuration-set-integ.template.json", + "terminationProtection": false, "validateOnSynth": false, + "notificationArns": [], "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/bd889c61b6018acea6fdd262c97b73b9af12855427e235914757eaeea73396ba.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/294964f715c1c7d130e5bf44115a08af6bb3a7f9885cf8512a4bcb1a196a1c3a.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -91,7 +93,9 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "ConfigurationSetIntegDefaultTestDeployAssert9B6AD46A.template.json", + "terminationProtection": false, "validateOnSynth": false, + "notificationArns": [], "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/tree.json index 5b689c4832049..2eeeaf7b9d400 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.js.snapshot/tree.json @@ -17,11 +17,15 @@ "path": "cdk-ses-configuration-set-integ/ConfigurationSet/Resource", "attributes": { "aws:cdk:cloudformation:type": "AWS::SES::ConfigurationSet", - "aws:cdk:cloudformation:props": {} + "aws:cdk:cloudformation:props": { + "deliveryOptions": { + "maxDeliverySeconds": 600 + } + } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.CfnConfigurationSet", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "Sns": { @@ -60,14 +64,14 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.CfnConfigurationSetEventDestination", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.ConfigurationSetEventDestination", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "CloudWatch": { @@ -110,20 +114,20 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.CfnConfigurationSetEventDestination", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.ConfigurationSetEventDestination", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.ConfigurationSet", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "Topic": { @@ -138,8 +142,8 @@ "aws:cdk:cloudformation:props": {} }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_sns.CfnTopic", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "Policy": { @@ -206,42 +210,42 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_sns.CfnTopicPolicy", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_sns.TopicPolicy", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_sns.Topic", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "cdk-ses-configuration-set-integ/BootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnParameter", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "cdk-ses-configuration-set-integ/CheckBootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnRule", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.Stack", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "ConfigurationSetInteg": { @@ -257,7 +261,7 @@ "path": "ConfigurationSetInteg/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.17" + "version": "10.4.2" } }, "DeployAssert": { @@ -268,22 +272,22 @@ "id": "BootstrapVersion", "path": "ConfigurationSetInteg/DefaultTest/DeployAssert/BootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnParameter", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } }, "CheckBootstrapVersion": { "id": "CheckBootstrapVersion", "path": "ConfigurationSetInteg/DefaultTest/DeployAssert/CheckBootstrapVersion", "constructInfo": { - "fqn": "aws-cdk-lib.CfnRule", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.Stack", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } }, @@ -303,13 +307,13 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.17" + "version": "10.4.2" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.App", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.4.2" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.ts index 5a212d34e6426..43876905fd3cd 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.configuration-set.ts @@ -1,4 +1,4 @@ -import { App, Stack, StackProps } from 'aws-cdk-lib'; +import { App, Duration, Stack, StackProps } from 'aws-cdk-lib'; import * as integ from '@aws-cdk/integ-tests-alpha'; import { Construct } from 'constructs'; import * as ses from 'aws-cdk-lib/aws-ses'; @@ -8,7 +8,9 @@ class TestStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - const configurationSet = new ses.ConfigurationSet(this, 'ConfigurationSet'); + const configurationSet = new ses.ConfigurationSet(this, 'ConfigurationSet', { + maxDeliveryDuration: Duration.minutes(10), + }); const topic = new sns.Topic(this, 'Topic'); @@ -32,5 +34,3 @@ const app = new App(); new integ.IntegTest(app, 'ConfigurationSetInteg', { testCases: [new TestStack(app, 'cdk-ses-configuration-set-integ')], }); - -app.synth(); diff --git a/packages/aws-cdk-lib/aws-ses/README.md b/packages/aws-cdk-lib/aws-ses/README.md index ad64f88448abd..15b3fd6d79ebc 100644 --- a/packages/aws-cdk-lib/aws-ses/README.md +++ b/packages/aws-cdk-lib/aws-ses/README.md @@ -134,6 +134,8 @@ set to an email, all of the rules in that configuration set are applied to the e Use the `ConfigurationSet` construct to create a configuration set: ```ts +import { Duration } from 'aws-cdk-lib'; + declare const myPool: ses.IDedicatedIpPool; new ses.ConfigurationSet(this, 'ConfigurationSet', { @@ -141,6 +143,10 @@ new ses.ConfigurationSet(this, 'ConfigurationSet', { suppressionReasons: ses.SuppressionReasons.COMPLAINTS_ONLY, tlsPolicy: ses.ConfigurationSetTlsPolicy.REQUIRE, dedicatedIpPool: myPool, + // Specify maximum delivery time + // This configuration can be useful in such cases as time-sensitive emails (like those containing a one-time-password), + // transactional emails, and email that you want to ensure isn't delivered during non-business hours. + maxDeliveryDuration: Duration.minutes(10), }); ``` diff --git a/packages/aws-cdk-lib/aws-ses/lib/configuration-set.ts b/packages/aws-cdk-lib/aws-ses/lib/configuration-set.ts index 5e6dd58faf3ac..4427f1c490eee 100644 --- a/packages/aws-cdk-lib/aws-ses/lib/configuration-set.ts +++ b/packages/aws-cdk-lib/aws-ses/lib/configuration-set.ts @@ -3,7 +3,7 @@ import { ConfigurationSetEventDestination, ConfigurationSetEventDestinationOptio import { IDedicatedIpPool } from './dedicated-ip-pool'; import { undefinedIfNoKeys } from './private/utils'; import { CfnConfigurationSet } from './ses.generated'; -import { IResource, Resource } from '../../core'; +import { Duration, IResource, Resource, Token } from '../../core'; /** * A configuration set @@ -80,6 +80,15 @@ export interface ConfigurationSetProps { * @default - VDM options not configured at the configuration set level. In this case, use account level settings. (To set the account level settings using CDK, use the `VdmAttributes` Construct.) */ readonly vdmOptions?: VdmOptions; + + /** + * The maximum amount of time that Amazon SES API v2 will attempt delivery of email. + * + * This value must be greater than or equal to 5 minutes and less than or equal to 14 hours. + * + * @default undefined - SES defaults to 14 hours + */ + readonly maxDeliveryDuration?: Duration; } /** @@ -158,10 +167,20 @@ export class ConfigurationSet extends Resource implements IConfigurationSet { physicalName: props.configurationSetName, }); + if (props.maxDeliveryDuration && !Token.isUnresolved(props.maxDeliveryDuration)) { + if (props.maxDeliveryDuration.toMilliseconds() < Duration.minutes(5).toMilliseconds()) { + throw new Error(`The maximum delivery duration must be greater than or equal to 5 minutes (300_000 milliseconds), got: ${props.maxDeliveryDuration.toMilliseconds()} milliseconds.`); + } + if (props.maxDeliveryDuration.toSeconds() > Duration.hours(14).toSeconds()) { + throw new Error(`The maximum delivery duration must be less than or equal to 14 hours (50400 seconds), got: ${props.maxDeliveryDuration.toSeconds()} seconds.`); + } + } + const configurationSet = new CfnConfigurationSet(this, 'Resource', { deliveryOptions: undefinedIfNoKeys({ sendingPoolName: props.dedicatedIpPool?.dedicatedIpPoolName, tlsPolicy: props.tlsPolicy, + maxDeliverySeconds: props.maxDeliveryDuration?.toSeconds(), }), name: this.physicalName, reputationOptions: undefinedIfNoKeys({ diff --git a/packages/aws-cdk-lib/aws-ses/test/configuration-set.test.ts b/packages/aws-cdk-lib/aws-ses/test/configuration-set.test.ts index 17549322a4b50..4ad57366b7182 100644 --- a/packages/aws-cdk-lib/aws-ses/test/configuration-set.test.ts +++ b/packages/aws-cdk-lib/aws-ses/test/configuration-set.test.ts @@ -1,5 +1,5 @@ import { Template, Match } from '../../assertions'; -import { Stack } from '../../core'; +import { Duration, Stack } from '../../core'; import { ConfigurationSet, ConfigurationSetTlsPolicy, DedicatedIpPool, SuppressionReasons } from '../lib'; let stack: Stack; @@ -21,6 +21,7 @@ test('configuration set with options', () => { suppressionReasons: SuppressionReasons.COMPLAINTS_ONLY, tlsPolicy: ConfigurationSetTlsPolicy.REQUIRE, dedicatedIpPool: new DedicatedIpPool(stack, 'Pool'), + maxDeliveryDuration: Duration.seconds(300), }); Template.fromStack(stack).hasResourceProperties('AWS::SES::ConfigurationSet', { @@ -29,6 +30,7 @@ test('configuration set with options', () => { Ref: 'PoolD3F588B8', }, TlsPolicy: 'REQUIRE', + MaxDeliverySeconds: 300, }, SuppressionOptions: { SuppressedReasons: [ @@ -89,3 +91,21 @@ test('configuration set with vdmOptions not configured', () => { VdmOptions: Match.absent(), }); }); + +describe('maxDeliveryDuration', () => { + test.each([Duration.millis(999), Duration.minutes(4)])('invalid duration less than 5 minutes %s', (maxDeliveryDuration) => { + expect(() => { + new ConfigurationSet(stack, 'ConfigurationSet', { + maxDeliveryDuration, + }); + }).toThrow(`The maximum delivery duration must be greater than or equal to 5 minutes (300_000 milliseconds), got: ${maxDeliveryDuration.toMilliseconds()} milliseconds.`); + }); + + test('invalid duration greater than 14 hours', () => { + expect(() => { + new ConfigurationSet(stack, 'ConfigurationSet', { + maxDeliveryDuration: Duration.hours(14).plus(Duration.seconds(1)), + }); + }).toThrow('The maximum delivery duration must be less than or equal to 14 hours (50400 seconds), got: 50401 seconds.'); + }); +});