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