1use async_trait::async_trait;
2use chrono::Utc;
3use http::{Method, StatusCode};
4use serde_json::{json, Value};
5use std::collections::HashMap;
6
7use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
8
9use crate::fanout::SesDeliveryContext;
10use crate::state::{
11 AccountDetails, ConfigurationSet, Contact, ContactList, CustomVerificationEmailTemplate,
12 DedicatedIp, DedicatedIpPool, EmailIdentity, EmailTemplate, EventDestination, ExportJob,
13 ImportJob, MultiRegionEndpoint, ReputationEntityState, SentEmail, SharedSesState,
14 SuppressedDestination, Tenant, TenantResourceAssociation, Topic, TopicPreference,
15};
16
17pub struct SesV2Service {
18 state: SharedSesState,
19 delivery_ctx: Option<SesDeliveryContext>,
20}
21
22impl SesV2Service {
23 pub fn new(state: SharedSesState) -> Self {
24 Self {
25 state,
26 delivery_ctx: None,
27 }
28 }
29
30 pub fn with_delivery(mut self, ctx: SesDeliveryContext) -> Self {
32 self.delivery_ctx = Some(ctx);
33 self
34 }
35
36 fn resolve_action(req: &AwsRequest) -> Option<(&str, Option<String>, Option<String>)> {
119 let segs = &req.path_segments;
120
121 if segs.len() < 3 || segs[0] != "v2" || segs[1] != "email" {
123 return None;
124 }
125
126 let decode = |s: &str| {
128 percent_encoding::percent_decode_str(s)
129 .decode_utf8_lossy()
130 .into_owned()
131 };
132 let resource = segs.get(3).map(|s| decode(s));
133
134 match (req.method.clone(), segs.len()) {
135 (Method::GET, 3) if segs[2] == "account" => Some(("GetAccount", None, None)),
137
138 (Method::POST, 3) if segs[2] == "identities" => {
140 Some(("CreateEmailIdentity", None, None))
141 }
142 (Method::GET, 3) if segs[2] == "identities" => {
143 Some(("ListEmailIdentities", None, None))
144 }
145 (Method::GET, 4) if segs[2] == "identities" => {
147 Some(("GetEmailIdentity", resource, None))
148 }
149 (Method::DELETE, 4) if segs[2] == "identities" => {
150 Some(("DeleteEmailIdentity", resource, None))
151 }
152
153 (Method::POST, 3) if segs[2] == "configuration-sets" => {
155 Some(("CreateConfigurationSet", None, None))
156 }
157 (Method::GET, 3) if segs[2] == "configuration-sets" => {
158 Some(("ListConfigurationSets", None, None))
159 }
160 (Method::GET, 4) if segs[2] == "configuration-sets" => {
162 Some(("GetConfigurationSet", resource, None))
163 }
164 (Method::DELETE, 4) if segs[2] == "configuration-sets" => {
165 Some(("DeleteConfigurationSet", resource, None))
166 }
167
168 (Method::POST, 3) if segs[2] == "templates" => {
170 Some(("CreateEmailTemplate", None, None))
171 }
172 (Method::GET, 3) if segs[2] == "templates" => Some(("ListEmailTemplates", None, None)),
173 (Method::GET, 4) if segs[2] == "templates" => {
175 Some(("GetEmailTemplate", resource, None))
176 }
177 (Method::PUT, 4) if segs[2] == "templates" => {
178 Some(("UpdateEmailTemplate", resource, None))
179 }
180 (Method::DELETE, 4) if segs[2] == "templates" => {
181 Some(("DeleteEmailTemplate", resource, None))
182 }
183
184 (Method::POST, 3) if segs[2] == "outbound-emails" => Some(("SendEmail", None, None)),
186
187 (Method::POST, 3) if segs[2] == "outbound-bulk-emails" => {
189 Some(("SendBulkEmail", None, None))
190 }
191
192 (Method::POST, 3) if segs[2] == "contact-lists" => {
194 Some(("CreateContactList", None, None))
195 }
196 (Method::GET, 3) if segs[2] == "contact-lists" => {
197 Some(("ListContactLists", None, None))
198 }
199 (Method::GET, 4) if segs[2] == "contact-lists" => {
201 Some(("GetContactList", resource, None))
202 }
203 (Method::PUT, 4) if segs[2] == "contact-lists" => {
204 Some(("UpdateContactList", resource, None))
205 }
206 (Method::DELETE, 4) if segs[2] == "contact-lists" => {
207 Some(("DeleteContactList", resource, None))
208 }
209 (Method::POST, 3) if segs[2] == "tags" => Some(("TagResource", None, None)),
211 (Method::DELETE, 3) if segs[2] == "tags" => Some(("UntagResource", None, None)),
212 (Method::GET, 3) if segs[2] == "tags" => Some(("ListTagsForResource", None, None)),
213
214 (Method::POST, 5) if segs[2] == "contact-lists" && segs[4] == "contacts" => {
216 Some(("CreateContact", resource, None))
217 }
218 (Method::GET, 5) if segs[2] == "contact-lists" && segs[4] == "contacts" => {
219 Some(("ListContacts", resource, None))
220 }
221 (Method::POST, 6)
223 if segs[2] == "contact-lists" && segs[4] == "contacts" && segs[5] == "list" =>
224 {
225 Some(("ListContacts", resource, None))
226 }
227 (Method::GET, 6) if segs[2] == "contact-lists" && segs[4] == "contacts" => {
229 Some(("GetContact", resource, Some(decode(&segs[5]))))
230 }
231 (Method::PUT, 6) if segs[2] == "contact-lists" && segs[4] == "contacts" => {
232 Some(("UpdateContact", resource, Some(decode(&segs[5]))))
233 }
234 (Method::DELETE, 6) if segs[2] == "contact-lists" && segs[4] == "contacts" => {
235 Some(("DeleteContact", resource, Some(decode(&segs[5]))))
236 }
237
238 (Method::PUT, 4) if segs[2] == "suppression" && segs[3] == "addresses" => {
240 Some(("PutSuppressedDestination", None, None))
241 }
242 (Method::GET, 4) if segs[2] == "suppression" && segs[3] == "addresses" => {
243 Some(("ListSuppressedDestinations", None, None))
244 }
245 (Method::GET, 5) if segs[2] == "suppression" && segs[3] == "addresses" => {
247 Some(("GetSuppressedDestination", Some(decode(&segs[4])), None))
248 }
249 (Method::DELETE, 5) if segs[2] == "suppression" && segs[3] == "addresses" => {
250 Some(("DeleteSuppressedDestination", Some(decode(&segs[4])), None))
251 }
252
253 (Method::POST, 5)
255 if segs[2] == "configuration-sets" && segs[4] == "event-destinations" =>
256 {
257 Some(("CreateConfigurationSetEventDestination", resource, None))
258 }
259 (Method::GET, 5)
260 if segs[2] == "configuration-sets" && segs[4] == "event-destinations" =>
261 {
262 Some(("GetConfigurationSetEventDestinations", resource, None))
263 }
264 (Method::PUT, 6)
266 if segs[2] == "configuration-sets" && segs[4] == "event-destinations" =>
267 {
268 Some((
269 "UpdateConfigurationSetEventDestination",
270 resource,
271 Some(decode(&segs[5])),
272 ))
273 }
274 (Method::DELETE, 6)
275 if segs[2] == "configuration-sets" && segs[4] == "event-destinations" =>
276 {
277 Some((
278 "DeleteConfigurationSetEventDestination",
279 resource,
280 Some(decode(&segs[5])),
281 ))
282 }
283
284 (Method::GET, 5) if segs[2] == "identities" && segs[4] == "policies" => {
286 Some(("GetEmailIdentityPolicies", resource, None))
287 }
288 (Method::POST, 6) if segs[2] == "identities" && segs[4] == "policies" => Some((
290 "CreateEmailIdentityPolicy",
291 resource,
292 Some(decode(&segs[5])),
293 )),
294 (Method::PUT, 6) if segs[2] == "identities" && segs[4] == "policies" => Some((
295 "UpdateEmailIdentityPolicy",
296 resource,
297 Some(decode(&segs[5])),
298 )),
299 (Method::DELETE, 6) if segs[2] == "identities" && segs[4] == "policies" => Some((
300 "DeleteEmailIdentityPolicy",
301 resource,
302 Some(decode(&segs[5])),
303 )),
304
305 (Method::PUT, 6)
307 if segs[2] == "identities" && segs[4] == "dkim" && segs[5] == "signing" =>
308 {
309 Some(("PutEmailIdentityDkimSigningAttributes", resource, None))
310 }
311
312 (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "dkim" => {
314 Some(("PutEmailIdentityDkimAttributes", resource, None))
315 }
316 (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "feedback" => {
318 Some(("PutEmailIdentityFeedbackAttributes", resource, None))
319 }
320 (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "mail-from" => {
322 Some(("PutEmailIdentityMailFromAttributes", resource, None))
323 }
324 (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "configuration-set" => {
326 Some(("PutEmailIdentityConfigurationSetAttributes", resource, None))
327 }
328
329 (Method::PUT, 5) if segs[2] == "configuration-sets" && segs[4] == "sending" => {
331 Some(("PutConfigurationSetSendingOptions", resource, None))
332 }
333 (Method::PUT, 5)
335 if segs[2] == "configuration-sets" && segs[4] == "delivery-options" =>
336 {
337 Some(("PutConfigurationSetDeliveryOptions", resource, None))
338 }
339 (Method::PUT, 5)
341 if segs[2] == "configuration-sets" && segs[4] == "tracking-options" =>
342 {
343 Some(("PutConfigurationSetTrackingOptions", resource, None))
344 }
345 (Method::PUT, 5)
347 if segs[2] == "configuration-sets" && segs[4] == "suppression-options" =>
348 {
349 Some(("PutConfigurationSetSuppressionOptions", resource, None))
350 }
351 (Method::PUT, 5)
353 if segs[2] == "configuration-sets" && segs[4] == "reputation-options" =>
354 {
355 Some(("PutConfigurationSetReputationOptions", resource, None))
356 }
357 (Method::PUT, 5) if segs[2] == "configuration-sets" && segs[4] == "vdm-options" => {
359 Some(("PutConfigurationSetVdmOptions", resource, None))
360 }
361 (Method::PUT, 5)
363 if segs[2] == "configuration-sets" && segs[4] == "archiving-options" =>
364 {
365 Some(("PutConfigurationSetArchivingOptions", resource, None))
366 }
367
368 (Method::POST, 3) if segs[2] == "custom-verification-email-templates" => {
370 Some(("CreateCustomVerificationEmailTemplate", None, None))
371 }
372 (Method::GET, 3) if segs[2] == "custom-verification-email-templates" => {
373 Some(("ListCustomVerificationEmailTemplates", None, None))
374 }
375 (Method::GET, 4) if segs[2] == "custom-verification-email-templates" => {
377 Some(("GetCustomVerificationEmailTemplate", resource, None))
378 }
379 (Method::PUT, 4) if segs[2] == "custom-verification-email-templates" => {
380 Some(("UpdateCustomVerificationEmailTemplate", resource, None))
381 }
382 (Method::DELETE, 4) if segs[2] == "custom-verification-email-templates" => {
383 Some(("DeleteCustomVerificationEmailTemplate", resource, None))
384 }
385
386 (Method::POST, 3) if segs[2] == "outbound-custom-verification-emails" => {
388 Some(("SendCustomVerificationEmail", None, None))
389 }
390
391 (Method::POST, 5) if segs[2] == "templates" && segs[4] == "render" => {
393 Some(("TestRenderEmailTemplate", resource, None))
394 }
395
396 (Method::POST, 3) if segs[2] == "dedicated-ip-pools" => {
398 Some(("CreateDedicatedIpPool", None, None))
399 }
400 (Method::GET, 3) if segs[2] == "dedicated-ip-pools" => {
401 Some(("ListDedicatedIpPools", None, None))
402 }
403 (Method::DELETE, 4) if segs[2] == "dedicated-ip-pools" => {
405 Some(("DeleteDedicatedIpPool", resource, None))
406 }
407 (Method::PUT, 5) if segs[2] == "dedicated-ip-pools" && segs[4] == "scaling" => {
412 Some(("PutDedicatedIpPoolScalingAttributes", resource, None))
413 }
414
415 (Method::GET, 3) if segs[2] == "dedicated-ips" => Some(("GetDedicatedIps", None, None)),
417 (Method::PUT, 5) if segs[2] == "dedicated-ips" && segs[4] == "pool" => {
419 Some(("PutDedicatedIpInPool", resource, None))
420 }
421 (Method::PUT, 5) if segs[2] == "dedicated-ips" && segs[4] == "warmup" => {
423 Some(("PutDedicatedIpWarmupAttributes", resource, None))
424 }
425 (Method::GET, 4) if segs[2] == "dedicated-ips" => {
427 Some(("GetDedicatedIp", resource, None))
428 }
429
430 (Method::PUT, 5)
432 if segs[2] == "account" && segs[3] == "dedicated-ips" && segs[4] == "warmup" =>
433 {
434 Some(("PutAccountDedicatedIpWarmupAttributes", None, None))
435 }
436
437 (Method::POST, 4) if segs[2] == "account" && segs[3] == "details" => {
439 Some(("PutAccountDetails", None, None))
440 }
441 (Method::PUT, 4) if segs[2] == "account" && segs[3] == "sending" => {
443 Some(("PutAccountSendingAttributes", None, None))
444 }
445 (Method::PUT, 4) if segs[2] == "account" && segs[3] == "suppression" => {
447 Some(("PutAccountSuppressionAttributes", None, None))
448 }
449 (Method::PUT, 4) if segs[2] == "account" && segs[3] == "vdm" => {
451 Some(("PutAccountVdmAttributes", None, None))
452 }
453
454 (Method::POST, 3) if segs[2] == "multi-region-endpoints" => {
456 Some(("CreateMultiRegionEndpoint", None, None))
457 }
458 (Method::GET, 3) if segs[2] == "multi-region-endpoints" => {
459 Some(("ListMultiRegionEndpoints", None, None))
460 }
461 (Method::GET, 4) if segs[2] == "multi-region-endpoints" => {
463 Some(("GetMultiRegionEndpoint", resource, None))
464 }
465 (Method::DELETE, 4) if segs[2] == "multi-region-endpoints" => {
466 Some(("DeleteMultiRegionEndpoint", resource, None))
467 }
468
469 (Method::POST, 3) if segs[2] == "import-jobs" => Some(("CreateImportJob", None, None)),
471 (Method::POST, 4) if segs[2] == "import-jobs" && segs[3] == "list" => {
473 Some(("ListImportJobs", None, None))
474 }
475 (Method::GET, 4) if segs[2] == "import-jobs" => Some(("GetImportJob", resource, None)),
477
478 (Method::POST, 3) if segs[2] == "export-jobs" => Some(("CreateExportJob", None, None)),
480 (Method::POST, 3) if segs[2] == "list-export-jobs" => {
482 Some(("ListExportJobs", None, None))
483 }
484 (Method::PUT, 5) if segs[2] == "export-jobs" && segs[4] == "cancel" => {
486 Some(("CancelExportJob", resource, None))
487 }
488 (Method::GET, 4) if segs[2] == "export-jobs" => Some(("GetExportJob", resource, None)),
490
491 (Method::POST, 3) if segs[2] == "tenants" => Some(("CreateTenant", None, None)),
493 (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "list" => {
495 Some(("ListTenants", None, None))
496 }
497 (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "get" => {
499 Some(("GetTenant", None, None))
500 }
501 (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "delete" => {
503 Some(("DeleteTenant", None, None))
504 }
505 (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "resources" => {
507 Some(("CreateTenantResourceAssociation", None, None))
508 }
509 (Method::POST, 5)
511 if segs[2] == "tenants" && segs[3] == "resources" && segs[4] == "delete" =>
512 {
513 Some(("DeleteTenantResourceAssociation", None, None))
514 }
515 (Method::POST, 5)
517 if segs[2] == "tenants" && segs[3] == "resources" && segs[4] == "list" =>
518 {
519 Some(("ListTenantResources", None, None))
520 }
521 (Method::POST, 5)
523 if segs[2] == "resources" && segs[3] == "tenants" && segs[4] == "list" =>
524 {
525 Some(("ListResourceTenants", None, None))
526 }
527
528 (Method::POST, 4) if segs[2] == "reputation" && segs[3] == "entities" => {
530 Some(("ListReputationEntities", None, None))
531 }
532 (Method::PUT, 7)
534 if segs[2] == "reputation"
535 && segs[3] == "entities"
536 && segs[6] == "customer-managed-status" =>
537 {
538 Some((
539 "UpdateReputationEntityCustomerManagedStatus",
540 Some(decode(&segs[4])),
541 Some(decode(&segs[5])),
542 ))
543 }
544 (Method::PUT, 7)
546 if segs[2] == "reputation" && segs[3] == "entities" && segs[6] == "policy" =>
547 {
548 Some((
549 "UpdateReputationEntityPolicy",
550 Some(decode(&segs[4])),
551 Some(decode(&segs[5])),
552 ))
553 }
554 (Method::GET, 6) if segs[2] == "reputation" && segs[3] == "entities" => Some((
556 "GetReputationEntity",
557 Some(decode(&segs[4])),
558 Some(decode(&segs[5])),
559 )),
560
561 (Method::POST, 4) if segs[2] == "metrics" && segs[3] == "batch" => {
563 Some(("BatchGetMetricData", None, None))
564 }
565
566 _ => None,
567 }
568 }
569
570 fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
571 serde_json::from_slice(&req.body).map_err(|_| {
572 AwsServiceError::aws_error(
573 StatusCode::BAD_REQUEST,
574 "BadRequestException",
575 "Invalid JSON in request body",
576 )
577 })
578 }
579
580 fn json_error(status: StatusCode, code: &str, message: &str) -> AwsResponse {
581 let body = json!({
582 "__type": code,
583 "message": message,
584 });
585 AwsResponse::json(status, body.to_string())
586 }
587
588 fn get_account(&self) -> Result<AwsResponse, AwsServiceError> {
589 let state = self.state.read();
590 let acct = &state.account_settings;
591 let production_access = acct
592 .details
593 .as_ref()
594 .and_then(|d| d.production_access_enabled)
595 .unwrap_or(true);
596 let mut response = json!({
597 "DedicatedIpAutoWarmupEnabled": acct.dedicated_ip_auto_warmup_enabled,
598 "EnforcementStatus": "HEALTHY",
599 "ProductionAccessEnabled": production_access,
600 "SendQuota": {
601 "Max24HourSend": 50000.0,
602 "MaxSendRate": 14.0,
603 "SentLast24Hours": state.sent_emails.iter()
604 .filter(|e| e.timestamp > Utc::now() - chrono::Duration::hours(24))
605 .count() as f64,
606 },
607 "SendingEnabled": acct.sending_enabled,
608 "SuppressionAttributes": {
609 "SuppressedReasons": acct.suppressed_reasons,
610 },
611 });
612 if let Some(ref details) = acct.details {
613 let mut d = json!({});
614 if let Some(ref mt) = details.mail_type {
615 d["MailType"] = json!(mt);
616 }
617 if let Some(ref url) = details.website_url {
618 d["WebsiteURL"] = json!(url);
619 }
620 if let Some(ref lang) = details.contact_language {
621 d["ContactLanguage"] = json!(lang);
622 }
623 if let Some(ref desc) = details.use_case_description {
624 d["UseCaseDescription"] = json!(desc);
625 }
626 if !details.additional_contact_email_addresses.is_empty() {
627 d["AdditionalContactEmailAddresses"] =
628 json!(details.additional_contact_email_addresses);
629 }
630 d["ReviewDetails"] = json!({
631 "Status": "GRANTED",
632 "CaseId": "fakecloud-case-001",
633 });
634 response["Details"] = d;
635 }
636 if let Some(ref vdm) = acct.vdm_attributes {
637 response["VdmAttributes"] = vdm.clone();
638 }
639 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
640 }
641
642 fn create_email_identity(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
643 let body: Value = Self::parse_body(req)?;
644 let identity_name = match body["EmailIdentity"].as_str() {
645 Some(name) => name.to_string(),
646 None => {
647 return Ok(Self::json_error(
648 StatusCode::BAD_REQUEST,
649 "BadRequestException",
650 "EmailIdentity is required",
651 ));
652 }
653 };
654
655 let mut state = self.state.write();
656
657 if state.identities.contains_key(&identity_name) {
658 return Ok(Self::json_error(
659 StatusCode::CONFLICT,
660 "AlreadyExistsException",
661 &format!("Identity {} already exists", identity_name),
662 ));
663 }
664
665 let identity_type = if identity_name.contains('@') {
666 "EMAIL_ADDRESS"
667 } else {
668 "DOMAIN"
669 };
670
671 let identity = EmailIdentity {
672 identity_name: identity_name.clone(),
673 identity_type: identity_type.to_string(),
674 verified: true,
675 created_at: Utc::now(),
676 dkim_signing_enabled: true,
677 dkim_signing_attributes_origin: "AWS_SES".to_string(),
678 dkim_domain_signing_private_key: None,
679 dkim_domain_signing_selector: None,
680 dkim_next_signing_key_length: None,
681 email_forwarding_enabled: true,
682 mail_from_domain: None,
683 mail_from_behavior_on_mx_failure: "USE_DEFAULT_VALUE".to_string(),
684 configuration_set_name: None,
685 };
686
687 state.identities.insert(identity_name, identity);
688
689 let response = json!({
690 "IdentityType": identity_type,
691 "VerifiedForSendingStatus": true,
692 "DkimAttributes": {
693 "SigningEnabled": true,
694 "Status": "SUCCESS",
695 "Tokens": [
696 "token1",
697 "token2",
698 "token3",
699 ],
700 },
701 });
702
703 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
704 }
705
706 fn list_email_identities(&self) -> Result<AwsResponse, AwsServiceError> {
707 let state = self.state.read();
708 let identities: Vec<Value> = state
709 .identities
710 .values()
711 .map(|id| {
712 json!({
713 "IdentityType": id.identity_type,
714 "IdentityName": id.identity_name,
715 "SendingEnabled": true,
716 })
717 })
718 .collect();
719
720 let response = json!({
721 "EmailIdentities": identities,
722 });
723
724 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
725 }
726
727 fn get_email_identity(&self, identity_name: &str) -> Result<AwsResponse, AwsServiceError> {
728 let state = self.state.read();
729 let identity = match state.identities.get(identity_name) {
730 Some(id) => id,
731 None => {
732 return Ok(Self::json_error(
733 StatusCode::NOT_FOUND,
734 "NotFoundException",
735 &format!("Identity {} does not exist", identity_name),
736 ));
737 }
738 };
739
740 let mail_from_domain = identity.mail_from_domain.as_deref().unwrap_or("");
741 let mail_from_status = if mail_from_domain.is_empty() {
742 "FAILED"
743 } else {
744 "SUCCESS"
745 };
746
747 let mut response = json!({
748 "IdentityType": identity.identity_type,
749 "VerifiedForSendingStatus": true,
750 "FeedbackForwardingStatus": identity.email_forwarding_enabled,
751 "DkimAttributes": {
752 "SigningEnabled": identity.dkim_signing_enabled,
753 "Status": "SUCCESS",
754 "SigningAttributesOrigin": identity.dkim_signing_attributes_origin,
755 "Tokens": [
756 "token1",
757 "token2",
758 "token3",
759 ],
760 },
761 "MailFromAttributes": {
762 "MailFromDomain": mail_from_domain,
763 "MailFromDomainStatus": mail_from_status,
764 "BehaviorOnMxFailure": identity.mail_from_behavior_on_mx_failure,
765 },
766 "Tags": [],
767 });
768
769 if let Some(ref cs) = identity.configuration_set_name {
770 response["ConfigurationSetName"] = json!(cs);
771 }
772
773 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
774 }
775
776 fn delete_email_identity(
777 &self,
778 identity_name: &str,
779 req: &AwsRequest,
780 ) -> Result<AwsResponse, AwsServiceError> {
781 let mut state = self.state.write();
782
783 if state.identities.remove(identity_name).is_none() {
784 return Ok(Self::json_error(
785 StatusCode::NOT_FOUND,
786 "NotFoundException",
787 &format!("Identity {} does not exist", identity_name),
788 ));
789 }
790
791 let arn = format!(
793 "arn:aws:ses:{}:{}:identity/{}",
794 req.region, req.account_id, identity_name
795 );
796 state.tags.remove(&arn);
797
798 state.identity_policies.remove(identity_name);
800
801 Ok(AwsResponse::json(StatusCode::OK, "{}"))
802 }
803
804 fn create_configuration_set(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
805 let body: Value = Self::parse_body(req)?;
806 let name = match body["ConfigurationSetName"].as_str() {
807 Some(n) => n.to_string(),
808 None => {
809 return Ok(Self::json_error(
810 StatusCode::BAD_REQUEST,
811 "BadRequestException",
812 "ConfigurationSetName is required",
813 ));
814 }
815 };
816
817 let mut state = self.state.write();
818
819 if state.configuration_sets.contains_key(&name) {
820 return Ok(Self::json_error(
821 StatusCode::CONFLICT,
822 "AlreadyExistsException",
823 &format!("Configuration set {} already exists", name),
824 ));
825 }
826
827 state.configuration_sets.insert(
828 name.clone(),
829 ConfigurationSet {
830 name,
831 sending_enabled: true,
832 tls_policy: "OPTIONAL".to_string(),
833 sending_pool_name: None,
834 custom_redirect_domain: None,
835 https_policy: None,
836 suppressed_reasons: Vec::new(),
837 reputation_metrics_enabled: false,
838 vdm_options: None,
839 archive_arn: None,
840 },
841 );
842
843 Ok(AwsResponse::json(StatusCode::OK, "{}"))
844 }
845
846 fn list_configuration_sets(&self) -> Result<AwsResponse, AwsServiceError> {
847 let state = self.state.read();
848 let sets: Vec<Value> = state
849 .configuration_sets
850 .keys()
851 .map(|name| json!(name))
852 .collect();
853
854 let response = json!({
855 "ConfigurationSets": sets,
856 });
857
858 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
859 }
860
861 fn get_configuration_set(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
862 let state = self.state.read();
863
864 let cs = match state.configuration_sets.get(name) {
865 Some(cs) => cs,
866 None => {
867 return Ok(Self::json_error(
868 StatusCode::NOT_FOUND,
869 "NotFoundException",
870 &format!("Configuration set {} does not exist", name),
871 ));
872 }
873 };
874
875 let mut delivery_options = json!({
876 "TlsPolicy": cs.tls_policy,
877 });
878 if let Some(ref pool) = cs.sending_pool_name {
879 delivery_options["SendingPoolName"] = json!(pool);
880 }
881
882 let mut tracking_options = json!({});
883 if let Some(ref domain) = cs.custom_redirect_domain {
884 tracking_options["CustomRedirectDomain"] = json!(domain);
885 }
886 if let Some(ref policy) = cs.https_policy {
887 tracking_options["HttpsPolicy"] = json!(policy);
888 }
889
890 let mut response = json!({
891 "ConfigurationSetName": name,
892 "DeliveryOptions": delivery_options,
893 "ReputationOptions": {
894 "ReputationMetricsEnabled": cs.reputation_metrics_enabled,
895 },
896 "SendingOptions": {
897 "SendingEnabled": cs.sending_enabled,
898 },
899 "Tags": [],
900 "TrackingOptions": tracking_options,
901 });
902
903 if !cs.suppressed_reasons.is_empty() {
904 response["SuppressionOptions"] = json!({
905 "SuppressedReasons": cs.suppressed_reasons,
906 });
907 }
908
909 if let Some(ref vdm) = cs.vdm_options {
910 response["VdmOptions"] = vdm.clone();
911 }
912
913 if let Some(ref arn) = cs.archive_arn {
914 response["ArchivingOptions"] = json!({
915 "ArchiveArn": arn,
916 });
917 }
918
919 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
920 }
921
922 fn delete_configuration_set(
923 &self,
924 name: &str,
925 req: &AwsRequest,
926 ) -> Result<AwsResponse, AwsServiceError> {
927 let mut state = self.state.write();
928
929 if state.configuration_sets.remove(name).is_none() {
930 return Ok(Self::json_error(
931 StatusCode::NOT_FOUND,
932 "NotFoundException",
933 &format!("Configuration set {} does not exist", name),
934 ));
935 }
936
937 let arn = format!(
939 "arn:aws:ses:{}:{}:configuration-set/{}",
940 req.region, req.account_id, name
941 );
942 state.tags.remove(&arn);
943
944 state.event_destinations.remove(name);
946
947 Ok(AwsResponse::json(StatusCode::OK, "{}"))
948 }
949
950 fn create_email_template(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
951 let body: Value = Self::parse_body(req)?;
952 let template_name = match body["TemplateName"].as_str() {
953 Some(n) => n.to_string(),
954 None => {
955 return Ok(Self::json_error(
956 StatusCode::BAD_REQUEST,
957 "BadRequestException",
958 "TemplateName is required",
959 ));
960 }
961 };
962
963 let mut state = self.state.write();
964
965 if state.templates.contains_key(&template_name) {
966 return Ok(Self::json_error(
967 StatusCode::CONFLICT,
968 "AlreadyExistsException",
969 &format!("Template {} already exists", template_name),
970 ));
971 }
972
973 let template = EmailTemplate {
974 template_name: template_name.clone(),
975 subject: body["TemplateContent"]["Subject"]
976 .as_str()
977 .map(|s| s.to_string()),
978 html_body: body["TemplateContent"]["Html"]
979 .as_str()
980 .map(|s| s.to_string()),
981 text_body: body["TemplateContent"]["Text"]
982 .as_str()
983 .map(|s| s.to_string()),
984 created_at: Utc::now(),
985 };
986
987 state.templates.insert(template_name, template);
988
989 Ok(AwsResponse::json(StatusCode::OK, "{}"))
990 }
991
992 fn list_email_templates(&self) -> Result<AwsResponse, AwsServiceError> {
993 let state = self.state.read();
994 let templates: Vec<Value> = state
995 .templates
996 .values()
997 .map(|t| {
998 json!({
999 "TemplateName": t.template_name,
1000 "CreatedTimestamp": t.created_at.timestamp() as f64,
1001 })
1002 })
1003 .collect();
1004
1005 let response = json!({
1006 "TemplatesMetadata": templates,
1007 });
1008
1009 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1010 }
1011
1012 fn get_email_template(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
1013 let state = self.state.read();
1014 let template = match state.templates.get(name) {
1015 Some(t) => t,
1016 None => {
1017 return Ok(Self::json_error(
1018 StatusCode::NOT_FOUND,
1019 "NotFoundException",
1020 &format!("Template {} does not exist", name),
1021 ));
1022 }
1023 };
1024
1025 let response = json!({
1026 "TemplateName": template.template_name,
1027 "TemplateContent": {
1028 "Subject": template.subject,
1029 "Html": template.html_body,
1030 "Text": template.text_body,
1031 },
1032 });
1033
1034 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1035 }
1036
1037 fn update_email_template(
1038 &self,
1039 name: &str,
1040 req: &AwsRequest,
1041 ) -> Result<AwsResponse, AwsServiceError> {
1042 let body: Value = Self::parse_body(req)?;
1043 let mut state = self.state.write();
1044
1045 let template = match state.templates.get_mut(name) {
1046 Some(t) => t,
1047 None => {
1048 return Ok(Self::json_error(
1049 StatusCode::NOT_FOUND,
1050 "NotFoundException",
1051 &format!("Template {} does not exist", name),
1052 ));
1053 }
1054 };
1055
1056 if let Some(subject) = body["TemplateContent"]["Subject"].as_str() {
1057 template.subject = Some(subject.to_string());
1058 }
1059 if let Some(html) = body["TemplateContent"]["Html"].as_str() {
1060 template.html_body = Some(html.to_string());
1061 }
1062 if let Some(text) = body["TemplateContent"]["Text"].as_str() {
1063 template.text_body = Some(text.to_string());
1064 }
1065
1066 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1067 }
1068
1069 fn delete_email_template(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
1070 let mut state = self.state.write();
1071
1072 if state.templates.remove(name).is_none() {
1073 return Ok(Self::json_error(
1074 StatusCode::NOT_FOUND,
1075 "NotFoundException",
1076 &format!("Template {} does not exist", name),
1077 ));
1078 }
1079
1080 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1081 }
1082
1083 fn send_email(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1084 let body: Value = Self::parse_body(req)?;
1085
1086 if !body["Content"].is_object()
1087 || (!body["Content"]["Simple"].is_object()
1088 && !body["Content"]["Raw"].is_object()
1089 && !body["Content"]["Template"].is_object())
1090 {
1091 return Ok(Self::json_error(
1092 StatusCode::BAD_REQUEST,
1093 "BadRequestException",
1094 "Content is required and must contain Simple, Raw, or Template",
1095 ));
1096 }
1097
1098 let from = body["FromEmailAddress"].as_str().unwrap_or("").to_string();
1099
1100 let to = extract_string_array(&body["Destination"]["ToAddresses"]);
1101 let cc = extract_string_array(&body["Destination"]["CcAddresses"]);
1102 let bcc = extract_string_array(&body["Destination"]["BccAddresses"]);
1103
1104 let config_set_name = body["ConfigurationSetName"].as_str().map(|s| s.to_string());
1105
1106 let (subject, html_body, text_body, raw_data, template_name, template_data) =
1107 if body["Content"]["Simple"].is_object() {
1108 let simple = &body["Content"]["Simple"];
1109 let subject = simple["Subject"]["Data"].as_str().map(|s| s.to_string());
1110 let html = simple["Body"]["Html"]["Data"]
1111 .as_str()
1112 .map(|s| s.to_string());
1113 let text = simple["Body"]["Text"]["Data"]
1114 .as_str()
1115 .map(|s| s.to_string());
1116 (subject, html, text, None, None, None)
1117 } else if body["Content"]["Raw"].is_object() {
1118 let raw = body["Content"]["Raw"]["Data"]
1119 .as_str()
1120 .map(|s| s.to_string());
1121 (None, None, None, raw, None, None)
1122 } else if body["Content"]["Template"].is_object() {
1123 let tmpl = &body["Content"]["Template"];
1124 let tmpl_name = tmpl["TemplateName"].as_str().map(|s| s.to_string());
1125 let tmpl_data = tmpl["TemplateData"].as_str().map(|s| s.to_string());
1126 (None, None, None, None, tmpl_name, tmpl_data)
1127 } else {
1128 (None, None, None, None, None, None)
1129 };
1130
1131 let message_id = uuid::Uuid::new_v4().to_string();
1132
1133 let sent = SentEmail {
1134 message_id: message_id.clone(),
1135 from,
1136 to,
1137 cc,
1138 bcc,
1139 subject,
1140 html_body,
1141 text_body,
1142 raw_data,
1143 template_name,
1144 template_data,
1145 timestamp: Utc::now(),
1146 };
1147
1148 if let Some(ref ctx) = self.delivery_ctx {
1150 crate::fanout::process_send_events(ctx, &sent, config_set_name.as_deref());
1151 }
1152
1153 self.state.write().sent_emails.push(sent);
1154
1155 let response = json!({
1156 "MessageId": message_id,
1157 });
1158
1159 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1160 }
1161
1162 fn create_contact_list(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1165 let body: Value = Self::parse_body(req)?;
1166 let name = match body["ContactListName"].as_str() {
1167 Some(n) => n.to_string(),
1168 None => {
1169 return Ok(Self::json_error(
1170 StatusCode::BAD_REQUEST,
1171 "BadRequestException",
1172 "ContactListName is required",
1173 ));
1174 }
1175 };
1176
1177 let mut state = self.state.write();
1178
1179 if state.contact_lists.contains_key(&name) {
1180 return Ok(Self::json_error(
1181 StatusCode::CONFLICT,
1182 "AlreadyExistsException",
1183 &format!("List with name {} already exists.", name),
1184 ));
1185 }
1186
1187 let topics = parse_topics(&body["Topics"]);
1188 let description = body["Description"].as_str().map(|s| s.to_string());
1189 let now = Utc::now();
1190
1191 state.contact_lists.insert(
1192 name.clone(),
1193 ContactList {
1194 contact_list_name: name.clone(),
1195 description,
1196 topics,
1197 created_at: now,
1198 last_updated_at: now,
1199 },
1200 );
1201 state.contacts.insert(name, HashMap::new());
1202
1203 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1204 }
1205
1206 fn get_contact_list(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
1207 let state = self.state.read();
1208 let list = match state.contact_lists.get(name) {
1209 Some(l) => l,
1210 None => {
1211 return Ok(Self::json_error(
1212 StatusCode::NOT_FOUND,
1213 "NotFoundException",
1214 &format!("List with name {} does not exist.", name),
1215 ));
1216 }
1217 };
1218
1219 let topics: Vec<Value> = list
1220 .topics
1221 .iter()
1222 .map(|t| {
1223 json!({
1224 "TopicName": t.topic_name,
1225 "DisplayName": t.display_name,
1226 "Description": t.description,
1227 "DefaultSubscriptionStatus": t.default_subscription_status,
1228 })
1229 })
1230 .collect();
1231
1232 let response = json!({
1233 "ContactListName": list.contact_list_name,
1234 "Description": list.description,
1235 "Topics": topics,
1236 "CreatedTimestamp": list.created_at.timestamp() as f64,
1237 "LastUpdatedTimestamp": list.last_updated_at.timestamp() as f64,
1238 "Tags": [],
1239 });
1240
1241 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1242 }
1243
1244 fn list_contact_lists(&self) -> Result<AwsResponse, AwsServiceError> {
1245 let state = self.state.read();
1246 let lists: Vec<Value> = state
1247 .contact_lists
1248 .values()
1249 .map(|l| {
1250 json!({
1251 "ContactListName": l.contact_list_name,
1252 "LastUpdatedTimestamp": l.last_updated_at.timestamp() as f64,
1253 })
1254 })
1255 .collect();
1256
1257 let response = json!({
1258 "ContactLists": lists,
1259 });
1260
1261 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1262 }
1263
1264 fn update_contact_list(
1265 &self,
1266 name: &str,
1267 req: &AwsRequest,
1268 ) -> Result<AwsResponse, AwsServiceError> {
1269 let body: Value = Self::parse_body(req)?;
1270 let mut state = self.state.write();
1271
1272 let list = match state.contact_lists.get_mut(name) {
1273 Some(l) => l,
1274 None => {
1275 return Ok(Self::json_error(
1276 StatusCode::NOT_FOUND,
1277 "NotFoundException",
1278 &format!("List with name {} does not exist.", name),
1279 ));
1280 }
1281 };
1282
1283 if let Some(desc) = body.get("Description") {
1284 list.description = desc.as_str().map(|s| s.to_string());
1285 }
1286 if body.get("Topics").is_some() {
1287 list.topics = parse_topics(&body["Topics"]);
1288 }
1289 list.last_updated_at = Utc::now();
1290
1291 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1292 }
1293
1294 fn delete_contact_list(
1295 &self,
1296 name: &str,
1297 req: &AwsRequest,
1298 ) -> Result<AwsResponse, AwsServiceError> {
1299 let mut state = self.state.write();
1300
1301 if state.contact_lists.remove(name).is_none() {
1302 return Ok(Self::json_error(
1303 StatusCode::NOT_FOUND,
1304 "NotFoundException",
1305 &format!("List with name {} does not exist.", name),
1306 ));
1307 }
1308
1309 state.contacts.remove(name);
1311
1312 let arn = format!(
1314 "arn:aws:ses:{}:{}:contact-list/{}",
1315 req.region, req.account_id, name
1316 );
1317 state.tags.remove(&arn);
1318
1319 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1320 }
1321
1322 fn create_contact(
1325 &self,
1326 list_name: &str,
1327 req: &AwsRequest,
1328 ) -> Result<AwsResponse, AwsServiceError> {
1329 let body: Value = Self::parse_body(req)?;
1330 let email = match body["EmailAddress"].as_str() {
1331 Some(e) => e.to_string(),
1332 None => {
1333 return Ok(Self::json_error(
1334 StatusCode::BAD_REQUEST,
1335 "BadRequestException",
1336 "EmailAddress is required",
1337 ));
1338 }
1339 };
1340
1341 let mut state = self.state.write();
1342
1343 if !state.contact_lists.contains_key(list_name) {
1344 return Ok(Self::json_error(
1345 StatusCode::NOT_FOUND,
1346 "NotFoundException",
1347 &format!("List with name {} does not exist.", list_name),
1348 ));
1349 }
1350
1351 let contacts = state.contacts.entry(list_name.to_string()).or_default();
1352
1353 if contacts.contains_key(&email) {
1354 return Ok(Self::json_error(
1355 StatusCode::CONFLICT,
1356 "AlreadyExistsException",
1357 &format!("Contact already exists in list {}", list_name),
1358 ));
1359 }
1360
1361 let topic_preferences = parse_topic_preferences(&body["TopicPreferences"]);
1362 let unsubscribe_all = body["UnsubscribeAll"].as_bool().unwrap_or(false);
1363 let attributes_data = body["AttributesData"].as_str().map(|s| s.to_string());
1364 let now = Utc::now();
1365
1366 contacts.insert(
1367 email.clone(),
1368 Contact {
1369 email_address: email,
1370 topic_preferences,
1371 unsubscribe_all,
1372 attributes_data,
1373 created_at: now,
1374 last_updated_at: now,
1375 },
1376 );
1377
1378 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1379 }
1380
1381 fn get_contact(&self, list_name: &str, email: &str) -> Result<AwsResponse, AwsServiceError> {
1382 let state = self.state.read();
1383
1384 if !state.contact_lists.contains_key(list_name) {
1385 return Ok(Self::json_error(
1386 StatusCode::NOT_FOUND,
1387 "NotFoundException",
1388 &format!("List with name {} does not exist.", list_name),
1389 ));
1390 }
1391
1392 let contact = state.contacts.get(list_name).and_then(|m| m.get(email));
1393
1394 let contact = match contact {
1395 Some(c) => c,
1396 None => {
1397 return Ok(Self::json_error(
1398 StatusCode::NOT_FOUND,
1399 "NotFoundException",
1400 &format!("Contact {} does not exist in list {}", email, list_name),
1401 ));
1402 }
1403 };
1404
1405 let list = state.contact_lists.get(list_name).unwrap();
1407 let topic_default_preferences: Vec<Value> = list
1408 .topics
1409 .iter()
1410 .map(|t| {
1411 json!({
1412 "TopicName": t.topic_name,
1413 "SubscriptionStatus": t.default_subscription_status,
1414 })
1415 })
1416 .collect();
1417
1418 let topic_preferences: Vec<Value> = contact
1419 .topic_preferences
1420 .iter()
1421 .map(|tp| {
1422 json!({
1423 "TopicName": tp.topic_name,
1424 "SubscriptionStatus": tp.subscription_status,
1425 })
1426 })
1427 .collect();
1428
1429 let mut response = json!({
1430 "ContactListName": list_name,
1431 "EmailAddress": contact.email_address,
1432 "TopicPreferences": topic_preferences,
1433 "TopicDefaultPreferences": topic_default_preferences,
1434 "UnsubscribeAll": contact.unsubscribe_all,
1435 "CreatedTimestamp": contact.created_at.timestamp() as f64,
1436 "LastUpdatedTimestamp": contact.last_updated_at.timestamp() as f64,
1437 });
1438
1439 if let Some(ref attrs) = contact.attributes_data {
1440 response["AttributesData"] = json!(attrs);
1441 }
1442
1443 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1444 }
1445
1446 fn list_contacts(&self, list_name: &str) -> Result<AwsResponse, AwsServiceError> {
1447 let state = self.state.read();
1448
1449 if !state.contact_lists.contains_key(list_name) {
1450 return Ok(Self::json_error(
1451 StatusCode::NOT_FOUND,
1452 "NotFoundException",
1453 &format!("List with name {} does not exist.", list_name),
1454 ));
1455 }
1456
1457 let contacts: Vec<Value> = state
1458 .contacts
1459 .get(list_name)
1460 .map(|m| {
1461 m.values()
1462 .map(|c| {
1463 let topic_prefs: Vec<Value> = c
1464 .topic_preferences
1465 .iter()
1466 .map(|tp| {
1467 json!({
1468 "TopicName": tp.topic_name,
1469 "SubscriptionStatus": tp.subscription_status,
1470 })
1471 })
1472 .collect();
1473
1474 let list = state.contact_lists.get(list_name).unwrap();
1476 let topic_defaults: Vec<Value> = list
1477 .topics
1478 .iter()
1479 .map(|t| {
1480 json!({
1481 "TopicName": t.topic_name,
1482 "SubscriptionStatus": t.default_subscription_status,
1483 })
1484 })
1485 .collect();
1486
1487 json!({
1488 "EmailAddress": c.email_address,
1489 "TopicPreferences": topic_prefs,
1490 "TopicDefaultPreferences": topic_defaults,
1491 "UnsubscribeAll": c.unsubscribe_all,
1492 "LastUpdatedTimestamp": c.last_updated_at.timestamp() as f64,
1493 })
1494 })
1495 .collect()
1496 })
1497 .unwrap_or_default();
1498
1499 let response = json!({
1500 "Contacts": contacts,
1501 });
1502
1503 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1504 }
1505
1506 fn update_contact(
1507 &self,
1508 list_name: &str,
1509 email: &str,
1510 req: &AwsRequest,
1511 ) -> Result<AwsResponse, AwsServiceError> {
1512 let body: Value = Self::parse_body(req)?;
1513 let mut state = self.state.write();
1514
1515 if !state.contact_lists.contains_key(list_name) {
1516 return Ok(Self::json_error(
1517 StatusCode::NOT_FOUND,
1518 "NotFoundException",
1519 &format!("List with name {} does not exist.", list_name),
1520 ));
1521 }
1522
1523 let contact = state
1524 .contacts
1525 .get_mut(list_name)
1526 .and_then(|m| m.get_mut(email));
1527
1528 let contact = match contact {
1529 Some(c) => c,
1530 None => {
1531 return Ok(Self::json_error(
1532 StatusCode::NOT_FOUND,
1533 "NotFoundException",
1534 &format!("Contact {} does not exist in list {}", email, list_name),
1535 ));
1536 }
1537 };
1538
1539 if body.get("TopicPreferences").is_some() {
1540 contact.topic_preferences = parse_topic_preferences(&body["TopicPreferences"]);
1541 }
1542 if let Some(unsub) = body["UnsubscribeAll"].as_bool() {
1543 contact.unsubscribe_all = unsub;
1544 }
1545 if let Some(attrs) = body.get("AttributesData") {
1546 contact.attributes_data = attrs.as_str().map(|s| s.to_string());
1547 }
1548 contact.last_updated_at = Utc::now();
1549
1550 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1551 }
1552
1553 fn delete_contact(&self, list_name: &str, email: &str) -> Result<AwsResponse, AwsServiceError> {
1554 let mut state = self.state.write();
1555
1556 if !state.contact_lists.contains_key(list_name) {
1557 return Ok(Self::json_error(
1558 StatusCode::NOT_FOUND,
1559 "NotFoundException",
1560 &format!("List with name {} does not exist.", list_name),
1561 ));
1562 }
1563
1564 let removed = state
1565 .contacts
1566 .get_mut(list_name)
1567 .and_then(|m| m.remove(email));
1568
1569 if removed.is_none() {
1570 return Ok(Self::json_error(
1571 StatusCode::NOT_FOUND,
1572 "NotFoundException",
1573 &format!("Contact {} does not exist in list {}", email, list_name),
1574 ));
1575 }
1576
1577 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1578 }
1579
1580 fn validate_resource_arn(&self, arn: &str) -> Option<AwsResponse> {
1585 let state = self.state.read();
1586
1587 let parts: Vec<&str> = arn.split(':').collect();
1589 if parts.len() < 6 {
1590 return Some(Self::json_error(
1591 StatusCode::NOT_FOUND,
1592 "NotFoundException",
1593 &format!("Resource not found: {arn}"),
1594 ));
1595 }
1596
1597 let resource = parts[5..].join(":");
1598 let found = if let Some(name) = resource.strip_prefix("identity/") {
1599 state.identities.contains_key(name)
1600 } else if let Some(name) = resource.strip_prefix("configuration-set/") {
1601 state.configuration_sets.contains_key(name)
1602 } else if let Some(name) = resource.strip_prefix("contact-list/") {
1603 state.contact_lists.contains_key(name)
1604 } else {
1605 false
1606 };
1607
1608 if found {
1609 None
1610 } else {
1611 Some(Self::json_error(
1612 StatusCode::NOT_FOUND,
1613 "NotFoundException",
1614 &format!("Resource not found: {arn}"),
1615 ))
1616 }
1617 }
1618
1619 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1620 let body: Value = Self::parse_body(req)?;
1621
1622 let arn = match body["ResourceArn"].as_str() {
1623 Some(a) => a.to_string(),
1624 None => {
1625 return Ok(Self::json_error(
1626 StatusCode::BAD_REQUEST,
1627 "BadRequestException",
1628 "ResourceArn is required",
1629 ));
1630 }
1631 };
1632
1633 let tags_arr = match body["Tags"].as_array() {
1634 Some(arr) => arr,
1635 None => {
1636 return Ok(Self::json_error(
1637 StatusCode::BAD_REQUEST,
1638 "BadRequestException",
1639 "Tags is required",
1640 ));
1641 }
1642 };
1643
1644 if let Some(resp) = self.validate_resource_arn(&arn) {
1645 return Ok(resp);
1646 }
1647
1648 let mut state = self.state.write();
1649 let tag_map = state.tags.entry(arn).or_default();
1650 for tag in tags_arr {
1651 if let (Some(k), Some(v)) = (tag["Key"].as_str(), tag["Value"].as_str()) {
1652 tag_map.insert(k.to_string(), v.to_string());
1653 }
1654 }
1655
1656 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1657 }
1658
1659 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1660 let arn = match req.query_params.get("ResourceArn") {
1662 Some(a) => a.to_string(),
1663 None => {
1664 return Ok(Self::json_error(
1665 StatusCode::BAD_REQUEST,
1666 "BadRequestException",
1667 "ResourceArn is required",
1668 ));
1669 }
1670 };
1671
1672 if let Some(resp) = self.validate_resource_arn(&arn) {
1673 return Ok(resp);
1674 }
1675
1676 let tag_keys: Vec<String> = form_urlencoded::parse(req.raw_query.as_bytes())
1678 .filter(|(k, _)| k == "TagKeys")
1679 .map(|(_, v)| v.into_owned())
1680 .collect();
1681
1682 let mut state = self.state.write();
1683 if let Some(tag_map) = state.tags.get_mut(&arn) {
1684 for key in &tag_keys {
1685 tag_map.remove(key);
1686 }
1687 }
1688
1689 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1690 }
1691
1692 fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1693 let arn = match req.query_params.get("ResourceArn") {
1694 Some(a) => a.to_string(),
1695 None => {
1696 return Ok(Self::json_error(
1697 StatusCode::BAD_REQUEST,
1698 "BadRequestException",
1699 "ResourceArn is required",
1700 ));
1701 }
1702 };
1703
1704 if let Some(resp) = self.validate_resource_arn(&arn) {
1705 return Ok(resp);
1706 }
1707
1708 let state = self.state.read();
1709 let tags = state.tags.get(&arn);
1710 let tags_json = match tags {
1711 Some(t) => fakecloud_core::tags::tags_to_json(t, "Key", "Value"),
1712 None => vec![],
1713 };
1714
1715 let response = json!({
1716 "Tags": tags_json,
1717 });
1718
1719 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1720 }
1721
1722 fn put_suppressed_destination(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1725 let body: Value = Self::parse_body(req)?;
1726 let email = match body["EmailAddress"].as_str() {
1727 Some(e) => e.to_string(),
1728 None => {
1729 return Ok(Self::json_error(
1730 StatusCode::BAD_REQUEST,
1731 "BadRequestException",
1732 "EmailAddress is required",
1733 ));
1734 }
1735 };
1736 let reason = match body["Reason"].as_str() {
1737 Some(r) if r == "BOUNCE" || r == "COMPLAINT" => r.to_string(),
1738 Some(_) => {
1739 return Ok(Self::json_error(
1740 StatusCode::BAD_REQUEST,
1741 "BadRequestException",
1742 "Reason must be BOUNCE or COMPLAINT",
1743 ));
1744 }
1745 None => {
1746 return Ok(Self::json_error(
1747 StatusCode::BAD_REQUEST,
1748 "BadRequestException",
1749 "Reason is required",
1750 ));
1751 }
1752 };
1753
1754 let mut state = self.state.write();
1755 state.suppressed_destinations.insert(
1756 email.clone(),
1757 SuppressedDestination {
1758 email_address: email,
1759 reason,
1760 last_update_time: Utc::now(),
1761 },
1762 );
1763
1764 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1765 }
1766
1767 fn get_suppressed_destination(&self, email: &str) -> Result<AwsResponse, AwsServiceError> {
1768 let state = self.state.read();
1769 let dest = match state.suppressed_destinations.get(email) {
1770 Some(d) => d,
1771 None => {
1772 return Ok(Self::json_error(
1773 StatusCode::NOT_FOUND,
1774 "NotFoundException",
1775 &format!("{} is not on the suppression list", email),
1776 ));
1777 }
1778 };
1779
1780 let response = json!({
1781 "SuppressedDestination": {
1782 "EmailAddress": dest.email_address,
1783 "Reason": dest.reason,
1784 "LastUpdateTime": dest.last_update_time.timestamp() as f64,
1785 }
1786 });
1787
1788 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1789 }
1790
1791 fn delete_suppressed_destination(&self, email: &str) -> Result<AwsResponse, AwsServiceError> {
1792 let mut state = self.state.write();
1793 if state.suppressed_destinations.remove(email).is_none() {
1794 return Ok(Self::json_error(
1795 StatusCode::NOT_FOUND,
1796 "NotFoundException",
1797 &format!("{} is not on the suppression list", email),
1798 ));
1799 }
1800 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1801 }
1802
1803 fn list_suppressed_destinations(&self) -> Result<AwsResponse, AwsServiceError> {
1804 let state = self.state.read();
1805 let summaries: Vec<Value> = state
1806 .suppressed_destinations
1807 .values()
1808 .map(|d| {
1809 json!({
1810 "EmailAddress": d.email_address,
1811 "Reason": d.reason,
1812 "LastUpdateTime": d.last_update_time.timestamp() as f64,
1813 })
1814 })
1815 .collect();
1816
1817 let response = json!({
1818 "SuppressedDestinationSummaries": summaries,
1819 });
1820
1821 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1822 }
1823
1824 fn create_configuration_set_event_destination(
1827 &self,
1828 config_set_name: &str,
1829 req: &AwsRequest,
1830 ) -> Result<AwsResponse, AwsServiceError> {
1831 let body: Value = Self::parse_body(req)?;
1832
1833 let state_read = self.state.read();
1834 if !state_read.configuration_sets.contains_key(config_set_name) {
1835 return Ok(Self::json_error(
1836 StatusCode::NOT_FOUND,
1837 "NotFoundException",
1838 &format!("Configuration set {} does not exist", config_set_name),
1839 ));
1840 }
1841 drop(state_read);
1842
1843 let dest_name = match body["EventDestinationName"].as_str() {
1844 Some(n) => n.to_string(),
1845 None => {
1846 return Ok(Self::json_error(
1847 StatusCode::BAD_REQUEST,
1848 "BadRequestException",
1849 "EventDestinationName is required",
1850 ));
1851 }
1852 };
1853
1854 let event_dest = parse_event_destination_definition(&dest_name, &body["EventDestination"]);
1855
1856 let mut state = self.state.write();
1857 let dests = state
1858 .event_destinations
1859 .entry(config_set_name.to_string())
1860 .or_default();
1861
1862 if dests.iter().any(|d| d.name == dest_name) {
1863 return Ok(Self::json_error(
1864 StatusCode::CONFLICT,
1865 "AlreadyExistsException",
1866 &format!("Event destination {} already exists", dest_name),
1867 ));
1868 }
1869
1870 dests.push(event_dest);
1871
1872 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1873 }
1874
1875 fn get_configuration_set_event_destinations(
1876 &self,
1877 config_set_name: &str,
1878 ) -> Result<AwsResponse, AwsServiceError> {
1879 let state = self.state.read();
1880
1881 if !state.configuration_sets.contains_key(config_set_name) {
1882 return Ok(Self::json_error(
1883 StatusCode::NOT_FOUND,
1884 "NotFoundException",
1885 &format!("Configuration set {} does not exist", config_set_name),
1886 ));
1887 }
1888
1889 let dests = state
1890 .event_destinations
1891 .get(config_set_name)
1892 .cloned()
1893 .unwrap_or_default();
1894
1895 let dests_json: Vec<Value> = dests.iter().map(event_destination_to_json).collect();
1896
1897 let response = json!({
1898 "EventDestinations": dests_json,
1899 });
1900
1901 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
1902 }
1903
1904 fn update_configuration_set_event_destination(
1905 &self,
1906 config_set_name: &str,
1907 dest_name: &str,
1908 req: &AwsRequest,
1909 ) -> Result<AwsResponse, AwsServiceError> {
1910 let body: Value = Self::parse_body(req)?;
1911
1912 let mut state = self.state.write();
1913
1914 if !state.configuration_sets.contains_key(config_set_name) {
1915 return Ok(Self::json_error(
1916 StatusCode::NOT_FOUND,
1917 "NotFoundException",
1918 &format!("Configuration set {} does not exist", config_set_name),
1919 ));
1920 }
1921
1922 let dests = state
1923 .event_destinations
1924 .entry(config_set_name.to_string())
1925 .or_default();
1926
1927 let existing = match dests.iter_mut().find(|d| d.name == dest_name) {
1928 Some(d) => d,
1929 None => {
1930 return Ok(Self::json_error(
1931 StatusCode::NOT_FOUND,
1932 "NotFoundException",
1933 &format!("Event destination {} does not exist", dest_name),
1934 ));
1935 }
1936 };
1937
1938 let updated = parse_event_destination_definition(dest_name, &body["EventDestination"]);
1939 *existing = updated;
1940
1941 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1942 }
1943
1944 fn delete_configuration_set_event_destination(
1945 &self,
1946 config_set_name: &str,
1947 dest_name: &str,
1948 ) -> Result<AwsResponse, AwsServiceError> {
1949 let mut state = self.state.write();
1950
1951 if !state.configuration_sets.contains_key(config_set_name) {
1952 return Ok(Self::json_error(
1953 StatusCode::NOT_FOUND,
1954 "NotFoundException",
1955 &format!("Configuration set {} does not exist", config_set_name),
1956 ));
1957 }
1958
1959 let dests = state
1960 .event_destinations
1961 .entry(config_set_name.to_string())
1962 .or_default();
1963
1964 let len_before = dests.len();
1965 dests.retain(|d| d.name != dest_name);
1966
1967 if dests.len() == len_before {
1968 return Ok(Self::json_error(
1969 StatusCode::NOT_FOUND,
1970 "NotFoundException",
1971 &format!("Event destination {} does not exist", dest_name),
1972 ));
1973 }
1974
1975 Ok(AwsResponse::json(StatusCode::OK, "{}"))
1976 }
1977
1978 fn create_email_identity_policy(
1981 &self,
1982 identity_name: &str,
1983 policy_name: &str,
1984 req: &AwsRequest,
1985 ) -> Result<AwsResponse, AwsServiceError> {
1986 let body: Value = Self::parse_body(req)?;
1987
1988 let policy = match body["Policy"].as_str() {
1989 Some(p) => p.to_string(),
1990 None => {
1991 return Ok(Self::json_error(
1992 StatusCode::BAD_REQUEST,
1993 "BadRequestException",
1994 "Policy is required",
1995 ));
1996 }
1997 };
1998
1999 let mut state = self.state.write();
2000
2001 if !state.identities.contains_key(identity_name) {
2002 return Ok(Self::json_error(
2003 StatusCode::NOT_FOUND,
2004 "NotFoundException",
2005 &format!("Identity {} does not exist", identity_name),
2006 ));
2007 }
2008
2009 let policies = state
2010 .identity_policies
2011 .entry(identity_name.to_string())
2012 .or_default();
2013
2014 if policies.contains_key(policy_name) {
2015 return Ok(Self::json_error(
2016 StatusCode::CONFLICT,
2017 "AlreadyExistsException",
2018 &format!("Policy {} already exists", policy_name),
2019 ));
2020 }
2021
2022 policies.insert(policy_name.to_string(), policy);
2023
2024 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2025 }
2026
2027 fn get_email_identity_policies(
2028 &self,
2029 identity_name: &str,
2030 ) -> Result<AwsResponse, AwsServiceError> {
2031 let state = self.state.read();
2032
2033 if !state.identities.contains_key(identity_name) {
2034 return Ok(Self::json_error(
2035 StatusCode::NOT_FOUND,
2036 "NotFoundException",
2037 &format!("Identity {} does not exist", identity_name),
2038 ));
2039 }
2040
2041 let policies = state
2042 .identity_policies
2043 .get(identity_name)
2044 .cloned()
2045 .unwrap_or_default();
2046
2047 let policies_json: Value = policies
2048 .into_iter()
2049 .map(|(k, v)| (k, Value::String(v)))
2050 .collect::<serde_json::Map<String, Value>>()
2051 .into();
2052
2053 let response = json!({
2054 "Policies": policies_json,
2055 });
2056
2057 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2058 }
2059
2060 fn update_email_identity_policy(
2061 &self,
2062 identity_name: &str,
2063 policy_name: &str,
2064 req: &AwsRequest,
2065 ) -> Result<AwsResponse, AwsServiceError> {
2066 let body: Value = Self::parse_body(req)?;
2067
2068 let policy = match body["Policy"].as_str() {
2069 Some(p) => p.to_string(),
2070 None => {
2071 return Ok(Self::json_error(
2072 StatusCode::BAD_REQUEST,
2073 "BadRequestException",
2074 "Policy is required",
2075 ));
2076 }
2077 };
2078
2079 let mut state = self.state.write();
2080
2081 if !state.identities.contains_key(identity_name) {
2082 return Ok(Self::json_error(
2083 StatusCode::NOT_FOUND,
2084 "NotFoundException",
2085 &format!("Identity {} does not exist", identity_name),
2086 ));
2087 }
2088
2089 let policies = state
2090 .identity_policies
2091 .entry(identity_name.to_string())
2092 .or_default();
2093
2094 if !policies.contains_key(policy_name) {
2095 return Ok(Self::json_error(
2096 StatusCode::NOT_FOUND,
2097 "NotFoundException",
2098 &format!("Policy {} does not exist", policy_name),
2099 ));
2100 }
2101
2102 policies.insert(policy_name.to_string(), policy);
2103
2104 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2105 }
2106
2107 fn delete_email_identity_policy(
2108 &self,
2109 identity_name: &str,
2110 policy_name: &str,
2111 ) -> Result<AwsResponse, AwsServiceError> {
2112 let mut state = self.state.write();
2113
2114 if !state.identities.contains_key(identity_name) {
2115 return Ok(Self::json_error(
2116 StatusCode::NOT_FOUND,
2117 "NotFoundException",
2118 &format!("Identity {} does not exist", identity_name),
2119 ));
2120 }
2121
2122 let policies = state
2123 .identity_policies
2124 .entry(identity_name.to_string())
2125 .or_default();
2126
2127 if policies.remove(policy_name).is_none() {
2128 return Ok(Self::json_error(
2129 StatusCode::NOT_FOUND,
2130 "NotFoundException",
2131 &format!("Policy {} does not exist", policy_name),
2132 ));
2133 }
2134
2135 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2136 }
2137
2138 fn put_email_identity_dkim_attributes(
2141 &self,
2142 identity_name: &str,
2143 req: &AwsRequest,
2144 ) -> Result<AwsResponse, AwsServiceError> {
2145 let body: Value = Self::parse_body(req)?;
2146 let mut state = self.state.write();
2147
2148 let identity = match state.identities.get_mut(identity_name) {
2149 Some(id) => id,
2150 None => {
2151 return Ok(Self::json_error(
2152 StatusCode::NOT_FOUND,
2153 "NotFoundException",
2154 &format!("Identity {} does not exist", identity_name),
2155 ));
2156 }
2157 };
2158
2159 if let Some(enabled) = body["SigningEnabled"].as_bool() {
2160 identity.dkim_signing_enabled = enabled;
2161 }
2162
2163 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2164 }
2165
2166 fn put_email_identity_dkim_signing_attributes(
2167 &self,
2168 identity_name: &str,
2169 req: &AwsRequest,
2170 ) -> Result<AwsResponse, AwsServiceError> {
2171 let body: Value = Self::parse_body(req)?;
2172 let mut state = self.state.write();
2173
2174 let identity = match state.identities.get_mut(identity_name) {
2175 Some(id) => id,
2176 None => {
2177 return Ok(Self::json_error(
2178 StatusCode::NOT_FOUND,
2179 "NotFoundException",
2180 &format!("Identity {} does not exist", identity_name),
2181 ));
2182 }
2183 };
2184
2185 if let Some(origin) = body["SigningAttributesOrigin"].as_str() {
2186 identity.dkim_signing_attributes_origin = origin.to_string();
2187 }
2188
2189 if let Some(attrs) = body.get("SigningAttributes") {
2190 if let Some(key) = attrs["DomainSigningPrivateKey"].as_str() {
2191 identity.dkim_domain_signing_private_key = Some(key.to_string());
2192 }
2193 if let Some(selector) = attrs["DomainSigningSelector"].as_str() {
2194 identity.dkim_domain_signing_selector = Some(selector.to_string());
2195 }
2196 if let Some(length) = attrs["NextSigningKeyLength"].as_str() {
2197 identity.dkim_next_signing_key_length = Some(length.to_string());
2198 }
2199 }
2200
2201 let response = json!({
2202 "DkimStatus": "SUCCESS",
2203 "DkimTokens": ["token1", "token2", "token3"],
2204 });
2205
2206 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2207 }
2208
2209 fn put_email_identity_feedback_attributes(
2210 &self,
2211 identity_name: &str,
2212 req: &AwsRequest,
2213 ) -> Result<AwsResponse, AwsServiceError> {
2214 let body: Value = Self::parse_body(req)?;
2215 let mut state = self.state.write();
2216
2217 let identity = match state.identities.get_mut(identity_name) {
2218 Some(id) => id,
2219 None => {
2220 return Ok(Self::json_error(
2221 StatusCode::NOT_FOUND,
2222 "NotFoundException",
2223 &format!("Identity {} does not exist", identity_name),
2224 ));
2225 }
2226 };
2227
2228 if let Some(enabled) = body["EmailForwardingEnabled"].as_bool() {
2229 identity.email_forwarding_enabled = enabled;
2230 }
2231
2232 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2233 }
2234
2235 fn put_email_identity_mail_from_attributes(
2236 &self,
2237 identity_name: &str,
2238 req: &AwsRequest,
2239 ) -> Result<AwsResponse, AwsServiceError> {
2240 let body: Value = Self::parse_body(req)?;
2241 let mut state = self.state.write();
2242
2243 let identity = match state.identities.get_mut(identity_name) {
2244 Some(id) => id,
2245 None => {
2246 return Ok(Self::json_error(
2247 StatusCode::NOT_FOUND,
2248 "NotFoundException",
2249 &format!("Identity {} does not exist", identity_name),
2250 ));
2251 }
2252 };
2253
2254 if let Some(domain) = body["MailFromDomain"].as_str() {
2255 identity.mail_from_domain = Some(domain.to_string());
2256 }
2257 if let Some(behavior) = body["BehaviorOnMxFailure"].as_str() {
2258 identity.mail_from_behavior_on_mx_failure = behavior.to_string();
2259 }
2260
2261 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2262 }
2263
2264 fn put_email_identity_configuration_set_attributes(
2265 &self,
2266 identity_name: &str,
2267 req: &AwsRequest,
2268 ) -> Result<AwsResponse, AwsServiceError> {
2269 let body: Value = Self::parse_body(req)?;
2270 let mut state = self.state.write();
2271
2272 let identity = match state.identities.get_mut(identity_name) {
2273 Some(id) => id,
2274 None => {
2275 return Ok(Self::json_error(
2276 StatusCode::NOT_FOUND,
2277 "NotFoundException",
2278 &format!("Identity {} does not exist", identity_name),
2279 ));
2280 }
2281 };
2282
2283 identity.configuration_set_name =
2284 body["ConfigurationSetName"].as_str().map(|s| s.to_string());
2285
2286 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2287 }
2288
2289 fn put_configuration_set_sending_options(
2292 &self,
2293 name: &str,
2294 req: &AwsRequest,
2295 ) -> Result<AwsResponse, AwsServiceError> {
2296 let body: Value = Self::parse_body(req)?;
2297 let mut state = self.state.write();
2298
2299 let cs = match state.configuration_sets.get_mut(name) {
2300 Some(cs) => cs,
2301 None => {
2302 return Ok(Self::json_error(
2303 StatusCode::NOT_FOUND,
2304 "NotFoundException",
2305 &format!("Configuration set {} does not exist", name),
2306 ));
2307 }
2308 };
2309
2310 if let Some(enabled) = body["SendingEnabled"].as_bool() {
2311 cs.sending_enabled = enabled;
2312 }
2313
2314 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2315 }
2316
2317 fn put_configuration_set_delivery_options(
2318 &self,
2319 name: &str,
2320 req: &AwsRequest,
2321 ) -> Result<AwsResponse, AwsServiceError> {
2322 let body: Value = Self::parse_body(req)?;
2323 let mut state = self.state.write();
2324
2325 let cs = match state.configuration_sets.get_mut(name) {
2326 Some(cs) => cs,
2327 None => {
2328 return Ok(Self::json_error(
2329 StatusCode::NOT_FOUND,
2330 "NotFoundException",
2331 &format!("Configuration set {} does not exist", name),
2332 ));
2333 }
2334 };
2335
2336 if let Some(policy) = body["TlsPolicy"].as_str() {
2337 cs.tls_policy = policy.to_string();
2338 }
2339 if let Some(pool) = body["SendingPoolName"].as_str() {
2340 cs.sending_pool_name = Some(pool.to_string());
2341 }
2342
2343 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2344 }
2345
2346 fn put_configuration_set_tracking_options(
2347 &self,
2348 name: &str,
2349 req: &AwsRequest,
2350 ) -> Result<AwsResponse, AwsServiceError> {
2351 let body: Value = Self::parse_body(req)?;
2352 let mut state = self.state.write();
2353
2354 let cs = match state.configuration_sets.get_mut(name) {
2355 Some(cs) => cs,
2356 None => {
2357 return Ok(Self::json_error(
2358 StatusCode::NOT_FOUND,
2359 "NotFoundException",
2360 &format!("Configuration set {} does not exist", name),
2361 ));
2362 }
2363 };
2364
2365 if let Some(domain) = body["CustomRedirectDomain"].as_str() {
2366 cs.custom_redirect_domain = Some(domain.to_string());
2367 }
2368 if let Some(policy) = body["HttpsPolicy"].as_str() {
2369 cs.https_policy = Some(policy.to_string());
2370 }
2371
2372 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2373 }
2374
2375 fn put_configuration_set_suppression_options(
2376 &self,
2377 name: &str,
2378 req: &AwsRequest,
2379 ) -> Result<AwsResponse, AwsServiceError> {
2380 let body: Value = Self::parse_body(req)?;
2381 let mut state = self.state.write();
2382
2383 let cs = match state.configuration_sets.get_mut(name) {
2384 Some(cs) => cs,
2385 None => {
2386 return Ok(Self::json_error(
2387 StatusCode::NOT_FOUND,
2388 "NotFoundException",
2389 &format!("Configuration set {} does not exist", name),
2390 ));
2391 }
2392 };
2393
2394 cs.suppressed_reasons = extract_string_array(&body["SuppressedReasons"]);
2395
2396 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2397 }
2398
2399 fn put_configuration_set_reputation_options(
2400 &self,
2401 name: &str,
2402 req: &AwsRequest,
2403 ) -> Result<AwsResponse, AwsServiceError> {
2404 let body: Value = Self::parse_body(req)?;
2405 let mut state = self.state.write();
2406
2407 let cs = match state.configuration_sets.get_mut(name) {
2408 Some(cs) => cs,
2409 None => {
2410 return Ok(Self::json_error(
2411 StatusCode::NOT_FOUND,
2412 "NotFoundException",
2413 &format!("Configuration set {} does not exist", name),
2414 ));
2415 }
2416 };
2417
2418 if let Some(enabled) = body["ReputationMetricsEnabled"].as_bool() {
2419 cs.reputation_metrics_enabled = enabled;
2420 }
2421
2422 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2423 }
2424
2425 fn put_configuration_set_vdm_options(
2426 &self,
2427 name: &str,
2428 req: &AwsRequest,
2429 ) -> Result<AwsResponse, AwsServiceError> {
2430 let body: Value = Self::parse_body(req)?;
2431 let mut state = self.state.write();
2432
2433 let cs = match state.configuration_sets.get_mut(name) {
2434 Some(cs) => cs,
2435 None => {
2436 return Ok(Self::json_error(
2437 StatusCode::NOT_FOUND,
2438 "NotFoundException",
2439 &format!("Configuration set {} does not exist", name),
2440 ));
2441 }
2442 };
2443
2444 cs.vdm_options = Some(body);
2445
2446 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2447 }
2448
2449 fn put_configuration_set_archiving_options(
2450 &self,
2451 name: &str,
2452 req: &AwsRequest,
2453 ) -> Result<AwsResponse, AwsServiceError> {
2454 let body: Value = Self::parse_body(req)?;
2455 let mut state = self.state.write();
2456
2457 let cs = match state.configuration_sets.get_mut(name) {
2458 Some(cs) => cs,
2459 None => {
2460 return Ok(Self::json_error(
2461 StatusCode::NOT_FOUND,
2462 "NotFoundException",
2463 &format!("Configuration set {} does not exist", name),
2464 ));
2465 }
2466 };
2467
2468 cs.archive_arn = body["ArchiveArn"].as_str().map(|s| s.to_string());
2469
2470 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2471 }
2472
2473 fn create_custom_verification_email_template(
2476 &self,
2477 req: &AwsRequest,
2478 ) -> Result<AwsResponse, AwsServiceError> {
2479 let body: Value = Self::parse_body(req)?;
2480
2481 let template_name = match body["TemplateName"].as_str() {
2482 Some(n) => n.to_string(),
2483 None => {
2484 return Ok(Self::json_error(
2485 StatusCode::BAD_REQUEST,
2486 "BadRequestException",
2487 "TemplateName is required",
2488 ));
2489 }
2490 };
2491
2492 let from_email = body["FromEmailAddress"].as_str().unwrap_or("").to_string();
2493 let subject = body["TemplateSubject"].as_str().unwrap_or("").to_string();
2494 let content = body["TemplateContent"].as_str().unwrap_or("").to_string();
2495 let success_url = body["SuccessRedirectionURL"]
2496 .as_str()
2497 .unwrap_or("")
2498 .to_string();
2499 let failure_url = body["FailureRedirectionURL"]
2500 .as_str()
2501 .unwrap_or("")
2502 .to_string();
2503
2504 let mut state = self.state.write();
2505
2506 if state
2507 .custom_verification_email_templates
2508 .contains_key(&template_name)
2509 {
2510 return Ok(Self::json_error(
2511 StatusCode::CONFLICT,
2512 "AlreadyExistsException",
2513 &format!(
2514 "Custom verification email template {} already exists",
2515 template_name
2516 ),
2517 ));
2518 }
2519
2520 state.custom_verification_email_templates.insert(
2521 template_name.clone(),
2522 CustomVerificationEmailTemplate {
2523 template_name,
2524 from_email_address: from_email,
2525 template_subject: subject,
2526 template_content: content,
2527 success_redirection_url: success_url,
2528 failure_redirection_url: failure_url,
2529 created_at: Utc::now(),
2530 },
2531 );
2532
2533 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2534 }
2535
2536 fn get_custom_verification_email_template(
2537 &self,
2538 name: &str,
2539 ) -> Result<AwsResponse, AwsServiceError> {
2540 let state = self.state.read();
2541 let tmpl = match state.custom_verification_email_templates.get(name) {
2542 Some(t) => t,
2543 None => {
2544 return Ok(Self::json_error(
2545 StatusCode::NOT_FOUND,
2546 "NotFoundException",
2547 &format!("Custom verification email template {} does not exist", name),
2548 ));
2549 }
2550 };
2551
2552 let response = json!({
2553 "TemplateName": tmpl.template_name,
2554 "FromEmailAddress": tmpl.from_email_address,
2555 "TemplateSubject": tmpl.template_subject,
2556 "TemplateContent": tmpl.template_content,
2557 "SuccessRedirectionURL": tmpl.success_redirection_url,
2558 "FailureRedirectionURL": tmpl.failure_redirection_url,
2559 });
2560
2561 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2562 }
2563
2564 fn list_custom_verification_email_templates(
2565 &self,
2566 req: &AwsRequest,
2567 ) -> Result<AwsResponse, AwsServiceError> {
2568 let state = self.state.read();
2569
2570 let page_size: usize = req
2571 .query_params
2572 .get("PageSize")
2573 .and_then(|s| s.parse().ok())
2574 .unwrap_or(20);
2575
2576 let mut templates: Vec<&CustomVerificationEmailTemplate> =
2577 state.custom_verification_email_templates.values().collect();
2578 templates.sort_by(|a, b| a.template_name.cmp(&b.template_name));
2579
2580 let next_token = req.query_params.get("NextToken");
2581 let start_idx = if let Some(token) = next_token {
2582 templates
2583 .iter()
2584 .position(|t| t.template_name == *token)
2585 .unwrap_or(0)
2586 } else {
2587 0
2588 };
2589
2590 let page: Vec<Value> = templates
2591 .iter()
2592 .skip(start_idx)
2593 .take(page_size)
2594 .map(|t| {
2595 json!({
2596 "TemplateName": t.template_name,
2597 "FromEmailAddress": t.from_email_address,
2598 "TemplateSubject": t.template_subject,
2599 "SuccessRedirectionURL": t.success_redirection_url,
2600 "FailureRedirectionURL": t.failure_redirection_url,
2601 })
2602 })
2603 .collect();
2604
2605 let mut response = json!({
2606 "CustomVerificationEmailTemplates": page,
2607 });
2608
2609 if start_idx + page_size < templates.len() {
2611 if let Some(next) = templates.get(start_idx + page_size) {
2612 response["NextToken"] = json!(next.template_name);
2613 }
2614 }
2615
2616 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2617 }
2618
2619 fn update_custom_verification_email_template(
2620 &self,
2621 name: &str,
2622 req: &AwsRequest,
2623 ) -> Result<AwsResponse, AwsServiceError> {
2624 let body: Value = Self::parse_body(req)?;
2625 let mut state = self.state.write();
2626
2627 let tmpl = match state.custom_verification_email_templates.get_mut(name) {
2628 Some(t) => t,
2629 None => {
2630 return Ok(Self::json_error(
2631 StatusCode::NOT_FOUND,
2632 "NotFoundException",
2633 &format!("Custom verification email template {} does not exist", name),
2634 ));
2635 }
2636 };
2637
2638 if let Some(from) = body["FromEmailAddress"].as_str() {
2639 tmpl.from_email_address = from.to_string();
2640 }
2641 if let Some(subject) = body["TemplateSubject"].as_str() {
2642 tmpl.template_subject = subject.to_string();
2643 }
2644 if let Some(content) = body["TemplateContent"].as_str() {
2645 tmpl.template_content = content.to_string();
2646 }
2647 if let Some(url) = body["SuccessRedirectionURL"].as_str() {
2648 tmpl.success_redirection_url = url.to_string();
2649 }
2650 if let Some(url) = body["FailureRedirectionURL"].as_str() {
2651 tmpl.failure_redirection_url = url.to_string();
2652 }
2653
2654 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2655 }
2656
2657 fn delete_custom_verification_email_template(
2658 &self,
2659 name: &str,
2660 ) -> Result<AwsResponse, AwsServiceError> {
2661 let mut state = self.state.write();
2662
2663 if state
2664 .custom_verification_email_templates
2665 .remove(name)
2666 .is_none()
2667 {
2668 return Ok(Self::json_error(
2669 StatusCode::NOT_FOUND,
2670 "NotFoundException",
2671 &format!("Custom verification email template {} does not exist", name),
2672 ));
2673 }
2674
2675 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2676 }
2677
2678 fn send_custom_verification_email(
2679 &self,
2680 req: &AwsRequest,
2681 ) -> Result<AwsResponse, AwsServiceError> {
2682 let body: Value = Self::parse_body(req)?;
2683
2684 let email_address = match body["EmailAddress"].as_str() {
2685 Some(e) => e.to_string(),
2686 None => {
2687 return Ok(Self::json_error(
2688 StatusCode::BAD_REQUEST,
2689 "BadRequestException",
2690 "EmailAddress is required",
2691 ));
2692 }
2693 };
2694
2695 let template_name = match body["TemplateName"].as_str() {
2696 Some(n) => n.to_string(),
2697 None => {
2698 return Ok(Self::json_error(
2699 StatusCode::BAD_REQUEST,
2700 "BadRequestException",
2701 "TemplateName is required",
2702 ));
2703 }
2704 };
2705
2706 {
2708 let state = self.state.read();
2709 if !state
2710 .custom_verification_email_templates
2711 .contains_key(&template_name)
2712 {
2713 return Ok(Self::json_error(
2714 StatusCode::NOT_FOUND,
2715 "NotFoundException",
2716 &format!(
2717 "Custom verification email template {} does not exist",
2718 template_name
2719 ),
2720 ));
2721 }
2722 }
2723
2724 let message_id = uuid::Uuid::new_v4().to_string();
2725
2726 let sent = SentEmail {
2728 message_id: message_id.clone(),
2729 from: String::new(),
2730 to: vec![email_address],
2731 cc: Vec::new(),
2732 bcc: Vec::new(),
2733 subject: Some(format!("Custom verification: {}", template_name)),
2734 html_body: None,
2735 text_body: None,
2736 raw_data: None,
2737 template_name: Some(template_name),
2738 template_data: None,
2739 timestamp: Utc::now(),
2740 };
2741
2742 self.state.write().sent_emails.push(sent);
2743
2744 let response = json!({
2745 "MessageId": message_id,
2746 });
2747
2748 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2749 }
2750
2751 fn test_render_email_template(
2754 &self,
2755 template_name: &str,
2756 req: &AwsRequest,
2757 ) -> Result<AwsResponse, AwsServiceError> {
2758 let body: Value = Self::parse_body(req)?;
2759
2760 let template_data_str = match body["TemplateData"].as_str() {
2761 Some(d) => d.to_string(),
2762 None => {
2763 return Ok(Self::json_error(
2764 StatusCode::BAD_REQUEST,
2765 "BadRequestException",
2766 "TemplateData is required",
2767 ));
2768 }
2769 };
2770
2771 let state = self.state.read();
2772 let template = match state.templates.get(template_name) {
2773 Some(t) => t,
2774 None => {
2775 return Ok(Self::json_error(
2776 StatusCode::NOT_FOUND,
2777 "NotFoundException",
2778 &format!("Template {} does not exist", template_name),
2779 ));
2780 }
2781 };
2782
2783 let data: HashMap<String, Value> =
2785 serde_json::from_str(&template_data_str).unwrap_or_default();
2786
2787 let substitute = |text: &str| -> String {
2788 let mut result = text.to_string();
2789 for (key, value) in &data {
2790 let placeholder = format!("{{{{{}}}}}", key);
2791 let replacement = match value {
2792 Value::String(s) => s.clone(),
2793 other => other.to_string(),
2794 };
2795 result = result.replace(&placeholder, &replacement);
2796 }
2797 result
2798 };
2799
2800 let rendered_subject = template
2801 .subject
2802 .as_deref()
2803 .map(&substitute)
2804 .unwrap_or_default();
2805 let rendered_html = template.html_body.as_deref().map(&substitute);
2806 let rendered_text = template.text_body.as_deref().map(&substitute);
2807
2808 let mut mime = format!("Subject: {}\r\n", rendered_subject);
2810 mime.push_str("MIME-Version: 1.0\r\n");
2811 mime.push_str("Content-Type: text/html; charset=UTF-8\r\n");
2812 mime.push_str("\r\n");
2813 if let Some(ref html) = rendered_html {
2814 mime.push_str(html);
2815 } else if let Some(ref text) = rendered_text {
2816 mime.push_str(text);
2817 }
2818
2819 let response = json!({
2820 "RenderedTemplate": mime,
2821 });
2822
2823 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2824 }
2825
2826 fn send_bulk_email(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2827 let body: Value = Self::parse_body(req)?;
2828
2829 let from = body["FromEmailAddress"].as_str().unwrap_or("").to_string();
2830 let config_set_name = body["ConfigurationSetName"].as_str().map(|s| s.to_string());
2831
2832 let entries = match body["BulkEmailEntries"].as_array() {
2833 Some(arr) if !arr.is_empty() => arr.clone(),
2834 _ => {
2835 return Ok(Self::json_error(
2836 StatusCode::BAD_REQUEST,
2837 "BadRequestException",
2838 "BulkEmailEntries is required and must not be empty",
2839 ));
2840 }
2841 };
2842
2843 let mut results = Vec::new();
2844
2845 for entry in &entries {
2846 let to = extract_string_array(&entry["Destination"]["ToAddresses"]);
2847 let cc = extract_string_array(&entry["Destination"]["CcAddresses"]);
2848 let bcc = extract_string_array(&entry["Destination"]["BccAddresses"]);
2849
2850 let message_id = uuid::Uuid::new_v4().to_string();
2851
2852 let template_name = body["DefaultContent"]["Template"]["TemplateName"]
2853 .as_str()
2854 .map(|s| s.to_string());
2855 let template_data = entry["ReplacementEmailContent"]["ReplacementTemplate"]
2856 ["ReplacementTemplateData"]
2857 .as_str()
2858 .or_else(|| body["DefaultContent"]["Template"]["TemplateData"].as_str())
2859 .map(|s| s.to_string());
2860
2861 let sent = SentEmail {
2862 message_id: message_id.clone(),
2863 from: from.clone(),
2864 to,
2865 cc,
2866 bcc,
2867 subject: None,
2868 html_body: None,
2869 text_body: None,
2870 raw_data: None,
2871 template_name,
2872 template_data,
2873 timestamp: Utc::now(),
2874 };
2875
2876 if let Some(ref ctx) = self.delivery_ctx {
2878 crate::fanout::process_send_events(ctx, &sent, config_set_name.as_deref());
2879 }
2880
2881 self.state.write().sent_emails.push(sent);
2882
2883 results.push(json!({
2884 "Status": "SUCCESS",
2885 "MessageId": message_id,
2886 }));
2887 }
2888
2889 let response = json!({
2890 "BulkEmailEntryResults": results,
2891 });
2892
2893 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2894 }
2895
2896 fn create_dedicated_ip_pool(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
2899 let body: Value = Self::parse_body(req)?;
2900 let pool_name = match body["PoolName"].as_str() {
2901 Some(n) => n.to_string(),
2902 None => {
2903 return Ok(Self::json_error(
2904 StatusCode::BAD_REQUEST,
2905 "BadRequestException",
2906 "PoolName is required",
2907 ));
2908 }
2909 };
2910 let scaling_mode = body["ScalingMode"]
2911 .as_str()
2912 .unwrap_or("STANDARD")
2913 .to_string();
2914
2915 let mut state = self.state.write();
2916
2917 if state.dedicated_ip_pools.contains_key(&pool_name) {
2918 return Ok(Self::json_error(
2919 StatusCode::CONFLICT,
2920 "AlreadyExistsException",
2921 &format!("Pool {} already exists", pool_name),
2922 ));
2923 }
2924
2925 if scaling_mode == "MANAGED" {
2927 let pool_idx = state.dedicated_ip_pools.len() as u8;
2928 for i in 1..=3 {
2929 let ip_addr = format!("198.51.100.{}", pool_idx * 10 + i);
2930 state.dedicated_ips.insert(
2931 ip_addr.clone(),
2932 DedicatedIp {
2933 ip: ip_addr,
2934 warmup_status: "NOT_APPLICABLE".to_string(),
2935 warmup_percentage: -1,
2936 pool_name: pool_name.clone(),
2937 },
2938 );
2939 }
2940 }
2941
2942 state.dedicated_ip_pools.insert(
2943 pool_name.clone(),
2944 DedicatedIpPool {
2945 pool_name,
2946 scaling_mode,
2947 },
2948 );
2949
2950 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2951 }
2952
2953 fn list_dedicated_ip_pools(&self) -> Result<AwsResponse, AwsServiceError> {
2954 let state = self.state.read();
2955 let pools: Vec<&str> = state
2956 .dedicated_ip_pools
2957 .keys()
2958 .map(|k| k.as_str())
2959 .collect();
2960 let response = json!({ "DedicatedIpPools": pools });
2961 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
2962 }
2963
2964 fn delete_dedicated_ip_pool(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
2965 let mut state = self.state.write();
2966 if state.dedicated_ip_pools.remove(name).is_none() {
2967 return Ok(Self::json_error(
2968 StatusCode::NOT_FOUND,
2969 "NotFoundException",
2970 &format!("Pool {} does not exist", name),
2971 ));
2972 }
2973 state.dedicated_ips.retain(|_, ip| ip.pool_name != name);
2975 Ok(AwsResponse::json(StatusCode::OK, "{}"))
2976 }
2977
2978 fn put_dedicated_ip_pool_scaling_attributes(
2979 &self,
2980 name: &str,
2981 req: &AwsRequest,
2982 ) -> Result<AwsResponse, AwsServiceError> {
2983 let body: Value = Self::parse_body(req)?;
2984 let scaling_mode = match body["ScalingMode"].as_str() {
2985 Some(m) => m.to_string(),
2986 None => {
2987 return Ok(Self::json_error(
2988 StatusCode::BAD_REQUEST,
2989 "BadRequestException",
2990 "ScalingMode is required",
2991 ));
2992 }
2993 };
2994
2995 let mut state = self.state.write();
2996 let pool = match state.dedicated_ip_pools.get_mut(name) {
2997 Some(p) => p,
2998 None => {
2999 return Ok(Self::json_error(
3000 StatusCode::NOT_FOUND,
3001 "NotFoundException",
3002 &format!("Pool {} does not exist", name),
3003 ));
3004 }
3005 };
3006
3007 if pool.scaling_mode == "MANAGED" && scaling_mode == "STANDARD" {
3008 return Ok(Self::json_error(
3009 StatusCode::BAD_REQUEST,
3010 "BadRequestException",
3011 "Cannot change scaling mode from MANAGED to STANDARD",
3012 ));
3013 }
3014
3015 let old_mode = pool.scaling_mode.clone();
3016 pool.scaling_mode = scaling_mode.clone();
3017
3018 if old_mode == "STANDARD" && scaling_mode == "MANAGED" {
3020 let pool_idx = state.dedicated_ip_pools.len() as u8;
3021 for i in 1..=3u8 {
3022 let ip_addr = format!("198.51.100.{}", pool_idx * 10 + i);
3023 state.dedicated_ips.insert(
3024 ip_addr.clone(),
3025 DedicatedIp {
3026 ip: ip_addr,
3027 warmup_status: "NOT_APPLICABLE".to_string(),
3028 warmup_percentage: -1,
3029 pool_name: name.to_string(),
3030 },
3031 );
3032 }
3033 }
3034
3035 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3036 }
3037
3038 fn get_dedicated_ip(&self, ip: &str) -> Result<AwsResponse, AwsServiceError> {
3041 let state = self.state.read();
3042 let dip = match state.dedicated_ips.get(ip) {
3043 Some(d) => d,
3044 None => {
3045 return Ok(Self::json_error(
3046 StatusCode::NOT_FOUND,
3047 "NotFoundException",
3048 &format!("Dedicated IP {} does not exist", ip),
3049 ));
3050 }
3051 };
3052 let response = json!({
3053 "DedicatedIp": {
3054 "Ip": dip.ip,
3055 "WarmupStatus": dip.warmup_status,
3056 "WarmupPercentage": dip.warmup_percentage,
3057 "PoolName": dip.pool_name,
3058 }
3059 });
3060 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3061 }
3062
3063 fn get_dedicated_ips(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3064 let state = self.state.read();
3065 let pool_filter = req.query_params.get("PoolName").map(|s| s.as_str());
3066 let ips: Vec<Value> = state
3067 .dedicated_ips
3068 .values()
3069 .filter(|ip| match pool_filter {
3070 Some(pool) => ip.pool_name == pool,
3071 None => true,
3072 })
3073 .map(|ip| {
3074 json!({
3075 "Ip": ip.ip,
3076 "WarmupStatus": ip.warmup_status,
3077 "WarmupPercentage": ip.warmup_percentage,
3078 "PoolName": ip.pool_name,
3079 })
3080 })
3081 .collect();
3082 let response = json!({ "DedicatedIps": ips });
3083 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3084 }
3085
3086 fn put_dedicated_ip_in_pool(
3087 &self,
3088 ip: &str,
3089 req: &AwsRequest,
3090 ) -> Result<AwsResponse, AwsServiceError> {
3091 let body: Value = Self::parse_body(req)?;
3092 let dest_pool = match body["DestinationPoolName"].as_str() {
3093 Some(p) => p.to_string(),
3094 None => {
3095 return Ok(Self::json_error(
3096 StatusCode::BAD_REQUEST,
3097 "BadRequestException",
3098 "DestinationPoolName is required",
3099 ));
3100 }
3101 };
3102
3103 let mut state = self.state.write();
3104
3105 if !state.dedicated_ip_pools.contains_key(&dest_pool) {
3106 return Ok(Self::json_error(
3107 StatusCode::NOT_FOUND,
3108 "NotFoundException",
3109 &format!("Pool {} does not exist", dest_pool),
3110 ));
3111 }
3112
3113 let dip = match state.dedicated_ips.get_mut(ip) {
3114 Some(d) => d,
3115 None => {
3116 return Ok(Self::json_error(
3117 StatusCode::NOT_FOUND,
3118 "NotFoundException",
3119 &format!("Dedicated IP {} does not exist", ip),
3120 ));
3121 }
3122 };
3123 dip.pool_name = dest_pool;
3124 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3125 }
3126
3127 fn put_dedicated_ip_warmup_attributes(
3128 &self,
3129 ip: &str,
3130 req: &AwsRequest,
3131 ) -> Result<AwsResponse, AwsServiceError> {
3132 let body: Value = Self::parse_body(req)?;
3133 let warmup_pct = match body["WarmupPercentage"].as_i64() {
3134 Some(p) => p as i32,
3135 None => {
3136 return Ok(Self::json_error(
3137 StatusCode::BAD_REQUEST,
3138 "BadRequestException",
3139 "WarmupPercentage is required",
3140 ));
3141 }
3142 };
3143
3144 let mut state = self.state.write();
3145 let dip = match state.dedicated_ips.get_mut(ip) {
3146 Some(d) => d,
3147 None => {
3148 return Ok(Self::json_error(
3149 StatusCode::NOT_FOUND,
3150 "NotFoundException",
3151 &format!("Dedicated IP {} does not exist", ip),
3152 ));
3153 }
3154 };
3155 dip.warmup_percentage = warmup_pct;
3156 dip.warmup_status = if warmup_pct >= 100 {
3157 "DONE".to_string()
3158 } else {
3159 "IN_PROGRESS".to_string()
3160 };
3161 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3162 }
3163
3164 fn put_account_dedicated_ip_warmup_attributes(
3165 &self,
3166 req: &AwsRequest,
3167 ) -> Result<AwsResponse, AwsServiceError> {
3168 let body: Value = Self::parse_body(req)?;
3169 let enabled = body["AutoWarmupEnabled"].as_bool().unwrap_or(false);
3170 self.state
3171 .write()
3172 .account_settings
3173 .dedicated_ip_auto_warmup_enabled = enabled;
3174 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3175 }
3176
3177 fn create_multi_region_endpoint(
3180 &self,
3181 req: &AwsRequest,
3182 ) -> Result<AwsResponse, AwsServiceError> {
3183 let body: Value = Self::parse_body(req)?;
3184 let endpoint_name = match body["EndpointName"].as_str() {
3185 Some(n) => n.to_string(),
3186 None => {
3187 return Ok(Self::json_error(
3188 StatusCode::BAD_REQUEST,
3189 "BadRequestException",
3190 "EndpointName is required",
3191 ));
3192 }
3193 };
3194
3195 let mut state = self.state.write();
3196 if state.multi_region_endpoints.contains_key(&endpoint_name) {
3197 return Ok(Self::json_error(
3198 StatusCode::CONFLICT,
3199 "AlreadyExistsException",
3200 &format!("Endpoint {} already exists", endpoint_name),
3201 ));
3202 }
3203
3204 let mut regions = Vec::new();
3206 if let Some(details) = body.get("Details") {
3207 if let Some(routes) = details["RoutesDetails"].as_array() {
3208 for r in routes {
3209 if let Some(region) = r["Region"].as_str() {
3210 regions.push(region.to_string());
3211 }
3212 }
3213 }
3214 }
3215 if !regions.contains(&state.region) {
3217 regions.insert(0, state.region.clone());
3218 }
3219
3220 let endpoint_id = format!(
3221 "ses-{}-{}",
3222 state.region,
3223 uuid::Uuid::new_v4().to_string().split('-').next().unwrap()
3224 );
3225 let now = Utc::now();
3226
3227 state.multi_region_endpoints.insert(
3228 endpoint_name.clone(),
3229 MultiRegionEndpoint {
3230 endpoint_name,
3231 endpoint_id: endpoint_id.clone(),
3232 status: "READY".to_string(),
3233 regions,
3234 created_at: now,
3235 last_updated_at: now,
3236 },
3237 );
3238
3239 let response = json!({
3240 "Status": "READY",
3241 "EndpointId": endpoint_id,
3242 });
3243 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3244 }
3245
3246 fn get_multi_region_endpoint(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
3247 let state = self.state.read();
3248 let ep = match state.multi_region_endpoints.get(name) {
3249 Some(e) => e,
3250 None => {
3251 return Ok(Self::json_error(
3252 StatusCode::NOT_FOUND,
3253 "NotFoundException",
3254 &format!("Endpoint {} does not exist", name),
3255 ));
3256 }
3257 };
3258
3259 let routes: Vec<Value> = ep.regions.iter().map(|r| json!({ "Region": r })).collect();
3260
3261 let response = json!({
3262 "EndpointName": ep.endpoint_name,
3263 "EndpointId": ep.endpoint_id,
3264 "Status": ep.status,
3265 "Routes": routes,
3266 "CreatedTimestamp": ep.created_at.timestamp() as f64,
3267 "LastUpdatedTimestamp": ep.last_updated_at.timestamp() as f64,
3268 });
3269 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3270 }
3271
3272 fn list_multi_region_endpoints(&self) -> Result<AwsResponse, AwsServiceError> {
3273 let state = self.state.read();
3274 let endpoints: Vec<Value> = state
3275 .multi_region_endpoints
3276 .values()
3277 .map(|ep| {
3278 json!({
3279 "EndpointName": ep.endpoint_name,
3280 "EndpointId": ep.endpoint_id,
3281 "Status": ep.status,
3282 "Regions": ep.regions,
3283 "CreatedTimestamp": ep.created_at.timestamp() as f64,
3284 "LastUpdatedTimestamp": ep.last_updated_at.timestamp() as f64,
3285 })
3286 })
3287 .collect();
3288 let response = json!({ "MultiRegionEndpoints": endpoints });
3289 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3290 }
3291
3292 fn delete_multi_region_endpoint(&self, name: &str) -> Result<AwsResponse, AwsServiceError> {
3293 let mut state = self.state.write();
3294 if state.multi_region_endpoints.remove(name).is_none() {
3295 return Ok(Self::json_error(
3296 StatusCode::NOT_FOUND,
3297 "NotFoundException",
3298 &format!("Endpoint {} does not exist", name),
3299 ));
3300 }
3301 let response = json!({ "Status": "DELETING" });
3302 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3303 }
3304
3305 fn put_account_details(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3308 let body: Value = Self::parse_body(req)?;
3309 let mail_type = match body["MailType"].as_str() {
3310 Some(m) => m.to_string(),
3311 None => {
3312 return Ok(Self::json_error(
3313 StatusCode::BAD_REQUEST,
3314 "BadRequestException",
3315 "MailType is required",
3316 ));
3317 }
3318 };
3319 let website_url = match body["WebsiteURL"].as_str() {
3320 Some(u) => u.to_string(),
3321 None => {
3322 return Ok(Self::json_error(
3323 StatusCode::BAD_REQUEST,
3324 "BadRequestException",
3325 "WebsiteURL is required",
3326 ));
3327 }
3328 };
3329 let contact_language = body["ContactLanguage"].as_str().map(|s| s.to_string());
3330 let use_case_description = body["UseCaseDescription"].as_str().map(|s| s.to_string());
3331 let additional = body["AdditionalContactEmailAddresses"]
3332 .as_array()
3333 .map(|arr| {
3334 arr.iter()
3335 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3336 .collect()
3337 })
3338 .unwrap_or_default();
3339 let production_access = body["ProductionAccessEnabled"].as_bool();
3340
3341 let mut state = self.state.write();
3342 state.account_settings.details = Some(AccountDetails {
3343 mail_type: Some(mail_type),
3344 website_url: Some(website_url),
3345 contact_language,
3346 use_case_description,
3347 additional_contact_email_addresses: additional,
3348 production_access_enabled: production_access,
3349 });
3350 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3351 }
3352
3353 fn put_account_sending_attributes(
3354 &self,
3355 req: &AwsRequest,
3356 ) -> Result<AwsResponse, AwsServiceError> {
3357 let body: Value = Self::parse_body(req)?;
3358 let enabled = body["SendingEnabled"].as_bool().unwrap_or(false);
3359 self.state.write().account_settings.sending_enabled = enabled;
3360 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3361 }
3362
3363 fn put_account_suppression_attributes(
3364 &self,
3365 req: &AwsRequest,
3366 ) -> Result<AwsResponse, AwsServiceError> {
3367 let body: Value = Self::parse_body(req)?;
3368 let reasons = body["SuppressedReasons"]
3369 .as_array()
3370 .map(|arr| {
3371 arr.iter()
3372 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3373 .collect()
3374 })
3375 .unwrap_or_default();
3376 self.state.write().account_settings.suppressed_reasons = reasons;
3377 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3378 }
3379
3380 fn put_account_vdm_attributes(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3381 let body: Value = Self::parse_body(req)?;
3382 let vdm = match body.get("VdmAttributes") {
3383 Some(v) => v.clone(),
3384 None => {
3385 return Ok(Self::json_error(
3386 StatusCode::BAD_REQUEST,
3387 "BadRequestException",
3388 "VdmAttributes is required",
3389 ));
3390 }
3391 };
3392 self.state.write().account_settings.vdm_attributes = Some(vdm);
3393 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3394 }
3395
3396 fn create_import_job(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3399 let body: Value = Self::parse_body(req)?;
3400
3401 let import_destination = match body.get("ImportDestination") {
3402 Some(v) if v.is_object() => v.clone(),
3403 _ => {
3404 return Ok(Self::json_error(
3405 StatusCode::BAD_REQUEST,
3406 "BadRequestException",
3407 "ImportDestination is required",
3408 ));
3409 }
3410 };
3411
3412 let import_data_source = match body.get("ImportDataSource") {
3413 Some(v) if v.is_object() => v.clone(),
3414 _ => {
3415 return Ok(Self::json_error(
3416 StatusCode::BAD_REQUEST,
3417 "BadRequestException",
3418 "ImportDataSource is required",
3419 ));
3420 }
3421 };
3422
3423 let job_id = uuid::Uuid::new_v4().to_string();
3424 let now = Utc::now();
3425
3426 let job = ImportJob {
3427 job_id: job_id.clone(),
3428 import_destination,
3429 import_data_source,
3430 job_status: "COMPLETED".to_string(),
3431 created_timestamp: now,
3432 completed_timestamp: Some(now),
3433 processed_records_count: 0,
3434 failed_records_count: 0,
3435 };
3436
3437 self.state.write().import_jobs.insert(job_id.clone(), job);
3438
3439 let response = json!({ "JobId": job_id });
3440 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3441 }
3442
3443 fn get_import_job(&self, job_id: &str) -> Result<AwsResponse, AwsServiceError> {
3444 let state = self.state.read();
3445 let job = match state.import_jobs.get(job_id) {
3446 Some(j) => j,
3447 None => {
3448 return Ok(Self::json_error(
3449 StatusCode::NOT_FOUND,
3450 "NotFoundException",
3451 &format!("Import job {} does not exist", job_id),
3452 ));
3453 }
3454 };
3455
3456 let mut response = json!({
3457 "JobId": job.job_id,
3458 "ImportDestination": job.import_destination,
3459 "ImportDataSource": job.import_data_source,
3460 "JobStatus": job.job_status,
3461 "CreatedTimestamp": job.created_timestamp.timestamp() as f64,
3462 "ProcessedRecordsCount": job.processed_records_count,
3463 "FailedRecordsCount": job.failed_records_count,
3464 });
3465 if let Some(ref ts) = job.completed_timestamp {
3466 response["CompletedTimestamp"] = json!(ts.timestamp() as f64);
3467 }
3468
3469 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3470 }
3471
3472 fn list_import_jobs(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3473 let body: Value = serde_json::from_slice(&req.body).unwrap_or(json!({}));
3474 let filter_type = body["ImportDestinationType"].as_str();
3475
3476 let state = self.state.read();
3477 let jobs: Vec<Value> = state
3478 .import_jobs
3479 .values()
3480 .filter(|j| {
3481 if let Some(ft) = filter_type {
3482 if j.import_destination
3484 .get("SuppressionListDestination")
3485 .is_some()
3486 && ft == "SUPPRESSION_LIST"
3487 {
3488 return true;
3489 }
3490 if j.import_destination.get("ContactListDestination").is_some()
3491 && ft == "CONTACT_LIST"
3492 {
3493 return true;
3494 }
3495 return false;
3496 }
3497 true
3498 })
3499 .map(|j| {
3500 let mut obj = json!({
3501 "JobId": j.job_id,
3502 "ImportDestination": j.import_destination,
3503 "JobStatus": j.job_status,
3504 "CreatedTimestamp": j.created_timestamp.timestamp() as f64,
3505 });
3506 if j.processed_records_count > 0 {
3507 obj["ProcessedRecordsCount"] = json!(j.processed_records_count);
3508 }
3509 if j.failed_records_count > 0 {
3510 obj["FailedRecordsCount"] = json!(j.failed_records_count);
3511 }
3512 obj
3513 })
3514 .collect();
3515
3516 let response = json!({ "ImportJobs": jobs });
3517 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3518 }
3519
3520 fn create_export_job(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3523 let body: Value = Self::parse_body(req)?;
3524
3525 let export_data_source = match body.get("ExportDataSource") {
3526 Some(v) if v.is_object() => v.clone(),
3527 _ => {
3528 return Ok(Self::json_error(
3529 StatusCode::BAD_REQUEST,
3530 "BadRequestException",
3531 "ExportDataSource is required",
3532 ));
3533 }
3534 };
3535
3536 let export_destination = match body.get("ExportDestination") {
3537 Some(v) if v.is_object() => v.clone(),
3538 _ => {
3539 return Ok(Self::json_error(
3540 StatusCode::BAD_REQUEST,
3541 "BadRequestException",
3542 "ExportDestination is required",
3543 ));
3544 }
3545 };
3546
3547 let export_source_type = if export_data_source.get("MetricsDataSource").is_some() {
3549 "METRICS_DATA"
3550 } else {
3551 "MESSAGE_INSIGHTS"
3552 };
3553
3554 let job_id = uuid::Uuid::new_v4().to_string();
3555 let now = Utc::now();
3556
3557 let job = ExportJob {
3558 job_id: job_id.clone(),
3559 export_source_type: export_source_type.to_string(),
3560 export_destination,
3561 export_data_source,
3562 job_status: "COMPLETED".to_string(),
3563 created_timestamp: now,
3564 completed_timestamp: Some(now),
3565 };
3566
3567 self.state.write().export_jobs.insert(job_id.clone(), job);
3568
3569 let response = json!({ "JobId": job_id });
3570 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3571 }
3572
3573 fn get_export_job(&self, job_id: &str) -> Result<AwsResponse, AwsServiceError> {
3574 let state = self.state.read();
3575 let job = match state.export_jobs.get(job_id) {
3576 Some(j) => j,
3577 None => {
3578 return Ok(Self::json_error(
3579 StatusCode::NOT_FOUND,
3580 "NotFoundException",
3581 &format!("Export job {} does not exist", job_id),
3582 ));
3583 }
3584 };
3585
3586 let mut response = json!({
3587 "JobId": job.job_id,
3588 "ExportSourceType": job.export_source_type,
3589 "JobStatus": job.job_status,
3590 "ExportDestination": job.export_destination,
3591 "ExportDataSource": job.export_data_source,
3592 "CreatedTimestamp": job.created_timestamp.timestamp() as f64,
3593 "Statistics": {
3594 "ProcessedRecordsCount": 0,
3595 "ExportedRecordsCount": 0,
3596 },
3597 });
3598 if let Some(ref ts) = job.completed_timestamp {
3599 response["CompletedTimestamp"] = json!(ts.timestamp() as f64);
3600 }
3601
3602 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3603 }
3604
3605 fn list_export_jobs(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3606 let body: Value = serde_json::from_slice(&req.body).unwrap_or(json!({}));
3607 let filter_status = body["JobStatus"].as_str();
3608 let filter_type = body["ExportSourceType"].as_str();
3609
3610 let state = self.state.read();
3611 let jobs: Vec<Value> = state
3612 .export_jobs
3613 .values()
3614 .filter(|j| {
3615 if let Some(s) = filter_status {
3616 if j.job_status != s {
3617 return false;
3618 }
3619 }
3620 if let Some(t) = filter_type {
3621 if j.export_source_type != t {
3622 return false;
3623 }
3624 }
3625 true
3626 })
3627 .map(|j| {
3628 let mut obj = json!({
3629 "JobId": j.job_id,
3630 "ExportSourceType": j.export_source_type,
3631 "JobStatus": j.job_status,
3632 "CreatedTimestamp": j.created_timestamp.timestamp() as f64,
3633 });
3634 if let Some(ref ts) = j.completed_timestamp {
3635 obj["CompletedTimestamp"] = json!(ts.timestamp() as f64);
3636 }
3637 obj
3638 })
3639 .collect();
3640
3641 let response = json!({ "ExportJobs": jobs });
3642 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3643 }
3644
3645 fn cancel_export_job(&self, job_id: &str) -> Result<AwsResponse, AwsServiceError> {
3646 let mut state = self.state.write();
3647 let job = match state.export_jobs.get_mut(job_id) {
3648 Some(j) => j,
3649 None => {
3650 return Ok(Self::json_error(
3651 StatusCode::NOT_FOUND,
3652 "NotFoundException",
3653 &format!("Export job {} does not exist", job_id),
3654 ));
3655 }
3656 };
3657
3658 if job.job_status == "COMPLETED" || job.job_status == "CANCELLED" {
3659 return Ok(Self::json_error(
3660 StatusCode::CONFLICT,
3661 "ConflictException",
3662 &format!("Export job {} is already {}", job_id, job.job_status),
3663 ));
3664 }
3665
3666 job.job_status = "CANCELLED".to_string();
3667 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3668 }
3669
3670 fn create_tenant(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3673 let body: Value = Self::parse_body(req)?;
3674 let tenant_name = match body["TenantName"].as_str() {
3675 Some(n) => n.to_string(),
3676 None => {
3677 return Ok(Self::json_error(
3678 StatusCode::BAD_REQUEST,
3679 "BadRequestException",
3680 "TenantName is required",
3681 ));
3682 }
3683 };
3684
3685 let mut state = self.state.write();
3686
3687 if state.tenants.contains_key(&tenant_name) {
3688 return Ok(Self::json_error(
3689 StatusCode::CONFLICT,
3690 "AlreadyExistsException",
3691 &format!("Tenant {} already exists", tenant_name),
3692 ));
3693 }
3694
3695 let tenant_id = uuid::Uuid::new_v4().to_string();
3696 let tenant_arn = format!(
3697 "arn:aws:ses:{}:{}:tenant/{}",
3698 req.region, req.account_id, tenant_id
3699 );
3700 let now = Utc::now();
3701
3702 let tags = body
3703 .get("Tags")
3704 .and_then(|v| v.as_array())
3705 .cloned()
3706 .unwrap_or_default();
3707
3708 let tenant = Tenant {
3709 tenant_name: tenant_name.clone(),
3710 tenant_id: tenant_id.clone(),
3711 tenant_arn: tenant_arn.clone(),
3712 created_timestamp: now,
3713 sending_status: "ENABLED".to_string(),
3714 tags: tags.clone(),
3715 };
3716
3717 state.tenants.insert(tenant_name.clone(), tenant);
3718
3719 let response = json!({
3720 "TenantName": tenant_name,
3721 "TenantId": tenant_id,
3722 "TenantArn": tenant_arn,
3723 "CreatedTimestamp": now.timestamp() as f64,
3724 "SendingStatus": "ENABLED",
3725 "Tags": tags,
3726 });
3727 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3728 }
3729
3730 fn get_tenant(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3731 let body: Value = Self::parse_body(req)?;
3732 let tenant_name = match body["TenantName"].as_str() {
3733 Some(n) => n,
3734 None => {
3735 return Ok(Self::json_error(
3736 StatusCode::BAD_REQUEST,
3737 "BadRequestException",
3738 "TenantName is required",
3739 ));
3740 }
3741 };
3742
3743 let state = self.state.read();
3744 let tenant = match state.tenants.get(tenant_name) {
3745 Some(t) => t,
3746 None => {
3747 return Ok(Self::json_error(
3748 StatusCode::NOT_FOUND,
3749 "NotFoundException",
3750 &format!("Tenant {} does not exist", tenant_name),
3751 ));
3752 }
3753 };
3754
3755 let response = json!({
3756 "Tenant": {
3757 "TenantName": tenant.tenant_name,
3758 "TenantId": tenant.tenant_id,
3759 "TenantArn": tenant.tenant_arn,
3760 "CreatedTimestamp": tenant.created_timestamp.timestamp() as f64,
3761 "SendingStatus": tenant.sending_status,
3762 "Tags": tenant.tags,
3763 }
3764 });
3765 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3766 }
3767
3768 fn list_tenants(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3769 let state = self.state.read();
3770 let tenants: Vec<Value> = state
3771 .tenants
3772 .values()
3773 .map(|t| {
3774 json!({
3775 "TenantName": t.tenant_name,
3776 "TenantId": t.tenant_id,
3777 "TenantArn": t.tenant_arn,
3778 "CreatedTimestamp": t.created_timestamp.timestamp() as f64,
3779 })
3780 })
3781 .collect();
3782
3783 let response = json!({ "Tenants": tenants });
3784 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3785 }
3786
3787 fn delete_tenant(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3788 let body: Value = Self::parse_body(req)?;
3789 let tenant_name = match body["TenantName"].as_str() {
3790 Some(n) => n,
3791 None => {
3792 return Ok(Self::json_error(
3793 StatusCode::BAD_REQUEST,
3794 "BadRequestException",
3795 "TenantName is required",
3796 ));
3797 }
3798 };
3799
3800 let mut state = self.state.write();
3801
3802 if state.tenants.remove(tenant_name).is_none() {
3803 return Ok(Self::json_error(
3804 StatusCode::NOT_FOUND,
3805 "NotFoundException",
3806 &format!("Tenant {} does not exist", tenant_name),
3807 ));
3808 }
3809
3810 state.tenant_resource_associations.remove(tenant_name);
3811
3812 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3813 }
3814
3815 fn create_tenant_resource_association(
3816 &self,
3817 req: &AwsRequest,
3818 ) -> Result<AwsResponse, AwsServiceError> {
3819 let body: Value = Self::parse_body(req)?;
3820 let tenant_name = match body["TenantName"].as_str() {
3821 Some(n) => n.to_string(),
3822 None => {
3823 return Ok(Self::json_error(
3824 StatusCode::BAD_REQUEST,
3825 "BadRequestException",
3826 "TenantName is required",
3827 ));
3828 }
3829 };
3830 let resource_arn = match body["ResourceArn"].as_str() {
3831 Some(a) => a.to_string(),
3832 None => {
3833 return Ok(Self::json_error(
3834 StatusCode::BAD_REQUEST,
3835 "BadRequestException",
3836 "ResourceArn is required",
3837 ));
3838 }
3839 };
3840
3841 let mut state = self.state.write();
3842
3843 if !state.tenants.contains_key(&tenant_name) {
3844 return Ok(Self::json_error(
3845 StatusCode::NOT_FOUND,
3846 "NotFoundException",
3847 &format!("Tenant {} does not exist", tenant_name),
3848 ));
3849 }
3850
3851 let assoc = TenantResourceAssociation {
3852 resource_arn,
3853 associated_timestamp: Utc::now(),
3854 };
3855
3856 state
3857 .tenant_resource_associations
3858 .entry(tenant_name)
3859 .or_default()
3860 .push(assoc);
3861
3862 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3863 }
3864
3865 fn delete_tenant_resource_association(
3866 &self,
3867 req: &AwsRequest,
3868 ) -> Result<AwsResponse, AwsServiceError> {
3869 let body: Value = Self::parse_body(req)?;
3870 let tenant_name = match body["TenantName"].as_str() {
3871 Some(n) => n,
3872 None => {
3873 return Ok(Self::json_error(
3874 StatusCode::BAD_REQUEST,
3875 "BadRequestException",
3876 "TenantName is required",
3877 ));
3878 }
3879 };
3880 let resource_arn = match body["ResourceArn"].as_str() {
3881 Some(a) => a,
3882 None => {
3883 return Ok(Self::json_error(
3884 StatusCode::BAD_REQUEST,
3885 "BadRequestException",
3886 "ResourceArn is required",
3887 ));
3888 }
3889 };
3890
3891 let mut state = self.state.write();
3892
3893 if let Some(assocs) = state.tenant_resource_associations.get_mut(tenant_name) {
3894 let before = assocs.len();
3895 assocs.retain(|a| a.resource_arn != resource_arn);
3896 if assocs.len() == before {
3897 return Ok(Self::json_error(
3898 StatusCode::NOT_FOUND,
3899 "NotFoundException",
3900 "Resource association not found",
3901 ));
3902 }
3903 } else {
3904 return Ok(Self::json_error(
3905 StatusCode::NOT_FOUND,
3906 "NotFoundException",
3907 "Resource association not found",
3908 ));
3909 }
3910
3911 Ok(AwsResponse::json(StatusCode::OK, "{}"))
3912 }
3913
3914 fn list_tenant_resources(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3915 let body: Value = Self::parse_body(req)?;
3916 let tenant_name = match body["TenantName"].as_str() {
3917 Some(n) => n,
3918 None => {
3919 return Ok(Self::json_error(
3920 StatusCode::BAD_REQUEST,
3921 "BadRequestException",
3922 "TenantName is required",
3923 ));
3924 }
3925 };
3926
3927 let state = self.state.read();
3928
3929 if !state.tenants.contains_key(tenant_name) {
3930 return Ok(Self::json_error(
3931 StatusCode::NOT_FOUND,
3932 "NotFoundException",
3933 &format!("Tenant {} does not exist", tenant_name),
3934 ));
3935 }
3936
3937 let resources: Vec<Value> = state
3938 .tenant_resource_associations
3939 .get(tenant_name)
3940 .map(|assocs| {
3941 assocs
3942 .iter()
3943 .map(|a| {
3944 json!({
3945 "ResourceType": "RESOURCE",
3946 "ResourceArn": a.resource_arn,
3947 })
3948 })
3949 .collect()
3950 })
3951 .unwrap_or_default();
3952
3953 let response = json!({ "TenantResources": resources });
3954 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3955 }
3956
3957 fn list_resource_tenants(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
3958 let body: Value = Self::parse_body(req)?;
3959 let resource_arn = match body["ResourceArn"].as_str() {
3960 Some(a) => a,
3961 None => {
3962 return Ok(Self::json_error(
3963 StatusCode::BAD_REQUEST,
3964 "BadRequestException",
3965 "ResourceArn is required",
3966 ));
3967 }
3968 };
3969
3970 let state = self.state.read();
3971 let mut resource_tenants: Vec<Value> = Vec::new();
3972
3973 for (tenant_name, assocs) in &state.tenant_resource_associations {
3974 for assoc in assocs {
3975 if assoc.resource_arn == resource_arn {
3976 if let Some(tenant) = state.tenants.get(tenant_name) {
3977 resource_tenants.push(json!({
3978 "TenantName": tenant.tenant_name,
3979 "TenantId": tenant.tenant_id,
3980 "ResourceArn": assoc.resource_arn,
3981 "AssociatedTimestamp": assoc.associated_timestamp.timestamp() as f64,
3982 }));
3983 }
3984 }
3985 }
3986 }
3987
3988 let response = json!({ "ResourceTenants": resource_tenants });
3989 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
3990 }
3991
3992 fn get_reputation_entity(
3995 &self,
3996 entity_type: &str,
3997 entity_ref: &str,
3998 ) -> Result<AwsResponse, AwsServiceError> {
3999 let key = format!("{}/{}", entity_type, entity_ref);
4000 let state = self.state.read();
4001
4002 let entity = match state.reputation_entities.get(&key) {
4003 Some(e) => e,
4004 None => {
4005 let response = json!({
4007 "ReputationEntity": {
4008 "ReputationEntityReference": entity_ref,
4009 "ReputationEntityType": entity_type,
4010 "SendingStatusAggregate": "ENABLED",
4011 "CustomerManagedStatus": {
4012 "SendingStatus": "ENABLED",
4013 },
4014 "AwsSesManagedStatus": {
4015 "SendingStatus": "ENABLED",
4016 },
4017 }
4018 });
4019 return Ok(AwsResponse::json(StatusCode::OK, response.to_string()));
4020 }
4021 };
4022
4023 let response = json!({
4024 "ReputationEntity": {
4025 "ReputationEntityReference": entity.reputation_entity_reference,
4026 "ReputationEntityType": entity.reputation_entity_type,
4027 "ReputationManagementPolicy": entity.reputation_management_policy,
4028 "SendingStatusAggregate": entity.sending_status_aggregate,
4029 "CustomerManagedStatus": {
4030 "SendingStatus": entity.customer_managed_status,
4031 },
4032 "AwsSesManagedStatus": {
4033 "SendingStatus": "ENABLED",
4034 },
4035 }
4036 });
4037 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
4038 }
4039
4040 fn list_reputation_entities(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
4041 let state = self.state.read();
4042 let entities: Vec<Value> = state
4043 .reputation_entities
4044 .values()
4045 .map(|e| {
4046 json!({
4047 "ReputationEntityReference": e.reputation_entity_reference,
4048 "ReputationEntityType": e.reputation_entity_type,
4049 "SendingStatusAggregate": e.sending_status_aggregate,
4050 })
4051 })
4052 .collect();
4053
4054 let response = json!({ "ReputationEntities": entities });
4055 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
4056 }
4057
4058 fn update_reputation_entity_customer_managed_status(
4059 &self,
4060 entity_type: &str,
4061 entity_ref: &str,
4062 req: &AwsRequest,
4063 ) -> Result<AwsResponse, AwsServiceError> {
4064 let body: Value = Self::parse_body(req)?;
4065 let sending_status = body["SendingStatus"]
4066 .as_str()
4067 .unwrap_or("ENABLED")
4068 .to_string();
4069
4070 let key = format!("{}/{}", entity_type, entity_ref);
4071 let mut state = self.state.write();
4072
4073 let entity =
4074 state
4075 .reputation_entities
4076 .entry(key)
4077 .or_insert_with(|| ReputationEntityState {
4078 reputation_entity_reference: entity_ref.to_string(),
4079 reputation_entity_type: entity_type.to_string(),
4080 reputation_management_policy: None,
4081 customer_managed_status: "ENABLED".to_string(),
4082 sending_status_aggregate: "ENABLED".to_string(),
4083 });
4084
4085 entity.customer_managed_status = sending_status;
4086
4087 Ok(AwsResponse::json(StatusCode::OK, "{}"))
4088 }
4089
4090 fn update_reputation_entity_policy(
4091 &self,
4092 entity_type: &str,
4093 entity_ref: &str,
4094 req: &AwsRequest,
4095 ) -> Result<AwsResponse, AwsServiceError> {
4096 let body: Value = Self::parse_body(req)?;
4097 let policy = body["ReputationEntityPolicy"]
4098 .as_str()
4099 .map(|s| s.to_string());
4100
4101 let key = format!("{}/{}", entity_type, entity_ref);
4102 let mut state = self.state.write();
4103
4104 let entity =
4105 state
4106 .reputation_entities
4107 .entry(key)
4108 .or_insert_with(|| ReputationEntityState {
4109 reputation_entity_reference: entity_ref.to_string(),
4110 reputation_entity_type: entity_type.to_string(),
4111 reputation_management_policy: None,
4112 customer_managed_status: "ENABLED".to_string(),
4113 sending_status_aggregate: "ENABLED".to_string(),
4114 });
4115
4116 entity.reputation_management_policy = policy;
4117
4118 Ok(AwsResponse::json(StatusCode::OK, "{}"))
4119 }
4120
4121 fn batch_get_metric_data(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
4124 let body: Value = Self::parse_body(req)?;
4125 let queries = body["Queries"].as_array().cloned().unwrap_or_default();
4126
4127 let results: Vec<Value> = queries
4128 .iter()
4129 .filter_map(|q| {
4130 let id = q["Id"].as_str()?;
4131 Some(json!({
4132 "Id": id,
4133 "Timestamps": [],
4134 "Values": [],
4135 }))
4136 })
4137 .collect();
4138
4139 let response = json!({
4140 "Results": results,
4141 "Errors": [],
4142 });
4143 Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
4144 }
4145}
4146
4147fn parse_topics(value: &Value) -> Vec<Topic> {
4148 value
4149 .as_array()
4150 .map(|arr| {
4151 arr.iter()
4152 .filter_map(|v| {
4153 let topic_name = v["TopicName"].as_str()?.to_string();
4154 let display_name = v["DisplayName"].as_str().unwrap_or("").to_string();
4155 let description = v["Description"].as_str().unwrap_or("").to_string();
4156 let default_subscription_status = v["DefaultSubscriptionStatus"]
4157 .as_str()
4158 .unwrap_or("OPT_OUT")
4159 .to_string();
4160 Some(Topic {
4161 topic_name,
4162 display_name,
4163 description,
4164 default_subscription_status,
4165 })
4166 })
4167 .collect()
4168 })
4169 .unwrap_or_default()
4170}
4171
4172fn parse_topic_preferences(value: &Value) -> Vec<TopicPreference> {
4173 value
4174 .as_array()
4175 .map(|arr| {
4176 arr.iter()
4177 .filter_map(|v| {
4178 let topic_name = v["TopicName"].as_str()?.to_string();
4179 let subscription_status = v["SubscriptionStatus"]
4180 .as_str()
4181 .unwrap_or("OPT_OUT")
4182 .to_string();
4183 Some(TopicPreference {
4184 topic_name,
4185 subscription_status,
4186 })
4187 })
4188 .collect()
4189 })
4190 .unwrap_or_default()
4191}
4192
4193fn extract_string_array(value: &Value) -> Vec<String> {
4194 value
4195 .as_array()
4196 .map(|arr| {
4197 arr.iter()
4198 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4199 .collect()
4200 })
4201 .unwrap_or_default()
4202}
4203
4204fn parse_event_destination_definition(name: &str, def: &Value) -> EventDestination {
4205 let enabled = def["Enabled"].as_bool().unwrap_or(false);
4206 let matching_event_types = extract_string_array(&def["MatchingEventTypes"]);
4207 let kinesis_firehose_destination = def
4208 .get("KinesisFirehoseDestination")
4209 .filter(|v| v.is_object())
4210 .cloned();
4211 let cloud_watch_destination = def
4212 .get("CloudWatchDestination")
4213 .filter(|v| v.is_object())
4214 .cloned();
4215 let sns_destination = def.get("SnsDestination").filter(|v| v.is_object()).cloned();
4216 let event_bridge_destination = def
4217 .get("EventBridgeDestination")
4218 .filter(|v| v.is_object())
4219 .cloned();
4220 let pinpoint_destination = def
4221 .get("PinpointDestination")
4222 .filter(|v| v.is_object())
4223 .cloned();
4224
4225 EventDestination {
4226 name: name.to_string(),
4227 enabled,
4228 matching_event_types,
4229 kinesis_firehose_destination,
4230 cloud_watch_destination,
4231 sns_destination,
4232 event_bridge_destination,
4233 pinpoint_destination,
4234 }
4235}
4236
4237fn event_destination_to_json(dest: &EventDestination) -> Value {
4238 let mut obj = json!({
4239 "Name": dest.name,
4240 "Enabled": dest.enabled,
4241 "MatchingEventTypes": dest.matching_event_types,
4242 });
4243 if let Some(ref v) = dest.kinesis_firehose_destination {
4244 obj["KinesisFirehoseDestination"] = v.clone();
4245 }
4246 if let Some(ref v) = dest.cloud_watch_destination {
4247 obj["CloudWatchDestination"] = v.clone();
4248 }
4249 if let Some(ref v) = dest.sns_destination {
4250 obj["SnsDestination"] = v.clone();
4251 }
4252 if let Some(ref v) = dest.event_bridge_destination {
4253 obj["EventBridgeDestination"] = v.clone();
4254 }
4255 if let Some(ref v) = dest.pinpoint_destination {
4256 obj["PinpointDestination"] = v.clone();
4257 }
4258 obj
4259}
4260
4261#[async_trait]
4262impl fakecloud_core::service::AwsService for SesV2Service {
4263 fn service_name(&self) -> &str {
4264 "ses"
4265 }
4266
4267 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
4268 if req.is_query_protocol {
4270 return crate::v1::handle_v1_action(&self.state, &req);
4271 }
4272
4273 let (action, resource_name, sub_resource) =
4274 Self::resolve_action(&req).ok_or_else(|| {
4275 AwsServiceError::aws_error(
4276 StatusCode::NOT_FOUND,
4277 "UnknownOperationException",
4278 format!("Unknown operation: {} {}", req.method, req.raw_path),
4279 )
4280 })?;
4281
4282 let res = resource_name.as_deref().unwrap_or("");
4283 let sub = sub_resource.as_deref().unwrap_or("");
4284
4285 match action {
4286 "GetAccount" => self.get_account(),
4287 "CreateEmailIdentity" => self.create_email_identity(&req),
4288 "ListEmailIdentities" => self.list_email_identities(),
4289 "GetEmailIdentity" => self.get_email_identity(res),
4290 "DeleteEmailIdentity" => self.delete_email_identity(res, &req),
4291 "CreateConfigurationSet" => self.create_configuration_set(&req),
4292 "ListConfigurationSets" => self.list_configuration_sets(),
4293 "GetConfigurationSet" => self.get_configuration_set(res),
4294 "DeleteConfigurationSet" => self.delete_configuration_set(res, &req),
4295 "CreateEmailTemplate" => self.create_email_template(&req),
4296 "ListEmailTemplates" => self.list_email_templates(),
4297 "GetEmailTemplate" => self.get_email_template(res),
4298 "UpdateEmailTemplate" => self.update_email_template(res, &req),
4299 "DeleteEmailTemplate" => self.delete_email_template(res),
4300 "SendEmail" => self.send_email(&req),
4301 "SendBulkEmail" => self.send_bulk_email(&req),
4302 "TagResource" => self.tag_resource(&req),
4303 "UntagResource" => self.untag_resource(&req),
4304 "ListTagsForResource" => self.list_tags_for_resource(&req),
4305 "CreateContactList" => self.create_contact_list(&req),
4306 "GetContactList" => self.get_contact_list(res),
4307 "ListContactLists" => self.list_contact_lists(),
4308 "UpdateContactList" => self.update_contact_list(res, &req),
4309 "DeleteContactList" => self.delete_contact_list(res, &req),
4310 "CreateContact" => self.create_contact(res, &req),
4311 "GetContact" => self.get_contact(res, sub),
4312 "ListContacts" => self.list_contacts(res),
4313 "UpdateContact" => self.update_contact(res, sub, &req),
4314 "DeleteContact" => self.delete_contact(res, sub),
4315 "PutSuppressedDestination" => self.put_suppressed_destination(&req),
4316 "GetSuppressedDestination" => self.get_suppressed_destination(res),
4317 "DeleteSuppressedDestination" => self.delete_suppressed_destination(res),
4318 "ListSuppressedDestinations" => self.list_suppressed_destinations(),
4319 "CreateConfigurationSetEventDestination" => {
4320 self.create_configuration_set_event_destination(res, &req)
4321 }
4322 "GetConfigurationSetEventDestinations" => {
4323 self.get_configuration_set_event_destinations(res)
4324 }
4325 "UpdateConfigurationSetEventDestination" => {
4326 self.update_configuration_set_event_destination(res, sub, &req)
4327 }
4328 "DeleteConfigurationSetEventDestination" => {
4329 self.delete_configuration_set_event_destination(res, sub)
4330 }
4331 "CreateEmailIdentityPolicy" => self.create_email_identity_policy(res, sub, &req),
4332 "GetEmailIdentityPolicies" => self.get_email_identity_policies(res),
4333 "UpdateEmailIdentityPolicy" => self.update_email_identity_policy(res, sub, &req),
4334 "DeleteEmailIdentityPolicy" => self.delete_email_identity_policy(res, sub),
4335 "PutEmailIdentityDkimAttributes" => self.put_email_identity_dkim_attributes(res, &req),
4336 "PutEmailIdentityDkimSigningAttributes" => {
4337 self.put_email_identity_dkim_signing_attributes(res, &req)
4338 }
4339 "PutEmailIdentityFeedbackAttributes" => {
4340 self.put_email_identity_feedback_attributes(res, &req)
4341 }
4342 "PutEmailIdentityMailFromAttributes" => {
4343 self.put_email_identity_mail_from_attributes(res, &req)
4344 }
4345 "PutEmailIdentityConfigurationSetAttributes" => {
4346 self.put_email_identity_configuration_set_attributes(res, &req)
4347 }
4348 "PutConfigurationSetSendingOptions" => {
4349 self.put_configuration_set_sending_options(res, &req)
4350 }
4351 "PutConfigurationSetDeliveryOptions" => {
4352 self.put_configuration_set_delivery_options(res, &req)
4353 }
4354 "PutConfigurationSetTrackingOptions" => {
4355 self.put_configuration_set_tracking_options(res, &req)
4356 }
4357 "PutConfigurationSetSuppressionOptions" => {
4358 self.put_configuration_set_suppression_options(res, &req)
4359 }
4360 "PutConfigurationSetReputationOptions" => {
4361 self.put_configuration_set_reputation_options(res, &req)
4362 }
4363 "PutConfigurationSetVdmOptions" => self.put_configuration_set_vdm_options(res, &req),
4364 "PutConfigurationSetArchivingOptions" => {
4365 self.put_configuration_set_archiving_options(res, &req)
4366 }
4367 "CreateCustomVerificationEmailTemplate" => {
4368 self.create_custom_verification_email_template(&req)
4369 }
4370 "GetCustomVerificationEmailTemplate" => {
4371 self.get_custom_verification_email_template(res)
4372 }
4373 "ListCustomVerificationEmailTemplates" => {
4374 self.list_custom_verification_email_templates(&req)
4375 }
4376 "UpdateCustomVerificationEmailTemplate" => {
4377 self.update_custom_verification_email_template(res, &req)
4378 }
4379 "DeleteCustomVerificationEmailTemplate" => {
4380 self.delete_custom_verification_email_template(res)
4381 }
4382 "SendCustomVerificationEmail" => self.send_custom_verification_email(&req),
4383 "TestRenderEmailTemplate" => self.test_render_email_template(res, &req),
4384 "CreateDedicatedIpPool" => self.create_dedicated_ip_pool(&req),
4385 "ListDedicatedIpPools" => self.list_dedicated_ip_pools(),
4386 "DeleteDedicatedIpPool" => self.delete_dedicated_ip_pool(res),
4387 "GetDedicatedIp" => self.get_dedicated_ip(res),
4388 "GetDedicatedIps" => self.get_dedicated_ips(&req),
4389 "PutDedicatedIpInPool" => self.put_dedicated_ip_in_pool(res, &req),
4390 "PutDedicatedIpPoolScalingAttributes" => {
4391 self.put_dedicated_ip_pool_scaling_attributes(res, &req)
4392 }
4393 "PutDedicatedIpWarmupAttributes" => self.put_dedicated_ip_warmup_attributes(res, &req),
4394 "PutAccountDedicatedIpWarmupAttributes" => {
4395 self.put_account_dedicated_ip_warmup_attributes(&req)
4396 }
4397 "CreateMultiRegionEndpoint" => self.create_multi_region_endpoint(&req),
4398 "GetMultiRegionEndpoint" => self.get_multi_region_endpoint(res),
4399 "ListMultiRegionEndpoints" => self.list_multi_region_endpoints(),
4400 "DeleteMultiRegionEndpoint" => self.delete_multi_region_endpoint(res),
4401 "PutAccountDetails" => self.put_account_details(&req),
4402 "PutAccountSendingAttributes" => self.put_account_sending_attributes(&req),
4403 "PutAccountSuppressionAttributes" => self.put_account_suppression_attributes(&req),
4404 "PutAccountVdmAttributes" => self.put_account_vdm_attributes(&req),
4405 "CreateImportJob" => self.create_import_job(&req),
4406 "GetImportJob" => self.get_import_job(res),
4407 "ListImportJobs" => self.list_import_jobs(&req),
4408 "CreateExportJob" => self.create_export_job(&req),
4409 "GetExportJob" => self.get_export_job(res),
4410 "ListExportJobs" => self.list_export_jobs(&req),
4411 "CancelExportJob" => self.cancel_export_job(res),
4412 "CreateTenant" => self.create_tenant(&req),
4413 "GetTenant" => self.get_tenant(&req),
4414 "ListTenants" => self.list_tenants(&req),
4415 "DeleteTenant" => self.delete_tenant(&req),
4416 "CreateTenantResourceAssociation" => self.create_tenant_resource_association(&req),
4417 "DeleteTenantResourceAssociation" => self.delete_tenant_resource_association(&req),
4418 "ListTenantResources" => self.list_tenant_resources(&req),
4419 "ListResourceTenants" => self.list_resource_tenants(&req),
4420 "GetReputationEntity" => self.get_reputation_entity(res, sub),
4421 "ListReputationEntities" => self.list_reputation_entities(&req),
4422 "UpdateReputationEntityCustomerManagedStatus" => {
4423 self.update_reputation_entity_customer_managed_status(res, sub, &req)
4424 }
4425 "UpdateReputationEntityPolicy" => self.update_reputation_entity_policy(res, sub, &req),
4426 "BatchGetMetricData" => self.batch_get_metric_data(&req),
4427 _ => Err(AwsServiceError::action_not_implemented("ses", action)),
4428 }
4429 }
4430
4431 fn supported_actions(&self) -> &[&str] {
4432 &[
4433 "GetAccount",
4434 "CreateEmailIdentity",
4435 "ListEmailIdentities",
4436 "GetEmailIdentity",
4437 "DeleteEmailIdentity",
4438 "CreateConfigurationSet",
4439 "ListConfigurationSets",
4440 "GetConfigurationSet",
4441 "DeleteConfigurationSet",
4442 "CreateEmailTemplate",
4443 "ListEmailTemplates",
4444 "GetEmailTemplate",
4445 "UpdateEmailTemplate",
4446 "DeleteEmailTemplate",
4447 "SendEmail",
4448 "SendBulkEmail",
4449 "TagResource",
4450 "UntagResource",
4451 "ListTagsForResource",
4452 "CreateContactList",
4453 "GetContactList",
4454 "ListContactLists",
4455 "UpdateContactList",
4456 "DeleteContactList",
4457 "CreateContact",
4458 "GetContact",
4459 "ListContacts",
4460 "UpdateContact",
4461 "DeleteContact",
4462 "PutSuppressedDestination",
4463 "GetSuppressedDestination",
4464 "DeleteSuppressedDestination",
4465 "ListSuppressedDestinations",
4466 "CreateConfigurationSetEventDestination",
4467 "GetConfigurationSetEventDestinations",
4468 "UpdateConfigurationSetEventDestination",
4469 "DeleteConfigurationSetEventDestination",
4470 "CreateEmailIdentityPolicy",
4471 "GetEmailIdentityPolicies",
4472 "UpdateEmailIdentityPolicy",
4473 "DeleteEmailIdentityPolicy",
4474 "PutEmailIdentityDkimAttributes",
4475 "PutEmailIdentityDkimSigningAttributes",
4476 "PutEmailIdentityFeedbackAttributes",
4477 "PutEmailIdentityMailFromAttributes",
4478 "PutEmailIdentityConfigurationSetAttributes",
4479 "PutConfigurationSetSendingOptions",
4480 "PutConfigurationSetDeliveryOptions",
4481 "PutConfigurationSetTrackingOptions",
4482 "PutConfigurationSetSuppressionOptions",
4483 "PutConfigurationSetReputationOptions",
4484 "PutConfigurationSetVdmOptions",
4485 "PutConfigurationSetArchivingOptions",
4486 "CreateCustomVerificationEmailTemplate",
4487 "GetCustomVerificationEmailTemplate",
4488 "ListCustomVerificationEmailTemplates",
4489 "UpdateCustomVerificationEmailTemplate",
4490 "DeleteCustomVerificationEmailTemplate",
4491 "SendCustomVerificationEmail",
4492 "TestRenderEmailTemplate",
4493 "CreateDedicatedIpPool",
4494 "ListDedicatedIpPools",
4495 "DeleteDedicatedIpPool",
4496 "GetDedicatedIp",
4497 "GetDedicatedIps",
4498 "PutDedicatedIpInPool",
4499 "PutDedicatedIpPoolScalingAttributes",
4500 "PutDedicatedIpWarmupAttributes",
4501 "PutAccountDedicatedIpWarmupAttributes",
4502 "CreateMultiRegionEndpoint",
4503 "GetMultiRegionEndpoint",
4504 "ListMultiRegionEndpoints",
4505 "DeleteMultiRegionEndpoint",
4506 "PutAccountDetails",
4507 "PutAccountSendingAttributes",
4508 "PutAccountSuppressionAttributes",
4509 "PutAccountVdmAttributes",
4510 "CreateImportJob",
4511 "GetImportJob",
4512 "ListImportJobs",
4513 "CreateExportJob",
4514 "GetExportJob",
4515 "ListExportJobs",
4516 "CancelExportJob",
4517 "CreateTenant",
4518 "GetTenant",
4519 "ListTenants",
4520 "DeleteTenant",
4521 "CreateTenantResourceAssociation",
4522 "DeleteTenantResourceAssociation",
4523 "ListTenantResources",
4524 "ListResourceTenants",
4525 "GetReputationEntity",
4526 "ListReputationEntities",
4527 "UpdateReputationEntityCustomerManagedStatus",
4528 "UpdateReputationEntityPolicy",
4529 "BatchGetMetricData",
4530 ]
4534 }
4535}
4536
4537#[cfg(test)]
4538mod tests {
4539 use super::*;
4540 use crate::state::SesState;
4541 use bytes::Bytes;
4542 use fakecloud_core::service::AwsService;
4543 use http::{HeaderMap, Method};
4544 use parking_lot::RwLock;
4545 use std::collections::HashMap;
4546 use std::sync::Arc;
4547
4548 fn make_state() -> SharedSesState {
4549 Arc::new(RwLock::new(SesState::new("123456789012", "us-east-1")))
4550 }
4551
4552 fn make_request(method: Method, path: &str, body: &str) -> AwsRequest {
4553 make_request_with_query(method, path, body, "", HashMap::new())
4554 }
4555
4556 fn make_request_with_query(
4557 method: Method,
4558 path: &str,
4559 body: &str,
4560 raw_query: &str,
4561 query_params: HashMap<String, String>,
4562 ) -> AwsRequest {
4563 let path_segments: Vec<String> = path
4564 .split('/')
4565 .filter(|s| !s.is_empty())
4566 .map(|s| s.to_string())
4567 .collect();
4568 AwsRequest {
4569 service: "ses".to_string(),
4570 action: String::new(),
4571 region: "us-east-1".to_string(),
4572 account_id: "123456789012".to_string(),
4573 request_id: "test-request-id".to_string(),
4574 headers: HeaderMap::new(),
4575 query_params,
4576 body: Bytes::from(body.to_string()),
4577 path_segments,
4578 raw_path: path.to_string(),
4579 raw_query: raw_query.to_string(),
4580 method,
4581 is_query_protocol: false,
4582 access_key_id: None,
4583 }
4584 }
4585
4586 #[tokio::test]
4587 async fn test_identity_lifecycle() {
4588 let state = make_state();
4589 let svc = SesV2Service::new(state);
4590
4591 let req = make_request(
4593 Method::POST,
4594 "/v2/email/identities",
4595 r#"{"EmailIdentity": "test@example.com"}"#,
4596 );
4597 let resp = svc.handle(req).await.unwrap();
4598 assert_eq!(resp.status, StatusCode::OK);
4599 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4600 assert_eq!(body["VerifiedForSendingStatus"], true);
4601 assert_eq!(body["IdentityType"], "EMAIL_ADDRESS");
4602
4603 let req = make_request(Method::GET, "/v2/email/identities", "");
4605 let resp = svc.handle(req).await.unwrap();
4606 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4607 assert_eq!(body["EmailIdentities"].as_array().unwrap().len(), 1);
4608
4609 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
4611 let resp = svc.handle(req).await.unwrap();
4612 assert_eq!(resp.status, StatusCode::OK);
4613 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4614 assert_eq!(body["VerifiedForSendingStatus"], true);
4615 assert_eq!(body["DkimAttributes"]["Status"], "SUCCESS");
4616
4617 let req = make_request(
4619 Method::DELETE,
4620 "/v2/email/identities/test%40example.com",
4621 "",
4622 );
4623 let resp = svc.handle(req).await.unwrap();
4624 assert_eq!(resp.status, StatusCode::OK);
4625
4626 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
4628 let resp = svc.handle(req).await.unwrap();
4629 assert_eq!(resp.status, StatusCode::NOT_FOUND);
4630 }
4631
4632 #[tokio::test]
4633 async fn test_domain_identity() {
4634 let state = make_state();
4635 let svc = SesV2Service::new(state);
4636
4637 let req = make_request(
4638 Method::POST,
4639 "/v2/email/identities",
4640 r#"{"EmailIdentity": "example.com"}"#,
4641 );
4642 let resp = svc.handle(req).await.unwrap();
4643 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4644 assert_eq!(body["IdentityType"], "DOMAIN");
4645 }
4646
4647 #[tokio::test]
4648 async fn test_duplicate_identity() {
4649 let state = make_state();
4650 let svc = SesV2Service::new(state);
4651
4652 let req = make_request(
4653 Method::POST,
4654 "/v2/email/identities",
4655 r#"{"EmailIdentity": "test@example.com"}"#,
4656 );
4657 svc.handle(req).await.unwrap();
4658
4659 let req = make_request(
4660 Method::POST,
4661 "/v2/email/identities",
4662 r#"{"EmailIdentity": "test@example.com"}"#,
4663 );
4664 let resp = svc.handle(req).await.unwrap();
4665 assert_eq!(resp.status, StatusCode::CONFLICT);
4666 }
4667
4668 #[tokio::test]
4669 async fn test_template_lifecycle() {
4670 let state = make_state();
4671 let svc = SesV2Service::new(state);
4672
4673 let req = make_request(
4675 Method::POST,
4676 "/v2/email/templates",
4677 r#"{"TemplateName": "welcome", "TemplateContent": {"Subject": "Welcome", "Html": "<h1>Hi</h1>", "Text": "Hi"}}"#,
4678 );
4679 let resp = svc.handle(req).await.unwrap();
4680 assert_eq!(resp.status, StatusCode::OK);
4681
4682 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
4684 let resp = svc.handle(req).await.unwrap();
4685 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4686 assert_eq!(body["TemplateName"], "welcome");
4687 assert_eq!(body["TemplateContent"]["Subject"], "Welcome");
4688
4689 let req = make_request(
4691 Method::PUT,
4692 "/v2/email/templates/welcome",
4693 r#"{"TemplateContent": {"Subject": "Updated Welcome"}}"#,
4694 );
4695 let resp = svc.handle(req).await.unwrap();
4696 assert_eq!(resp.status, StatusCode::OK);
4697
4698 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
4700 let resp = svc.handle(req).await.unwrap();
4701 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4702 assert_eq!(body["TemplateContent"]["Subject"], "Updated Welcome");
4703
4704 let req = make_request(Method::GET, "/v2/email/templates", "");
4706 let resp = svc.handle(req).await.unwrap();
4707 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4708 assert_eq!(body["TemplatesMetadata"].as_array().unwrap().len(), 1);
4709
4710 let req = make_request(Method::DELETE, "/v2/email/templates/welcome", "");
4712 let resp = svc.handle(req).await.unwrap();
4713 assert_eq!(resp.status, StatusCode::OK);
4714
4715 let req = make_request(Method::GET, "/v2/email/templates/welcome", "");
4717 let resp = svc.handle(req).await.unwrap();
4718 assert_eq!(resp.status, StatusCode::NOT_FOUND);
4719 }
4720
4721 #[tokio::test]
4722 async fn test_send_email() {
4723 let state = make_state();
4724 let svc = SesV2Service::new(state.clone());
4725
4726 let req = make_request(
4727 Method::POST,
4728 "/v2/email/outbound-emails",
4729 r#"{
4730 "FromEmailAddress": "sender@example.com",
4731 "Destination": {
4732 "ToAddresses": ["recipient@example.com"]
4733 },
4734 "Content": {
4735 "Simple": {
4736 "Subject": {"Data": "Test Subject"},
4737 "Body": {
4738 "Text": {"Data": "Hello world"},
4739 "Html": {"Data": "<p>Hello world</p>"}
4740 }
4741 }
4742 }
4743 }"#,
4744 );
4745 let resp = svc.handle(req).await.unwrap();
4746 assert_eq!(resp.status, StatusCode::OK);
4747 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4748 assert!(body["MessageId"].as_str().is_some());
4749
4750 let s = state.read();
4752 assert_eq!(s.sent_emails.len(), 1);
4753 assert_eq!(s.sent_emails[0].from, "sender@example.com");
4754 assert_eq!(s.sent_emails[0].to, vec!["recipient@example.com"]);
4755 assert_eq!(s.sent_emails[0].subject.as_deref(), Some("Test Subject"));
4756 }
4757
4758 #[tokio::test]
4759 async fn test_get_account() {
4760 let state = make_state();
4761 let svc = SesV2Service::new(state);
4762
4763 let req = make_request(Method::GET, "/v2/email/account", "");
4764 let resp = svc.handle(req).await.unwrap();
4765 assert_eq!(resp.status, StatusCode::OK);
4766 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4767 assert_eq!(body["SendingEnabled"], true);
4768 assert!(body["SendQuota"]["Max24HourSend"].as_f64().unwrap() > 0.0);
4769 }
4770
4771 #[tokio::test]
4772 async fn test_configuration_set_lifecycle() {
4773 let state = make_state();
4774 let svc = SesV2Service::new(state);
4775
4776 let req = make_request(
4778 Method::POST,
4779 "/v2/email/configuration-sets",
4780 r#"{"ConfigurationSetName": "my-config"}"#,
4781 );
4782 let resp = svc.handle(req).await.unwrap();
4783 assert_eq!(resp.status, StatusCode::OK);
4784
4785 let req = make_request(Method::GET, "/v2/email/configuration-sets/my-config", "");
4787 let resp = svc.handle(req).await.unwrap();
4788 assert_eq!(resp.status, StatusCode::OK);
4789 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4790 assert_eq!(body["ConfigurationSetName"], "my-config");
4791
4792 let req = make_request(Method::GET, "/v2/email/configuration-sets", "");
4794 let resp = svc.handle(req).await.unwrap();
4795 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4796 assert_eq!(body["ConfigurationSets"].as_array().unwrap().len(), 1);
4797
4798 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
4800 let resp = svc.handle(req).await.unwrap();
4801 assert_eq!(resp.status, StatusCode::OK);
4802
4803 let req = make_request(Method::GET, "/v2/email/configuration-sets/my-config", "");
4805 let resp = svc.handle(req).await.unwrap();
4806 assert_eq!(resp.status, StatusCode::NOT_FOUND);
4807 }
4808
4809 #[tokio::test]
4810 async fn test_send_email_raw_content() {
4811 let state = make_state();
4812 let svc = SesV2Service::new(state.clone());
4813
4814 let req = make_request(
4815 Method::POST,
4816 "/v2/email/outbound-emails",
4817 r#"{
4818 "FromEmailAddress": "sender@example.com",
4819 "Destination": {
4820 "ToAddresses": ["to@example.com"]
4821 },
4822 "Content": {
4823 "Raw": {
4824 "Data": "From: sender@example.com\r\nTo: to@example.com\r\nSubject: Raw\r\n\r\nBody"
4825 }
4826 }
4827 }"#,
4828 );
4829 let resp = svc.handle(req).await.unwrap();
4830 assert_eq!(resp.status, StatusCode::OK);
4831 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4832 assert!(body["MessageId"].as_str().is_some());
4833
4834 let s = state.read();
4835 assert_eq!(s.sent_emails.len(), 1);
4836 assert!(s.sent_emails[0].raw_data.is_some());
4837 assert!(
4838 s.sent_emails[0].subject.is_none(),
4839 "Raw emails should not have parsed subject"
4840 );
4841 }
4842
4843 #[tokio::test]
4844 async fn test_send_email_template_content() {
4845 let state = make_state();
4846 let svc = SesV2Service::new(state.clone());
4847
4848 let req = make_request(
4849 Method::POST,
4850 "/v2/email/outbound-emails",
4851 r#"{
4852 "FromEmailAddress": "sender@example.com",
4853 "Destination": {
4854 "ToAddresses": ["to@example.com"]
4855 },
4856 "Content": {
4857 "Template": {
4858 "TemplateName": "welcome",
4859 "TemplateData": "{\"name\": \"Alice\"}"
4860 }
4861 }
4862 }"#,
4863 );
4864 let resp = svc.handle(req).await.unwrap();
4865 assert_eq!(resp.status, StatusCode::OK);
4866
4867 let s = state.read();
4868 assert_eq!(s.sent_emails.len(), 1);
4869 assert_eq!(s.sent_emails[0].template_name.as_deref(), Some("welcome"));
4870 assert_eq!(
4871 s.sent_emails[0].template_data.as_deref(),
4872 Some("{\"name\": \"Alice\"}")
4873 );
4874 }
4875
4876 #[tokio::test]
4877 async fn test_send_email_missing_content() {
4878 let state = make_state();
4879 let svc = SesV2Service::new(state);
4880
4881 let req = make_request(
4882 Method::POST,
4883 "/v2/email/outbound-emails",
4884 r#"{"FromEmailAddress": "sender@example.com", "Destination": {"ToAddresses": ["to@example.com"]}}"#,
4885 );
4886 let resp = svc.handle(req).await.unwrap();
4887 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
4888 }
4889
4890 #[tokio::test]
4891 async fn test_send_email_with_cc_and_bcc() {
4892 let state = make_state();
4893 let svc = SesV2Service::new(state.clone());
4894
4895 let req = make_request(
4896 Method::POST,
4897 "/v2/email/outbound-emails",
4898 r#"{
4899 "FromEmailAddress": "sender@example.com",
4900 "Destination": {
4901 "ToAddresses": ["to@example.com"],
4902 "CcAddresses": ["cc@example.com"],
4903 "BccAddresses": ["bcc@example.com"]
4904 },
4905 "Content": {
4906 "Simple": {
4907 "Subject": {"Data": "Test"},
4908 "Body": {"Text": {"Data": "Hello"}}
4909 }
4910 }
4911 }"#,
4912 );
4913 let resp = svc.handle(req).await.unwrap();
4914 assert_eq!(resp.status, StatusCode::OK);
4915
4916 let s = state.read();
4917 assert_eq!(s.sent_emails[0].cc, vec!["cc@example.com"]);
4918 assert_eq!(s.sent_emails[0].bcc, vec!["bcc@example.com"]);
4919 }
4920
4921 #[tokio::test]
4922 async fn test_send_bulk_email() {
4923 let state = make_state();
4924 let svc = SesV2Service::new(state.clone());
4925
4926 let req = make_request(
4927 Method::POST,
4928 "/v2/email/outbound-bulk-emails",
4929 r#"{
4930 "FromEmailAddress": "sender@example.com",
4931 "DefaultContent": {
4932 "Template": {
4933 "TemplateName": "bulk-template",
4934 "TemplateData": "{\"default\": true}"
4935 }
4936 },
4937 "BulkEmailEntries": [
4938 {"Destination": {"ToAddresses": ["a@example.com"]}},
4939 {"Destination": {"ToAddresses": ["b@example.com"]}}
4940 ]
4941 }"#,
4942 );
4943 let resp = svc.handle(req).await.unwrap();
4944 assert_eq!(resp.status, StatusCode::OK);
4945 let body: Value = serde_json::from_slice(&resp.body).unwrap();
4946 let results = body["BulkEmailEntryResults"].as_array().unwrap();
4947 assert_eq!(results.len(), 2);
4948 assert_eq!(results[0]["Status"], "SUCCESS");
4949 assert_eq!(results[1]["Status"], "SUCCESS");
4950
4951 let s = state.read();
4952 assert_eq!(s.sent_emails.len(), 2);
4953 assert_eq!(s.sent_emails[0].to, vec!["a@example.com"]);
4954 assert_eq!(s.sent_emails[1].to, vec!["b@example.com"]);
4955 }
4956
4957 #[tokio::test]
4958 async fn test_send_bulk_email_empty_entries() {
4959 let state = make_state();
4960 let svc = SesV2Service::new(state);
4961
4962 let req = make_request(
4963 Method::POST,
4964 "/v2/email/outbound-bulk-emails",
4965 r#"{"FromEmailAddress": "s@example.com", "BulkEmailEntries": []}"#,
4966 );
4967 let resp = svc.handle(req).await.unwrap();
4968 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
4969 }
4970
4971 #[tokio::test]
4972 async fn test_delete_nonexistent_identity() {
4973 let state = make_state();
4974 let svc = SesV2Service::new(state);
4975
4976 let req = make_request(
4977 Method::DELETE,
4978 "/v2/email/identities/nobody%40example.com",
4979 "",
4980 );
4981 let resp = svc.handle(req).await.unwrap();
4982 assert_eq!(resp.status, StatusCode::NOT_FOUND);
4983 }
4984
4985 #[tokio::test]
4986 async fn test_duplicate_configuration_set() {
4987 let state = make_state();
4988 let svc = SesV2Service::new(state);
4989
4990 let req = make_request(
4991 Method::POST,
4992 "/v2/email/configuration-sets",
4993 r#"{"ConfigurationSetName": "dup-config"}"#,
4994 );
4995 svc.handle(req).await.unwrap();
4996
4997 let req = make_request(
4998 Method::POST,
4999 "/v2/email/configuration-sets",
5000 r#"{"ConfigurationSetName": "dup-config"}"#,
5001 );
5002 let resp = svc.handle(req).await.unwrap();
5003 assert_eq!(resp.status, StatusCode::CONFLICT);
5004 }
5005
5006 #[tokio::test]
5007 async fn test_duplicate_template() {
5008 let state = make_state();
5009 let svc = SesV2Service::new(state);
5010
5011 let req = make_request(
5012 Method::POST,
5013 "/v2/email/templates",
5014 r#"{"TemplateName": "dup-tmpl", "TemplateContent": {}}"#,
5015 );
5016 svc.handle(req).await.unwrap();
5017
5018 let req = make_request(
5019 Method::POST,
5020 "/v2/email/templates",
5021 r#"{"TemplateName": "dup-tmpl", "TemplateContent": {}}"#,
5022 );
5023 let resp = svc.handle(req).await.unwrap();
5024 assert_eq!(resp.status, StatusCode::CONFLICT);
5025 }
5026
5027 #[tokio::test]
5028 async fn test_delete_nonexistent_template() {
5029 let state = make_state();
5030 let svc = SesV2Service::new(state);
5031
5032 let req = make_request(Method::DELETE, "/v2/email/templates/nope", "");
5033 let resp = svc.handle(req).await.unwrap();
5034 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5035 }
5036
5037 #[tokio::test]
5038 async fn test_delete_nonexistent_configuration_set() {
5039 let state = make_state();
5040 let svc = SesV2Service::new(state);
5041
5042 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/nope", "");
5043 let resp = svc.handle(req).await.unwrap();
5044 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5045 }
5046
5047 #[tokio::test]
5048 async fn test_unknown_route() {
5049 let state = make_state();
5050 let svc = SesV2Service::new(state);
5051
5052 let req = make_request(Method::GET, "/v2/email/unknown-resource", "");
5053 let result = svc.handle(req).await;
5054 assert!(result.is_err(), "Unknown route should return error");
5055 }
5056
5057 #[tokio::test]
5058 async fn test_update_nonexistent_template() {
5059 let state = make_state();
5060 let svc = SesV2Service::new(state);
5061
5062 let req = make_request(
5063 Method::PUT,
5064 "/v2/email/templates/nonexistent",
5065 r#"{"TemplateContent": {"Subject": "Updated"}}"#,
5066 );
5067 let resp = svc.handle(req).await.unwrap();
5068 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5069 }
5070
5071 #[tokio::test]
5072 async fn test_invalid_json_body() {
5073 let state = make_state();
5074 let svc = SesV2Service::new(state);
5075
5076 let req = make_request(Method::POST, "/v2/email/identities", "not valid json {{{");
5077 let result = svc.handle(req).await;
5078 assert!(result.is_err(), "Invalid JSON body should return error");
5079 }
5080
5081 #[tokio::test]
5082 async fn test_create_identity_missing_name() {
5083 let state = make_state();
5084 let svc = SesV2Service::new(state);
5085
5086 let req = make_request(Method::POST, "/v2/email/identities", r#"{}"#);
5087 let resp = svc.handle(req).await.unwrap();
5088 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
5089 }
5090
5091 #[tokio::test]
5094 async fn test_contact_list_lifecycle() {
5095 let state = make_state();
5096 let svc = SesV2Service::new(state);
5097
5098 let req = make_request(
5100 Method::POST,
5101 "/v2/email/contact-lists",
5102 r#"{
5103 "ContactListName": "my-list",
5104 "Description": "Test list",
5105 "Topics": [
5106 {
5107 "TopicName": "newsletters",
5108 "DisplayName": "Newsletters",
5109 "Description": "Weekly newsletters",
5110 "DefaultSubscriptionStatus": "OPT_IN"
5111 }
5112 ]
5113 }"#,
5114 );
5115 let resp = svc.handle(req).await.unwrap();
5116 assert_eq!(resp.status, StatusCode::OK);
5117
5118 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
5120 let resp = svc.handle(req).await.unwrap();
5121 assert_eq!(resp.status, StatusCode::OK);
5122 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5123 assert_eq!(body["ContactListName"], "my-list");
5124 assert_eq!(body["Description"], "Test list");
5125 assert_eq!(body["Topics"][0]["TopicName"], "newsletters");
5126 assert_eq!(body["Topics"][0]["DefaultSubscriptionStatus"], "OPT_IN");
5127 assert!(body["CreatedTimestamp"].as_f64().is_some());
5128 assert!(body["LastUpdatedTimestamp"].as_f64().is_some());
5129
5130 let req = make_request(Method::GET, "/v2/email/contact-lists", "{}");
5132 let resp = svc.handle(req).await.unwrap();
5133 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5134 assert_eq!(body["ContactLists"].as_array().unwrap().len(), 1);
5135 assert_eq!(body["ContactLists"][0]["ContactListName"], "my-list");
5136
5137 let req = make_request(
5139 Method::PUT,
5140 "/v2/email/contact-lists/my-list",
5141 r#"{
5142 "Description": "Updated description",
5143 "Topics": [
5144 {
5145 "TopicName": "newsletters",
5146 "DisplayName": "Updated Newsletters",
5147 "Description": "Updated desc",
5148 "DefaultSubscriptionStatus": "OPT_OUT"
5149 },
5150 {
5151 "TopicName": "promotions",
5152 "DisplayName": "Promotions",
5153 "Description": "Promo emails",
5154 "DefaultSubscriptionStatus": "OPT_OUT"
5155 }
5156 ]
5157 }"#,
5158 );
5159 let resp = svc.handle(req).await.unwrap();
5160 assert_eq!(resp.status, StatusCode::OK);
5161
5162 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
5164 let resp = svc.handle(req).await.unwrap();
5165 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5166 assert_eq!(body["Description"], "Updated description");
5167 assert_eq!(body["Topics"].as_array().unwrap().len(), 2);
5168
5169 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
5171 let resp = svc.handle(req).await.unwrap();
5172 assert_eq!(resp.status, StatusCode::OK);
5173
5174 let req = make_request(Method::GET, "/v2/email/contact-lists/my-list", "{}");
5176 let resp = svc.handle(req).await.unwrap();
5177 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5178 }
5179
5180 #[tokio::test]
5181 async fn test_duplicate_contact_list() {
5182 let state = make_state();
5183 let svc = SesV2Service::new(state);
5184
5185 let req = make_request(
5186 Method::POST,
5187 "/v2/email/contact-lists",
5188 r#"{"ContactListName": "dup-list"}"#,
5189 );
5190 svc.handle(req).await.unwrap();
5191
5192 let req = make_request(
5193 Method::POST,
5194 "/v2/email/contact-lists",
5195 r#"{"ContactListName": "dup-list"}"#,
5196 );
5197 let resp = svc.handle(req).await.unwrap();
5198 assert_eq!(resp.status, StatusCode::CONFLICT);
5199 }
5200
5201 #[tokio::test]
5202 async fn test_contact_list_not_found() {
5203 let state = make_state();
5204 let svc = SesV2Service::new(state);
5205
5206 let req = make_request(Method::GET, "/v2/email/contact-lists/nonexistent", "{}");
5207 let resp = svc.handle(req).await.unwrap();
5208 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5209 }
5210
5211 #[tokio::test]
5214 async fn test_contact_lifecycle() {
5215 let state = make_state();
5216 let svc = SesV2Service::new(state);
5217
5218 let req = make_request(
5220 Method::POST,
5221 "/v2/email/contact-lists",
5222 r#"{
5223 "ContactListName": "my-list",
5224 "Topics": [
5225 {
5226 "TopicName": "newsletters",
5227 "DisplayName": "Newsletters",
5228 "Description": "Weekly newsletters",
5229 "DefaultSubscriptionStatus": "OPT_OUT"
5230 }
5231 ]
5232 }"#,
5233 );
5234 svc.handle(req).await.unwrap();
5235
5236 let req = make_request(
5238 Method::POST,
5239 "/v2/email/contact-lists/my-list/contacts",
5240 r#"{
5241 "EmailAddress": "user@example.com",
5242 "TopicPreferences": [
5243 {"TopicName": "newsletters", "SubscriptionStatus": "OPT_IN"}
5244 ],
5245 "UnsubscribeAll": false
5246 }"#,
5247 );
5248 let resp = svc.handle(req).await.unwrap();
5249 assert_eq!(resp.status, StatusCode::OK);
5250
5251 let req = make_request(
5253 Method::GET,
5254 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
5255 "{}",
5256 );
5257 let resp = svc.handle(req).await.unwrap();
5258 assert_eq!(resp.status, StatusCode::OK);
5259 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5260 assert_eq!(body["EmailAddress"], "user@example.com");
5261 assert_eq!(body["ContactListName"], "my-list");
5262 assert_eq!(body["UnsubscribeAll"], false);
5263 assert_eq!(body["TopicPreferences"][0]["TopicName"], "newsletters");
5264 assert_eq!(body["TopicPreferences"][0]["SubscriptionStatus"], "OPT_IN");
5265 assert_eq!(
5266 body["TopicDefaultPreferences"][0]["SubscriptionStatus"],
5267 "OPT_OUT"
5268 );
5269 assert!(body["CreatedTimestamp"].as_f64().is_some());
5270
5271 let req = make_request(
5273 Method::GET,
5274 "/v2/email/contact-lists/my-list/contacts",
5275 "{}",
5276 );
5277 let resp = svc.handle(req).await.unwrap();
5278 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5279 assert_eq!(body["Contacts"].as_array().unwrap().len(), 1);
5280 assert_eq!(body["Contacts"][0]["EmailAddress"], "user@example.com");
5281
5282 let req = make_request(
5284 Method::PUT,
5285 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
5286 r#"{
5287 "TopicPreferences": [
5288 {"TopicName": "newsletters", "SubscriptionStatus": "OPT_OUT"}
5289 ],
5290 "UnsubscribeAll": true
5291 }"#,
5292 );
5293 let resp = svc.handle(req).await.unwrap();
5294 assert_eq!(resp.status, StatusCode::OK);
5295
5296 let req = make_request(
5298 Method::GET,
5299 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
5300 "{}",
5301 );
5302 let resp = svc.handle(req).await.unwrap();
5303 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5304 assert_eq!(body["UnsubscribeAll"], true);
5305 assert_eq!(body["TopicPreferences"][0]["SubscriptionStatus"], "OPT_OUT");
5306
5307 let req = make_request(
5309 Method::DELETE,
5310 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
5311 "",
5312 );
5313 let resp = svc.handle(req).await.unwrap();
5314 assert_eq!(resp.status, StatusCode::OK);
5315
5316 let req = make_request(
5318 Method::GET,
5319 "/v2/email/contact-lists/my-list/contacts/user%40example.com",
5320 "{}",
5321 );
5322 let resp = svc.handle(req).await.unwrap();
5323 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5324 }
5325
5326 #[tokio::test]
5327 async fn test_duplicate_contact() {
5328 let state = make_state();
5329 let svc = SesV2Service::new(state);
5330
5331 let req = make_request(
5332 Method::POST,
5333 "/v2/email/contact-lists",
5334 r#"{"ContactListName": "my-list"}"#,
5335 );
5336 svc.handle(req).await.unwrap();
5337
5338 let req = make_request(
5339 Method::POST,
5340 "/v2/email/contact-lists/my-list/contacts",
5341 r#"{"EmailAddress": "dup@example.com"}"#,
5342 );
5343 svc.handle(req).await.unwrap();
5344
5345 let req = make_request(
5346 Method::POST,
5347 "/v2/email/contact-lists/my-list/contacts",
5348 r#"{"EmailAddress": "dup@example.com"}"#,
5349 );
5350 let resp = svc.handle(req).await.unwrap();
5351 assert_eq!(resp.status, StatusCode::CONFLICT);
5352 }
5353
5354 #[tokio::test]
5355 async fn test_contact_in_nonexistent_list() {
5356 let state = make_state();
5357 let svc = SesV2Service::new(state);
5358
5359 let req = make_request(
5360 Method::POST,
5361 "/v2/email/contact-lists/no-such-list/contacts",
5362 r#"{"EmailAddress": "user@example.com"}"#,
5363 );
5364 let resp = svc.handle(req).await.unwrap();
5365 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5366 }
5367
5368 #[tokio::test]
5369 async fn test_get_nonexistent_contact() {
5370 let state = make_state();
5371 let svc = SesV2Service::new(state);
5372
5373 let req = make_request(
5374 Method::POST,
5375 "/v2/email/contact-lists",
5376 r#"{"ContactListName": "my-list"}"#,
5377 );
5378 svc.handle(req).await.unwrap();
5379
5380 let req = make_request(
5381 Method::GET,
5382 "/v2/email/contact-lists/my-list/contacts/nobody%40example.com",
5383 "{}",
5384 );
5385 let resp = svc.handle(req).await.unwrap();
5386 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5387 }
5388
5389 #[tokio::test]
5390 async fn test_delete_contact_list_cascades_contacts() {
5391 let state = make_state();
5392 let svc = SesV2Service::new(state.clone());
5393
5394 let req = make_request(
5396 Method::POST,
5397 "/v2/email/contact-lists",
5398 r#"{"ContactListName": "my-list"}"#,
5399 );
5400 svc.handle(req).await.unwrap();
5401
5402 let req = make_request(
5403 Method::POST,
5404 "/v2/email/contact-lists/my-list/contacts",
5405 r#"{"EmailAddress": "user@example.com"}"#,
5406 );
5407 svc.handle(req).await.unwrap();
5408
5409 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
5411 svc.handle(req).await.unwrap();
5412
5413 let s = state.read();
5415 assert!(!s.contacts.contains_key("my-list"));
5416 }
5417
5418 #[tokio::test]
5419 async fn test_tag_resource() {
5420 let state = make_state();
5421 let svc = SesV2Service::new(state.clone());
5422
5423 let req = make_request(
5425 Method::POST,
5426 "/v2/email/identities",
5427 r#"{"EmailIdentity": "test@example.com"}"#,
5428 );
5429 svc.handle(req).await.unwrap();
5430
5431 let req = make_request(
5433 Method::POST,
5434 "/v2/email/tags",
5435 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com", "Tags": [{"Key": "env", "Value": "prod"}, {"Key": "team", "Value": "backend"}]}"#,
5436 );
5437 let resp = svc.handle(req).await.unwrap();
5438 assert_eq!(resp.status, StatusCode::OK);
5439
5440 let mut qp = HashMap::new();
5442 qp.insert(
5443 "ResourceArn".to_string(),
5444 "arn:aws:ses:us-east-1:123456789012:identity/test@example.com".to_string(),
5445 );
5446 let req = make_request_with_query(
5447 Method::GET,
5448 "/v2/email/tags",
5449 "",
5450 "ResourceArn=arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
5451 qp,
5452 );
5453 let resp = svc.handle(req).await.unwrap();
5454 assert_eq!(resp.status, StatusCode::OK);
5455 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5456 let tags = body["Tags"].as_array().unwrap();
5457 assert_eq!(tags.len(), 2);
5458 }
5459
5460 #[tokio::test]
5461 async fn test_untag_resource() {
5462 let state = make_state();
5463 let svc = SesV2Service::new(state.clone());
5464
5465 let req = make_request(
5467 Method::POST,
5468 "/v2/email/identities",
5469 r#"{"EmailIdentity": "test@example.com"}"#,
5470 );
5471 svc.handle(req).await.unwrap();
5472
5473 let arn = "arn:aws:ses:us-east-1:123456789012:identity/test@example.com";
5474
5475 let req = make_request(
5477 Method::POST,
5478 "/v2/email/tags",
5479 &format!(
5480 r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "env", "Value": "prod"}}, {{"Key": "team", "Value": "backend"}}]}}"#
5481 ),
5482 );
5483 svc.handle(req).await.unwrap();
5484
5485 let mut qp = HashMap::new();
5487 qp.insert("ResourceArn".to_string(), arn.to_string());
5488 qp.insert("TagKeys".to_string(), "env".to_string());
5489 let raw_query = format!("ResourceArn={}&TagKeys=env", urlencoded(arn));
5490 let req = make_request_with_query(Method::DELETE, "/v2/email/tags", "", &raw_query, qp);
5491 let resp = svc.handle(req).await.unwrap();
5492 assert_eq!(resp.status, StatusCode::OK);
5493
5494 let s = state.read();
5496 let tags = s.tags.get(arn).unwrap();
5497 assert_eq!(tags.len(), 1);
5498 assert_eq!(tags.get("team").unwrap(), "backend");
5499 }
5500
5501 #[tokio::test]
5502 async fn test_tag_nonexistent_resource() {
5503 let state = make_state();
5504 let svc = SesV2Service::new(state);
5505
5506 let req = make_request(
5507 Method::POST,
5508 "/v2/email/tags",
5509 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/nope", "Tags": [{"Key": "k", "Value": "v"}]}"#,
5510 );
5511 let resp = svc.handle(req).await.unwrap();
5512 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5513 }
5514
5515 #[tokio::test]
5516 async fn test_delete_identity_removes_tags() {
5517 let state = make_state();
5518 let svc = SesV2Service::new(state.clone());
5519
5520 let req = make_request(
5521 Method::POST,
5522 "/v2/email/identities",
5523 r#"{"EmailIdentity": "test@example.com"}"#,
5524 );
5525 svc.handle(req).await.unwrap();
5526
5527 let arn = "arn:aws:ses:us-east-1:123456789012:identity/test@example.com";
5528 let req = make_request(
5529 Method::POST,
5530 "/v2/email/tags",
5531 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
5532 );
5533 svc.handle(req).await.unwrap();
5534
5535 let req = make_request(
5537 Method::DELETE,
5538 "/v2/email/identities/test%40example.com",
5539 "",
5540 );
5541 svc.handle(req).await.unwrap();
5542
5543 let s = state.read();
5545 assert!(!s.tags.contains_key(arn));
5546 }
5547
5548 #[tokio::test]
5549 async fn test_delete_config_set_removes_tags() {
5550 let state = make_state();
5551 let svc = SesV2Service::new(state.clone());
5552
5553 let req = make_request(
5554 Method::POST,
5555 "/v2/email/configuration-sets",
5556 r#"{"ConfigurationSetName": "my-config"}"#,
5557 );
5558 svc.handle(req).await.unwrap();
5559
5560 let arn = "arn:aws:ses:us-east-1:123456789012:configuration-set/my-config";
5561 let req = make_request(
5562 Method::POST,
5563 "/v2/email/tags",
5564 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
5565 );
5566 svc.handle(req).await.unwrap();
5567
5568 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
5570 svc.handle(req).await.unwrap();
5571
5572 let s = state.read();
5573 assert!(!s.tags.contains_key(arn));
5574 }
5575
5576 #[tokio::test]
5577 async fn test_delete_contact_list_removes_tags() {
5578 let state = make_state();
5579 let svc = SesV2Service::new(state.clone());
5580
5581 let req = make_request(
5582 Method::POST,
5583 "/v2/email/contact-lists",
5584 r#"{"ContactListName": "my-list"}"#,
5585 );
5586 svc.handle(req).await.unwrap();
5587
5588 let arn = "arn:aws:ses:us-east-1:123456789012:contact-list/my-list";
5589 let req = make_request(
5590 Method::POST,
5591 "/v2/email/tags",
5592 &format!(r#"{{"ResourceArn": "{arn}", "Tags": [{{"Key": "k", "Value": "v"}}]}}"#),
5593 );
5594 svc.handle(req).await.unwrap();
5595
5596 let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
5598 svc.handle(req).await.unwrap();
5599
5600 let s = state.read();
5601 assert!(!s.tags.contains_key(arn));
5602 }
5603
5604 fn urlencoded(s: &str) -> String {
5605 form_urlencoded::byte_serialize(s.as_bytes()).collect()
5606 }
5607
5608 #[tokio::test]
5611 async fn test_suppressed_destination_lifecycle() {
5612 let state = make_state();
5613 let svc = SesV2Service::new(state);
5614
5615 let req = make_request(
5617 Method::PUT,
5618 "/v2/email/suppression/addresses",
5619 r#"{"EmailAddress": "bounce@example.com", "Reason": "BOUNCE"}"#,
5620 );
5621 let resp = svc.handle(req).await.unwrap();
5622 assert_eq!(resp.status, StatusCode::OK);
5623
5624 let req = make_request(
5626 Method::GET,
5627 "/v2/email/suppression/addresses/bounce%40example.com",
5628 "",
5629 );
5630 let resp = svc.handle(req).await.unwrap();
5631 assert_eq!(resp.status, StatusCode::OK);
5632 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5633 assert_eq!(
5634 body["SuppressedDestination"]["EmailAddress"],
5635 "bounce@example.com"
5636 );
5637 assert_eq!(body["SuppressedDestination"]["Reason"], "BOUNCE");
5638 assert!(body["SuppressedDestination"]["LastUpdateTime"]
5639 .as_f64()
5640 .is_some());
5641
5642 let req = make_request(Method::GET, "/v2/email/suppression/addresses", "");
5644 let resp = svc.handle(req).await.unwrap();
5645 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5646 assert_eq!(
5647 body["SuppressedDestinationSummaries"]
5648 .as_array()
5649 .unwrap()
5650 .len(),
5651 1
5652 );
5653
5654 let req = make_request(
5656 Method::DELETE,
5657 "/v2/email/suppression/addresses/bounce%40example.com",
5658 "",
5659 );
5660 let resp = svc.handle(req).await.unwrap();
5661 assert_eq!(resp.status, StatusCode::OK);
5662
5663 let req = make_request(
5665 Method::GET,
5666 "/v2/email/suppression/addresses/bounce%40example.com",
5667 "",
5668 );
5669 let resp = svc.handle(req).await.unwrap();
5670 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5671 }
5672
5673 #[tokio::test]
5674 async fn test_suppressed_destination_complaint() {
5675 let state = make_state();
5676 let svc = SesV2Service::new(state);
5677
5678 let req = make_request(
5679 Method::PUT,
5680 "/v2/email/suppression/addresses",
5681 r#"{"EmailAddress": "complaint@example.com", "Reason": "COMPLAINT"}"#,
5682 );
5683 let resp = svc.handle(req).await.unwrap();
5684 assert_eq!(resp.status, StatusCode::OK);
5685
5686 let req = make_request(
5687 Method::GET,
5688 "/v2/email/suppression/addresses/complaint%40example.com",
5689 "",
5690 );
5691 let resp = svc.handle(req).await.unwrap();
5692 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5693 assert_eq!(body["SuppressedDestination"]["Reason"], "COMPLAINT");
5694 }
5695
5696 #[tokio::test]
5697 async fn test_suppressed_destination_invalid_reason() {
5698 let state = make_state();
5699 let svc = SesV2Service::new(state);
5700
5701 let req = make_request(
5702 Method::PUT,
5703 "/v2/email/suppression/addresses",
5704 r#"{"EmailAddress": "bad@example.com", "Reason": "INVALID"}"#,
5705 );
5706 let resp = svc.handle(req).await.unwrap();
5707 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
5708 }
5709
5710 #[tokio::test]
5711 async fn test_suppressed_destination_upsert() {
5712 let state = make_state();
5713 let svc = SesV2Service::new(state);
5714
5715 let req = make_request(
5717 Method::PUT,
5718 "/v2/email/suppression/addresses",
5719 r#"{"EmailAddress": "user@example.com", "Reason": "BOUNCE"}"#,
5720 );
5721 svc.handle(req).await.unwrap();
5722
5723 let req = make_request(
5725 Method::PUT,
5726 "/v2/email/suppression/addresses",
5727 r#"{"EmailAddress": "user@example.com", "Reason": "COMPLAINT"}"#,
5728 );
5729 svc.handle(req).await.unwrap();
5730
5731 let req = make_request(
5732 Method::GET,
5733 "/v2/email/suppression/addresses/user%40example.com",
5734 "",
5735 );
5736 let resp = svc.handle(req).await.unwrap();
5737 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5738 assert_eq!(body["SuppressedDestination"]["Reason"], "COMPLAINT");
5739 }
5740
5741 #[tokio::test]
5742 async fn test_delete_nonexistent_suppressed_destination() {
5743 let state = make_state();
5744 let svc = SesV2Service::new(state);
5745
5746 let req = make_request(
5747 Method::DELETE,
5748 "/v2/email/suppression/addresses/nobody%40example.com",
5749 "",
5750 );
5751 let resp = svc.handle(req).await.unwrap();
5752 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5753 }
5754
5755 #[tokio::test]
5758 async fn test_event_destination_lifecycle() {
5759 let state = make_state();
5760 let svc = SesV2Service::new(state);
5761
5762 let req = make_request(
5764 Method::POST,
5765 "/v2/email/configuration-sets",
5766 r#"{"ConfigurationSetName": "my-config"}"#,
5767 );
5768 svc.handle(req).await.unwrap();
5769
5770 let req = make_request(
5772 Method::POST,
5773 "/v2/email/configuration-sets/my-config/event-destinations",
5774 r#"{
5775 "EventDestinationName": "my-dest",
5776 "EventDestination": {
5777 "Enabled": true,
5778 "MatchingEventTypes": ["SEND", "BOUNCE"],
5779 "SnsDestination": {"TopicArn": "arn:aws:sns:us-east-1:123456789012:my-topic"}
5780 }
5781 }"#,
5782 );
5783 let resp = svc.handle(req).await.unwrap();
5784 assert_eq!(resp.status, StatusCode::OK);
5785
5786 let req = make_request(
5788 Method::GET,
5789 "/v2/email/configuration-sets/my-config/event-destinations",
5790 "",
5791 );
5792 let resp = svc.handle(req).await.unwrap();
5793 assert_eq!(resp.status, StatusCode::OK);
5794 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5795 let dests = body["EventDestinations"].as_array().unwrap();
5796 assert_eq!(dests.len(), 1);
5797 assert_eq!(dests[0]["Name"], "my-dest");
5798 assert_eq!(dests[0]["Enabled"], true);
5799 assert_eq!(dests[0]["MatchingEventTypes"], json!(["SEND", "BOUNCE"]));
5800 assert_eq!(
5801 dests[0]["SnsDestination"]["TopicArn"],
5802 "arn:aws:sns:us-east-1:123456789012:my-topic"
5803 );
5804
5805 let req = make_request(
5807 Method::PUT,
5808 "/v2/email/configuration-sets/my-config/event-destinations/my-dest",
5809 r#"{
5810 "EventDestination": {
5811 "Enabled": false,
5812 "MatchingEventTypes": ["DELIVERY"],
5813 "SnsDestination": {"TopicArn": "arn:aws:sns:us-east-1:123456789012:updated-topic"}
5814 }
5815 }"#,
5816 );
5817 let resp = svc.handle(req).await.unwrap();
5818 assert_eq!(resp.status, StatusCode::OK);
5819
5820 let req = make_request(
5822 Method::GET,
5823 "/v2/email/configuration-sets/my-config/event-destinations",
5824 "",
5825 );
5826 let resp = svc.handle(req).await.unwrap();
5827 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5828 let dests = body["EventDestinations"].as_array().unwrap();
5829 assert_eq!(dests[0]["Enabled"], false);
5830 assert_eq!(dests[0]["MatchingEventTypes"], json!(["DELIVERY"]));
5831
5832 let req = make_request(
5834 Method::DELETE,
5835 "/v2/email/configuration-sets/my-config/event-destinations/my-dest",
5836 "",
5837 );
5838 let resp = svc.handle(req).await.unwrap();
5839 assert_eq!(resp.status, StatusCode::OK);
5840
5841 let req = make_request(
5843 Method::GET,
5844 "/v2/email/configuration-sets/my-config/event-destinations",
5845 "",
5846 );
5847 let resp = svc.handle(req).await.unwrap();
5848 let body: Value = serde_json::from_slice(&resp.body).unwrap();
5849 assert!(body["EventDestinations"].as_array().unwrap().is_empty());
5850 }
5851
5852 #[tokio::test]
5853 async fn test_event_destination_config_set_not_found() {
5854 let state = make_state();
5855 let svc = SesV2Service::new(state);
5856
5857 let req = make_request(
5858 Method::POST,
5859 "/v2/email/configuration-sets/nonexistent/event-destinations",
5860 r#"{
5861 "EventDestinationName": "dest",
5862 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
5863 }"#,
5864 );
5865 let resp = svc.handle(req).await.unwrap();
5866 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5867 }
5868
5869 #[tokio::test]
5870 async fn test_event_destination_duplicate() {
5871 let state = make_state();
5872 let svc = SesV2Service::new(state);
5873
5874 let req = make_request(
5875 Method::POST,
5876 "/v2/email/configuration-sets",
5877 r#"{"ConfigurationSetName": "my-config"}"#,
5878 );
5879 svc.handle(req).await.unwrap();
5880
5881 let body = r#"{
5882 "EventDestinationName": "dup-dest",
5883 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
5884 }"#;
5885
5886 let req = make_request(
5887 Method::POST,
5888 "/v2/email/configuration-sets/my-config/event-destinations",
5889 body,
5890 );
5891 svc.handle(req).await.unwrap();
5892
5893 let req = make_request(
5894 Method::POST,
5895 "/v2/email/configuration-sets/my-config/event-destinations",
5896 body,
5897 );
5898 let resp = svc.handle(req).await.unwrap();
5899 assert_eq!(resp.status, StatusCode::CONFLICT);
5900 }
5901
5902 #[tokio::test]
5903 async fn test_update_nonexistent_event_destination() {
5904 let state = make_state();
5905 let svc = SesV2Service::new(state);
5906
5907 let req = make_request(
5908 Method::POST,
5909 "/v2/email/configuration-sets",
5910 r#"{"ConfigurationSetName": "my-config"}"#,
5911 );
5912 svc.handle(req).await.unwrap();
5913
5914 let req = make_request(
5915 Method::PUT,
5916 "/v2/email/configuration-sets/my-config/event-destinations/nonexistent",
5917 r#"{"EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}}"#,
5918 );
5919 let resp = svc.handle(req).await.unwrap();
5920 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5921 }
5922
5923 #[tokio::test]
5924 async fn test_delete_nonexistent_event_destination() {
5925 let state = make_state();
5926 let svc = SesV2Service::new(state);
5927
5928 let req = make_request(
5929 Method::POST,
5930 "/v2/email/configuration-sets",
5931 r#"{"ConfigurationSetName": "my-config"}"#,
5932 );
5933 svc.handle(req).await.unwrap();
5934
5935 let req = make_request(
5936 Method::DELETE,
5937 "/v2/email/configuration-sets/my-config/event-destinations/nonexistent",
5938 "",
5939 );
5940 let resp = svc.handle(req).await.unwrap();
5941 assert_eq!(resp.status, StatusCode::NOT_FOUND);
5942 }
5943
5944 #[tokio::test]
5945 async fn test_event_destinations_cleaned_on_config_set_delete() {
5946 let state = make_state();
5947 let svc = SesV2Service::new(state.clone());
5948
5949 let req = make_request(
5950 Method::POST,
5951 "/v2/email/configuration-sets",
5952 r#"{"ConfigurationSetName": "my-config"}"#,
5953 );
5954 svc.handle(req).await.unwrap();
5955
5956 let req = make_request(
5957 Method::POST,
5958 "/v2/email/configuration-sets/my-config/event-destinations",
5959 r#"{
5960 "EventDestinationName": "dest1",
5961 "EventDestination": {"Enabled": true, "MatchingEventTypes": ["SEND"]}
5962 }"#,
5963 );
5964 svc.handle(req).await.unwrap();
5965
5966 let req = make_request(Method::DELETE, "/v2/email/configuration-sets/my-config", "");
5967 svc.handle(req).await.unwrap();
5968
5969 let s = state.read();
5970 assert!(!s.event_destinations.contains_key("my-config"));
5971 }
5972
5973 #[tokio::test]
5976 async fn test_identity_policy_lifecycle() {
5977 let state = make_state();
5978 let svc = SesV2Service::new(state);
5979
5980 let req = make_request(
5982 Method::POST,
5983 "/v2/email/identities",
5984 r#"{"EmailIdentity": "test@example.com"}"#,
5985 );
5986 svc.handle(req).await.unwrap();
5987
5988 let policy_doc = r#"{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"ses:SendEmail","Resource":"*"}]}"#;
5990 let req = make_request(
5991 Method::POST,
5992 "/v2/email/identities/test%40example.com/policies/my-policy",
5993 &format!(
5994 r#"{{"Policy": {}}}"#,
5995 serde_json::to_string(policy_doc).unwrap()
5996 ),
5997 );
5998 let resp = svc.handle(req).await.unwrap();
5999 assert_eq!(resp.status, StatusCode::OK);
6000
6001 let req = make_request(
6003 Method::GET,
6004 "/v2/email/identities/test%40example.com/policies",
6005 "",
6006 );
6007 let resp = svc.handle(req).await.unwrap();
6008 assert_eq!(resp.status, StatusCode::OK);
6009 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6010 assert!(body["Policies"]["my-policy"].is_string());
6011 assert_eq!(body["Policies"]["my-policy"].as_str().unwrap(), policy_doc);
6012
6013 let updated_doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
6015 let req = make_request(
6016 Method::PUT,
6017 "/v2/email/identities/test%40example.com/policies/my-policy",
6018 &format!(
6019 r#"{{"Policy": {}}}"#,
6020 serde_json::to_string(updated_doc).unwrap()
6021 ),
6022 );
6023 let resp = svc.handle(req).await.unwrap();
6024 assert_eq!(resp.status, StatusCode::OK);
6025
6026 let req = make_request(
6028 Method::GET,
6029 "/v2/email/identities/test%40example.com/policies",
6030 "",
6031 );
6032 let resp = svc.handle(req).await.unwrap();
6033 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6034 assert_eq!(body["Policies"]["my-policy"].as_str().unwrap(), updated_doc);
6035
6036 let req = make_request(
6038 Method::DELETE,
6039 "/v2/email/identities/test%40example.com/policies/my-policy",
6040 "",
6041 );
6042 let resp = svc.handle(req).await.unwrap();
6043 assert_eq!(resp.status, StatusCode::OK);
6044
6045 let req = make_request(
6047 Method::GET,
6048 "/v2/email/identities/test%40example.com/policies",
6049 "",
6050 );
6051 let resp = svc.handle(req).await.unwrap();
6052 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6053 assert!(body["Policies"].as_object().unwrap().is_empty());
6054 }
6055
6056 #[tokio::test]
6057 async fn test_identity_policy_identity_not_found() {
6058 let state = make_state();
6059 let svc = SesV2Service::new(state);
6060
6061 let req = make_request(
6062 Method::POST,
6063 "/v2/email/identities/nonexistent%40example.com/policies/my-policy",
6064 r#"{"Policy": "{}"}"#,
6065 );
6066 let resp = svc.handle(req).await.unwrap();
6067 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6068 }
6069
6070 #[tokio::test]
6071 async fn test_identity_policy_duplicate() {
6072 let state = make_state();
6073 let svc = SesV2Service::new(state);
6074
6075 let req = make_request(
6076 Method::POST,
6077 "/v2/email/identities",
6078 r#"{"EmailIdentity": "test@example.com"}"#,
6079 );
6080 svc.handle(req).await.unwrap();
6081
6082 let req = make_request(
6083 Method::POST,
6084 "/v2/email/identities/test%40example.com/policies/my-policy",
6085 r#"{"Policy": "{}"}"#,
6086 );
6087 svc.handle(req).await.unwrap();
6088
6089 let req = make_request(
6090 Method::POST,
6091 "/v2/email/identities/test%40example.com/policies/my-policy",
6092 r#"{"Policy": "{}"}"#,
6093 );
6094 let resp = svc.handle(req).await.unwrap();
6095 assert_eq!(resp.status, StatusCode::CONFLICT);
6096 }
6097
6098 #[tokio::test]
6099 async fn test_update_nonexistent_policy() {
6100 let state = make_state();
6101 let svc = SesV2Service::new(state);
6102
6103 let req = make_request(
6104 Method::POST,
6105 "/v2/email/identities",
6106 r#"{"EmailIdentity": "test@example.com"}"#,
6107 );
6108 svc.handle(req).await.unwrap();
6109
6110 let req = make_request(
6111 Method::PUT,
6112 "/v2/email/identities/test%40example.com/policies/nonexistent",
6113 r#"{"Policy": "{}"}"#,
6114 );
6115 let resp = svc.handle(req).await.unwrap();
6116 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6117 }
6118
6119 #[tokio::test]
6120 async fn test_delete_nonexistent_policy() {
6121 let state = make_state();
6122 let svc = SesV2Service::new(state);
6123
6124 let req = make_request(
6125 Method::POST,
6126 "/v2/email/identities",
6127 r#"{"EmailIdentity": "test@example.com"}"#,
6128 );
6129 svc.handle(req).await.unwrap();
6130
6131 let req = make_request(
6132 Method::DELETE,
6133 "/v2/email/identities/test%40example.com/policies/nonexistent",
6134 "",
6135 );
6136 let resp = svc.handle(req).await.unwrap();
6137 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6138 }
6139
6140 #[tokio::test]
6141 async fn test_policies_cleaned_on_identity_delete() {
6142 let state = make_state();
6143 let svc = SesV2Service::new(state.clone());
6144
6145 let req = make_request(
6146 Method::POST,
6147 "/v2/email/identities",
6148 r#"{"EmailIdentity": "test@example.com"}"#,
6149 );
6150 svc.handle(req).await.unwrap();
6151
6152 let req = make_request(
6153 Method::POST,
6154 "/v2/email/identities/test%40example.com/policies/my-policy",
6155 r#"{"Policy": "{}"}"#,
6156 );
6157 svc.handle(req).await.unwrap();
6158
6159 let req = make_request(
6160 Method::DELETE,
6161 "/v2/email/identities/test%40example.com",
6162 "",
6163 );
6164 svc.handle(req).await.unwrap();
6165
6166 let s = state.read();
6167 assert!(!s.identity_policies.contains_key("test@example.com"));
6168 }
6169
6170 #[tokio::test]
6173 async fn test_put_email_identity_dkim_attributes() {
6174 let state = make_state();
6175 let svc = SesV2Service::new(state.clone());
6176
6177 let req = make_request(
6179 Method::POST,
6180 "/v2/email/identities",
6181 r#"{"EmailIdentity": "example.com"}"#,
6182 );
6183 svc.handle(req).await.unwrap();
6184
6185 let req = make_request(
6187 Method::PUT,
6188 "/v2/email/identities/example.com/dkim",
6189 r#"{"SigningEnabled": false}"#,
6190 );
6191 let resp = svc.handle(req).await.unwrap();
6192 assert_eq!(resp.status, StatusCode::OK);
6193
6194 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
6196 let resp = svc.handle(req).await.unwrap();
6197 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6198 assert_eq!(body["DkimAttributes"]["SigningEnabled"], false);
6199 }
6200
6201 #[tokio::test]
6202 async fn test_put_email_identity_dkim_attributes_not_found() {
6203 let state = make_state();
6204 let svc = SesV2Service::new(state);
6205
6206 let req = make_request(
6207 Method::PUT,
6208 "/v2/email/identities/nonexistent.com/dkim",
6209 r#"{"SigningEnabled": false}"#,
6210 );
6211 let resp = svc.handle(req).await.unwrap();
6212 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6213 }
6214
6215 #[tokio::test]
6216 async fn test_put_email_identity_dkim_signing_attributes() {
6217 let state = make_state();
6218 let svc = SesV2Service::new(state.clone());
6219
6220 let req = make_request(
6221 Method::POST,
6222 "/v2/email/identities",
6223 r#"{"EmailIdentity": "example.com"}"#,
6224 );
6225 svc.handle(req).await.unwrap();
6226
6227 let req = make_request(
6228 Method::PUT,
6229 "/v2/email/identities/example.com/dkim/signing",
6230 r#"{"SigningAttributesOrigin": "EXTERNAL", "SigningAttributes": {"DomainSigningPrivateKey": "key123", "DomainSigningSelector": "sel1"}}"#,
6231 );
6232 let resp = svc.handle(req).await.unwrap();
6233 assert_eq!(resp.status, StatusCode::OK);
6234 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6235 assert_eq!(body["DkimStatus"], "SUCCESS");
6236 assert!(!body["DkimTokens"].as_array().unwrap().is_empty());
6237
6238 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
6240 let resp = svc.handle(req).await.unwrap();
6241 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6242 assert_eq!(
6243 body["DkimAttributes"]["SigningAttributesOrigin"],
6244 "EXTERNAL"
6245 );
6246 }
6247
6248 #[tokio::test]
6249 async fn test_put_email_identity_feedback_attributes() {
6250 let state = make_state();
6251 let svc = SesV2Service::new(state.clone());
6252
6253 let req = make_request(
6254 Method::POST,
6255 "/v2/email/identities",
6256 r#"{"EmailIdentity": "test@example.com"}"#,
6257 );
6258 svc.handle(req).await.unwrap();
6259
6260 let req = make_request(
6261 Method::PUT,
6262 "/v2/email/identities/test%40example.com/feedback",
6263 r#"{"EmailForwardingEnabled": false}"#,
6264 );
6265 let resp = svc.handle(req).await.unwrap();
6266 assert_eq!(resp.status, StatusCode::OK);
6267
6268 let req = make_request(Method::GET, "/v2/email/identities/test%40example.com", "");
6269 let resp = svc.handle(req).await.unwrap();
6270 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6271 assert_eq!(body["FeedbackForwardingStatus"], false);
6272 }
6273
6274 #[tokio::test]
6275 async fn test_put_email_identity_mail_from_attributes() {
6276 let state = make_state();
6277 let svc = SesV2Service::new(state.clone());
6278
6279 let req = make_request(
6280 Method::POST,
6281 "/v2/email/identities",
6282 r#"{"EmailIdentity": "example.com"}"#,
6283 );
6284 svc.handle(req).await.unwrap();
6285
6286 let req = make_request(
6287 Method::PUT,
6288 "/v2/email/identities/example.com/mail-from",
6289 r#"{"MailFromDomain": "mail.example.com", "BehaviorOnMxFailure": "REJECT_MESSAGE"}"#,
6290 );
6291 let resp = svc.handle(req).await.unwrap();
6292 assert_eq!(resp.status, StatusCode::OK);
6293
6294 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
6295 let resp = svc.handle(req).await.unwrap();
6296 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6297 assert_eq!(
6298 body["MailFromAttributes"]["MailFromDomain"],
6299 "mail.example.com"
6300 );
6301 assert_eq!(
6302 body["MailFromAttributes"]["BehaviorOnMxFailure"],
6303 "REJECT_MESSAGE"
6304 );
6305 }
6306
6307 #[tokio::test]
6308 async fn test_put_email_identity_configuration_set_attributes() {
6309 let state = make_state();
6310 let svc = SesV2Service::new(state.clone());
6311
6312 let req = make_request(
6313 Method::POST,
6314 "/v2/email/identities",
6315 r#"{"EmailIdentity": "example.com"}"#,
6316 );
6317 svc.handle(req).await.unwrap();
6318
6319 let req = make_request(
6320 Method::PUT,
6321 "/v2/email/identities/example.com/configuration-set",
6322 r#"{"ConfigurationSetName": "my-config"}"#,
6323 );
6324 let resp = svc.handle(req).await.unwrap();
6325 assert_eq!(resp.status, StatusCode::OK);
6326
6327 let req = make_request(Method::GET, "/v2/email/identities/example.com", "");
6328 let resp = svc.handle(req).await.unwrap();
6329 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6330 assert_eq!(body["ConfigurationSetName"], "my-config");
6331 }
6332
6333 #[tokio::test]
6336 async fn test_put_configuration_set_sending_options() {
6337 let state = make_state();
6338 let svc = SesV2Service::new(state);
6339
6340 let req = make_request(
6342 Method::POST,
6343 "/v2/email/configuration-sets",
6344 r#"{"ConfigurationSetName": "test-config"}"#,
6345 );
6346 svc.handle(req).await.unwrap();
6347
6348 let req = make_request(
6350 Method::PUT,
6351 "/v2/email/configuration-sets/test-config/sending",
6352 r#"{"SendingEnabled": false}"#,
6353 );
6354 let resp = svc.handle(req).await.unwrap();
6355 assert_eq!(resp.status, StatusCode::OK);
6356
6357 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6359 let resp = svc.handle(req).await.unwrap();
6360 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6361 assert_eq!(body["SendingOptions"]["SendingEnabled"], false);
6362 }
6363
6364 #[tokio::test]
6365 async fn test_put_configuration_set_sending_options_not_found() {
6366 let state = make_state();
6367 let svc = SesV2Service::new(state);
6368
6369 let req = make_request(
6370 Method::PUT,
6371 "/v2/email/configuration-sets/nonexistent/sending",
6372 r#"{"SendingEnabled": false}"#,
6373 );
6374 let resp = svc.handle(req).await.unwrap();
6375 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6376 }
6377
6378 #[tokio::test]
6379 async fn test_put_configuration_set_delivery_options() {
6380 let state = make_state();
6381 let svc = SesV2Service::new(state);
6382
6383 let req = make_request(
6384 Method::POST,
6385 "/v2/email/configuration-sets",
6386 r#"{"ConfigurationSetName": "test-config"}"#,
6387 );
6388 svc.handle(req).await.unwrap();
6389
6390 let req = make_request(
6391 Method::PUT,
6392 "/v2/email/configuration-sets/test-config/delivery-options",
6393 r#"{"TlsPolicy": "REQUIRE", "SendingPoolName": "my-pool"}"#,
6394 );
6395 let resp = svc.handle(req).await.unwrap();
6396 assert_eq!(resp.status, StatusCode::OK);
6397
6398 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6399 let resp = svc.handle(req).await.unwrap();
6400 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6401 assert_eq!(body["DeliveryOptions"]["TlsPolicy"], "REQUIRE");
6402 assert_eq!(body["DeliveryOptions"]["SendingPoolName"], "my-pool");
6403 }
6404
6405 #[tokio::test]
6406 async fn test_put_configuration_set_tracking_options() {
6407 let state = make_state();
6408 let svc = SesV2Service::new(state);
6409
6410 let req = make_request(
6411 Method::POST,
6412 "/v2/email/configuration-sets",
6413 r#"{"ConfigurationSetName": "test-config"}"#,
6414 );
6415 svc.handle(req).await.unwrap();
6416
6417 let req = make_request(
6418 Method::PUT,
6419 "/v2/email/configuration-sets/test-config/tracking-options",
6420 r#"{"CustomRedirectDomain": "track.example.com", "HttpsPolicy": "REQUIRE"}"#,
6421 );
6422 let resp = svc.handle(req).await.unwrap();
6423 assert_eq!(resp.status, StatusCode::OK);
6424
6425 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6426 let resp = svc.handle(req).await.unwrap();
6427 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6428 assert_eq!(
6429 body["TrackingOptions"]["CustomRedirectDomain"],
6430 "track.example.com"
6431 );
6432 assert_eq!(body["TrackingOptions"]["HttpsPolicy"], "REQUIRE");
6433 }
6434
6435 #[tokio::test]
6436 async fn test_put_configuration_set_suppression_options() {
6437 let state = make_state();
6438 let svc = SesV2Service::new(state);
6439
6440 let req = make_request(
6441 Method::POST,
6442 "/v2/email/configuration-sets",
6443 r#"{"ConfigurationSetName": "test-config"}"#,
6444 );
6445 svc.handle(req).await.unwrap();
6446
6447 let req = make_request(
6448 Method::PUT,
6449 "/v2/email/configuration-sets/test-config/suppression-options",
6450 r#"{"SuppressedReasons": ["BOUNCE", "COMPLAINT"]}"#,
6451 );
6452 let resp = svc.handle(req).await.unwrap();
6453 assert_eq!(resp.status, StatusCode::OK);
6454
6455 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6456 let resp = svc.handle(req).await.unwrap();
6457 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6458 let reasons = body["SuppressionOptions"]["SuppressedReasons"]
6459 .as_array()
6460 .unwrap();
6461 assert_eq!(reasons.len(), 2);
6462 }
6463
6464 #[tokio::test]
6465 async fn test_put_configuration_set_reputation_options() {
6466 let state = make_state();
6467 let svc = SesV2Service::new(state);
6468
6469 let req = make_request(
6470 Method::POST,
6471 "/v2/email/configuration-sets",
6472 r#"{"ConfigurationSetName": "test-config"}"#,
6473 );
6474 svc.handle(req).await.unwrap();
6475
6476 let req = make_request(
6477 Method::PUT,
6478 "/v2/email/configuration-sets/test-config/reputation-options",
6479 r#"{"ReputationMetricsEnabled": true}"#,
6480 );
6481 let resp = svc.handle(req).await.unwrap();
6482 assert_eq!(resp.status, StatusCode::OK);
6483
6484 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6485 let resp = svc.handle(req).await.unwrap();
6486 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6487 assert_eq!(body["ReputationOptions"]["ReputationMetricsEnabled"], true);
6488 }
6489
6490 #[tokio::test]
6491 async fn test_put_configuration_set_vdm_options() {
6492 let state = make_state();
6493 let svc = SesV2Service::new(state);
6494
6495 let req = make_request(
6496 Method::POST,
6497 "/v2/email/configuration-sets",
6498 r#"{"ConfigurationSetName": "test-config"}"#,
6499 );
6500 svc.handle(req).await.unwrap();
6501
6502 let req = make_request(
6503 Method::PUT,
6504 "/v2/email/configuration-sets/test-config/vdm-options",
6505 r#"{"DashboardOptions": {"EngagementMetrics": "ENABLED"}, "GuardianOptions": {"OptimizedSharedDelivery": "ENABLED"}}"#,
6506 );
6507 let resp = svc.handle(req).await.unwrap();
6508 assert_eq!(resp.status, StatusCode::OK);
6509
6510 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6511 let resp = svc.handle(req).await.unwrap();
6512 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6513 assert_eq!(
6514 body["VdmOptions"]["DashboardOptions"]["EngagementMetrics"],
6515 "ENABLED"
6516 );
6517 }
6518
6519 #[tokio::test]
6520 async fn test_put_configuration_set_archiving_options() {
6521 let state = make_state();
6522 let svc = SesV2Service::new(state);
6523
6524 let req = make_request(
6525 Method::POST,
6526 "/v2/email/configuration-sets",
6527 r#"{"ConfigurationSetName": "test-config"}"#,
6528 );
6529 svc.handle(req).await.unwrap();
6530
6531 let req = make_request(
6532 Method::PUT,
6533 "/v2/email/configuration-sets/test-config/archiving-options",
6534 r#"{"ArchiveArn": "arn:aws:ses:us-east-1:123456789012:mailmanager-archive/my-archive"}"#,
6535 );
6536 let resp = svc.handle(req).await.unwrap();
6537 assert_eq!(resp.status, StatusCode::OK);
6538
6539 let req = make_request(Method::GET, "/v2/email/configuration-sets/test-config", "");
6540 let resp = svc.handle(req).await.unwrap();
6541 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6542 assert!(body["ArchivingOptions"]["ArchiveArn"]
6543 .as_str()
6544 .unwrap()
6545 .contains("my-archive"));
6546 }
6547
6548 #[tokio::test]
6551 async fn test_custom_verification_email_template_lifecycle() {
6552 let state = make_state();
6553 let svc = SesV2Service::new(state);
6554
6555 let req = make_request(
6557 Method::POST,
6558 "/v2/email/custom-verification-email-templates",
6559 r#"{
6560 "TemplateName": "my-verification",
6561 "FromEmailAddress": "noreply@example.com",
6562 "TemplateSubject": "Verify your email",
6563 "TemplateContent": "<h1>Please verify</h1>",
6564 "SuccessRedirectionURL": "https://example.com/success",
6565 "FailureRedirectionURL": "https://example.com/failure"
6566 }"#,
6567 );
6568 let resp = svc.handle(req).await.unwrap();
6569 assert_eq!(resp.status, StatusCode::OK);
6570
6571 let req = make_request(
6573 Method::GET,
6574 "/v2/email/custom-verification-email-templates/my-verification",
6575 "",
6576 );
6577 let resp = svc.handle(req).await.unwrap();
6578 assert_eq!(resp.status, StatusCode::OK);
6579 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6580 assert_eq!(body["TemplateName"], "my-verification");
6581 assert_eq!(body["FromEmailAddress"], "noreply@example.com");
6582 assert_eq!(body["TemplateSubject"], "Verify your email");
6583 assert_eq!(body["TemplateContent"], "<h1>Please verify</h1>");
6584 assert_eq!(body["SuccessRedirectionURL"], "https://example.com/success");
6585 assert_eq!(body["FailureRedirectionURL"], "https://example.com/failure");
6586
6587 let req = make_request(
6589 Method::GET,
6590 "/v2/email/custom-verification-email-templates",
6591 "",
6592 );
6593 let resp = svc.handle(req).await.unwrap();
6594 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6595 assert_eq!(
6596 body["CustomVerificationEmailTemplates"]
6597 .as_array()
6598 .unwrap()
6599 .len(),
6600 1
6601 );
6602
6603 let req = make_request(
6605 Method::PUT,
6606 "/v2/email/custom-verification-email-templates/my-verification",
6607 r#"{"TemplateSubject": "Updated subject"}"#,
6608 );
6609 let resp = svc.handle(req).await.unwrap();
6610 assert_eq!(resp.status, StatusCode::OK);
6611
6612 let req = make_request(
6614 Method::GET,
6615 "/v2/email/custom-verification-email-templates/my-verification",
6616 "",
6617 );
6618 let resp = svc.handle(req).await.unwrap();
6619 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6620 assert_eq!(body["TemplateSubject"], "Updated subject");
6621
6622 let req = make_request(
6624 Method::DELETE,
6625 "/v2/email/custom-verification-email-templates/my-verification",
6626 "",
6627 );
6628 let resp = svc.handle(req).await.unwrap();
6629 assert_eq!(resp.status, StatusCode::OK);
6630
6631 let req = make_request(
6633 Method::GET,
6634 "/v2/email/custom-verification-email-templates/my-verification",
6635 "",
6636 );
6637 let resp = svc.handle(req).await.unwrap();
6638 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6639 }
6640
6641 #[tokio::test]
6642 async fn test_duplicate_custom_verification_template() {
6643 let state = make_state();
6644 let svc = SesV2Service::new(state);
6645
6646 let body = r#"{
6647 "TemplateName": "dup-tmpl",
6648 "FromEmailAddress": "a@b.com",
6649 "TemplateSubject": "s",
6650 "TemplateContent": "c",
6651 "SuccessRedirectionURL": "https://ok",
6652 "FailureRedirectionURL": "https://fail"
6653 }"#;
6654
6655 let req = make_request(
6656 Method::POST,
6657 "/v2/email/custom-verification-email-templates",
6658 body,
6659 );
6660 svc.handle(req).await.unwrap();
6661
6662 let req = make_request(
6663 Method::POST,
6664 "/v2/email/custom-verification-email-templates",
6665 body,
6666 );
6667 let resp = svc.handle(req).await.unwrap();
6668 assert_eq!(resp.status, StatusCode::CONFLICT);
6669 }
6670
6671 #[tokio::test]
6672 async fn test_send_custom_verification_email() {
6673 let state = make_state();
6674 let svc = SesV2Service::new(state.clone());
6675
6676 let req = make_request(
6678 Method::POST,
6679 "/v2/email/custom-verification-email-templates",
6680 r#"{
6681 "TemplateName": "verify",
6682 "FromEmailAddress": "a@b.com",
6683 "TemplateSubject": "Verify",
6684 "TemplateContent": "content",
6685 "SuccessRedirectionURL": "https://ok",
6686 "FailureRedirectionURL": "https://fail"
6687 }"#,
6688 );
6689 svc.handle(req).await.unwrap();
6690
6691 let req = make_request(
6693 Method::POST,
6694 "/v2/email/outbound-custom-verification-emails",
6695 r#"{"EmailAddress": "user@example.com", "TemplateName": "verify"}"#,
6696 );
6697 let resp = svc.handle(req).await.unwrap();
6698 assert_eq!(resp.status, StatusCode::OK);
6699 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6700 assert!(body["MessageId"].as_str().is_some());
6701
6702 let s = state.read();
6704 assert_eq!(s.sent_emails.len(), 1);
6705 assert_eq!(s.sent_emails[0].to, vec!["user@example.com"]);
6706 }
6707
6708 #[tokio::test]
6709 async fn test_send_custom_verification_email_template_not_found() {
6710 let state = make_state();
6711 let svc = SesV2Service::new(state);
6712
6713 let req = make_request(
6714 Method::POST,
6715 "/v2/email/outbound-custom-verification-emails",
6716 r#"{"EmailAddress": "user@example.com", "TemplateName": "nonexistent"}"#,
6717 );
6718 let resp = svc.handle(req).await.unwrap();
6719 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6720 }
6721
6722 #[tokio::test]
6725 async fn test_render_email_template() {
6726 let state = make_state();
6727 let svc = SesV2Service::new(state);
6728
6729 let req = make_request(
6731 Method::POST,
6732 "/v2/email/templates",
6733 r#"{
6734 "TemplateName": "greet",
6735 "TemplateContent": {
6736 "Subject": "Hello {{name}}",
6737 "Html": "<h1>Welcome, {{name}}!</h1><p>Your code is {{code}}.</p>",
6738 "Text": "Welcome, {{name}}! Your code is {{code}}."
6739 }
6740 }"#,
6741 );
6742 svc.handle(req).await.unwrap();
6743
6744 let req = make_request(
6746 Method::POST,
6747 "/v2/email/templates/greet/render",
6748 r#"{"TemplateData": "{\"name\": \"Alice\", \"code\": \"1234\"}"}"#,
6749 );
6750 let resp = svc.handle(req).await.unwrap();
6751 assert_eq!(resp.status, StatusCode::OK);
6752 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6753 let rendered = body["RenderedTemplate"].as_str().unwrap();
6754 assert!(rendered.contains("Subject: Hello Alice"));
6755 assert!(rendered.contains("Welcome, Alice!"));
6756 assert!(rendered.contains("Your code is 1234."));
6757 }
6758
6759 #[tokio::test]
6760 async fn test_render_email_template_not_found() {
6761 let state = make_state();
6762 let svc = SesV2Service::new(state);
6763
6764 let req = make_request(
6765 Method::POST,
6766 "/v2/email/templates/nonexistent/render",
6767 r#"{"TemplateData": "{}"}"#,
6768 );
6769 let resp = svc.handle(req).await.unwrap();
6770 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6771 }
6772
6773 #[tokio::test]
6774 async fn test_render_email_template_missing_data() {
6775 let state = make_state();
6776 let svc = SesV2Service::new(state);
6777
6778 let req = make_request(
6780 Method::POST,
6781 "/v2/email/templates",
6782 r#"{"TemplateName": "t1", "TemplateContent": {"Subject": "Hi"}}"#,
6783 );
6784 svc.handle(req).await.unwrap();
6785
6786 let req = make_request(Method::POST, "/v2/email/templates/t1/render", r#"{}"#);
6787 let resp = svc.handle(req).await.unwrap();
6788 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
6789 }
6790
6791 #[tokio::test]
6794 async fn test_dedicated_ip_pool_lifecycle() {
6795 let state = make_state();
6796 let svc = SesV2Service::new(state);
6797
6798 let req = make_request(
6800 Method::POST,
6801 "/v2/email/dedicated-ip-pools",
6802 r#"{"PoolName": "my-pool", "ScalingMode": "STANDARD"}"#,
6803 );
6804 let resp = svc.handle(req).await.unwrap();
6805 assert_eq!(resp.status, StatusCode::OK);
6806
6807 let req = make_request(Method::GET, "/v2/email/dedicated-ip-pools", "");
6809 let resp = svc.handle(req).await.unwrap();
6810 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6811 assert_eq!(body["DedicatedIpPools"].as_array().unwrap().len(), 1);
6812
6813 let req = make_request(
6815 Method::POST,
6816 "/v2/email/dedicated-ip-pools",
6817 r#"{"PoolName": "my-pool"}"#,
6818 );
6819 let resp = svc.handle(req).await.unwrap();
6820 assert_eq!(resp.status, StatusCode::CONFLICT);
6821
6822 let req = make_request(Method::DELETE, "/v2/email/dedicated-ip-pools/my-pool", "");
6824 let resp = svc.handle(req).await.unwrap();
6825 assert_eq!(resp.status, StatusCode::OK);
6826
6827 let req = make_request(Method::DELETE, "/v2/email/dedicated-ip-pools/my-pool", "");
6829 let resp = svc.handle(req).await.unwrap();
6830 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6831 }
6832
6833 #[tokio::test]
6834 async fn test_managed_pool_generates_ips() {
6835 let state = make_state();
6836 let svc = SesV2Service::new(state);
6837
6838 let req = make_request(
6840 Method::POST,
6841 "/v2/email/dedicated-ip-pools",
6842 r#"{"PoolName": "managed-pool", "ScalingMode": "MANAGED"}"#,
6843 );
6844 let resp = svc.handle(req).await.unwrap();
6845 assert_eq!(resp.status, StatusCode::OK);
6846
6847 let req = make_request_with_query(
6849 Method::GET,
6850 "/v2/email/dedicated-ips",
6851 "",
6852 "PoolName=managed-pool",
6853 {
6854 let mut m = HashMap::new();
6855 m.insert("PoolName".to_string(), "managed-pool".to_string());
6856 m
6857 },
6858 );
6859 let resp = svc.handle(req).await.unwrap();
6860 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6861 let ips = body["DedicatedIps"].as_array().unwrap();
6862 assert_eq!(ips.len(), 3);
6863 assert_eq!(ips[0]["WarmupStatus"], "NOT_APPLICABLE");
6864 assert_eq!(ips[0]["WarmupPercentage"], -1);
6865 }
6866
6867 #[tokio::test]
6868 async fn test_dedicated_ip_operations() {
6869 let state = make_state();
6870 let svc = SesV2Service::new(state);
6871
6872 let req = make_request(
6874 Method::POST,
6875 "/v2/email/dedicated-ip-pools",
6876 r#"{"PoolName": "pool-a", "ScalingMode": "MANAGED"}"#,
6877 );
6878 svc.handle(req).await.unwrap();
6879
6880 let req = make_request(
6881 Method::POST,
6882 "/v2/email/dedicated-ip-pools",
6883 r#"{"PoolName": "pool-b", "ScalingMode": "STANDARD"}"#,
6884 );
6885 svc.handle(req).await.unwrap();
6886
6887 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
6889 let resp = svc.handle(req).await.unwrap();
6890 assert_eq!(resp.status, StatusCode::OK);
6891 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6892 assert_eq!(body["DedicatedIp"]["PoolName"], "pool-a");
6893
6894 let req = make_request(
6896 Method::PUT,
6897 "/v2/email/dedicated-ips/198.51.100.1/pool",
6898 r#"{"DestinationPoolName": "pool-b"}"#,
6899 );
6900 let resp = svc.handle(req).await.unwrap();
6901 assert_eq!(resp.status, StatusCode::OK);
6902
6903 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
6905 let resp = svc.handle(req).await.unwrap();
6906 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6907 assert_eq!(body["DedicatedIp"]["PoolName"], "pool-b");
6908
6909 let req = make_request(
6911 Method::PUT,
6912 "/v2/email/dedicated-ips/198.51.100.1/warmup",
6913 r#"{"WarmupPercentage": 50}"#,
6914 );
6915 let resp = svc.handle(req).await.unwrap();
6916 assert_eq!(resp.status, StatusCode::OK);
6917
6918 let req = make_request(Method::GET, "/v2/email/dedicated-ips/198.51.100.1", "");
6919 let resp = svc.handle(req).await.unwrap();
6920 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6921 assert_eq!(body["DedicatedIp"]["WarmupPercentage"], 50);
6922 assert_eq!(body["DedicatedIp"]["WarmupStatus"], "IN_PROGRESS");
6923
6924 let req = make_request(Method::GET, "/v2/email/dedicated-ips/1.2.3.4", "");
6926 let resp = svc.handle(req).await.unwrap();
6927 assert_eq!(resp.status, StatusCode::NOT_FOUND);
6928 }
6929
6930 #[tokio::test]
6931 async fn test_pool_scaling_attributes() {
6932 let state = make_state();
6933 let svc = SesV2Service::new(state);
6934
6935 let req = make_request(
6936 Method::POST,
6937 "/v2/email/dedicated-ip-pools",
6938 r#"{"PoolName": "scalable", "ScalingMode": "STANDARD"}"#,
6939 );
6940 svc.handle(req).await.unwrap();
6941
6942 let req = make_request(
6944 Method::PUT,
6945 "/v2/email/dedicated-ip-pools/scalable/scaling",
6946 r#"{"ScalingMode": "MANAGED"}"#,
6947 );
6948 let resp = svc.handle(req).await.unwrap();
6949 assert_eq!(resp.status, StatusCode::OK);
6950
6951 let req = make_request(
6953 Method::PUT,
6954 "/v2/email/dedicated-ip-pools/scalable/scaling",
6955 r#"{"ScalingMode": "STANDARD"}"#,
6956 );
6957 let resp = svc.handle(req).await.unwrap();
6958 assert_eq!(resp.status, StatusCode::BAD_REQUEST);
6959 }
6960
6961 #[tokio::test]
6962 async fn test_account_dedicated_ip_warmup() {
6963 let state = make_state();
6964 let svc = SesV2Service::new(state);
6965
6966 let req = make_request(
6967 Method::PUT,
6968 "/v2/email/account/dedicated-ips/warmup",
6969 r#"{"AutoWarmupEnabled": true}"#,
6970 );
6971 let resp = svc.handle(req).await.unwrap();
6972 assert_eq!(resp.status, StatusCode::OK);
6973
6974 let req = make_request(Method::GET, "/v2/email/account", "");
6975 let resp = svc.handle(req).await.unwrap();
6976 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6977 assert_eq!(body["DedicatedIpAutoWarmupEnabled"], true);
6978 }
6979
6980 #[tokio::test]
6983 async fn test_multi_region_endpoint_lifecycle() {
6984 let state = make_state();
6985 let svc = SesV2Service::new(state);
6986
6987 let req = make_request(
6989 Method::POST,
6990 "/v2/email/multi-region-endpoints",
6991 r#"{"EndpointName": "global-ep", "Details": {"RoutesDetails": [{"Region": "us-west-2"}]}}"#,
6992 );
6993 let resp = svc.handle(req).await.unwrap();
6994 assert_eq!(resp.status, StatusCode::OK);
6995 let body: Value = serde_json::from_slice(&resp.body).unwrap();
6996 assert_eq!(body["Status"], "READY");
6997 assert!(body["EndpointId"].as_str().is_some());
6998
6999 let req = make_request(
7001 Method::GET,
7002 "/v2/email/multi-region-endpoints/global-ep",
7003 "",
7004 );
7005 let resp = svc.handle(req).await.unwrap();
7006 assert_eq!(resp.status, StatusCode::OK);
7007 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7008 assert_eq!(body["EndpointName"], "global-ep");
7009 assert_eq!(body["Status"], "READY");
7010 let routes = body["Routes"].as_array().unwrap();
7011 assert!(!routes.is_empty());
7012
7013 let req = make_request(Method::GET, "/v2/email/multi-region-endpoints", "");
7015 let resp = svc.handle(req).await.unwrap();
7016 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7017 assert_eq!(body["MultiRegionEndpoints"].as_array().unwrap().len(), 1);
7018
7019 let req = make_request(
7021 Method::POST,
7022 "/v2/email/multi-region-endpoints",
7023 r#"{"EndpointName": "global-ep", "Details": {"RoutesDetails": [{"Region": "eu-west-1"}]}}"#,
7024 );
7025 let resp = svc.handle(req).await.unwrap();
7026 assert_eq!(resp.status, StatusCode::CONFLICT);
7027
7028 let req = make_request(
7030 Method::DELETE,
7031 "/v2/email/multi-region-endpoints/global-ep",
7032 "",
7033 );
7034 let resp = svc.handle(req).await.unwrap();
7035 assert_eq!(resp.status, StatusCode::OK);
7036 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7037 assert_eq!(body["Status"], "DELETING");
7038
7039 let req = make_request(
7041 Method::GET,
7042 "/v2/email/multi-region-endpoints/global-ep",
7043 "",
7044 );
7045 let resp = svc.handle(req).await.unwrap();
7046 assert_eq!(resp.status, StatusCode::NOT_FOUND);
7047 }
7048
7049 #[tokio::test]
7052 async fn test_account_details() {
7053 let state = make_state();
7054 let svc = SesV2Service::new(state);
7055
7056 let req = make_request(
7057 Method::POST,
7058 "/v2/email/account/details",
7059 r#"{"MailType": "TRANSACTIONAL", "WebsiteURL": "https://example.com", "UseCaseDescription": "Testing"}"#,
7060 );
7061 let resp = svc.handle(req).await.unwrap();
7062 assert_eq!(resp.status, StatusCode::OK);
7063
7064 let req = make_request(Method::GET, "/v2/email/account", "");
7065 let resp = svc.handle(req).await.unwrap();
7066 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7067 assert_eq!(body["Details"]["MailType"], "TRANSACTIONAL");
7068 assert_eq!(body["Details"]["WebsiteURL"], "https://example.com");
7069 assert_eq!(body["Details"]["UseCaseDescription"], "Testing");
7070 }
7071
7072 #[tokio::test]
7073 async fn test_account_sending_attributes() {
7074 let state = make_state();
7075 let svc = SesV2Service::new(state);
7076
7077 let req = make_request(
7079 Method::PUT,
7080 "/v2/email/account/sending",
7081 r#"{"SendingEnabled": false}"#,
7082 );
7083 let resp = svc.handle(req).await.unwrap();
7084 assert_eq!(resp.status, StatusCode::OK);
7085
7086 let req = make_request(Method::GET, "/v2/email/account", "");
7087 let resp = svc.handle(req).await.unwrap();
7088 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7089 assert_eq!(body["SendingEnabled"], false);
7090
7091 let req = make_request(
7093 Method::PUT,
7094 "/v2/email/account/sending",
7095 r#"{"SendingEnabled": true}"#,
7096 );
7097 svc.handle(req).await.unwrap();
7098
7099 let req = make_request(Method::GET, "/v2/email/account", "");
7100 let resp = svc.handle(req).await.unwrap();
7101 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7102 assert_eq!(body["SendingEnabled"], true);
7103 }
7104
7105 #[tokio::test]
7106 async fn test_account_suppression_attributes() {
7107 let state = make_state();
7108 let svc = SesV2Service::new(state);
7109
7110 let req = make_request(
7111 Method::PUT,
7112 "/v2/email/account/suppression",
7113 r#"{"SuppressedReasons": ["BOUNCE", "COMPLAINT"]}"#,
7114 );
7115 let resp = svc.handle(req).await.unwrap();
7116 assert_eq!(resp.status, StatusCode::OK);
7117
7118 let req = make_request(Method::GET, "/v2/email/account", "");
7119 let resp = svc.handle(req).await.unwrap();
7120 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7121 let reasons = body["SuppressionAttributes"]["SuppressedReasons"]
7122 .as_array()
7123 .unwrap();
7124 assert_eq!(reasons.len(), 2);
7125 }
7126
7127 #[tokio::test]
7128 async fn test_account_vdm_attributes() {
7129 let state = make_state();
7130 let svc = SesV2Service::new(state);
7131
7132 let req = make_request(
7133 Method::PUT,
7134 "/v2/email/account/vdm",
7135 r#"{"VdmAttributes": {"VdmEnabled": "ENABLED", "DashboardAttributes": {"EngagementMetrics": "ENABLED"}}}"#,
7136 );
7137 let resp = svc.handle(req).await.unwrap();
7138 assert_eq!(resp.status, StatusCode::OK);
7139
7140 let req = make_request(Method::GET, "/v2/email/account", "");
7141 let resp = svc.handle(req).await.unwrap();
7142 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7143 assert_eq!(body["VdmAttributes"]["VdmEnabled"], "ENABLED");
7144 }
7145
7146 #[tokio::test]
7147 async fn test_import_job_lifecycle() {
7148 let state = make_state();
7149 let svc = SesV2Service::new(state);
7150
7151 let req = make_request(
7153 Method::POST,
7154 "/v2/email/import-jobs",
7155 r#"{
7156 "ImportDestination": {
7157 "SuppressionListDestination": {"SuppressionListImportAction": "PUT"}
7158 },
7159 "ImportDataSource": {
7160 "S3Url": "s3://bucket/file.csv",
7161 "DataFormat": "CSV"
7162 }
7163 }"#,
7164 );
7165 let resp = svc.handle(req).await.unwrap();
7166 assert_eq!(resp.status, StatusCode::OK);
7167 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7168 let job_id = body["JobId"].as_str().unwrap().to_string();
7169
7170 let req = make_request(
7172 Method::GET,
7173 &format!("/v2/email/import-jobs/{}", job_id),
7174 "",
7175 );
7176 let resp = svc.handle(req).await.unwrap();
7177 assert_eq!(resp.status, StatusCode::OK);
7178 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7179 assert_eq!(body["JobId"], job_id);
7180 assert_eq!(body["JobStatus"], "COMPLETED");
7181
7182 let req = make_request(Method::POST, "/v2/email/import-jobs/list", "{}");
7184 let resp = svc.handle(req).await.unwrap();
7185 assert_eq!(resp.status, StatusCode::OK);
7186 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7187 assert_eq!(body["ImportJobs"].as_array().unwrap().len(), 1);
7188
7189 let req = make_request(Method::GET, "/v2/email/import-jobs/nonexistent", "");
7191 let resp = svc.handle(req).await.unwrap();
7192 assert_eq!(resp.status, StatusCode::NOT_FOUND);
7193 }
7194
7195 #[tokio::test]
7196 async fn test_export_job_lifecycle() {
7197 let state = make_state();
7198 let svc = SesV2Service::new(state);
7199
7200 let req = make_request(
7202 Method::POST,
7203 "/v2/email/export-jobs",
7204 r#"{
7205 "ExportDataSource": {
7206 "MetricsDataSource": {
7207 "Dimensions": {},
7208 "Namespace": "VDM",
7209 "Metrics": []
7210 }
7211 },
7212 "ExportDestination": {
7213 "DataFormat": "CSV",
7214 "S3Url": "s3://bucket/export"
7215 }
7216 }"#,
7217 );
7218 let resp = svc.handle(req).await.unwrap();
7219 assert_eq!(resp.status, StatusCode::OK);
7220 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7221 let job_id = body["JobId"].as_str().unwrap().to_string();
7222
7223 let req = make_request(
7225 Method::GET,
7226 &format!("/v2/email/export-jobs/{}", job_id),
7227 "",
7228 );
7229 let resp = svc.handle(req).await.unwrap();
7230 assert_eq!(resp.status, StatusCode::OK);
7231 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7232 assert_eq!(body["JobId"], job_id);
7233 assert_eq!(body["JobStatus"], "COMPLETED");
7234 assert_eq!(body["ExportSourceType"], "METRICS_DATA");
7235
7236 let req = make_request(Method::POST, "/v2/email/list-export-jobs", "{}");
7238 let resp = svc.handle(req).await.unwrap();
7239 assert_eq!(resp.status, StatusCode::OK);
7240 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7241 assert_eq!(body["ExportJobs"].as_array().unwrap().len(), 1);
7242
7243 let req = make_request(
7245 Method::PUT,
7246 &format!("/v2/email/export-jobs/{}/cancel", job_id),
7247 "",
7248 );
7249 let resp = svc.handle(req).await.unwrap();
7250 assert_eq!(resp.status, StatusCode::CONFLICT);
7251 }
7252
7253 #[tokio::test]
7254 async fn test_tenant_lifecycle() {
7255 let state = make_state();
7256 let svc = SesV2Service::new(state);
7257
7258 let req = make_request(
7260 Method::POST,
7261 "/v2/email/tenants",
7262 r#"{"TenantName": "my-tenant"}"#,
7263 );
7264 let resp = svc.handle(req).await.unwrap();
7265 assert_eq!(resp.status, StatusCode::OK);
7266 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7267 assert_eq!(body["TenantName"], "my-tenant");
7268 assert!(body["TenantId"].as_str().is_some());
7269 assert_eq!(body["SendingStatus"], "ENABLED");
7270
7271 let req = make_request(
7273 Method::POST,
7274 "/v2/email/tenants/get",
7275 r#"{"TenantName": "my-tenant"}"#,
7276 );
7277 let resp = svc.handle(req).await.unwrap();
7278 assert_eq!(resp.status, StatusCode::OK);
7279 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7280 assert_eq!(body["Tenant"]["TenantName"], "my-tenant");
7281
7282 let req = make_request(Method::POST, "/v2/email/tenants/list", "{}");
7284 let resp = svc.handle(req).await.unwrap();
7285 assert_eq!(resp.status, StatusCode::OK);
7286 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7287 assert_eq!(body["Tenants"].as_array().unwrap().len(), 1);
7288
7289 let req = make_request(
7291 Method::POST,
7292 "/v2/email/tenants/resources",
7293 r#"{"TenantName": "my-tenant", "ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
7294 );
7295 let resp = svc.handle(req).await.unwrap();
7296 assert_eq!(resp.status, StatusCode::OK);
7297
7298 let req = make_request(
7300 Method::POST,
7301 "/v2/email/tenants/resources/list",
7302 r#"{"TenantName": "my-tenant"}"#,
7303 );
7304 let resp = svc.handle(req).await.unwrap();
7305 assert_eq!(resp.status, StatusCode::OK);
7306 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7307 assert_eq!(body["TenantResources"].as_array().unwrap().len(), 1);
7308
7309 let req = make_request(
7311 Method::POST,
7312 "/v2/email/resources/tenants/list",
7313 r#"{"ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
7314 );
7315 let resp = svc.handle(req).await.unwrap();
7316 assert_eq!(resp.status, StatusCode::OK);
7317 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7318 assert_eq!(body["ResourceTenants"].as_array().unwrap().len(), 1);
7319
7320 let req = make_request(
7322 Method::POST,
7323 "/v2/email/tenants/resources/delete",
7324 r#"{"TenantName": "my-tenant", "ResourceArn": "arn:aws:ses:us-east-1:123456789012:identity/test@example.com"}"#,
7325 );
7326 let resp = svc.handle(req).await.unwrap();
7327 assert_eq!(resp.status, StatusCode::OK);
7328
7329 let req = make_request(
7331 Method::POST,
7332 "/v2/email/tenants/resources/list",
7333 r#"{"TenantName": "my-tenant"}"#,
7334 );
7335 let resp = svc.handle(req).await.unwrap();
7336 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7337 assert!(body["TenantResources"].as_array().unwrap().is_empty());
7338
7339 let req = make_request(
7341 Method::POST,
7342 "/v2/email/tenants/delete",
7343 r#"{"TenantName": "my-tenant"}"#,
7344 );
7345 let resp = svc.handle(req).await.unwrap();
7346 assert_eq!(resp.status, StatusCode::OK);
7347
7348 let req = make_request(
7350 Method::POST,
7351 "/v2/email/tenants/get",
7352 r#"{"TenantName": "my-tenant"}"#,
7353 );
7354 let resp = svc.handle(req).await.unwrap();
7355 assert_eq!(resp.status, StatusCode::NOT_FOUND);
7356 }
7357
7358 #[tokio::test]
7359 async fn test_reputation_entity() {
7360 let state = make_state();
7361 let svc = SesV2Service::new(state);
7362
7363 let req = make_request(
7365 Method::GET,
7366 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
7367 "",
7368 );
7369 let resp = svc.handle(req).await.unwrap();
7370 assert_eq!(resp.status, StatusCode::OK);
7371 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7372 assert_eq!(
7373 body["ReputationEntity"]["SendingStatusAggregate"],
7374 "ENABLED"
7375 );
7376
7377 let req = make_request(
7379 Method::PUT,
7380 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com/customer-managed-status",
7381 r#"{"SendingStatus": "DISABLED"}"#,
7382 );
7383 let resp = svc.handle(req).await.unwrap();
7384 assert_eq!(resp.status, StatusCode::OK);
7385
7386 let req = make_request(
7388 Method::PUT,
7389 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com/policy",
7390 r#"{"ReputationEntityPolicy": "arn:aws:ses:us-east-1:123456789012:policy/my-policy"}"#,
7391 );
7392 let resp = svc.handle(req).await.unwrap();
7393 assert_eq!(resp.status, StatusCode::OK);
7394
7395 let req = make_request(
7397 Method::GET,
7398 "/v2/email/reputation/entities/RESOURCE/arn%3Aaws%3Ases%3Aus-east-1%3A123456789012%3Aidentity%2Ftest%40example.com",
7399 "",
7400 );
7401 let resp = svc.handle(req).await.unwrap();
7402 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7403 assert_eq!(
7404 body["ReputationEntity"]["CustomerManagedStatus"]["SendingStatus"],
7405 "DISABLED"
7406 );
7407
7408 let req = make_request(Method::POST, "/v2/email/reputation/entities", "{}");
7410 let resp = svc.handle(req).await.unwrap();
7411 assert_eq!(resp.status, StatusCode::OK);
7412 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7413 assert_eq!(body["ReputationEntities"].as_array().unwrap().len(), 1);
7414 }
7415
7416 #[tokio::test]
7417 async fn test_batch_get_metric_data() {
7418 let state = make_state();
7419 let svc = SesV2Service::new(state);
7420
7421 let req = make_request(
7422 Method::POST,
7423 "/v2/email/metrics/batch",
7424 r#"{
7425 "Queries": [
7426 {
7427 "Id": "q1",
7428 "Namespace": "VDM",
7429 "Metric": "SEND",
7430 "StartDate": "2024-01-01T00:00:00Z",
7431 "EndDate": "2024-01-02T00:00:00Z"
7432 }
7433 ]
7434 }"#,
7435 );
7436 let resp = svc.handle(req).await.unwrap();
7437 assert_eq!(resp.status, StatusCode::OK);
7438 let body: Value = serde_json::from_slice(&resp.body).unwrap();
7439 assert_eq!(body["Results"].as_array().unwrap().len(), 1);
7440 assert_eq!(body["Results"][0]["Id"], "q1");
7441 assert!(body["Errors"].as_array().unwrap().is_empty());
7442 }
7443
7444 #[tokio::test]
7445 async fn test_duplicate_tenant() {
7446 let state = make_state();
7447 let svc = SesV2Service::new(state);
7448
7449 let req = make_request(
7450 Method::POST,
7451 "/v2/email/tenants",
7452 r#"{"TenantName": "dup"}"#,
7453 );
7454 svc.handle(req).await.unwrap();
7455
7456 let req = make_request(
7457 Method::POST,
7458 "/v2/email/tenants",
7459 r#"{"TenantName": "dup"}"#,
7460 );
7461 let resp = svc.handle(req).await.unwrap();
7462 assert_eq!(resp.status, StatusCode::CONFLICT);
7463 }
7464}