@kensio/yulin 1.21.1 → 1.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-commands.js +13 -5
- package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-options.d.ts +1 -1
- package/dist/service/ses/command/configuration-set/sim-ses-configuration-set-options.js +9 -9
- package/dist/service/ses/command/send/send.command.d.ts +10 -1
- package/dist/service/ses/command/send/sim-ses-read-content.d.ts +2 -1
- package/dist/service/ses/command/send/sim-ses-send-email.js +1 -0
- package/dist/service/ses/command/send/sim-ses-service-send.js +1 -0
- package/dist/service/ses/command/send/sim-ses-simple-content.js +26 -5
- package/dist/service/ses/command/send/sim-ses-template-send.js +1 -0
- package/dist/service/ses/command/suppression/suppression.command.d.ts +3 -2
- package/dist/service/ses/configuration-set/sim-ses-configuration-set.d.ts +9 -10
- package/dist/service/ses/configuration-set/sim-ses-configuration-set.js +7 -8
- package/dist/service/ses/email/sim-ses-sent-email-store.d.ts +8 -0
- package/dist/service/ses/email/sim-ses-sent-email-store.js +17 -0
- package/dist/service/ses/email/sim-ses-sent-email.d.ts +18 -0
- package/dist/service/ses/email/sim-ses-sent-email.js +3 -0
- package/dist/service/ses/error/sim-ses.error.d.ts +10 -0
- package/dist/service/ses/error/sim-ses.error.js +10 -0
- package/dist/service/ses/feedback/sim-ses-feedback-recorder.d.ts +34 -0
- package/dist/service/ses/feedback/sim-ses-feedback-recorder.js +55 -0
- package/dist/service/ses/index.d.ts +3 -2
- package/dist/service/ses/index.js +2 -1
- package/dist/service/ses/sim-ses-commands.d.ts +2 -0
- package/dist/service/ses/sim-ses-commands.js +9 -0
- package/dist/service/ses/sim-ses-configuration-sets.d.ts +4 -4
- package/dist/service/ses/sim-ses-configuration-sets.js +4 -4
- package/dist/service/ses/sim-ses-suppression.d.ts +3 -2
- package/dist/service/ses/sim-ses-suppression.js +3 -2
- package/dist/service/ses/sim-ses-v2.d.ts +13 -0
- package/dist/service/ses/sim-ses-v2.js +13 -0
- package/dist/service/ses/suppression/sim-ses-suppressed-destination.d.ts +3 -4
- package/dist/service/ses/suppression/sim-ses-suppressed-destination.js +3 -4
- package/dist/service/ses/suppression/sim-ses-suppression-list.d.ts +4 -4
- package/dist/service/ses/suppression/sim-ses-suppression-list.js +4 -4
- package/docs/services/ses/README.md +164 -41
- package/package.json +1 -1
|
@@ -82,15 +82,13 @@ export class SimSesConfigurationSetCommands {
|
|
|
82
82
|
/**
|
|
83
83
|
* What GetConfigurationSet answers with.
|
|
84
84
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* Sending, delivery and reputation options report their applied defaults.
|
|
86
|
+
* Suppression options stay absent unless the set declared an override.
|
|
87
87
|
*/
|
|
88
88
|
function reported(configurationSet) {
|
|
89
89
|
return {
|
|
90
90
|
ConfigurationSetName: configurationSet.configurationSetName,
|
|
91
|
-
|
|
92
|
-
SuppressedReasons: configurationSet.suppressedReasons,
|
|
93
|
-
},
|
|
91
|
+
...reportedSuppressionOptions(configurationSet),
|
|
94
92
|
SendingOptions: { SendingEnabled: configurationSet.sendingEnabled },
|
|
95
93
|
DeliveryOptions: {
|
|
96
94
|
TlsPolicy: configurationSet.deliveryOptions.tlsPolicy,
|
|
@@ -102,3 +100,13 @@ function reported(configurationSet) {
|
|
|
102
100
|
},
|
|
103
101
|
};
|
|
104
102
|
}
|
|
103
|
+
function reportedSuppressionOptions(configurationSet) {
|
|
104
|
+
if (configurationSet.suppressedReasons === undefined) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
SuppressionOptions: {
|
|
109
|
+
SuppressedReasons: configurationSet.suppressedReasons,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -5,6 +5,6 @@ import type { SimCreateConfigurationSetCommandInput } from "./configuration-set.
|
|
|
5
5
|
* refuses.
|
|
6
6
|
*
|
|
7
7
|
* A set declaring none of these gets the defaults real SES applies: sending
|
|
8
|
-
* on,
|
|
8
|
+
* on, account-level suppression, optional TLS and no reputation metrics.
|
|
9
9
|
*/
|
|
10
10
|
export declare function readSimSesConfigurationSetOptions(input: SimCreateConfigurationSetCommandInput): SimSesConfigurationSetOptions;
|
|
@@ -11,11 +11,11 @@ const tlsPolicies = new Set(["REQUIRE", "OPTIONAL"]);
|
|
|
11
11
|
* refuses.
|
|
12
12
|
*
|
|
13
13
|
* A set declaring none of these gets the defaults real SES applies: sending
|
|
14
|
-
* on,
|
|
14
|
+
* on, account-level suppression, optional TLS and no reputation metrics.
|
|
15
15
|
*/
|
|
16
16
|
export function readSimSesConfigurationSetOptions(input) {
|
|
17
17
|
return {
|
|
18
|
-
suppressedReasons: readSuppressedReasons(input.SuppressionOptions
|
|
18
|
+
suppressedReasons: readSuppressedReasons(input.SuppressionOptions),
|
|
19
19
|
sendingEnabled: input.SendingOptions?.SendingEnabled ?? true,
|
|
20
20
|
deliveryOptions: readDeliveryOptions(input.DeliveryOptions),
|
|
21
21
|
reputationOptions: {
|
|
@@ -26,15 +26,15 @@ export function readSimSesConfigurationSetOptions(input) {
|
|
|
26
26
|
/**
|
|
27
27
|
* The suppression reasons a set names, refusing one SES has no meaning for.
|
|
28
28
|
*
|
|
29
|
-
* An absent `SuppressionOptions`
|
|
30
|
-
* account-level
|
|
31
|
-
*
|
|
29
|
+
* An absent `SuppressionOptions` returns undefined, which preserves the
|
|
30
|
+
* account-level fallback. A present group with no reasons returns an empty
|
|
31
|
+
* override and disables suppression for messages sent through the set.
|
|
32
32
|
*/
|
|
33
|
-
function readSuppressedReasons(
|
|
34
|
-
if (
|
|
35
|
-
return
|
|
33
|
+
function readSuppressedReasons(options) {
|
|
34
|
+
if (options === undefined) {
|
|
35
|
+
return undefined;
|
|
36
36
|
}
|
|
37
|
-
return
|
|
37
|
+
return (options.SuppressedReasons ?? []).map((reason) => {
|
|
38
38
|
if (!isSuppressedReason(reason)) {
|
|
39
39
|
throw new SimSesBadRequestException(`1 validation error detected: Value '${reason}' at ` +
|
|
40
40
|
`'suppressionOptions.suppressedReasons' failed to satisfy ` +
|
|
@@ -11,11 +11,20 @@ export interface SimSesBody {
|
|
|
11
11
|
readonly Text?: SimSesContent | undefined;
|
|
12
12
|
readonly Html?: SimSesContent | undefined;
|
|
13
13
|
}
|
|
14
|
+
export interface SimSesAttachment {
|
|
15
|
+
readonly RawContent?: Uint8Array | undefined;
|
|
16
|
+
readonly ContentDisposition?: "ATTACHMENT" | "INLINE" | undefined;
|
|
17
|
+
readonly FileName?: string | undefined;
|
|
18
|
+
readonly ContentDescription?: string | undefined;
|
|
19
|
+
readonly ContentId?: string | undefined;
|
|
20
|
+
readonly ContentTransferEncoding?: "BASE64" | "QUOTED_PRINTABLE" | "SEVEN_BIT" | undefined;
|
|
21
|
+
readonly ContentType?: string | undefined;
|
|
22
|
+
}
|
|
14
23
|
export interface SimSesMessage {
|
|
15
24
|
readonly Subject?: SimSesContent | undefined;
|
|
16
25
|
readonly Body?: SimSesBody | undefined;
|
|
17
26
|
readonly Headers?: readonly unknown[] | undefined;
|
|
18
|
-
readonly Attachments?: readonly
|
|
27
|
+
readonly Attachments?: readonly SimSesAttachment[] | undefined;
|
|
19
28
|
}
|
|
20
29
|
export interface SimSesRawMessage {
|
|
21
30
|
readonly Data?: Uint8Array | string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SimSesSentEmailBody } from "../../email/sim-ses-sent-email.js";
|
|
1
|
+
import type { SimSesSentEmailAttachment, SimSesSentEmailBody } from "../../email/sim-ses-sent-email.js";
|
|
2
2
|
/**
|
|
3
3
|
* What a message says, read out of the content of a send.
|
|
4
4
|
*
|
|
@@ -11,6 +11,7 @@ import type { SimSesSentEmailBody } from "../../email/sim-ses-sent-email.js";
|
|
|
11
11
|
export interface SimSesReadContent {
|
|
12
12
|
readonly subject: string;
|
|
13
13
|
readonly body: SimSesSentEmailBody;
|
|
14
|
+
readonly attachments: readonly SimSesSentEmailAttachment[];
|
|
14
15
|
/** The template this was rendered from, if it was rendered from a stored one. */
|
|
15
16
|
readonly templateName: string | undefined;
|
|
16
17
|
/** The data the placeholders were filled from, parsed out of the JSON. */
|
|
@@ -65,6 +65,7 @@ export class SimSesSendEmail {
|
|
|
65
65
|
replyToAddresses: [...(input.ReplyToAddresses ?? [])],
|
|
66
66
|
subject: content.subject,
|
|
67
67
|
body: content.body,
|
|
68
|
+
attachments: content.attachments,
|
|
68
69
|
templateName: content.templateName,
|
|
69
70
|
templateData: content.templateData,
|
|
70
71
|
configurationSetName,
|
|
@@ -62,6 +62,7 @@ export class SimSesServiceSend {
|
|
|
62
62
|
replyToAddresses: [...request.replyToAddresses],
|
|
63
63
|
subject: request.subject,
|
|
64
64
|
body: { text: request.body, html: undefined },
|
|
65
|
+
attachments: [],
|
|
65
66
|
templateName: undefined,
|
|
66
67
|
templateData: undefined,
|
|
67
68
|
configurationSetName,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SimSesBadRequestException
|
|
1
|
+
import { SimSesBadRequestException } from "../../error/sim-ses.error.js";
|
|
2
2
|
/**
|
|
3
3
|
* Read a message a send wrote out in full, refusing what real SES refuses and
|
|
4
4
|
* what this simulation does not model.
|
|
@@ -8,10 +8,6 @@ import { SimSesBadRequestException, SimSesUnsupportedOperationException, } from
|
|
|
8
8
|
* not have accepted.
|
|
9
9
|
*/
|
|
10
10
|
export function readSimSesSimpleMessage(message) {
|
|
11
|
-
if (message.Attachments !== undefined) {
|
|
12
|
-
throw new SimSesUnsupportedOperationException("Attachments are not simulated, so SendEmail refuses them rather than " +
|
|
13
|
-
"recording a message without them");
|
|
14
|
-
}
|
|
15
11
|
const subject = message.Subject?.Data;
|
|
16
12
|
if (subject === undefined) {
|
|
17
13
|
throw new SimSesBadRequestException("1 validation error detected: Value at 'content.simple.subject' " +
|
|
@@ -26,7 +22,32 @@ export function readSimSesSimpleMessage(message) {
|
|
|
26
22
|
return {
|
|
27
23
|
subject,
|
|
28
24
|
body: { text, html },
|
|
25
|
+
attachments: (message.Attachments ?? []).map((attachment, index) => readAttachment(attachment, index)),
|
|
29
26
|
templateName: undefined,
|
|
30
27
|
templateData: undefined,
|
|
31
28
|
};
|
|
32
29
|
}
|
|
30
|
+
/** Read and validate one attachment from a simple message. */
|
|
31
|
+
function readAttachment(attachment, index) {
|
|
32
|
+
if (attachment.RawContent === undefined) {
|
|
33
|
+
throw missingAttachmentMember(index, "rawContent");
|
|
34
|
+
}
|
|
35
|
+
if (attachment.FileName === undefined) {
|
|
36
|
+
throw missingAttachmentMember(index, "fileName");
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
rawContent: Uint8Array.from(attachment.RawContent),
|
|
40
|
+
fileName: attachment.FileName,
|
|
41
|
+
contentType: attachment.ContentType,
|
|
42
|
+
contentDisposition: attachment.ContentDisposition,
|
|
43
|
+
contentDescription: attachment.ContentDescription,
|
|
44
|
+
contentId: attachment.ContentId,
|
|
45
|
+
contentTransferEncoding: attachment.ContentTransferEncoding,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Build the SES validation error for a missing attachment member. */
|
|
49
|
+
function missingAttachmentMember(index, member) {
|
|
50
|
+
return new SimSesBadRequestException("1 validation error detected: Value at " +
|
|
51
|
+
`'content.simple.attachments.${String(index)}.${member}' failed to ` +
|
|
52
|
+
"satisfy constraint: Member must not be null");
|
|
53
|
+
}
|
|
@@ -2,8 +2,9 @@ import type { SimResponseMetadata } from "../../../aws/metadata/response-metadat
|
|
|
2
2
|
/**
|
|
3
3
|
* What a suppression list entry says, in the shape the SES v2 API reports it.
|
|
4
4
|
*
|
|
5
|
-
* `Attributes` is left out.
|
|
6
|
-
* address on the list
|
|
5
|
+
* `Attributes` is left out. Real SES uses it for details of the feedback event
|
|
6
|
+
* that put the address on the list. Explicit simulator feedback records the
|
|
7
|
+
* reason and time only.
|
|
7
8
|
*/
|
|
8
9
|
export interface SimSesSuppressedDestinationDetail {
|
|
9
10
|
readonly EmailAddress: string;
|
|
@@ -32,7 +32,7 @@ export interface SimSesConfigurationSetReputationOptions {
|
|
|
32
32
|
* Everything a configuration set holds apart from its name.
|
|
33
33
|
*/
|
|
34
34
|
export interface SimSesConfigurationSetOptions {
|
|
35
|
-
readonly suppressedReasons: readonly SimSesSuppressedReason[];
|
|
35
|
+
readonly suppressedReasons: readonly SimSesSuppressedReason[] | undefined;
|
|
36
36
|
readonly sendingEnabled: boolean;
|
|
37
37
|
readonly deliveryOptions: SimSesConfigurationSetDeliveryOptions;
|
|
38
38
|
readonly reputationOptions: SimSesConfigurationSetReputationOptions;
|
|
@@ -48,11 +48,9 @@ interface SimSesConfigurationSetProperties {
|
|
|
48
48
|
* A configuration set is a named group of settings a send can be made under,
|
|
49
49
|
* covering suppression, whether sending is on, how the message is handed on
|
|
50
50
|
* and whether reputation metrics are published. None of it changes what a
|
|
51
|
-
* message says
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* Sending is the exception, and it is left to the work that attaches a set to
|
|
55
|
-
* a send. Here the switch is recorded and acted on by nothing.
|
|
51
|
+
* message says. The sending switch controls acceptance, and the suppression
|
|
52
|
+
* reasons control feedback. Delivery and reputation options remain state for
|
|
53
|
+
* a test to read back.
|
|
56
54
|
*/
|
|
57
55
|
export declare class SimSesConfigurationSet {
|
|
58
56
|
readonly configurationSetName: string;
|
|
@@ -60,11 +58,12 @@ export declare class SimSesConfigurationSet {
|
|
|
60
58
|
/**
|
|
61
59
|
* The reasons that would put a recipient on the account suppression list.
|
|
62
60
|
*
|
|
63
|
-
*
|
|
64
|
-
* to the account setting
|
|
65
|
-
* suppression
|
|
61
|
+
* Undefined where the set declares no `SuppressionOptions`, which makes a
|
|
62
|
+
* send fall back to the account setting. An empty list is an explicit
|
|
63
|
+
* override that disables suppression for feedback recorded for a message
|
|
64
|
+
* sent through this set.
|
|
66
65
|
*/
|
|
67
|
-
readonly suppressedReasons: readonly SimSesSuppressedReason[];
|
|
66
|
+
readonly suppressedReasons: readonly SimSesSuppressedReason[] | undefined;
|
|
68
67
|
/** Whether SES would accept a send made through this set. */
|
|
69
68
|
readonly sendingEnabled: boolean;
|
|
70
69
|
readonly deliveryOptions: SimSesConfigurationSetDeliveryOptions;
|
|
@@ -10,11 +10,9 @@ export const simSesSuppressedReasons = [
|
|
|
10
10
|
* A configuration set is a named group of settings a send can be made under,
|
|
11
11
|
* covering suppression, whether sending is on, how the message is handed on
|
|
12
12
|
* and whether reputation metrics are published. None of it changes what a
|
|
13
|
-
* message says
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Sending is the exception, and it is left to the work that attaches a set to
|
|
17
|
-
* a send. Here the switch is recorded and acted on by nothing.
|
|
13
|
+
* message says. The sending switch controls acceptance, and the suppression
|
|
14
|
+
* reasons control feedback. Delivery and reputation options remain state for
|
|
15
|
+
* a test to read back.
|
|
18
16
|
*/
|
|
19
17
|
export class SimSesConfigurationSet {
|
|
20
18
|
configurationSetName;
|
|
@@ -22,9 +20,10 @@ export class SimSesConfigurationSet {
|
|
|
22
20
|
/**
|
|
23
21
|
* The reasons that would put a recipient on the account suppression list.
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
* to the account setting
|
|
27
|
-
* suppression
|
|
23
|
+
* Undefined where the set declares no `SuppressionOptions`, which makes a
|
|
24
|
+
* send fall back to the account setting. An empty list is an explicit
|
|
25
|
+
* override that disables suppression for feedback recorded for a message
|
|
26
|
+
* sent through this set.
|
|
28
27
|
*/
|
|
29
28
|
suppressedReasons;
|
|
30
29
|
/** Whether SES would accept a send made through this set. */
|
|
@@ -26,6 +26,14 @@ export declare class SimSesSentEmailStore {
|
|
|
26
26
|
* Every message this scope has accepted, oldest first.
|
|
27
27
|
*/
|
|
28
28
|
get all(): readonly SimSesSentEmail[];
|
|
29
|
+
/**
|
|
30
|
+
* Find an accepted message by the id SES returned for it.
|
|
31
|
+
*/
|
|
32
|
+
find(messageId: string): SimSesSentEmail | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Get an accepted message by id, refusing an id this scope never issued.
|
|
35
|
+
*/
|
|
36
|
+
require(messageId: string): SimSesSentEmail;
|
|
29
37
|
/**
|
|
30
38
|
* Keep a message SES has accepted.
|
|
31
39
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SimAwsMessageLog } from "../../aws/message/sim-aws-message-log.js";
|
|
2
|
+
import { SimSesFeedbackError } from "../error/sim-ses.error.js";
|
|
2
3
|
const hoursInADay = 24;
|
|
3
4
|
const millisecondsInAnHour = 60 * 60 * 1000;
|
|
4
5
|
/**
|
|
@@ -25,6 +26,22 @@ export class SimSesSentEmailStore {
|
|
|
25
26
|
get all() {
|
|
26
27
|
return [...this.#sent];
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Find an accepted message by the id SES returned for it.
|
|
31
|
+
*/
|
|
32
|
+
find(messageId) {
|
|
33
|
+
return this.#sent.find((email) => email.messageId === messageId);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get an accepted message by id, refusing an id this scope never issued.
|
|
37
|
+
*/
|
|
38
|
+
require(messageId) {
|
|
39
|
+
const email = this.find(messageId);
|
|
40
|
+
if (email === undefined) {
|
|
41
|
+
throw new SimSesFeedbackError(`No accepted sim SES message has id ${messageId}.`);
|
|
42
|
+
}
|
|
43
|
+
return email;
|
|
44
|
+
}
|
|
28
45
|
/**
|
|
29
46
|
* Keep a message SES has accepted.
|
|
30
47
|
*
|
|
@@ -19,6 +19,21 @@ export interface SimSesSentEmailBody {
|
|
|
19
19
|
readonly text: string | undefined;
|
|
20
20
|
readonly html: string | undefined;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* One structured attachment on a simple message.
|
|
24
|
+
*
|
|
25
|
+
* The bytes and metadata use the values from the SendEmail request. SES would
|
|
26
|
+
* use them to assemble MIME content after accepting the request.
|
|
27
|
+
*/
|
|
28
|
+
export interface SimSesSentEmailAttachment {
|
|
29
|
+
readonly rawContent: Uint8Array;
|
|
30
|
+
readonly fileName: string;
|
|
31
|
+
readonly contentType: string | undefined;
|
|
32
|
+
readonly contentDisposition: "ATTACHMENT" | "INLINE" | undefined;
|
|
33
|
+
readonly contentDescription: string | undefined;
|
|
34
|
+
readonly contentId: string | undefined;
|
|
35
|
+
readonly contentTransferEncoding: "BASE64" | "QUOTED_PRINTABLE" | "SEVEN_BIT" | undefined;
|
|
36
|
+
}
|
|
22
37
|
/**
|
|
23
38
|
* A recipient the account's suppression list held back.
|
|
24
39
|
*
|
|
@@ -37,6 +52,7 @@ interface SimSesSentEmailProperties {
|
|
|
37
52
|
readonly replyToAddresses: readonly string[];
|
|
38
53
|
readonly subject: string;
|
|
39
54
|
readonly body: SimSesSentEmailBody;
|
|
55
|
+
readonly attachments: readonly SimSesSentEmailAttachment[];
|
|
40
56
|
readonly templateName: string | undefined;
|
|
41
57
|
readonly templateData: Readonly<Record<string, unknown>> | undefined;
|
|
42
58
|
readonly configurationSetName: string | undefined;
|
|
@@ -64,6 +80,8 @@ export declare class SimSesSentEmail {
|
|
|
64
80
|
readonly replyToAddresses: readonly string[];
|
|
65
81
|
readonly subject: string;
|
|
66
82
|
readonly body: SimSesSentEmailBody;
|
|
83
|
+
/** Structured attachments in request order. */
|
|
84
|
+
readonly attachments: readonly SimSesSentEmailAttachment[];
|
|
67
85
|
/**
|
|
68
86
|
* The template this message was rendered from, if it was rendered from a
|
|
69
87
|
* stored one. A message written out in full has none, and so does one
|
|
@@ -19,6 +19,8 @@ export class SimSesSentEmail {
|
|
|
19
19
|
replyToAddresses;
|
|
20
20
|
subject;
|
|
21
21
|
body;
|
|
22
|
+
/** Structured attachments in request order. */
|
|
23
|
+
attachments;
|
|
22
24
|
/**
|
|
23
25
|
* The template this message was rendered from, if it was rendered from a
|
|
24
26
|
* stored one. A message written out in full has none, and so does one
|
|
@@ -51,6 +53,7 @@ export class SimSesSentEmail {
|
|
|
51
53
|
this.replyToAddresses = properties.replyToAddresses;
|
|
52
54
|
this.subject = properties.subject;
|
|
53
55
|
this.body = properties.body;
|
|
56
|
+
this.attachments = properties.attachments;
|
|
54
57
|
this.templateName = properties.templateName;
|
|
55
58
|
this.templateData = properties.templateData;
|
|
56
59
|
this.configurationSetName = properties.configurationSetName;
|
|
@@ -79,3 +79,13 @@ export declare class SimSesSendingPausedException extends SimSesError {
|
|
|
79
79
|
readonly name = "SendingPausedException";
|
|
80
80
|
constructor(message: string);
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A feedback event that cannot belong to the recorded message it names.
|
|
84
|
+
*
|
|
85
|
+
* This is a simulator diagnostic rather than an AWS error. A test supplies
|
|
86
|
+
* feedback through `recordFeedback`, and the error is raised at that call so
|
|
87
|
+
* an unrelated message or recipient cannot alter the suppression list.
|
|
88
|
+
*/
|
|
89
|
+
export declare class SimSesFeedbackError extends SimSesError {
|
|
90
|
+
readonly name = "SimSesFeedbackError";
|
|
91
|
+
}
|
|
@@ -88,3 +88,13 @@ export class SimSesSendingPausedException extends SimSesError {
|
|
|
88
88
|
super(message, { httpStatusCode: 400 });
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* A feedback event that cannot belong to the recorded message it names.
|
|
93
|
+
*
|
|
94
|
+
* This is a simulator diagnostic rather than an AWS error. A test supplies
|
|
95
|
+
* feedback through `recordFeedback`, and the error is raised at that call so
|
|
96
|
+
* an unrelated message or recipient cannot alter the suppression list.
|
|
97
|
+
*/
|
|
98
|
+
export class SimSesFeedbackError extends SimSesError {
|
|
99
|
+
name = "SimSesFeedbackError";
|
|
100
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SimClock } from "../../../util/clock/sim-clock.js";
|
|
2
|
+
import type { SimSesAccount } from "../account/sim-ses-account.js";
|
|
3
|
+
import type { SimSesConfigurationSetStore } from "../configuration-set/sim-ses-configuration-set-store.js";
|
|
4
|
+
import type { SimSesSentEmailStore } from "../email/sim-ses-sent-email-store.js";
|
|
5
|
+
import type { SimSesSuppressionList } from "../suppression/sim-ses-suppression-list.js";
|
|
6
|
+
import { type SimSesSuppressionReason } from "../suppression/sim-ses-suppression-reason.js";
|
|
7
|
+
import type { SimSesSuppressedDestination } from "../suppression/sim-ses-suppressed-destination.js";
|
|
8
|
+
/** A hard bounce or complaint reported for one accepted message recipient. */
|
|
9
|
+
export interface SimSesFeedback {
|
|
10
|
+
readonly messageId: string;
|
|
11
|
+
readonly emailAddress: string;
|
|
12
|
+
readonly reason: SimSesSuppressionReason;
|
|
13
|
+
}
|
|
14
|
+
interface SimSesFeedbackRecorderProperties {
|
|
15
|
+
readonly sent: SimSesSentEmailStore;
|
|
16
|
+
readonly configurationSets: SimSesConfigurationSetStore;
|
|
17
|
+
readonly account: SimSesAccount;
|
|
18
|
+
readonly suppression: SimSesSuppressionList;
|
|
19
|
+
readonly clock: SimClock;
|
|
20
|
+
}
|
|
21
|
+
/** Applies explicit delivery feedback to the account suppression list. */
|
|
22
|
+
export declare class SimSesFeedbackRecorder {
|
|
23
|
+
#private;
|
|
24
|
+
constructor(properties: SimSesFeedbackRecorderProperties);
|
|
25
|
+
/**
|
|
26
|
+
* Record a hard bounce or complaint for one recipient of an accepted
|
|
27
|
+
* message.
|
|
28
|
+
*
|
|
29
|
+
* The returned entry is undefined when suppression for the feedback reason
|
|
30
|
+
* was inactive for this send.
|
|
31
|
+
*/
|
|
32
|
+
record(feedback: SimSesFeedback): SimSesSuppressedDestination | undefined;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { simSesBareAddress } from "../email/sim-ses-address.js";
|
|
2
|
+
import { SimSesFeedbackError } from "../error/sim-ses.error.js";
|
|
3
|
+
import { requiredSimSesSuppressionAddress } from "../suppression/sim-ses-suppression-address.js";
|
|
4
|
+
import { requiredSimSesSuppressionReason, } from "../suppression/sim-ses-suppression-reason.js";
|
|
5
|
+
/** Applies explicit delivery feedback to the account suppression list. */
|
|
6
|
+
export class SimSesFeedbackRecorder {
|
|
7
|
+
#sent;
|
|
8
|
+
#configurationSets;
|
|
9
|
+
#account;
|
|
10
|
+
#suppression;
|
|
11
|
+
#clock;
|
|
12
|
+
constructor(properties) {
|
|
13
|
+
this.#sent = properties.sent;
|
|
14
|
+
this.#configurationSets = properties.configurationSets;
|
|
15
|
+
this.#account = properties.account;
|
|
16
|
+
this.#suppression = properties.suppression;
|
|
17
|
+
this.#clock = properties.clock;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Record a hard bounce or complaint for one recipient of an accepted
|
|
21
|
+
* message.
|
|
22
|
+
*
|
|
23
|
+
* The returned entry is undefined when suppression for the feedback reason
|
|
24
|
+
* was inactive for this send.
|
|
25
|
+
*/
|
|
26
|
+
record(feedback) {
|
|
27
|
+
const email = this.#sent.require(feedback.messageId);
|
|
28
|
+
const emailAddress = requiredSimSesSuppressionAddress(feedback.emailAddress);
|
|
29
|
+
const reason = requiredSimSesSuppressionReason(feedback.reason);
|
|
30
|
+
const recipient = this.#recipient(email, emailAddress);
|
|
31
|
+
if (!this.#isActive(email, reason)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return this.#suppression.put(recipient, reason, this.#clock.now());
|
|
35
|
+
}
|
|
36
|
+
#recipient(email, emailAddress) {
|
|
37
|
+
const wanted = emailAddress.toLowerCase();
|
|
38
|
+
const recipient = email.recipients
|
|
39
|
+
.map((address) => simSesBareAddress(address))
|
|
40
|
+
.find((address) => address.toLowerCase() === wanted);
|
|
41
|
+
if (recipient === undefined) {
|
|
42
|
+
throw new SimSesFeedbackError(`${emailAddress} is not a recipient of sim SES message ${email.messageId}.`);
|
|
43
|
+
}
|
|
44
|
+
return recipient;
|
|
45
|
+
}
|
|
46
|
+
#isActive(email, reason) {
|
|
47
|
+
if (email.configurationSetName !== undefined) {
|
|
48
|
+
const configurationSet = this.#configurationSets.find(email.configurationSetName);
|
|
49
|
+
if (configurationSet?.suppressedReasons !== undefined) {
|
|
50
|
+
return configurationSet.suppressedReasons.includes(reason);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return this.#account.isSuppressedFor(reason);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -9,11 +9,12 @@ export { requiredSimSesIdentityName, simSesIdentityDomain, simSesIdentityKey, ty
|
|
|
9
9
|
export { simSesArnPrefix, simSesConfigurationSetArn, simSesIdentityArn, } from "./sim-ses-arn.js";
|
|
10
10
|
export { SimSesConfigurationSet, type SimSesConfigurationSetDeliveryOptions, type SimSesConfigurationSetOptions, type SimSesConfigurationSetReputationOptions, type SimSesSuppressedReason, } from "./configuration-set/sim-ses-configuration-set.js";
|
|
11
11
|
export { SimSesConfigurationSetStore } from "./configuration-set/sim-ses-configuration-set-store.js";
|
|
12
|
-
export { SimSesSentEmail, type SimSesSentEmailBody, type SimSesSentEmailDestination, type SimSesSuppressedRecipient, } from "./email/sim-ses-sent-email.js";
|
|
12
|
+
export { SimSesSentEmail, type SimSesSentEmailAttachment, type SimSesSentEmailBody, type SimSesSentEmailDestination, type SimSesSuppressedRecipient, } from "./email/sim-ses-sent-email.js";
|
|
13
13
|
export { SimSesSentEmailStore } from "./email/sim-ses-sent-email-store.js";
|
|
14
|
+
export { type SimSesFeedback, SimSesFeedbackRecorder, } from "./feedback/sim-ses-feedback-recorder.js";
|
|
14
15
|
export { requiredSimSesFromAddress, simSesBareAddress, } from "./email/sim-ses-address.js";
|
|
15
16
|
export { SimSesSuppressedDestination } from "./suppression/sim-ses-suppressed-destination.js";
|
|
16
17
|
export { SimSesSuppressionList } from "./suppression/sim-ses-suppression-list.js";
|
|
17
18
|
export { requiredSimSesSuppressionReason, type SimSesSuppressionReason, } from "./suppression/sim-ses-suppression-reason.js";
|
|
18
19
|
export { SimSesAccount, type SimSesAccountContactDetails, type SimSesSendQuota, } from "./account/sim-ses-account.js";
|
|
19
|
-
export { SimSesAlreadyExistsException, SimSesBadRequestException, SimSesError, type SimSesErrorMetadata, SimSesMessageRejected, SimSesNotFoundException, SimSesSendingPausedException, SimSesUnsupportedOperationException, } from "./error/sim-ses.error.js";
|
|
20
|
+
export { SimSesAlreadyExistsException, SimSesBadRequestException, SimSesError, SimSesFeedbackError, type SimSesErrorMetadata, SimSesMessageRejected, SimSesNotFoundException, SimSesSendingPausedException, SimSesUnsupportedOperationException, } from "./error/sim-ses.error.js";
|
|
@@ -8,9 +8,10 @@ export { SimSesConfigurationSet, } from "./configuration-set/sim-ses-configurati
|
|
|
8
8
|
export { SimSesConfigurationSetStore } from "./configuration-set/sim-ses-configuration-set-store.js";
|
|
9
9
|
export { SimSesSentEmail, } from "./email/sim-ses-sent-email.js";
|
|
10
10
|
export { SimSesSentEmailStore } from "./email/sim-ses-sent-email-store.js";
|
|
11
|
+
export { SimSesFeedbackRecorder, } from "./feedback/sim-ses-feedback-recorder.js";
|
|
11
12
|
export { requiredSimSesFromAddress, simSesBareAddress, } from "./email/sim-ses-address.js";
|
|
12
13
|
export { SimSesSuppressedDestination } from "./suppression/sim-ses-suppressed-destination.js";
|
|
13
14
|
export { SimSesSuppressionList } from "./suppression/sim-ses-suppression-list.js";
|
|
14
15
|
export { requiredSimSesSuppressionReason, } from "./suppression/sim-ses-suppression-reason.js";
|
|
15
16
|
export { SimSesAccount, } from "./account/sim-ses-account.js";
|
|
16
|
-
export { SimSesAlreadyExistsException, SimSesBadRequestException, SimSesError, SimSesMessageRejected, SimSesNotFoundException, SimSesSendingPausedException, SimSesUnsupportedOperationException, } from "./error/sim-ses.error.js";
|
|
17
|
+
export { SimSesAlreadyExistsException, SimSesBadRequestException, SimSesError, SimSesFeedbackError, SimSesMessageRejected, SimSesNotFoundException, SimSesSendingPausedException, SimSesUnsupportedOperationException, } from "./error/sim-ses.error.js";
|
|
@@ -11,6 +11,7 @@ import { SimSesServiceSend } from "./command/send/sim-ses-service-send.js";
|
|
|
11
11
|
import { SimSesSuppressionCommands } from "./command/suppression/sim-ses-suppression-commands.js";
|
|
12
12
|
import { SimSesSuppressionList } from "./suppression/sim-ses-suppression-list.js";
|
|
13
13
|
import { SimSesSentEmailStore } from "./email/sim-ses-sent-email-store.js";
|
|
14
|
+
import { SimSesFeedbackRecorder } from "./feedback/sim-ses-feedback-recorder.js";
|
|
14
15
|
import { SimSesConfigurationSetStore } from "./configuration-set/sim-ses-configuration-set-store.js";
|
|
15
16
|
import { SimSesIdentityStore } from "./identity/sim-ses-identity-store.js";
|
|
16
17
|
import { SimSesTemplateCommands } from "./command/template/sim-ses-template-commands.js";
|
|
@@ -43,6 +44,7 @@ export declare class SimSesCommands {
|
|
|
43
44
|
readonly templateCommands: SimSesTemplateCommands;
|
|
44
45
|
readonly configurationSetCommands: SimSesConfigurationSetCommands;
|
|
45
46
|
readonly sendEmail: SimSesSendEmail;
|
|
47
|
+
readonly feedback: SimSesFeedbackRecorder;
|
|
46
48
|
/**
|
|
47
49
|
* The way another simulated service reaches this SES, which is the send
|
|
48
50
|
* without the IAM authorization a caller's request goes through.
|
|
@@ -16,6 +16,7 @@ import { SimSesVerifiedIdentityCheck } from "./command/send/sim-ses-verified-ide
|
|
|
16
16
|
import { SimSesSuppressionCommands } from "./command/suppression/sim-ses-suppression-commands.js";
|
|
17
17
|
import { SimSesSuppressionList } from "./suppression/sim-ses-suppression-list.js";
|
|
18
18
|
import { SimSesSentEmailStore } from "./email/sim-ses-sent-email-store.js";
|
|
19
|
+
import { SimSesFeedbackRecorder } from "./feedback/sim-ses-feedback-recorder.js";
|
|
19
20
|
import { SimSesConfigurationSetStore } from "./configuration-set/sim-ses-configuration-set-store.js";
|
|
20
21
|
import { SimSesIdentityStore } from "./identity/sim-ses-identity-store.js";
|
|
21
22
|
import { SimSesTemplateCommands } from "./command/template/sim-ses-template-commands.js";
|
|
@@ -39,6 +40,7 @@ export class SimSesCommands {
|
|
|
39
40
|
templateCommands;
|
|
40
41
|
configurationSetCommands;
|
|
41
42
|
sendEmail;
|
|
43
|
+
feedback;
|
|
42
44
|
/**
|
|
43
45
|
* The way another simulated service reaches this SES, which is the send
|
|
44
46
|
* without the IAM authorization a caller's request goes through.
|
|
@@ -103,6 +105,13 @@ export class SimSesCommands {
|
|
|
103
105
|
authorizer,
|
|
104
106
|
clock: background,
|
|
105
107
|
});
|
|
108
|
+
this.feedback = new SimSesFeedbackRecorder({
|
|
109
|
+
sent,
|
|
110
|
+
configurationSets,
|
|
111
|
+
account,
|
|
112
|
+
suppression,
|
|
113
|
+
clock: background,
|
|
114
|
+
});
|
|
106
115
|
this.serviceSend = new SimSesServiceSend({
|
|
107
116
|
sent,
|
|
108
117
|
identityCheck,
|
|
@@ -10,10 +10,10 @@ import { SimSesSuppression } from "./sim-ses-suppression.js";
|
|
|
10
10
|
* separate classes because one class holding every SES operation grows by a
|
|
11
11
|
* method with each one added.
|
|
12
12
|
*
|
|
13
|
-
* A configuration set
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* A configuration set holds suppression reasons, the sending switch, delivery
|
|
14
|
+
* options and the reputation switch. The sending switch acts on acceptance.
|
|
15
|
+
* Suppression reasons act when feedback is recorded for an accepted message.
|
|
16
|
+
* The remaining options are held for a test to read back.
|
|
17
17
|
*/
|
|
18
18
|
export declare abstract class SimSesConfigurationSets extends SimSesSuppression {
|
|
19
19
|
/**
|
|
@@ -7,10 +7,10 @@ import { SimSesSuppression } from "./sim-ses-suppression.js";
|
|
|
7
7
|
* separate classes because one class holding every SES operation grows by a
|
|
8
8
|
* method with each one added.
|
|
9
9
|
*
|
|
10
|
-
* A configuration set
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* A configuration set holds suppression reasons, the sending switch, delivery
|
|
11
|
+
* options and the reputation switch. The sending switch acts on acceptance.
|
|
12
|
+
* Suppression reasons act when feedback is recorded for an accepted message.
|
|
13
|
+
* The remaining options are held for a test to read back.
|
|
14
14
|
*/
|
|
15
15
|
export class SimSesConfigurationSets extends SimSesSuppression {
|
|
16
16
|
/**
|
|
@@ -13,8 +13,9 @@ interface SimSesSuppressionProperties {
|
|
|
13
13
|
* SES operation grows by a method with each one added, and the suppression
|
|
14
14
|
* commands are the part of the API a reader can take in on their own.
|
|
15
15
|
*
|
|
16
|
-
* Real SES fills this list from hard bounces and complaints.
|
|
17
|
-
*
|
|
16
|
+
* Real SES fills this list from hard bounces and complaints. A test supplies
|
|
17
|
+
* that feedback explicitly, and callers may also manage entries through the
|
|
18
|
+
* suppression commands.
|
|
18
19
|
*/
|
|
19
20
|
export declare abstract class SimSesSuppression {
|
|
20
21
|
protected readonly commands: SimSesCommands;
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* SES operation grows by a method with each one added, and the suppression
|
|
7
7
|
* commands are the part of the API a reader can take in on their own.
|
|
8
8
|
*
|
|
9
|
-
* Real SES fills this list from hard bounces and complaints.
|
|
10
|
-
*
|
|
9
|
+
* Real SES fills this list from hard bounces and complaints. A test supplies
|
|
10
|
+
* that feedback explicitly, and callers may also manage entries through the
|
|
11
|
+
* suppression commands.
|
|
11
12
|
*/
|
|
12
13
|
export class SimSesSuppression {
|
|
13
14
|
commands;
|
|
@@ -3,6 +3,8 @@ import type * as simSesCommands from "./command/sim-ses-command.types.js";
|
|
|
3
3
|
import type { SimSesRequestOptions } from "./command/sim-ses-request-options.js";
|
|
4
4
|
import type * as simSesServiceSend from "./command/send/sim-ses-service-send.js";
|
|
5
5
|
import type { SimSesSentEmail } from "./email/sim-ses-sent-email.js";
|
|
6
|
+
import type { SimSesFeedback } from "./feedback/sim-ses-feedback-recorder.js";
|
|
7
|
+
import type { SimSesSuppressedDestination } from "./suppression/sim-ses-suppressed-destination.js";
|
|
6
8
|
import type { SimSesIdentity } from "./identity/sim-ses-identity.js";
|
|
7
9
|
import type { SimSesTemplate } from "./template/sim-ses-template.js";
|
|
8
10
|
import { SimSesCfnResourceFactory } from "./cfn/sim-ses-cfn-resource-factory.js";
|
|
@@ -39,6 +41,17 @@ export declare class SimSesV2 extends SimSesConfigurationSets {
|
|
|
39
41
|
* account keeps no such record, which is exactly why a test needs one.
|
|
40
42
|
*/
|
|
41
43
|
sentEmails(): readonly SimSesSentEmail[];
|
|
44
|
+
/**
|
|
45
|
+
* Record a hard bounce or complaint for one recipient of an accepted
|
|
46
|
+
* message.
|
|
47
|
+
*
|
|
48
|
+
* Real feedback happens after SES hands a message to a mail system. A test
|
|
49
|
+
* process cannot observe that exchange, so this simulator-side operation
|
|
50
|
+
* supplies the feedback explicitly. Active feedback adds or updates the
|
|
51
|
+
* account suppression entry at the current simulated time. Inactive
|
|
52
|
+
* feedback returns undefined and leaves the list unchanged.
|
|
53
|
+
*/
|
|
54
|
+
recordFeedback(feedback: SimSesFeedback): SimSesSuppressedDestination | undefined;
|
|
42
55
|
/**
|
|
43
56
|
* Find an identity by the address or domain it names.
|
|
44
57
|
*
|
|
@@ -38,6 +38,19 @@ export class SimSesV2 extends SimSesConfigurationSets {
|
|
|
38
38
|
sentEmails() {
|
|
39
39
|
return this.commands.sent.all;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Record a hard bounce or complaint for one recipient of an accepted
|
|
43
|
+
* message.
|
|
44
|
+
*
|
|
45
|
+
* Real feedback happens after SES hands a message to a mail system. A test
|
|
46
|
+
* process cannot observe that exchange, so this simulator-side operation
|
|
47
|
+
* supplies the feedback explicitly. Active feedback adds or updates the
|
|
48
|
+
* account suppression entry at the current simulated time. Inactive
|
|
49
|
+
* feedback returns undefined and leaves the list unchanged.
|
|
50
|
+
*/
|
|
51
|
+
recordFeedback(feedback) {
|
|
52
|
+
return this.commands.feedback.record(feedback);
|
|
53
|
+
}
|
|
41
54
|
/**
|
|
42
55
|
* Find an identity by the address or domain it names.
|
|
43
56
|
*
|
|
@@ -7,10 +7,9 @@ interface SimSesSuppressedDestinationProperties {
|
|
|
7
7
|
/**
|
|
8
8
|
* One address on an account's suppression list.
|
|
9
9
|
*
|
|
10
|
-
* Real SES also reports a `MessageId` and a `FeedbackId` on
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* is no feedback event to point at.
|
|
10
|
+
* Real SES also reports a `MessageId` and a `FeedbackId` on an address added by
|
|
11
|
+
* delivery feedback. This simulation records the reason and time only. It
|
|
12
|
+
* does not publish or retain a feedback event.
|
|
14
13
|
*/
|
|
15
14
|
export declare class SimSesSuppressedDestination {
|
|
16
15
|
/** The address as it was given, case and all. */
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One address on an account's suppression list.
|
|
3
3
|
*
|
|
4
|
-
* Real SES also reports a `MessageId` and a `FeedbackId` on
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* is no feedback event to point at.
|
|
4
|
+
* Real SES also reports a `MessageId` and a `FeedbackId` on an address added by
|
|
5
|
+
* delivery feedback. This simulation records the reason and time only. It
|
|
6
|
+
* does not publish or retain a feedback event.
|
|
8
7
|
*/
|
|
9
8
|
export class SimSesSuppressedDestination {
|
|
10
9
|
/** The address as it was given, case and all. */
|
|
@@ -3,10 +3,10 @@ import type { SimSesSuppressionReason } from "./sim-ses-suppression-reason.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* The account-level suppression list of one simulated SES scope.
|
|
5
5
|
*
|
|
6
|
-
* Real SES fills this from hard bounces and complaints.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* run.
|
|
6
|
+
* Real SES fills this from hard bounces and complaints. A test supplies those
|
|
7
|
+
* events through the simulator's feedback operation. The ordinary suppression
|
|
8
|
+
* commands also manage the same entries, giving application support tooling
|
|
9
|
+
* and operations scripts somewhere to run.
|
|
10
10
|
*
|
|
11
11
|
* The list is region scoped, as identities and sends are. An address
|
|
12
12
|
* suppressed in one region says nothing about another.
|
|
@@ -3,10 +3,10 @@ import { SimSesSuppressedDestination } from "./sim-ses-suppressed-destination.js
|
|
|
3
3
|
/**
|
|
4
4
|
* The account-level suppression list of one simulated SES scope.
|
|
5
5
|
*
|
|
6
|
-
* Real SES fills this from hard bounces and complaints.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* run.
|
|
6
|
+
* Real SES fills this from hard bounces and complaints. A test supplies those
|
|
7
|
+
* events through the simulator's feedback operation. The ordinary suppression
|
|
8
|
+
* commands also manage the same entries, giving application support tooling
|
|
9
|
+
* and operations scripts somewhere to run.
|
|
10
10
|
*
|
|
11
11
|
* The list is region scoped, as identities and sends are. An address
|
|
12
12
|
* suppressed in one region says nothing about another.
|
|
@@ -14,7 +14,7 @@ SES specific types are imported from the `@kensio/yulin/ses` subpath.
|
|
|
14
14
|
## Asserting on a message that was sent
|
|
15
15
|
|
|
16
16
|
`sentEmails()` hands over the record. Each message carries who it was from, the three recipient
|
|
17
|
-
lists, the subject, the body and the message id SES answered with.
|
|
17
|
+
lists, the subject, the body, its attachments and the message id SES answered with.
|
|
18
18
|
|
|
19
19
|
```typescript sim-ses-send-and-assert
|
|
20
20
|
/**
|
|
@@ -59,6 +59,60 @@ read. The three recipient lists stay apart, leaving a test free to assert that a
|
|
|
59
59
|
`body` keeps `text` and `html` apart too. A message sent with only an HTML body reports `undefined`
|
|
60
60
|
for its text, and never the markup.
|
|
61
61
|
|
|
62
|
+
### Asserting on an attachment
|
|
63
|
+
|
|
64
|
+
A simple message may carry structured attachments. The record keeps them in request order.
|
|
65
|
+
`rawContent` contains a copy of the bytes from the request, and the other fields contain the supplied
|
|
66
|
+
SES attachment metadata.
|
|
67
|
+
|
|
68
|
+
```typescript sim-ses-attachments
|
|
69
|
+
/**
|
|
70
|
+
* Sending a generated CSV and reading it from the simulated SES record.
|
|
71
|
+
*/
|
|
72
|
+
|
|
73
|
+
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
74
|
+
|
|
75
|
+
import { SimAws } from "@kensio/yulin";
|
|
76
|
+
|
|
77
|
+
const simAws = new SimAws();
|
|
78
|
+
const ses = simAws.sesV2();
|
|
79
|
+
|
|
80
|
+
ses.verifyIdentity("hello@example.com");
|
|
81
|
+
ses.verifyIdentity("someone@example.org");
|
|
82
|
+
|
|
83
|
+
const csv = new TextEncoder().encode("word,meaning\n你好,hello\n");
|
|
84
|
+
|
|
85
|
+
await ses.sendEmail(
|
|
86
|
+
new SendEmailCommand({
|
|
87
|
+
FromEmailAddress: "hello@example.com",
|
|
88
|
+
Destination: { ToAddresses: ["someone@example.org"] },
|
|
89
|
+
Content: {
|
|
90
|
+
Simple: {
|
|
91
|
+
Subject: { Data: "Your vocabulary backup" },
|
|
92
|
+
Body: { Text: { Data: "Your backup is attached." } },
|
|
93
|
+
Attachments: [
|
|
94
|
+
{
|
|
95
|
+
RawContent: csv,
|
|
96
|
+
FileName: "vocabulary.csv",
|
|
97
|
+
ContentType: "text/csv; charset=utf-8",
|
|
98
|
+
ContentDisposition: "ATTACHMENT",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const [email] = ses.sentEmails();
|
|
107
|
+
const [attachment] = email?.attachments ?? [];
|
|
108
|
+
|
|
109
|
+
// vocabulary.csv "word,meaning\n你好,hello\n"
|
|
110
|
+
console.log(
|
|
111
|
+
attachment?.fileName,
|
|
112
|
+
new TextDecoder().decode(attachment?.rawContent),
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
62
116
|
## Verifying identities
|
|
63
117
|
|
|
64
118
|
Real SES verifies an email address by emailing it a link and a domain by looking for DNS records.
|
|
@@ -303,8 +357,9 @@ sending switch, the delivery options and the reputation switch are all declared
|
|
|
303
357
|
SES holds a set as state. A test can then assert what a stack declared, with no AWS account to read
|
|
304
358
|
it back from.
|
|
305
359
|
|
|
306
|
-
A set is attached to an identity, or named on a send. `SendingEnabled`
|
|
307
|
-
|
|
360
|
+
A set is attached to an identity, or named on a send. `SendingEnabled` acts during acceptance.
|
|
361
|
+
Suppression options decide whether explicit bounce or complaint feedback adds the recipient to the
|
|
362
|
+
account suppression list. The rest are held for a test to read back.
|
|
308
363
|
|
|
309
364
|
```typescript sim-ses-configuration-sets
|
|
310
365
|
/**
|
|
@@ -350,8 +405,13 @@ console.log(ses.allConfigurationSets().map((set) => set.configurationSetName));
|
|
|
350
405
|
```
|
|
351
406
|
|
|
352
407
|
A set declaring only its name gets the defaults real SES applies. Sending is on, TLS is optional,
|
|
353
|
-
reputation metrics are off and
|
|
354
|
-
|
|
408
|
+
reputation metrics are off and suppression falls back to the account reasons.
|
|
409
|
+
`GetConfigurationSet` reports the sending, delivery and reputation defaults. It leaves suppression
|
|
410
|
+
options absent where the set has no override.
|
|
411
|
+
|
|
412
|
+
An explicit empty override is different. `SuppressionOptions: { SuppressedReasons: [] }` disables
|
|
413
|
+
suppression for feedback on messages sent through that set. Simulated SES retains the empty list so
|
|
414
|
+
it does not fall back to the account reasons.
|
|
355
415
|
|
|
356
416
|
`findConfigurationSet` reaches one set by name and `allConfigurationSets` hands over every set in
|
|
357
417
|
the scope, oldest first.
|
|
@@ -436,9 +496,9 @@ set at a time.
|
|
|
436
496
|
domain and the events that report a click. The Virtual Deliverability Manager reports on engagement,
|
|
437
497
|
which this simulation never measures.
|
|
438
498
|
|
|
439
|
-
A set's suppression reasons
|
|
440
|
-
|
|
441
|
-
|
|
499
|
+
A set's suppression reasons override the account reasons when `recordFeedback` records a hard bounce
|
|
500
|
+
or complaint for a message sent through it. A set with no suppression options falls back to the
|
|
501
|
+
account reasons. The feedback entry goes onto the account suppression list.
|
|
442
502
|
|
|
443
503
|
`TlsPolicy` accepts `REQUIRE` and `OPTIONAL` and refuses anything else, and a `SuppressedReasons`
|
|
444
504
|
entry that is neither `BOUNCE` nor `COMPLAINT` is refused too. `MaxDeliverySeconds` takes whole
|
|
@@ -644,9 +704,9 @@ review is beyond what a test can assert on anyway.
|
|
|
644
704
|
## The suppression list
|
|
645
705
|
|
|
646
706
|
Real SES holds an account-level suppression list and fills it from hard bounces and complaints.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
707
|
+
Tests supply that feedback explicitly with `recordFeedback`. Suppression commands manage the same
|
|
708
|
+
list. The support tool that lists suppressed addresses, the form that removes one and the script
|
|
709
|
+
that seeds the list all have somewhere to run.
|
|
650
710
|
|
|
651
711
|
`PutSuppressedDestination`, `GetSuppressedDestination`, `ListSuppressedDestinations` and
|
|
652
712
|
`DeleteSuppressedDestination` manage it.
|
|
@@ -705,6 +765,70 @@ carries the answer. `suppressedRecipients` names who was held back and why,
|
|
|
705
765
|
and `isFullySuppressed` is the narrower question of whether the message reached nobody. A message to
|
|
706
766
|
two recipients with one of them suppressed went to the other.
|
|
707
767
|
|
|
768
|
+
### Recording a hard bounce or complaint
|
|
769
|
+
|
|
770
|
+
`recordFeedback` takes the message id of an accepted message, one of its recipient addresses and a
|
|
771
|
+
`BOUNCE` or `COMPLAINT` reason. Active feedback adds or updates the account suppression entry at the
|
|
772
|
+
current simulated time. The existing suppression commands and `suppressedDestinations()` read the
|
|
773
|
+
result.
|
|
774
|
+
|
|
775
|
+
```typescript sim-ses-feedback-suppression
|
|
776
|
+
/**
|
|
777
|
+
* Recording a hard bounce and observing the next send being suppressed.
|
|
778
|
+
*/
|
|
779
|
+
|
|
780
|
+
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
781
|
+
|
|
782
|
+
import { SimAws, SimFixedClock } from "@kensio/yulin";
|
|
783
|
+
|
|
784
|
+
const simAws = new SimAws({
|
|
785
|
+
clock: new SimFixedClock(new Date("2026-08-31T09:00:00.000Z")),
|
|
786
|
+
});
|
|
787
|
+
const ses = simAws.sesV2();
|
|
788
|
+
|
|
789
|
+
ses.verifyIdentity("hello@example.com");
|
|
790
|
+
ses.verifyIdentity("someone@example.org");
|
|
791
|
+
|
|
792
|
+
const message = new SendEmailCommand({
|
|
793
|
+
FromEmailAddress: "hello@example.com",
|
|
794
|
+
Destination: { ToAddresses: ["someone@example.org"] },
|
|
795
|
+
Content: {
|
|
796
|
+
Simple: {
|
|
797
|
+
Subject: { Data: "Welcome" },
|
|
798
|
+
Body: { Text: { Data: "Hi there" } },
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
const accepted = await ses.sendEmail(message);
|
|
804
|
+
|
|
805
|
+
ses.recordFeedback({
|
|
806
|
+
messageId: accepted.MessageId!,
|
|
807
|
+
emailAddress: "someone@example.org",
|
|
808
|
+
reason: "BOUNCE",
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
await ses.sendEmail(message);
|
|
812
|
+
|
|
813
|
+
const suppressed = ses.suppressedDestinations()[0];
|
|
814
|
+
const later = ses.sentEmails()[1];
|
|
815
|
+
|
|
816
|
+
// "BOUNCE" "2026-08-31T09:00:00.000Z" true
|
|
817
|
+
console.log(
|
|
818
|
+
suppressed?.reason,
|
|
819
|
+
suppressed?.lastUpdateTime.toISOString(),
|
|
820
|
+
later?.isFullySuppressed,
|
|
821
|
+
);
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
The operation refuses a message id this SES scope did not issue and an address that was not a
|
|
825
|
+
recipient of that message. Recipient matching ignores case and a display name, while the suppression
|
|
826
|
+
entry keeps the address spelling from the accepted message.
|
|
827
|
+
|
|
828
|
+
Without a configuration-set override, the account's `SuppressedReasons` decide whether feedback is
|
|
829
|
+
active. A configuration set with suppression options replaces those reasons for messages sent
|
|
830
|
+
through it. An empty override leaves the suppression list unchanged.
|
|
831
|
+
|
|
708
832
|
`ListSuppressedDestinations` pages with `PageSize` and `NextToken`, and narrows with `Reasons`,
|
|
709
833
|
`StartDate` and `EndDate`. Removing an address that was never on the list succeeds, so a form that
|
|
710
834
|
removes one twice has no failure to handle.
|
|
@@ -938,29 +1062,29 @@ time forward past the window sees the count fall the way an account's would.
|
|
|
938
1062
|
|
|
939
1063
|
## Simulated commands
|
|
940
1064
|
|
|
941
|
-
| Command | Notes
|
|
942
|
-
| --------------------------------- |
|
|
943
|
-
| `SendEmail` | `Content.Simple
|
|
944
|
-
| `CreateEmailIdentity` | Starts unverified. `Tags`, `DkimSigningAttributes` and `ConfigurationSetName` are held and reported back.
|
|
945
|
-
| `GetEmailIdentity` | Reports the DKIM, MAIL FROM, feedback, configuration set and tag settings the identity holds.
|
|
946
|
-
| `ListEmailIdentities` | Paged with `PageSize` and `NextToken`.
|
|
947
|
-
| `DeleteEmailIdentity` |
|
|
948
|
-
| `CreateEmailTemplate` | Substitution only. `Tags` are refused.
|
|
949
|
-
| `GetEmailTemplate` | Reports the wording with its placeholders unrendered.
|
|
950
|
-
| `UpdateEmailTemplate` | Replaces the wording outright, keeping the creation time.
|
|
951
|
-
| `ListEmailTemplates` | Names and creation times only, paged.
|
|
952
|
-
| `DeleteEmailTemplate` |
|
|
953
|
-
| `CreateConfigurationSet` | `TrackingOptions`, `VdmOptions` and `Tags` are refused.
|
|
954
|
-
| `GetConfigurationSet` | Reports
|
|
955
|
-
| `ListConfigurationSets` | Names only, paged with `PageSize` and `NextToken`.
|
|
956
|
-
| `DeleteConfigurationSet` |
|
|
957
|
-
| `GetAccount` | Reports `SuppressionAttributes` alongside the quota.
|
|
958
|
-
| `PutAccountDetails` | `MailType` and `WebsiteURL` are required, as on real SES.
|
|
959
|
-
| `PutAccountSuppressionAttributes` | No reasons at all turns the suppression list off.
|
|
960
|
-
| `PutSuppressedDestination` | Accepted in the sandbox, which real SES refuses.
|
|
961
|
-
| `GetSuppressedDestination` |
|
|
962
|
-
| `ListSuppressedDestinations` | Paged, and narrowed by `Reasons`, `StartDate` and `EndDate`.
|
|
963
|
-
| `DeleteSuppressedDestination` | Removing an address that is not on the list succeeds.
|
|
1065
|
+
| Command | Notes |
|
|
1066
|
+
| --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
1067
|
+
| `SendEmail` | `Content.Simple`, including structured attachments, and `Content.Template`. Recorded rather than delivered. |
|
|
1068
|
+
| `CreateEmailIdentity` | Starts unverified. `Tags`, `DkimSigningAttributes` and `ConfigurationSetName` are held and reported back. |
|
|
1069
|
+
| `GetEmailIdentity` | Reports the DKIM, MAIL FROM, feedback, configuration set and tag settings the identity holds. |
|
|
1070
|
+
| `ListEmailIdentities` | Paged with `PageSize` and `NextToken`. |
|
|
1071
|
+
| `DeleteEmailIdentity` | |
|
|
1072
|
+
| `CreateEmailTemplate` | Substitution only. `Tags` are refused. |
|
|
1073
|
+
| `GetEmailTemplate` | Reports the wording with its placeholders unrendered. |
|
|
1074
|
+
| `UpdateEmailTemplate` | Replaces the wording outright, keeping the creation time. |
|
|
1075
|
+
| `ListEmailTemplates` | Names and creation times only, paged. |
|
|
1076
|
+
| `DeleteEmailTemplate` | |
|
|
1077
|
+
| `CreateConfigurationSet` | `TrackingOptions`, `VdmOptions` and `Tags` are refused. |
|
|
1078
|
+
| `GetConfigurationSet` | Reports applied defaults and preserves whether suppression options were absent. |
|
|
1079
|
+
| `ListConfigurationSets` | Names only, paged with `PageSize` and `NextToken`. |
|
|
1080
|
+
| `DeleteConfigurationSet` | |
|
|
1081
|
+
| `GetAccount` | Reports `SuppressionAttributes` alongside the quota. |
|
|
1082
|
+
| `PutAccountDetails` | `MailType` and `WebsiteURL` are required, as on real SES. |
|
|
1083
|
+
| `PutAccountSuppressionAttributes` | No reasons at all turns the suppression list off. |
|
|
1084
|
+
| `PutSuppressedDestination` | Accepted in the sandbox, which real SES refuses. |
|
|
1085
|
+
| `GetSuppressedDestination` | |
|
|
1086
|
+
| `ListSuppressedDestinations` | Paged, and narrowed by `Reasons`, `StartDate` and `EndDate`. |
|
|
1087
|
+
| `DeleteSuppressedDestination` | Removing an address that is not on the list succeeds. |
|
|
964
1088
|
|
|
965
1089
|
Anything else refuses on send with `SimSdkUnsupportedCommandError`.
|
|
966
1090
|
|
|
@@ -978,19 +1102,18 @@ Anything else refuses on send with `SimSdkUnsupportedCommandError`.
|
|
|
978
1102
|
the template. Template data holding an object where the template wants a value is refused too,
|
|
979
1103
|
where real Handlebars would render `[object Object]`.
|
|
980
1104
|
- **`SendBulkEmail` is absent**, along with its per-recipient replacement data.
|
|
981
|
-
- **Nothing is delivered
|
|
982
|
-
event destinations.
|
|
1105
|
+
- **Nothing is delivered.** A test supplies a hard bounce or complaint through `recordFeedback`.
|
|
1106
|
+
Feedback is never generated automatically, and there are no event destinations.
|
|
983
1107
|
- **A configuration set acts on one send.** `SendingEnabled` refuses a send made through the set.
|
|
984
|
-
|
|
985
|
-
|
|
1108
|
+
Suppression reasons act on explicit feedback. Delivery options and the reputation switch are held
|
|
1109
|
+
and read back.
|
|
986
1110
|
- **A set name nothing created is still accepted.** Real SES refuses one on an identity and on a
|
|
987
1111
|
send. Both stand here and the name is recorded, because a test failing over a set missing from a
|
|
988
1112
|
local setup fails for a reason unrelated to what it asserts.
|
|
989
1113
|
- **A configuration set holds what it was created with.** The `Put` commands that change one group
|
|
990
1114
|
of options are absent. A set cannot be changed once it exists.
|
|
991
|
-
- **
|
|
992
|
-
|
|
993
|
-
or a complaint would suppress for, and both are absent.
|
|
1115
|
+
- **Feedback is explicit.** Hard bounces and complaints update the suppression list only when a test
|
|
1116
|
+
calls `recordFeedback`. Soft bounces are absent.
|
|
994
1117
|
- **Tenant-level suppression lists are left out.** A suppression command carrying `TenantName` is
|
|
995
1118
|
refused rather than answered from the account-level list.
|
|
996
1119
|
- **`PutSuppressedDestination` works in the sandbox.** Real SES refuses it until an account has
|
package/package.json
CHANGED