Skip to content

Commit

Permalink
fix(cognito): quote or mime-encode fromName to comply RFC 5322 (#23227
Browse files Browse the repository at this point in the history
)

Closes #18903

When `fromName` of `UserPoolEmail.withSES()` does not comply RFC 5322 atom or quoted-string, it will be quoted or mime-encoded.

For example:
|`fromName`|Template|Description|
|-|-|-|
|`'simple atom'`|`"simple atom <address@example.com>"`|as is|
|`'"quoted string"'`|`"\"quoted string\" <address@example.com>"`|as is|
|`'あいう'`|`"=?UTF-8?B?44GC44GE44GG?= <address@example.com>"`|mime encode (RFC 2047)|
|`'name@company'`|`"\"name@company\" <address@example.com>"`|make quoted-string|

For details, see [RFC 5322 Section 3.4](https://www.rfc-editor.org/rfc/rfc5322#section-3.4) and [RFC 2047](https://www.rfc-editor.org/rfc/rfc2047)

----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Construct Runtime Dependencies:

* [ ] This PR adds new construct runtime dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-construct-runtime-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Tietew committed Dec 19, 2022
1 parent ecedb00 commit 78d474a
Show file tree
Hide file tree
Showing 12 changed files with 450 additions and 59 deletions.
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-cognito/README.md
Expand Up @@ -431,6 +431,18 @@ new cognito.UserPool(this, 'myuserpool', {
});
```

If `fromName` does not comply RFC 5322 atom or quoted-string, it will be quoted or mime-encoded.

```ts
new cognito.UserPool(this, 'myuserpool', {
email: cognito.UserPoolEmail.withSES({
fromEmail: 'noreply@myawesomeapp.com',
fromName: 'myname@mycompany.com',
}),
});
// => From: "myname@mycompany.com" <noreply@myawesomeapp.com>
```

### Device Tracking

User pools can be configured to track devices that users have logged in to.
Expand Down
63 changes: 60 additions & 3 deletions packages/@aws-cdk/aws-cognito/lib/user-pool-email.ts
Expand Up @@ -164,9 +164,10 @@ class SESEmail extends UserPoolEmail {
throw new Error('Your stack region cannot be determined so "sesRegion" is required in SESOptions');
}

let from = this.options.fromEmail;
let from = encodeAndTest(this.options.fromEmail);
if (this.options.fromName) {
from = `${this.options.fromName} <${this.options.fromEmail}>`;
const fromName = formatFromName(this.options.fromName);
from = `${fromName} <${from}>`;
}

if (this.options.sesVerifiedDomain) {
Expand All @@ -177,7 +178,7 @@ class SESEmail extends UserPoolEmail {
}

return {
from: encodeAndTest(from),
from,
replyToEmailAddress: encodeAndTest(this.options.replyTo),
configurationSet: this.options.configurationSetName,
emailSendingAccount: 'DEVELOPER',
Expand All @@ -202,3 +203,59 @@ function encodeAndTest(input: string | undefined): string | undefined {
return undefined;
}
}

/**
* Formats `fromName` to comply RFC 5322
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.4
*/
function formatFromName(fromName: string): string {
// mime encode for non US-ASCII characters
// see RFC 2047 for details https://www.rfc-editor.org/rfc/rfc2047
if (!isAscii(fromName)) {
const base64Name = Buffer.from(fromName, 'utf-8').toString('base64');
return `=?UTF-8?B?${base64Name}?=`;
}

// makes a quoted-string unless fromName is a phrase (only atext and space)
// or a quoted-string already
if (!(isSimplePhrase(fromName) || isQuotedString(fromName))) {
// in quoted-string, `\` and `"` should be escaped by `\`
// e.g. `"foo \"bar\" \\baz"`
const quotedName = fromName.replace(/[\\"]/g, (ch) => `\\${ch}`);
return `"${quotedName}"`;
}

// otherwise, returns as is
return fromName;
}

/**
* Returns whether the input is a printable US-ASCII string
*/
function isAscii(input: string): boolean {
// U+0020 (space) - U+007E (`~`)
return /^[\u0020-\u007E]+$/u.test(input);
}

/**
* Returns whether the input is a phrase excluding quoted-string
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.2
*/
function isSimplePhrase(input: string): boolean {
return /^[\w !#$%&'*+-\/=?^_`{|}~]+$/.test(input);
}

/**
* Returns whether the input is already a quoted-string
*
* @see https://www.rfc-editor.org/rfc/rfc5322#section-3.2.4
*/
function isQuotedString(input: string): boolean {
// in quoted-string, `\` and `"` should be esacaped by `\`
//
// match: `"foo.bar"` / `"foo \"bar\""` / `"foo \\ bar"`
// not match: `"bare " dquote"` / `"unclosed escape \"` / `"unclosed dquote`
return /^"(?:[^\\"]|\\.)*"$/.test(input);
}
@@ -0,0 +1,19 @@
{
"version": "22.0.0",
"files": {
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
"source": {
"path": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
@@ -0,0 +1,36 @@
{
"Parameters": {
"BootstrapVersion": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
}
},
"Rules": {
"CheckBootstrapVersion": {
"Assertions": [
{
"Assert": {
"Fn::Not": [
{
"Fn::Contains": [
[
"1",
"2",
"3",
"4",
"5"
],
{
"Ref": "BootstrapVersion"
}
]
}
]
},
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
}
]
}
}
}
@@ -1 +1 @@
{"version":"20.0.0"}
{"version":"22.0.0"}
@@ -1,15 +1,15 @@
{
"version": "20.0.0",
"version": "22.0.0",
"files": {
"3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091": {
"55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838": {
"source": {
"path": "integ-user-pool-signup-code.template.json",
"path": "integ-user-ses-email.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091.json",
"objectKey": "55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
Expand Down
Expand Up @@ -20,7 +20,7 @@
},
"EmailConfiguration": {
"EmailSendingAccount": "DEVELOPER",
"From": "noreply@example.com",
"From": "\"myname@mycompany.com\" <noreply@example.com>",
"ReplyToEmailAddress": "support@example.com",
"SourceArn": {
"Fn::Join": [
Expand Down
@@ -1,14 +1,12 @@
{
"version": "20.0.0",
"version": "22.0.0",
"testCases": {
"integ.user-pool-ses-email": {
"IntegTest/DefaultTest": {
"stacks": [
"integ-user-pool-signup-code"
"integ-user-ses-email"
],
"diffAssets": false,
"stackUpdateWorkflow": true
"assertionStack": "IntegTest/DefaultTest/DeployAssert",
"assertionStackName": "IntegTestDefaultTestDeployAssertE3E7D2A4"
}
},
"synthContext": {},
"enableLookups": false
}
}
@@ -1,33 +1,27 @@
{
"version": "20.0.0",
"version": "22.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
},
"integ-user-pool-signup-code.assets": {
"integ-user-ses-email.assets": {
"type": "cdk:asset-manifest",
"properties": {
"file": "integ-user-pool-signup-code.assets.json",
"file": "integ-user-ses-email.assets.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"integ-user-pool-signup-code": {
"integ-user-ses-email": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "integ-user-pool-signup-code.template.json",
"templateFile": "integ-user-ses-email.template.json",
"validateOnSynth": false,
"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}/3294a2beef1e4a711276251bf311cdf22b70152f30241f0d155898e2ab9ad091.json",
"stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/55384e618066ba251d0576ca224e2109d90f1e97e067d2d9bfb1476d43fff838.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
"integ-user-pool-signup-code.assets"
"integ-user-ses-email.assets"
],
"lookupRole": {
"arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}",
Expand All @@ -36,35 +30,88 @@
}
},
"dependencies": [
"integ-user-pool-signup-code.assets"
"integ-user-ses-email.assets"
],
"metadata": {
"/integ-user-pool-signup-code/myuserpool/Resource": [
"/integ-user-ses-email/myuserpool/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "myuserpool01998219"
}
],
"/integ-user-pool-signup-code/user-pool-id": [
"/integ-user-ses-email/user-pool-id": [
{
"type": "aws:cdk:logicalId",
"data": "userpoolid"
}
],
"/integ-user-pool-signup-code/BootstrapVersion": [
"/integ-user-ses-email/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "BootstrapVersion"
}
],
"/integ-user-pool-signup-code/CheckBootstrapVersion": [
"/integ-user-ses-email/CheckBootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "CheckBootstrapVersion"
}
]
},
"displayName": "integ-user-pool-signup-code"
"displayName": "integ-user-ses-email"
},
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets": {
"type": "cdk:asset-manifest",
"properties": {
"file": "IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"IntegTestDefaultTestDeployAssertE3E7D2A4": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json",
"validateOnSynth": false,
"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",
"requiresBootstrapStackVersion": 6,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
"additionalDependencies": [
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets"
],
"lookupRole": {
"arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}",
"requiresBootstrapStackVersion": 8,
"bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
}
},
"dependencies": [
"IntegTestDefaultTestDeployAssertE3E7D2A4.assets"
],
"metadata": {
"/IntegTest/DefaultTest/DeployAssert/BootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "BootstrapVersion"
}
],
"/IntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [
{
"type": "aws:cdk:logicalId",
"data": "CheckBootstrapVersion"
}
]
},
"displayName": "IntegTest/DefaultTest/DeployAssert"
},
"Tree": {
"type": "cdk:tree",
"properties": {
"file": "tree.json"
}
}
}
}

0 comments on commit 78d474a

Please sign in to comment.