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