Skip to content

Commit 763ea37

Browse files
okinakaKenshin OKINAKA
andauthored
feat(ses): implement email template CRUD with stored, inline, and ARN variants (#573)
* feat(ses): implement email template CRUD with stored, inline, and ARN variants Adds CreateTemplate / GetTemplate / UpdateTemplate / DeleteTemplate / ListTemplates on both V2 REST JSON (/v2/email/templates) and V1 Query. SendEmail / SendTemplatedEmail resolve templates in three forms with Mustache-style {{var}} substitution against TemplateData: - TemplateName references a stored template - TemplateArn extracts the name from the template/<name> segment of an SES template ARN (region and account segments are not validated, mirroring how KMS / Secrets Manager handle ARNs in Floci) - TemplateContent (V2 only) supplies inline subject / text / html without storing the template first V2 Content.Template requires exactly one of TemplateName, TemplateArn, or TemplateContent; violations return BadRequestException (400). V1 SendTemplatedEmail accepts TemplateArn either alone or alongside Template (name) — the name is used for resolution when both are present, matching AWS where boto3 and the Java SDK keep Template as a required field and treat TemplateArn as supplementary for cross-account addressing. Templates persist region-scoped via StorageFactory in ses-templates.json. Template names reject leading/trailing whitespace (same rule as identities) and are validated through a single validateTemplateName helper reachable from every resolution path. listTemplates is sorted by CreatedTimestamp with TemplateName as a tiebreaker for deterministic output. Error codes: the service layer throws V1-style codes (TemplateDoesNotExist / AlreadyExists / InvalidTemplate / InvalidParameterValue); SesController remaps them to NotFoundException (404) / AlreadyExistsException (400) / BadRequestException (400) for V2 callers. * test(ses): add Python and Java SDK compatibility tests for templates Covers boto3 (ses / sesv2) and AWS Java SDK v2 (SesClient / SesV2Client) against the SES email template CRUD, inline TemplateContent, and TemplateArn paths. Happy path, duplicate-create rejection, missing-template lookups, Mustache-style variable substitution, and 3-way selector conflicts on V2 Content.Template are exercised on both SDK surfaces. Adds: - Python: ses_client / sesv2_client conftest fixtures and a tests/test_ses_templates.py suite covering V1 + V2 - Java: sesv2 Maven dependency, SesV2Client factory in TestFixtures, and a SesTemplateTest covering V1 + V2 * docs(ses): document email template CRUD and templated send Adds the new V1 template actions (CreateTemplate / GetTemplate / UpdateTemplate / DeleteTemplate / ListTemplates / SendTemplatedEmail) and the V2 /v2/email/templates endpoints to the SES service reference, and extends the README service matrix to mention template support on the SES / SES v2 rows. * fix(ses): add template actions to SES_ACTIONS for unsigned request routing Unsigned clients (curl, aws --no-sign-request) were falling through to the SQS handler because the six template Query actions were not registered in SES_ACTIONS. * fix(ses): narrow exception catch to JsonProcessingException in template endpoints Broad Exception catch was masking unexpected errors (e.g. NPE) as 400 responses. Narrowed to JsonProcessingException which is the only checked exception from objectMapper.readTree(). * test(ses): add template variable substitution edge cases and cross-region isolation Add SesServiceTemplateTest with 14 unit tests for applyTemplateData covering undefined variables, spaced/hyphenated names, unclosed braces, non-string JSON values, regex metacharacters, and case sensitivity. Add cross-region isolation integration test verifying templates created in one region are not visible in another. * test(ses): add awscli compatibility tests for template CRUD and templated send Cover create, get, update, list, delete template and send-templated-email via the AWS CLI. Uses JSON parameter format to avoid awscli parser issues with Mustache-style {{variable}} syntax. --------- Co-authored-by: Kenshin OKINAKA <[email protected]>
1 parent e0dc45e commit 763ea37

18 files changed

Lines changed: 2583 additions & 14 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ All default images are configurable via environment variables, useful for pinnin
203203
| **EC2** | In-process | VPCs, subnets, security groups, instances, AMIs, key pairs, internet gateways, route tables, Elastic IPs, tags |
204204
| **ACM** | In-process | Certificate issuance, validation lifecycle |
205205
| **ECR** | In-process + **real OCI registry** | Repositories, image push / pull via stock `docker`, image-backed Lambda functions |
206-
| **SES** | In-process | Send email / raw email, identity verification, DKIM attributes |
207-
| **SES v2 (HTTP)** | In-process | REST JSON API, identities, DKIM, feedback attributes, account sending |
206+
| **SES** | In-process | Send email / raw email, identity verification, DKIM attributes, email templates with `{{var}}` substitution |
207+
| **SES v2 (HTTP)** | In-process | REST JSON API, identities, DKIM, feedback attributes, account sending, email templates with `{{var}}` substitution |
208208
| **OpenSearch** | **Real Docker containers** | Domain CRUD, tags, versions, instance types, upgrade stubs |
209209
| **AppConfig** | In-process | Applications, environments, profiles, hosted configuration versions, deployments |
210210
| **AppConfigData** | In-process | Configuration sessions, dynamic configuration retrieval |

compatibility-tests/sdk-test-awscli/test/ses.bats

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,97 @@ teardown_file() {
140140
refute_output --partial "$TEST_EMAIL"
141141
refute_output --partial "$TEST_DOMAIN"
142142
}
143+
144+
# ──────────────── Template CRUD ────────────────
145+
146+
@test "SES: create template" {
147+
local tpl_json
148+
tpl_json=$(cat <<TMPL
149+
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Hello {{name}}","TextPart":"Hi {{name}} from {{team}}","HtmlPart":"<p>Hi {{name}}</p>"}
150+
TMPL
151+
)
152+
run aws_cmd ses create-template --template "$tpl_json"
153+
assert_success
154+
}
155+
156+
@test "SES: create template - duplicate rejected" {
157+
local tpl_json
158+
tpl_json=$(cat <<TMPL
159+
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Dup","TextPart":"Dup"}
160+
TMPL
161+
)
162+
run aws_cmd ses create-template --template "$tpl_json"
163+
assert_failure
164+
assert_output --partial "AlreadyExists"
165+
}
166+
167+
@test "SES: get template" {
168+
run aws_cmd ses get-template --template-name "cli-tpl-${BATS_ROOT_PID}"
169+
assert_success
170+
name=$(json_get "$output" '.Template.TemplateName')
171+
subject=$(json_get "$output" '.Template.SubjectPart')
172+
[ "$name" = "cli-tpl-${BATS_ROOT_PID}" ]
173+
[ "$subject" = "Hello {{name}}" ]
174+
}
175+
176+
@test "SES: get template - not found" {
177+
run aws_cmd ses get-template --template-name "nonexistent-tpl"
178+
assert_failure
179+
assert_output --partial "TemplateDoesNotExist"
180+
}
181+
182+
@test "SES: update template" {
183+
local tpl_json
184+
tpl_json=$(cat <<TMPL
185+
{"TemplateName":"cli-tpl-${BATS_ROOT_PID}","SubjectPart":"Updated {{name}}","TextPart":"Updated {{name}} {{team}}","HtmlPart":"<p>Updated {{name}}</p>"}
186+
TMPL
187+
)
188+
run aws_cmd ses update-template --template "$tpl_json"
189+
assert_success
190+
191+
run aws_cmd ses get-template --template-name "cli-tpl-${BATS_ROOT_PID}"
192+
assert_success
193+
subject=$(json_get "$output" '.Template.SubjectPart')
194+
[ "$subject" = "Updated {{name}}" ]
195+
}
196+
197+
@test "SES: list templates includes created" {
198+
run aws_cmd ses list-templates
199+
assert_success
200+
assert_output --partial "cli-tpl-${BATS_ROOT_PID}"
201+
}
202+
203+
@test "SES: send templated email" {
204+
# Ensure an identity exists for sending
205+
aws_cmd ses verify-email-identity --email-address "[email protected]" >/dev/null 2>&1 || true
206+
207+
run aws_cmd ses send-templated-email \
208+
--source "[email protected]" \
209+
--destination "[email protected]" \
210+
--template "cli-tpl-${BATS_ROOT_PID}" \
211+
--template-data '{"name":"Alice","team":"floci"}'
212+
assert_success
213+
message_id=$(json_get "$output" '.MessageId')
214+
[ -n "$message_id" ]
215+
}
216+
217+
@test "SES: send templated email - unknown template" {
218+
run aws_cmd ses send-templated-email \
219+
--source "[email protected]" \
220+
--destination "[email protected]" \
221+
--template "nonexistent-tpl" \
222+
--template-data '{}'
223+
assert_failure
224+
assert_output --partial "TemplateDoesNotExist"
225+
}
226+
227+
@test "SES: delete template" {
228+
run aws_cmd ses delete-template --template-name "cli-tpl-${BATS_ROOT_PID}"
229+
assert_success
230+
}
231+
232+
@test "SES: delete template - not found" {
233+
run aws_cmd ses delete-template --template-name "nonexistent-tpl"
234+
assert_failure
235+
assert_output --partial "TemplateDoesNotExist"
236+
}

compatibility-tests/sdk-test-java/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@
174174
<groupId>software.amazon.awssdk</groupId>
175175
<artifactId>ses</artifactId>
176176
</dependency>
177+
<!-- SES V2 client -->
178+
<dependency>
179+
<groupId>software.amazon.awssdk</groupId>
180+
<artifactId>sesv2</artifactId>
181+
</dependency>
177182
<!-- OpenSearch Service management client -->
178183
<dependency>
179184
<groupId>software.amazon.awssdk</groupId>

compatibility-tests/sdk-test-java/src/main/java/com/floci/test/TestFixtures.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import software.amazon.awssdk.services.s3control.endpoints.S3ControlEndpointProvider;
2222
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
2323
import software.amazon.awssdk.services.ses.SesClient;
24+
import software.amazon.awssdk.services.sesv2.SesV2Client;
2425
import software.amazon.awssdk.services.sfn.SfnClient;
2526
import software.amazon.awssdk.services.sns.SnsClient;
2627
import software.amazon.awssdk.services.sqs.SqsClient;
@@ -392,6 +393,14 @@ public static SesClient sesClient() {
392393
.build();
393394
}
394395

396+
public static SesV2Client sesV2Client() {
397+
return SesV2Client.builder()
398+
.endpointOverride(ENDPOINT)
399+
.region(REGION)
400+
.credentialsProvider(CREDENTIALS)
401+
.build();
402+
}
403+
395404
public static RdsClient rdsClient() {
396405
return RdsClient.builder()
397406
.endpointOverride(ENDPOINT)

0 commit comments

Comments
 (0)