1use chrono::{DateTime, Utc};
2use parking_lot::RwLock;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct EmailIdentity {
9 pub identity_name: String,
10 pub identity_type: String,
11 pub verified: bool,
12 pub created_at: DateTime<Utc>,
13 pub dkim_signing_enabled: bool,
15 pub dkim_signing_attributes_origin: String,
16 pub dkim_domain_signing_private_key: Option<String>,
17 pub dkim_domain_signing_selector: Option<String>,
18 pub dkim_next_signing_key_length: Option<String>,
19 #[serde(default)]
23 pub dkim_public_key_b64: Option<String>,
24 pub email_forwarding_enabled: bool,
26 pub mail_from_domain: Option<String>,
28 pub mail_from_behavior_on_mx_failure: String,
29 #[serde(default)]
33 pub mail_from_domain_status: String,
34 pub configuration_set_name: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct EmailTemplate {
40 pub template_name: String,
41 pub subject: Option<String>,
42 pub html_body: Option<String>,
43 pub text_body: Option<String>,
44 pub created_at: DateTime<Utc>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ConfigurationSet {
49 pub name: String,
50 pub sending_enabled: bool,
52 pub tls_policy: String,
54 pub sending_pool_name: Option<String>,
55 pub custom_redirect_domain: Option<String>,
57 pub https_policy: Option<String>,
58 pub suppressed_reasons: Vec<String>,
60 pub reputation_metrics_enabled: bool,
62 pub vdm_options: Option<serde_json::Value>,
64 pub archive_arn: Option<String>,
66 #[serde(default)]
73 pub archiving_options_present: bool,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CustomVerificationEmailTemplate {
78 pub template_name: String,
79 pub from_email_address: String,
80 pub template_subject: String,
81 pub template_content: String,
82 pub success_redirection_url: String,
83 pub failure_redirection_url: String,
84 pub created_at: DateTime<Utc>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SentEmail {
89 pub message_id: String,
90 pub from: String,
91 pub to: Vec<String>,
92 pub cc: Vec<String>,
93 pub bcc: Vec<String>,
94 pub subject: Option<String>,
95 pub html_body: Option<String>,
96 pub text_body: Option<String>,
97 pub raw_data: Option<String>,
98 pub template_name: Option<String>,
99 pub template_data: Option<String>,
100 #[serde(default)]
104 pub dkim_signature: Option<String>,
105 #[serde(default)]
111 pub headers: Vec<(String, String)>,
112 pub timestamp: DateTime<Utc>,
113 #[serde(default)]
115 pub email_tags: Vec<(String, String)>,
116 #[serde(default)]
118 pub delivery_insights: Vec<EmailRecipientInsight>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct EmailRecipientInsight {
124 pub destination: String,
125 pub isp: String,
126 pub events: Vec<DeliveryInsightEvent>,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
131pub struct DeliveryInsightEvent {
132 pub timestamp: DateTime<Utc>,
133 pub event_type: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub bounce_type: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub bounce_sub_type: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub diagnostic_code: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub complaint_sub_type: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub complaint_feedback_type: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SentBounce {
148 pub bounce_message_id: String,
149 pub original_message_id: String,
150 pub bounce_sender: String,
151 pub bounced_recipients: Vec<String>,
152 pub timestamp: DateTime<Utc>,
153 #[serde(default)]
157 pub bounced_recipient_info: Vec<BouncedRecipientInfo>,
158 #[serde(default)]
161 pub explanation: Option<String>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct BouncedRecipientInfo {
166 pub recipient: String,
167 pub bounce_type: String,
168 pub action: String,
169 pub status: String,
170 pub diagnostic_code: String,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ContactList {
175 pub contact_list_name: String,
176 pub description: Option<String>,
177 pub topics: Vec<Topic>,
178 pub created_at: DateTime<Utc>,
179 pub last_updated_at: DateTime<Utc>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct Topic {
184 pub topic_name: String,
185 pub display_name: String,
186 pub description: String,
187 pub default_subscription_status: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct Contact {
192 pub email_address: String,
193 pub topic_preferences: Vec<TopicPreference>,
194 pub unsubscribe_all: bool,
195 pub attributes_data: Option<String>,
196 pub created_at: DateTime<Utc>,
197 pub last_updated_at: DateTime<Utc>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct TopicPreference {
202 pub topic_name: String,
203 pub subscription_status: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct SuppressedDestination {
208 pub email_address: String,
209 pub reason: String,
210 pub last_update_time: DateTime<Utc>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct EventDestination {
215 pub name: String,
216 pub enabled: bool,
217 pub matching_event_types: Vec<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub kinesis_firehose_destination: Option<serde_json::Value>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub cloud_watch_destination: Option<serde_json::Value>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub sns_destination: Option<serde_json::Value>,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub event_bridge_destination: Option<serde_json::Value>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub pinpoint_destination: Option<serde_json::Value>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct DedicatedIpPool {
232 pub pool_name: String,
233 pub scaling_mode: String,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct DedicatedIp {
238 pub ip: String,
239 pub warmup_status: String,
240 pub warmup_percentage: i32,
241 pub pool_name: String,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct MultiRegionEndpoint {
246 pub endpoint_name: String,
247 pub endpoint_id: String,
248 pub status: String,
249 pub regions: Vec<String>,
250 pub created_at: DateTime<Utc>,
251 pub last_updated_at: DateTime<Utc>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, Default)]
255pub struct AccountDetails {
256 pub mail_type: Option<String>,
257 pub website_url: Option<String>,
258 pub contact_language: Option<String>,
259 pub use_case_description: Option<String>,
260 pub additional_contact_email_addresses: Vec<String>,
261 pub production_access_enabled: Option<bool>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, Default)]
265pub struct AccountSettings {
266 pub sending_enabled: bool,
267 pub dedicated_ip_auto_warmup_enabled: bool,
268 pub suppressed_reasons: Vec<String>,
269 pub vdm_attributes: Option<serde_json::Value>,
270 pub details: Option<AccountDetails>,
271 #[serde(default)]
275 pub production_access_enabled: bool,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct ImportJob {
280 pub job_id: String,
281 pub import_destination: serde_json::Value,
282 pub import_data_source: serde_json::Value,
283 pub job_status: String,
284 pub created_timestamp: DateTime<Utc>,
285 pub completed_timestamp: Option<DateTime<Utc>>,
286 pub processed_records_count: i32,
287 pub failed_records_count: i32,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct ExportJob {
292 pub job_id: String,
293 pub export_source_type: String,
294 pub export_destination: serde_json::Value,
295 pub export_data_source: serde_json::Value,
296 pub job_status: String,
297 pub created_timestamp: DateTime<Utc>,
298 pub completed_timestamp: Option<DateTime<Utc>>,
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct Tenant {
303 pub tenant_name: String,
304 pub tenant_id: String,
305 pub tenant_arn: String,
306 pub created_timestamp: DateTime<Utc>,
307 pub sending_status: String,
308 pub tags: Vec<serde_json::Value>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct TenantResourceAssociation {
313 pub resource_arn: String,
314 pub associated_timestamp: DateTime<Utc>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct ReputationEntityState {
319 pub reputation_entity_reference: String,
320 pub reputation_entity_type: String,
321 pub reputation_management_policy: Option<String>,
322 pub customer_managed_status: String,
323 pub sending_status_aggregate: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct ReceiptRuleSet {
330 pub name: String,
331 pub rules: Vec<ReceiptRule>,
332 pub created_at: DateTime<Utc>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ReceiptRule {
337 pub name: String,
338 pub enabled: bool,
339 pub scan_enabled: bool,
340 pub tls_policy: String,
341 pub recipients: Vec<String>,
342 pub actions: Vec<ReceiptAction>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub enum ReceiptAction {
347 S3 {
348 bucket_name: String,
349 object_key_prefix: Option<String>,
350 topic_arn: Option<String>,
351 kms_key_arn: Option<String>,
352 },
353 Sns {
354 topic_arn: String,
355 encoding: Option<String>,
356 },
357 Lambda {
358 function_arn: String,
359 invocation_type: Option<String>,
360 topic_arn: Option<String>,
361 },
362 Bounce {
363 smtp_reply_code: String,
364 message: String,
365 sender: String,
366 status_code: Option<String>,
367 topic_arn: Option<String>,
368 },
369 AddHeader {
370 header_name: String,
371 header_value: String,
372 },
373 Stop {
374 scope: String,
375 topic_arn: Option<String>,
376 },
377 Workmail {
378 organization_arn: String,
379 topic_arn: Option<String>,
380 },
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct ReceiptFilter {
385 pub name: String,
386 pub ip_filter: IpFilter,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct IpFilter {
391 pub cidr: String,
392 pub policy: String, }
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct SmtpSubmission {
401 pub message_id: String,
402 pub from: String,
403 pub to: Vec<String>,
404 pub subject: Option<String>,
405 pub raw_size_bytes: usize,
406 pub received_at: DateTime<Utc>,
407 pub auth_user: String,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct EventDestinationDispatch {
417 pub destination_name: String,
418 pub destination_type: String,
420 pub event_type: String,
421 pub message_id: String,
422 pub dispatched_at: DateTime<Utc>,
423 pub target_arn: String,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct InboundEmail {
430 pub message_id: String,
431 pub from: String,
432 pub to: Vec<String>,
433 pub subject: String,
434 pub body: String,
435 pub matched_rules: Vec<String>,
436 pub actions_executed: Vec<String>,
437 pub timestamp: DateTime<Utc>,
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct SesState {
442 pub account_id: String,
443 pub region: String,
444 #[serde(default)]
445 pub identities: BTreeMap<String, EmailIdentity>,
446 #[serde(default)]
447 pub configuration_sets: BTreeMap<String, ConfigurationSet>,
448 #[serde(default)]
449 pub templates: BTreeMap<String, EmailTemplate>,
450 #[serde(default, skip_serializing)]
451 pub sent_emails: Vec<SentEmail>,
452 #[serde(default, skip_serializing)]
453 pub bounces: Vec<SentBounce>,
454 pub contact_lists: BTreeMap<String, ContactList>,
455 pub contacts: BTreeMap<String, BTreeMap<String, Contact>>,
456 pub tags: BTreeMap<String, BTreeMap<String, String>>,
458 pub suppressed_destinations: BTreeMap<String, SuppressedDestination>,
460 pub event_destinations: BTreeMap<String, Vec<EventDestination>>,
462 pub identity_policies: BTreeMap<String, BTreeMap<String, String>>,
464 pub custom_verification_email_templates: BTreeMap<String, CustomVerificationEmailTemplate>,
466 pub dedicated_ip_pools: BTreeMap<String, DedicatedIpPool>,
468 pub dedicated_ips: BTreeMap<String, DedicatedIp>,
470 pub multi_region_endpoints: BTreeMap<String, MultiRegionEndpoint>,
472 pub account_settings: AccountSettings,
474 pub import_jobs: BTreeMap<String, ImportJob>,
476 pub export_jobs: BTreeMap<String, ExportJob>,
478 pub tenants: BTreeMap<String, Tenant>,
480 pub tenant_resource_associations: BTreeMap<String, Vec<TenantResourceAssociation>>,
482 pub reputation_entities: BTreeMap<String, ReputationEntityState>,
484 pub receipt_rule_sets: BTreeMap<String, ReceiptRuleSet>,
487 pub active_receipt_rule_set: Option<String>,
489 pub receipt_filters: BTreeMap<String, ReceiptFilter>,
491 #[serde(default, skip_serializing)]
493 pub inbound_emails: Vec<InboundEmail>,
494 #[serde(default, skip_serializing)]
497 pub smtp_submissions: Vec<SmtpSubmission>,
498 #[serde(default, skip_serializing)]
504 pub event_destination_dispatches: Vec<EventDestinationDispatch>,
505 #[serde(default)]
507 pub deliverability_dashboard: DeliverabilityDashboard,
508 #[serde(default)]
510 pub deliverability_test_reports: BTreeMap<String, DeliverabilityTestReport>,
511 #[serde(default)]
513 pub vdm_recommendations: Vec<VdmRecommendation>,
514 #[serde(default)]
519 pub suppressed_drops_total: u64,
520}
521
522#[derive(Debug, Clone, Default, Serialize, Deserialize)]
523pub struct DeliverabilityDashboard {
524 pub enabled: bool,
525 pub subscribed_domains: Vec<SubscribedDomain>,
526 pub subscription_expiry_date: Option<DateTime<Utc>>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct SubscribedDomain {
531 pub domain: String,
532 pub subscription_start_date: DateTime<Utc>,
533 pub inbox_placement_tracking_option_global: bool,
534 pub inbox_placement_tracking_option_tracked_isps: Vec<String>,
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
538pub struct DeliverabilityTestReport {
539 pub report_id: String,
540 pub report_name: String,
541 pub subject: String,
542 pub from_email: String,
543 pub create_date: DateTime<Utc>,
544 pub deliverability_test_status: String, pub tags: Vec<(String, String)>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct VdmRecommendation {
550 pub resource_arn: String,
551 pub recommendation_type: String,
552 pub description: String,
553 pub status: String,
554 pub created_timestamp: DateTime<Utc>,
555 pub last_updated_timestamp: DateTime<Utc>,
556 pub impact: String,
557}
558
559pub const SES_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
560
561#[derive(Debug, Serialize, Deserialize)]
562pub struct SesSnapshot {
563 pub schema_version: u32,
564 #[serde(default)]
565 pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<SesState>>,
566 #[serde(default)]
567 pub state: Option<SesState>,
568}
569
570impl SesState {
571 pub fn new(account_id: &str, region: &str) -> Self {
572 Self {
573 account_id: account_id.to_string(),
574 region: region.to_string(),
575 identities: BTreeMap::new(),
576 configuration_sets: BTreeMap::new(),
577 templates: BTreeMap::new(),
578 sent_emails: Vec::new(),
579 bounces: Vec::new(),
580 contact_lists: BTreeMap::new(),
581 contacts: BTreeMap::new(),
582 tags: BTreeMap::new(),
583 suppressed_destinations: BTreeMap::new(),
584 event_destinations: BTreeMap::new(),
585 identity_policies: BTreeMap::new(),
586 custom_verification_email_templates: BTreeMap::new(),
587 dedicated_ip_pools: BTreeMap::new(),
588 dedicated_ips: BTreeMap::new(),
589 multi_region_endpoints: BTreeMap::new(),
590 account_settings: AccountSettings {
597 sending_enabled: true,
598 dedicated_ip_auto_warmup_enabled: false,
599 suppressed_reasons: Vec::new(),
600 vdm_attributes: None,
601 details: None,
602 production_access_enabled: true,
603 },
604 import_jobs: BTreeMap::new(),
605 export_jobs: BTreeMap::new(),
606 tenants: BTreeMap::new(),
607 tenant_resource_associations: BTreeMap::new(),
608 reputation_entities: BTreeMap::new(),
609 receipt_rule_sets: BTreeMap::new(),
610 active_receipt_rule_set: None,
611 receipt_filters: BTreeMap::new(),
612 inbound_emails: Vec::new(),
613 smtp_submissions: Vec::new(),
614 event_destination_dispatches: Vec::new(),
615 deliverability_dashboard: DeliverabilityDashboard::default(),
616 deliverability_test_reports: BTreeMap::new(),
617 vdm_recommendations: Vec::new(),
618 suppressed_drops_total: 0,
619 }
620 }
621
622 pub fn reset(&mut self) {
624 let account_id = std::mem::take(&mut self.account_id);
625 let region = std::mem::take(&mut self.region);
626 *self = Self::new(&account_id, ®ion);
627 }
628
629 pub fn effective_suppressed_reasons(&self, config_set_name: Option<&str>) -> Vec<String> {
637 if let Some(name) = config_set_name {
638 if let Some(cs) = self.configuration_sets.get(name) {
639 if !cs.suppressed_reasons.is_empty() {
640 return cs.suppressed_reasons.clone();
641 }
642 }
643 }
644 if !self.account_settings.suppressed_reasons.is_empty() {
645 return self.account_settings.suppressed_reasons.clone();
646 }
647 vec!["BOUNCE".to_string(), "COMPLAINT".to_string()]
648 }
649
650 pub fn suppressed_match(
655 &self,
656 address: &str,
657 config_set_name: Option<&str>,
658 ) -> Option<&SuppressedDestination> {
659 let key = address.trim().to_ascii_lowercase();
660 let entry = self.suppressed_destinations.iter().find_map(|(k, v)| {
661 if k.trim().eq_ignore_ascii_case(&key) {
662 Some(v)
663 } else {
664 None
665 }
666 })?;
667 let reasons = self.effective_suppressed_reasons(config_set_name);
668 if reasons.iter().any(|r| r == &entry.reason) {
669 Some(entry)
670 } else {
671 None
672 }
673 }
674}
675
676pub type SharedSesState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<SesState>>>;
677
678impl fakecloud_core::multi_account::AccountState for SesState {
679 fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
680 Self::new(account_id, region)
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn new_initializes_defaults() {
690 let state = SesState::new("123456789012", "us-east-1");
691 assert_eq!(state.account_id, "123456789012");
692 assert_eq!(state.region, "us-east-1");
693 assert!(state.identities.is_empty());
694 assert!(state.configuration_sets.is_empty());
695 assert!(state.account_settings.sending_enabled);
696 }
697
698 #[test]
699 fn new_initializes_introspection_buffers_empty() {
700 let state = SesState::new("123456789012", "us-east-1");
701 assert!(state.smtp_submissions.is_empty());
702 assert!(state.event_destination_dispatches.is_empty());
703 }
704
705 #[test]
706 fn smtp_submission_round_trips_through_state() {
707 let mut state = SesState::new("123456789012", "us-east-1");
708 state.smtp_submissions.push(SmtpSubmission {
709 message_id: "smtp-1".to_string(),
710 from: "src@example.com".to_string(),
711 to: vec!["dst@example.com".to_string()],
712 subject: Some("hi".to_string()),
713 raw_size_bytes: 42,
714 received_at: Utc::now(),
715 auth_user: "user".to_string(),
716 });
717 assert_eq!(state.smtp_submissions.len(), 1);
718 assert_eq!(state.smtp_submissions[0].auth_user, "user");
719 state.reset();
720 assert!(state.smtp_submissions.is_empty());
721 }
722
723 #[test]
724 fn event_destination_dispatch_round_trips() {
725 let mut state = SesState::new("123456789012", "us-east-1");
726 state
727 .event_destination_dispatches
728 .push(EventDestinationDispatch {
729 destination_name: "fh".to_string(),
730 destination_type: "firehose".to_string(),
731 event_type: "SEND".to_string(),
732 message_id: "msg-1".to_string(),
733 dispatched_at: Utc::now(),
734 target_arn: "arn:aws:firehose:us-east-1:123456789012:deliverystream/ds1"
735 .to_string(),
736 });
737 assert_eq!(state.event_destination_dispatches.len(), 1);
738 assert_eq!(
739 state.event_destination_dispatches[0].destination_type,
740 "firehose"
741 );
742 }
743
744 #[test]
745 fn reset_preserves_account_region() {
746 let mut state = SesState::new("123456789012", "eu-west-1");
747 state.account_settings.sending_enabled = false;
748 state.reset();
749 assert_eq!(state.account_id, "123456789012");
750 assert_eq!(state.region, "eu-west-1");
751 assert!(state.account_settings.sending_enabled);
752 }
753}