Skip to main content

fakecloud_ses/
service.rs

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    /// Attach a delivery context for cross-service event fanout.
31    pub fn with_delivery(mut self, ctx: SesDeliveryContext) -> Self {
32        self.delivery_ctx = Some(ctx);
33        self
34    }
35
36    /// Determine the action from the HTTP method and path segments.
37    /// SES v2 uses REST-style routing with base path /v2/email/:
38    ///   GET    /v2/email/account                         -> GetAccount
39    ///   POST   /v2/email/identities                      -> CreateEmailIdentity
40    ///   GET    /v2/email/identities                      -> ListEmailIdentities
41    ///   GET    /v2/email/identities/{id}                 -> GetEmailIdentity
42    ///   DELETE /v2/email/identities/{id}                 -> DeleteEmailIdentity
43    ///   POST   /v2/email/configuration-sets              -> CreateConfigurationSet
44    ///   GET    /v2/email/configuration-sets              -> ListConfigurationSets
45    ///   GET    /v2/email/configuration-sets/{name}       -> GetConfigurationSet
46    ///   DELETE /v2/email/configuration-sets/{name}       -> DeleteConfigurationSet
47    ///   POST   /v2/email/templates                       -> CreateEmailTemplate
48    ///   GET    /v2/email/templates                       -> ListEmailTemplates
49    ///   GET    /v2/email/templates/{name}                -> GetEmailTemplate
50    ///   PUT    /v2/email/templates/{name}                -> UpdateEmailTemplate
51    ///   DELETE /v2/email/templates/{name}                -> DeleteEmailTemplate
52    ///   POST   /v2/email/outbound-emails                 -> SendEmail
53    ///   POST   /v2/email/outbound-bulk-emails            -> SendBulkEmail
54    ///   POST   /v2/email/tags                            -> TagResource
55    ///   DELETE /v2/email/tags                            -> UntagResource
56    ///   GET    /v2/email/tags                            -> ListTagsForResource
57    ///   POST   /v2/email/contact-lists                   -> CreateContactList
58    ///   GET    /v2/email/contact-lists                   -> ListContactLists
59    ///   GET    /v2/email/contact-lists/{name}            -> GetContactList
60    ///   PUT    /v2/email/contact-lists/{name}            -> UpdateContactList
61    ///   DELETE /v2/email/contact-lists/{name}            -> DeleteContactList
62    ///   POST   /v2/email/contact-lists/{name}/contacts   -> CreateContact
63    ///   GET    /v2/email/contact-lists/{name}/contacts   -> ListContacts
64    ///   GET    /v2/email/contact-lists/{name}/contacts/{email} -> GetContact
65    ///   PUT    /v2/email/contact-lists/{name}/contacts/{email} -> UpdateContact
66    ///   DELETE /v2/email/contact-lists/{name}/contacts/{email} -> DeleteContact
67    ///   PUT    /v2/email/suppression/addresses            -> PutSuppressedDestination
68    ///   GET    /v2/email/suppression/addresses            -> ListSuppressedDestinations
69    ///   GET    /v2/email/suppression/addresses/{email}    -> GetSuppressedDestination
70    ///   DELETE /v2/email/suppression/addresses/{email}    -> DeleteSuppressedDestination
71    ///   POST   /v2/email/configuration-sets/{name}/event-destinations -> CreateConfigurationSetEventDestination
72    ///   GET    /v2/email/configuration-sets/{name}/event-destinations -> GetConfigurationSetEventDestinations
73    ///   PUT    /v2/email/configuration-sets/{name}/event-destinations/{dest} -> UpdateConfigurationSetEventDestination
74    ///   DELETE /v2/email/configuration-sets/{name}/event-destinations/{dest} -> DeleteConfigurationSetEventDestination
75    ///   POST   /v2/email/identities/{id}/policies/{policy} -> CreateEmailIdentityPolicy
76    ///   GET    /v2/email/identities/{id}/policies         -> GetEmailIdentityPolicies
77    ///   PUT    /v2/email/identities/{id}/policies/{policy} -> UpdateEmailIdentityPolicy
78    ///   DELETE /v2/email/identities/{id}/policies/{policy} -> DeleteEmailIdentityPolicy
79    ///   PUT    /v2/email/identities/{id}/dkim              -> PutEmailIdentityDkimAttributes
80    ///   PUT    /v2/email/identities/{id}/dkim/signing      -> PutEmailIdentityDkimSigningAttributes
81    ///   PUT    /v2/email/identities/{id}/feedback          -> PutEmailIdentityFeedbackAttributes
82    ///   PUT    /v2/email/identities/{id}/mail-from         -> PutEmailIdentityMailFromAttributes
83    ///   PUT    /v2/email/identities/{id}/configuration-set -> PutEmailIdentityConfigurationSetAttributes
84    ///   PUT    /v2/email/configuration-sets/{name}/sending             -> PutConfigurationSetSendingOptions
85    ///   PUT    /v2/email/configuration-sets/{name}/delivery-options    -> PutConfigurationSetDeliveryOptions
86    ///   PUT    /v2/email/configuration-sets/{name}/tracking-options    -> PutConfigurationSetTrackingOptions
87    ///   PUT    /v2/email/configuration-sets/{name}/suppression-options -> PutConfigurationSetSuppressionOptions
88    ///   PUT    /v2/email/configuration-sets/{name}/reputation-options  -> PutConfigurationSetReputationOptions
89    ///   PUT    /v2/email/configuration-sets/{name}/vdm-options         -> PutConfigurationSetVdmOptions
90    ///   PUT    /v2/email/configuration-sets/{name}/archiving-options   -> PutConfigurationSetArchivingOptions
91    ///   POST   /v2/email/custom-verification-email-templates           -> CreateCustomVerificationEmailTemplate
92    ///   GET    /v2/email/custom-verification-email-templates            -> ListCustomVerificationEmailTemplates
93    ///   GET    /v2/email/custom-verification-email-templates/{name}     -> GetCustomVerificationEmailTemplate
94    ///   PUT    /v2/email/custom-verification-email-templates/{name}     -> UpdateCustomVerificationEmailTemplate
95    ///   DELETE /v2/email/custom-verification-email-templates/{name}     -> DeleteCustomVerificationEmailTemplate
96    ///   POST   /v2/email/outbound-custom-verification-emails            -> SendCustomVerificationEmail
97    ///   POST   /v2/email/templates/{name}/render                        -> TestRenderEmailTemplate
98    ///   POST   /v2/email/import-jobs                                     -> CreateImportJob
99    ///   POST   /v2/email/import-jobs/list                                -> ListImportJobs
100    ///   GET    /v2/email/import-jobs/{id}                                -> GetImportJob
101    ///   POST   /v2/email/export-jobs                                     -> CreateExportJob
102    ///   POST   /v2/email/list-export-jobs                                -> ListExportJobs
103    ///   PUT    /v2/email/export-jobs/{id}/cancel                         -> CancelExportJob
104    ///   GET    /v2/email/export-jobs/{id}                                -> GetExportJob
105    ///   POST   /v2/email/tenants                                         -> CreateTenant
106    ///   POST   /v2/email/tenants/list                                    -> ListTenants
107    ///   POST   /v2/email/tenants/get                                     -> GetTenant
108    ///   POST   /v2/email/tenants/delete                                  -> DeleteTenant
109    ///   POST   /v2/email/tenants/resources                               -> CreateTenantResourceAssociation
110    ///   POST   /v2/email/tenants/resources/delete                        -> DeleteTenantResourceAssociation
111    ///   POST   /v2/email/tenants/resources/list                          -> ListTenantResources
112    ///   POST   /v2/email/resources/tenants/list                          -> ListResourceTenants
113    ///   POST   /v2/email/reputation/entities                             -> ListReputationEntities
114    ///   PUT    /v2/email/reputation/entities/{type}/{ref}/customer-managed-status -> UpdateReputationEntityCustomerManagedStatus
115    ///   PUT    /v2/email/reputation/entities/{type}/{ref}/policy          -> UpdateReputationEntityPolicy
116    ///   GET    /v2/email/reputation/entities/{type}/{ref}                 -> GetReputationEntity
117    ///   POST   /v2/email/metrics/batch                                   -> BatchGetMetricData
118    fn resolve_action(req: &AwsRequest) -> Option<(&str, Option<String>, Option<String>)> {
119        let segs = &req.path_segments;
120
121        // Expect first two segments to be "v2" and "email"
122        if segs.len() < 3 || segs[0] != "v2" || segs[1] != "email" {
123            return None;
124        }
125
126        // URL-decode the resource name (e.g. test%40example.com -> test@example.com)
127        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            // /v2/email/account
136            (Method::GET, 3) if segs[2] == "account" => Some(("GetAccount", None, None)),
137
138            // /v2/email/identities
139            (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            // /v2/email/identities/{id}
146            (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            // /v2/email/configuration-sets
154            (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            // /v2/email/configuration-sets/{name}
161            (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            // /v2/email/templates
169            (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            // /v2/email/templates/{name}
174            (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            // /v2/email/outbound-emails
185            (Method::POST, 3) if segs[2] == "outbound-emails" => Some(("SendEmail", None, None)),
186
187            // /v2/email/outbound-bulk-emails
188            (Method::POST, 3) if segs[2] == "outbound-bulk-emails" => {
189                Some(("SendBulkEmail", None, None))
190            }
191
192            // /v2/email/contact-lists
193            (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            // /v2/email/contact-lists/{name}
200            (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            // /v2/email/tags
210            (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            // /v2/email/contact-lists/{name}/contacts
215            (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            // /v2/email/contact-lists/{name}/contacts/list (SDK sends POST for ListContacts)
222            (Method::POST, 6)
223                if segs[2] == "contact-lists" && segs[4] == "contacts" && segs[5] == "list" =>
224            {
225                Some(("ListContacts", resource, None))
226            }
227            // /v2/email/contact-lists/{name}/contacts/{email}
228            (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            // /v2/email/suppression/addresses
239            (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            // /v2/email/suppression/addresses/{email}
246            (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            // /v2/email/configuration-sets/{name}/event-destinations
254            (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            // /v2/email/configuration-sets/{name}/event-destinations/{dest-name}
265            (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            // /v2/email/identities/{id}/policies
285            (Method::GET, 5) if segs[2] == "identities" && segs[4] == "policies" => {
286                Some(("GetEmailIdentityPolicies", resource, None))
287            }
288            // /v2/email/identities/{id}/policies/{policy-name}
289            (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            // /v2/email/identities/{id}/dkim/signing (6 segments, must come before dkim at 5)
306            (Method::PUT, 6)
307                if segs[2] == "identities" && segs[4] == "dkim" && segs[5] == "signing" =>
308            {
309                Some(("PutEmailIdentityDkimSigningAttributes", resource, None))
310            }
311
312            // /v2/email/identities/{id}/dkim
313            (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "dkim" => {
314                Some(("PutEmailIdentityDkimAttributes", resource, None))
315            }
316            // /v2/email/identities/{id}/feedback
317            (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "feedback" => {
318                Some(("PutEmailIdentityFeedbackAttributes", resource, None))
319            }
320            // /v2/email/identities/{id}/mail-from
321            (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "mail-from" => {
322                Some(("PutEmailIdentityMailFromAttributes", resource, None))
323            }
324            // /v2/email/identities/{id}/configuration-set
325            (Method::PUT, 5) if segs[2] == "identities" && segs[4] == "configuration-set" => {
326                Some(("PutEmailIdentityConfigurationSetAttributes", resource, None))
327            }
328
329            // /v2/email/configuration-sets/{name}/sending
330            (Method::PUT, 5) if segs[2] == "configuration-sets" && segs[4] == "sending" => {
331                Some(("PutConfigurationSetSendingOptions", resource, None))
332            }
333            // /v2/email/configuration-sets/{name}/delivery-options
334            (Method::PUT, 5)
335                if segs[2] == "configuration-sets" && segs[4] == "delivery-options" =>
336            {
337                Some(("PutConfigurationSetDeliveryOptions", resource, None))
338            }
339            // /v2/email/configuration-sets/{name}/tracking-options
340            (Method::PUT, 5)
341                if segs[2] == "configuration-sets" && segs[4] == "tracking-options" =>
342            {
343                Some(("PutConfigurationSetTrackingOptions", resource, None))
344            }
345            // /v2/email/configuration-sets/{name}/suppression-options
346            (Method::PUT, 5)
347                if segs[2] == "configuration-sets" && segs[4] == "suppression-options" =>
348            {
349                Some(("PutConfigurationSetSuppressionOptions", resource, None))
350            }
351            // /v2/email/configuration-sets/{name}/reputation-options
352            (Method::PUT, 5)
353                if segs[2] == "configuration-sets" && segs[4] == "reputation-options" =>
354            {
355                Some(("PutConfigurationSetReputationOptions", resource, None))
356            }
357            // /v2/email/configuration-sets/{name}/vdm-options
358            (Method::PUT, 5) if segs[2] == "configuration-sets" && segs[4] == "vdm-options" => {
359                Some(("PutConfigurationSetVdmOptions", resource, None))
360            }
361            // /v2/email/configuration-sets/{name}/archiving-options
362            (Method::PUT, 5)
363                if segs[2] == "configuration-sets" && segs[4] == "archiving-options" =>
364            {
365                Some(("PutConfigurationSetArchivingOptions", resource, None))
366            }
367
368            // /v2/email/custom-verification-email-templates
369            (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            // /v2/email/custom-verification-email-templates/{name}
376            (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            // /v2/email/outbound-custom-verification-emails
387            (Method::POST, 3) if segs[2] == "outbound-custom-verification-emails" => {
388                Some(("SendCustomVerificationEmail", None, None))
389            }
390
391            // /v2/email/templates/{name}/render
392            (Method::POST, 5) if segs[2] == "templates" && segs[4] == "render" => {
393                Some(("TestRenderEmailTemplate", resource, None))
394            }
395
396            // /v2/email/dedicated-ip-pools
397            (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            // /v2/email/dedicated-ip-pools/{name}
404            (Method::DELETE, 4) if segs[2] == "dedicated-ip-pools" => {
405                Some(("DeleteDedicatedIpPool", resource, None))
406            }
407            // Note: GetDedicatedIpPool is not in scope but the SDK may hit it via
408            // the dedicated-ip-pools/{name} GET path — we route to a pool-level getter.
409
410            // /v2/email/dedicated-ip-pools/{name}/scaling
411            (Method::PUT, 5) if segs[2] == "dedicated-ip-pools" && segs[4] == "scaling" => {
412                Some(("PutDedicatedIpPoolScalingAttributes", resource, None))
413            }
414
415            // /v2/email/dedicated-ips
416            (Method::GET, 3) if segs[2] == "dedicated-ips" => Some(("GetDedicatedIps", None, None)),
417            // /v2/email/dedicated-ips/{ip}/pool (5 segments, must come before 4-segment match)
418            (Method::PUT, 5) if segs[2] == "dedicated-ips" && segs[4] == "pool" => {
419                Some(("PutDedicatedIpInPool", resource, None))
420            }
421            // /v2/email/dedicated-ips/{ip}/warmup
422            (Method::PUT, 5) if segs[2] == "dedicated-ips" && segs[4] == "warmup" => {
423                Some(("PutDedicatedIpWarmupAttributes", resource, None))
424            }
425            // /v2/email/dedicated-ips/{ip}
426            (Method::GET, 4) if segs[2] == "dedicated-ips" => {
427                Some(("GetDedicatedIp", resource, None))
428            }
429
430            // /v2/email/account/dedicated-ips/warmup
431            (Method::PUT, 5)
432                if segs[2] == "account" && segs[3] == "dedicated-ips" && segs[4] == "warmup" =>
433            {
434                Some(("PutAccountDedicatedIpWarmupAttributes", None, None))
435            }
436
437            // /v2/email/account/details
438            (Method::POST, 4) if segs[2] == "account" && segs[3] == "details" => {
439                Some(("PutAccountDetails", None, None))
440            }
441            // /v2/email/account/sending
442            (Method::PUT, 4) if segs[2] == "account" && segs[3] == "sending" => {
443                Some(("PutAccountSendingAttributes", None, None))
444            }
445            // /v2/email/account/suppression
446            (Method::PUT, 4) if segs[2] == "account" && segs[3] == "suppression" => {
447                Some(("PutAccountSuppressionAttributes", None, None))
448            }
449            // /v2/email/account/vdm
450            (Method::PUT, 4) if segs[2] == "account" && segs[3] == "vdm" => {
451                Some(("PutAccountVdmAttributes", None, None))
452            }
453
454            // /v2/email/multi-region-endpoints
455            (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            // /v2/email/multi-region-endpoints/{name}
462            (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            // /v2/email/import-jobs
470            (Method::POST, 3) if segs[2] == "import-jobs" => Some(("CreateImportJob", None, None)),
471            // /v2/email/import-jobs/list (SDK sends POST for ListImportJobs)
472            (Method::POST, 4) if segs[2] == "import-jobs" && segs[3] == "list" => {
473                Some(("ListImportJobs", None, None))
474            }
475            // /v2/email/import-jobs/{id}
476            (Method::GET, 4) if segs[2] == "import-jobs" => Some(("GetImportJob", resource, None)),
477
478            // /v2/email/export-jobs
479            (Method::POST, 3) if segs[2] == "export-jobs" => Some(("CreateExportJob", None, None)),
480            // /v2/email/list-export-jobs (SDK sends POST for ListExportJobs)
481            (Method::POST, 3) if segs[2] == "list-export-jobs" => {
482                Some(("ListExportJobs", None, None))
483            }
484            // /v2/email/export-jobs/{id}/cancel
485            (Method::PUT, 5) if segs[2] == "export-jobs" && segs[4] == "cancel" => {
486                Some(("CancelExportJob", resource, None))
487            }
488            // /v2/email/export-jobs/{id}
489            (Method::GET, 4) if segs[2] == "export-jobs" => Some(("GetExportJob", resource, None)),
490
491            // /v2/email/tenants
492            (Method::POST, 3) if segs[2] == "tenants" => Some(("CreateTenant", None, None)),
493            // /v2/email/tenants/list
494            (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "list" => {
495                Some(("ListTenants", None, None))
496            }
497            // /v2/email/tenants/get
498            (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "get" => {
499                Some(("GetTenant", None, None))
500            }
501            // /v2/email/tenants/delete
502            (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "delete" => {
503                Some(("DeleteTenant", None, None))
504            }
505            // /v2/email/tenants/resources (CreateTenantResourceAssociation)
506            (Method::POST, 4) if segs[2] == "tenants" && segs[3] == "resources" => {
507                Some(("CreateTenantResourceAssociation", None, None))
508            }
509            // /v2/email/tenants/resources/delete (DeleteTenantResourceAssociation)
510            (Method::POST, 5)
511                if segs[2] == "tenants" && segs[3] == "resources" && segs[4] == "delete" =>
512            {
513                Some(("DeleteTenantResourceAssociation", None, None))
514            }
515            // /v2/email/tenants/resources/list (ListTenantResources)
516            (Method::POST, 5)
517                if segs[2] == "tenants" && segs[3] == "resources" && segs[4] == "list" =>
518            {
519                Some(("ListTenantResources", None, None))
520            }
521            // /v2/email/resources/tenants/list (ListResourceTenants)
522            (Method::POST, 5)
523                if segs[2] == "resources" && segs[3] == "tenants" && segs[4] == "list" =>
524            {
525                Some(("ListResourceTenants", None, None))
526            }
527
528            // /v2/email/reputation/entities (ListReputationEntities)
529            (Method::POST, 4) if segs[2] == "reputation" && segs[3] == "entities" => {
530                Some(("ListReputationEntities", None, None))
531            }
532            // /v2/email/reputation/entities/{type}/{ref}/customer-managed-status
533            (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            // /v2/email/reputation/entities/{type}/{ref}/policy
545            (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            // /v2/email/reputation/entities/{type}/{ref}
555            (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            // /v2/email/metrics/batch
562            (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        // Remove tags for this identity
792        let arn = format!(
793            "arn:aws:ses:{}:{}:identity/{}",
794            req.region, req.account_id, identity_name
795        );
796        state.tags.remove(&arn);
797
798        // Remove policies for this identity
799        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        // Remove tags for this configuration set
938        let arn = format!(
939            "arn:aws:ses:{}:{}:configuration-set/{}",
940            req.region, req.account_id, name
941        );
942        state.tags.remove(&arn);
943
944        // Remove event destinations for this configuration set
945        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        // Event fanout: check suppression list, generate events, deliver to destinations
1149        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    // --- Contact List operations ---
1163
1164    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        // Also delete all contacts in this list
1310        state.contacts.remove(name);
1311
1312        // Remove tags for this contact list
1313        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    // --- Contact operations ---
1323
1324    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        // Build TopicDefaultPreferences from the contact list's topics
1406        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                        // Build TopicDefaultPreferences from the list's topics
1475                        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    // --- Tag operations ---
1581
1582    /// Validate that a resource ARN refers to an existing resource.
1583    /// Returns `None` if the resource exists, or `Some(error_response)` if not.
1584    fn validate_resource_arn(&self, arn: &str) -> Option<AwsResponse> {
1585        let state = self.state.read();
1586
1587        // Parse ARN: arn:aws:ses:{region}:{account}:{resource-type}/{name}
1588        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        // ResourceArn and TagKeys come as query params
1661        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        // Parse TagKeys from raw query string (supports repeated params)
1677        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    // --- Suppression List operations ---
1723
1724    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    // --- Event Destination operations ---
1825
1826    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    // --- Email Identity Policy operations ---
1979
1980    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    // --- Identity Attribute operations ---
2139
2140    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    // --- Configuration Set Options ---
2290
2291    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    // --- Custom Verification Email Template operations ---
2474
2475    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        // Set NextToken if there are more results
2610        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        // Verify template exists
2707        {
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        // Store as a sent email for introspection
2727        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    // --- TestRenderEmailTemplate ---
2752
2753    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        // Parse template data JSON
2784        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        // Build a simplified MIME message
2809        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            // Event fanout for each bulk entry
2877            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    // ── Dedicated IP Pools ──────────────────────────────────────────────
2897
2898    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        // For MANAGED pools, generate some fake IPs
2926        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        // Remove IPs associated with this pool
2974        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 changing from STANDARD to MANAGED, generate IPs
3019        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    // ── Dedicated IPs ───────────────────────────────────────────────────
3039
3040    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    // ── Multi-region Endpoints ──────────────────────────────────────────
3178
3179    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        // Extract regions from Details.RoutesDetails[].Region
3205        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        // The primary region is always the current region
3216        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    // ── Account Settings ────────────────────────────────────────────────
3306
3307    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    // --- Import Job operations ---
3397
3398    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                    // Check if import destination matches
3483                    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    // --- Export Job operations ---
3521
3522    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        // Determine export source type from the data source
3548        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    // --- Tenant operations ---
3671
3672    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    // --- Reputation Entity operations ---
3993
3994    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                // Return a default entity for any reference
4006                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    // --- Metrics ---
4122
4123    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        // Route v1 Query protocol requests to the v1 module.
4269        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            // NOTE: SES v1 receipt rule/filter actions are implemented (see v1.rs)
4531            // but excluded from the conformance audit because there is no SES v1
4532            // Smithy model (only sesv2.json exists) to generate checksums from.
4533        ]
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        // Create identity
4592        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        // List identities
4604        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        // Get identity
4610        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        // Delete identity
4618        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        // Verify deleted
4627        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        // Create template
4674        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        // Get template
4683        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        // Update template
4690        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        // Verify update
4699        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        // List templates
4705        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        // Delete template
4711        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        // Verify deleted
4716        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        // Verify stored
4751        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        // Create
4777        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        // Get
4786        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        // List
4793        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        // Delete
4799        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        // Verify deleted
4804        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    // --- Contact List tests ---
5092
5093    #[tokio::test]
5094    async fn test_contact_list_lifecycle() {
5095        let state = make_state();
5096        let svc = SesV2Service::new(state);
5097
5098        // Create contact list with topics
5099        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        // Get contact list
5119        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        // List contact lists
5131        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        // Update contact list
5138        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        // Verify update
5163        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        // Delete contact list
5170        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        // Verify deleted
5175        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    // --- Contact tests ---
5212
5213    #[tokio::test]
5214    async fn test_contact_lifecycle() {
5215        let state = make_state();
5216        let svc = SesV2Service::new(state);
5217
5218        // Create contact list first
5219        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        // Create contact
5237        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        // Get contact
5252        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        // List contacts
5272        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        // Update contact
5283        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        // Verify update
5297        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        // Delete contact
5308        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        // Verify deleted
5317        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        // Create list and contact
5395        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        // Delete the contact list
5410        let req = make_request(Method::DELETE, "/v2/email/contact-lists/my-list", "");
5411        svc.handle(req).await.unwrap();
5412
5413        // Verify contacts map is cleaned up
5414        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        // Create an identity
5424        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        // Tag it
5432        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        // List tags
5441        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        // Create an identity
5466        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        // Tag it
5476        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        // Untag - remove "env"
5486        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        // Verify only "team" remains
5495        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        // Delete identity
5536        let req = make_request(
5537            Method::DELETE,
5538            "/v2/email/identities/test%40example.com",
5539            "",
5540        );
5541        svc.handle(req).await.unwrap();
5542
5543        // Tags should be gone
5544        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        // Delete config set
5569        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        // Delete contact list
5597        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    // --- Suppression List tests ---
5609
5610    #[tokio::test]
5611    async fn test_suppressed_destination_lifecycle() {
5612        let state = make_state();
5613        let svc = SesV2Service::new(state);
5614
5615        // Put suppressed destination
5616        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        // Get suppressed destination
5625        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        // List suppressed destinations
5643        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        // Delete suppressed destination
5655        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        // Verify deleted
5664        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        // Put with BOUNCE
5716        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        // Put again with COMPLAINT (upsert)
5724        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    // --- Event Destination tests ---
5756
5757    #[tokio::test]
5758    async fn test_event_destination_lifecycle() {
5759        let state = make_state();
5760        let svc = SesV2Service::new(state);
5761
5762        // Create config set first
5763        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        // Create event destination
5771        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        // Get event destinations
5787        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        // Update event destination
5806        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        // Verify update
5821        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        // Delete event destination
5833        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        // Verify deleted
5842        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    // --- Email Identity Policy tests ---
5974
5975    #[tokio::test]
5976    async fn test_identity_policy_lifecycle() {
5977        let state = make_state();
5978        let svc = SesV2Service::new(state);
5979
5980        // Create identity first
5981        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        // Create policy
5989        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        // Get policies
6002        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        // Update policy
6014        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        // Verify update
6027        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        // Delete policy
6037        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        // Verify deleted
6046        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    // --- Identity Attribute tests ---
6171
6172    #[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        // Create identity first
6178        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        // Disable DKIM signing
6186        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        // Verify via GetEmailIdentity
6195        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        // Verify stored
6239        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    // --- Configuration Set Options tests ---
6334
6335    #[tokio::test]
6336    async fn test_put_configuration_set_sending_options() {
6337        let state = make_state();
6338        let svc = SesV2Service::new(state);
6339
6340        // Create config set
6341        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        // Disable sending
6349        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        // Verify
6358        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    // --- Custom Verification Email Template tests ---
6549
6550    #[tokio::test]
6551    async fn test_custom_verification_email_template_lifecycle() {
6552        let state = make_state();
6553        let svc = SesV2Service::new(state);
6554
6555        // Create
6556        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        // Get
6572        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        // List
6588        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        // Update
6604        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        // Verify update
6613        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        // Delete
6623        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        // Verify deleted
6632        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        // Create template first
6677        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        // Send
6692        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        // Verify stored in sent_emails
6703        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    // --- TestRenderEmailTemplate tests ---
6723
6724    #[tokio::test]
6725    async fn test_render_email_template() {
6726        let state = make_state();
6727        let svc = SesV2Service::new(state);
6728
6729        // Create template
6730        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        // Render
6745        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        // Create template
6779        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    // ── Dedicated IP Pool tests ─────────────────────────────────────────
6792
6793    #[tokio::test]
6794    async fn test_dedicated_ip_pool_lifecycle() {
6795        let state = make_state();
6796        let svc = SesV2Service::new(state);
6797
6798        // Create pool
6799        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        // List pools
6808        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        // Duplicate
6814        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        // Delete pool
6823        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        // Delete non-existent
6828        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        // Create managed pool
6839        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        // List dedicated IPs filtered by pool
6848        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        // Create two pools
6873        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        // Get a specific IP
6888        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        // Move IP to pool-b
6895        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        // Verify it moved
6904        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        // Set warmup
6910        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        // Non-existent IP
6925        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        // Change to MANAGED
6943        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        // Cannot change from MANAGED to STANDARD
6952        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    // ── Multi-region Endpoint tests ─────────────────────────────────────
6981
6982    #[tokio::test]
6983    async fn test_multi_region_endpoint_lifecycle() {
6984        let state = make_state();
6985        let svc = SesV2Service::new(state);
6986
6987        // Create
6988        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        // Get
7000        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        // List
7014        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        // Duplicate
7020        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        // Delete
7029        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        // Get after delete
7040        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    // ── Account Settings tests ──────────────────────────────────────────
7050
7051    #[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        // Disable sending
7078        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        // Re-enable
7092        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        // Create import job
7152        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        // Get import job
7171        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        // List import jobs
7183        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        // Get non-existent job
7190        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        // Create export job
7201        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        // Get export job
7224        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        // List export jobs
7237        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        // Cancel — should fail since already COMPLETED
7244        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        // Create tenant
7259        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        // Get tenant
7272        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        // List tenants
7283        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        // Create resource association
7290        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        // List tenant resources
7299        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        // List resource tenants
7310        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        // Delete resource association
7321        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        // Verify association is gone
7330        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        // Delete tenant
7340        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        // Verify deleted
7349        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        // Get default reputation entity (auto-created)
7364        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        // Update customer managed status
7378        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        // Update policy
7387        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        // Verify via get
7396        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        // List reputation entities
7409        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}