use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailIdentity {
pub identity_name: String,
pub identity_type: String,
pub verified: bool,
pub created_at: DateTime<Utc>,
pub dkim_signing_enabled: bool,
pub dkim_signing_attributes_origin: String,
pub dkim_domain_signing_private_key: Option<String>,
pub dkim_domain_signing_selector: Option<String>,
pub dkim_next_signing_key_length: Option<String>,
#[serde(default)]
pub dkim_public_key_b64: Option<String>,
pub email_forwarding_enabled: bool,
pub mail_from_domain: Option<String>,
pub mail_from_behavior_on_mx_failure: String,
#[serde(default)]
pub mail_from_domain_status: String,
pub configuration_set_name: Option<String>,
#[serde(default)]
pub bounce_topic: Option<String>,
#[serde(default)]
pub complaint_topic: Option<String>,
#[serde(default)]
pub delivery_topic: Option<String>,
#[serde(default)]
pub verification_token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplate {
pub template_name: String,
pub subject: Option<String>,
pub html_body: Option<String>,
pub text_body: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigurationSet {
pub name: String,
pub sending_enabled: bool,
pub tls_policy: String,
pub sending_pool_name: Option<String>,
#[serde(default)]
pub max_delivery_seconds: Option<i64>,
pub custom_redirect_domain: Option<String>,
pub https_policy: Option<String>,
pub suppressed_reasons: Vec<String>,
pub reputation_metrics_enabled: bool,
pub vdm_options: Option<serde_json::Value>,
pub archive_arn: Option<String>,
#[serde(default)]
pub archiving_options_present: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomVerificationEmailTemplate {
pub template_name: String,
pub from_email_address: String,
pub template_subject: String,
pub template_content: String,
pub success_redirection_url: String,
pub failure_redirection_url: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SentEmail {
pub message_id: String,
pub from: String,
pub to: Vec<String>,
pub cc: Vec<String>,
pub bcc: Vec<String>,
pub subject: Option<String>,
pub html_body: Option<String>,
pub text_body: Option<String>,
pub raw_data: Option<String>,
pub template_name: Option<String>,
pub template_data: Option<String>,
#[serde(default)]
pub dkim_signature: Option<String>,
#[serde(default)]
pub headers: Vec<(String, String)>,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub email_tags: Vec<(String, String)>,
#[serde(default)]
pub delivery_insights: Vec<EmailRecipientInsight>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailRecipientInsight {
pub destination: String,
pub isp: String,
pub events: Vec<DeliveryInsightEvent>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DeliveryInsightEvent {
pub timestamp: DateTime<Utc>,
pub event_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounce_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounce_sub_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostic_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complaint_sub_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complaint_feedback_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SentBounce {
pub bounce_message_id: String,
pub original_message_id: String,
pub bounce_sender: String,
pub bounced_recipients: Vec<String>,
pub timestamp: DateTime<Utc>,
#[serde(default)]
pub bounced_recipient_info: Vec<BouncedRecipientInfo>,
#[serde(default)]
pub explanation: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BouncedRecipientInfo {
pub recipient: String,
pub bounce_type: String,
pub action: String,
pub status: String,
pub diagnostic_code: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContactList {
pub contact_list_name: String,
pub description: Option<String>,
pub topics: Vec<Topic>,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Topic {
pub topic_name: String,
pub display_name: String,
pub description: String,
pub default_subscription_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
pub email_address: String,
pub topic_preferences: Vec<TopicPreference>,
pub unsubscribe_all: bool,
pub attributes_data: Option<String>,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopicPreference {
pub topic_name: String,
pub subscription_status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuppressedDestination {
pub email_address: String,
pub reason: String,
pub last_update_time: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventDestination {
pub name: String,
pub enabled: bool,
pub matching_event_types: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kinesis_firehose_destination: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cloud_watch_destination: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sns_destination: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub event_bridge_destination: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pinpoint_destination: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DedicatedIpPool {
pub pool_name: String,
pub scaling_mode: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DedicatedIp {
pub ip: String,
pub warmup_status: String,
pub warmup_percentage: i32,
pub pool_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiRegionEndpoint {
pub endpoint_name: String,
pub endpoint_id: String,
pub status: String,
pub regions: Vec<String>,
pub created_at: DateTime<Utc>,
pub last_updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AccountDetails {
pub mail_type: Option<String>,
pub website_url: Option<String>,
pub contact_language: Option<String>,
pub use_case_description: Option<String>,
pub additional_contact_email_addresses: Vec<String>,
pub production_access_enabled: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AccountSettings {
pub sending_enabled: bool,
pub dedicated_ip_auto_warmup_enabled: bool,
pub suppressed_reasons: Vec<String>,
pub vdm_attributes: Option<serde_json::Value>,
pub details: Option<AccountDetails>,
#[serde(default)]
pub production_access_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportJob {
pub job_id: String,
pub import_destination: serde_json::Value,
pub import_data_source: serde_json::Value,
pub job_status: String,
pub created_timestamp: DateTime<Utc>,
pub completed_timestamp: Option<DateTime<Utc>>,
pub processed_records_count: i32,
pub failed_records_count: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportJob {
pub job_id: String,
pub export_source_type: String,
pub export_destination: serde_json::Value,
pub export_data_source: serde_json::Value,
pub job_status: String,
pub created_timestamp: DateTime<Utc>,
pub completed_timestamp: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tenant {
pub tenant_name: String,
pub tenant_id: String,
pub tenant_arn: String,
pub created_timestamp: DateTime<Utc>,
pub sending_status: String,
pub tags: Vec<serde_json::Value>,
#[serde(default)]
pub suppression_attributes: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantResourceAssociation {
pub resource_arn: String,
pub associated_timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReputationEntityState {
pub reputation_entity_reference: String,
pub reputation_entity_type: String,
pub reputation_management_policy: Option<String>,
pub customer_managed_status: String,
pub sending_status_aggregate: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptRuleSet {
pub name: String,
pub rules: Vec<ReceiptRule>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptRule {
pub name: String,
pub enabled: bool,
pub scan_enabled: bool,
pub tls_policy: String,
pub recipients: Vec<String>,
pub actions: Vec<ReceiptAction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ReceiptAction {
S3 {
bucket_name: String,
object_key_prefix: Option<String>,
topic_arn: Option<String>,
kms_key_arn: Option<String>,
},
Sns {
topic_arn: String,
encoding: Option<String>,
},
Lambda {
function_arn: String,
invocation_type: Option<String>,
topic_arn: Option<String>,
},
Bounce {
smtp_reply_code: String,
message: String,
sender: String,
status_code: Option<String>,
topic_arn: Option<String>,
},
AddHeader {
header_name: String,
header_value: String,
},
Stop {
scope: String,
topic_arn: Option<String>,
},
Workmail {
organization_arn: String,
topic_arn: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReceiptFilter {
pub name: String,
pub ip_filter: IpFilter,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpFilter {
pub cidr: String,
pub policy: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SmtpSubmission {
pub message_id: String,
pub from: String,
pub to: Vec<String>,
pub subject: Option<String>,
pub raw_size_bytes: usize,
pub received_at: DateTime<Utc>,
pub auth_user: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventDestinationDispatch {
pub destination_name: String,
pub destination_type: String,
pub event_type: String,
pub message_id: String,
pub dispatched_at: DateTime<Utc>,
pub target_arn: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InboundEmail {
pub message_id: String,
pub from: String,
pub to: Vec<String>,
pub subject: String,
pub body: String,
pub matched_rules: Vec<String>,
pub actions_executed: Vec<String>,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SesState {
pub account_id: String,
pub region: String,
#[serde(default)]
pub identities: BTreeMap<String, EmailIdentity>,
#[serde(default)]
pub configuration_sets: BTreeMap<String, ConfigurationSet>,
#[serde(default)]
pub templates: BTreeMap<String, EmailTemplate>,
#[serde(default, skip_serializing)]
pub sent_emails: Vec<SentEmail>,
#[serde(default, skip_serializing)]
pub bounces: Vec<SentBounce>,
pub contact_lists: BTreeMap<String, ContactList>,
pub contacts: BTreeMap<String, BTreeMap<String, Contact>>,
pub tags: BTreeMap<String, BTreeMap<String, String>>,
pub suppressed_destinations: BTreeMap<String, SuppressedDestination>,
pub event_destinations: BTreeMap<String, Vec<EventDestination>>,
pub identity_policies: BTreeMap<String, BTreeMap<String, String>>,
pub custom_verification_email_templates: BTreeMap<String, CustomVerificationEmailTemplate>,
pub dedicated_ip_pools: BTreeMap<String, DedicatedIpPool>,
pub dedicated_ips: BTreeMap<String, DedicatedIp>,
pub multi_region_endpoints: BTreeMap<String, MultiRegionEndpoint>,
pub account_settings: AccountSettings,
pub import_jobs: BTreeMap<String, ImportJob>,
pub export_jobs: BTreeMap<String, ExportJob>,
pub tenants: BTreeMap<String, Tenant>,
pub tenant_resource_associations: BTreeMap<String, Vec<TenantResourceAssociation>>,
pub reputation_entities: BTreeMap<String, ReputationEntityState>,
pub receipt_rule_sets: BTreeMap<String, ReceiptRuleSet>,
pub active_receipt_rule_set: Option<String>,
pub receipt_filters: BTreeMap<String, ReceiptFilter>,
#[serde(default, skip_serializing)]
pub inbound_emails: Vec<InboundEmail>,
#[serde(default, skip_serializing)]
pub smtp_submissions: Vec<SmtpSubmission>,
#[serde(default, skip_serializing)]
pub event_destination_dispatches: Vec<EventDestinationDispatch>,
#[serde(default)]
pub deliverability_dashboard: DeliverabilityDashboard,
#[serde(default)]
pub deliverability_test_reports: BTreeMap<String, DeliverabilityTestReport>,
#[serde(default)]
pub vdm_recommendations: Vec<VdmRecommendation>,
#[serde(default)]
pub suppressed_drops_total: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeliverabilityDashboard {
pub enabled: bool,
pub subscribed_domains: Vec<SubscribedDomain>,
pub subscription_expiry_date: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribedDomain {
pub domain: String,
pub subscription_start_date: DateTime<Utc>,
pub inbox_placement_tracking_option_global: bool,
pub inbox_placement_tracking_option_tracked_isps: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeliverabilityTestReport {
pub report_id: String,
pub report_name: String,
pub subject: String,
pub from_email: String,
pub create_date: DateTime<Utc>,
pub deliverability_test_status: String, pub tags: Vec<(String, String)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VdmRecommendation {
pub resource_arn: String,
pub recommendation_type: String,
pub description: String,
pub status: String,
pub created_timestamp: DateTime<Utc>,
pub last_updated_timestamp: DateTime<Utc>,
pub impact: String,
}
pub const SES_SNAPSHOT_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Serialize, Deserialize)]
pub struct SesSnapshot {
pub schema_version: u32,
#[serde(default)]
pub accounts: Option<fakecloud_core::multi_account::MultiAccountState<SesState>>,
#[serde(default)]
pub state: Option<SesState>,
}
impl SesState {
pub fn new(account_id: &str, region: &str) -> Self {
Self {
account_id: account_id.to_string(),
region: region.to_string(),
identities: BTreeMap::new(),
configuration_sets: BTreeMap::new(),
templates: BTreeMap::new(),
sent_emails: Vec::new(),
bounces: Vec::new(),
contact_lists: BTreeMap::new(),
contacts: BTreeMap::new(),
tags: BTreeMap::new(),
suppressed_destinations: BTreeMap::new(),
event_destinations: BTreeMap::new(),
identity_policies: BTreeMap::new(),
custom_verification_email_templates: BTreeMap::new(),
dedicated_ip_pools: BTreeMap::new(),
dedicated_ips: BTreeMap::new(),
multi_region_endpoints: BTreeMap::new(),
account_settings: AccountSettings {
sending_enabled: true,
dedicated_ip_auto_warmup_enabled: false,
suppressed_reasons: Vec::new(),
vdm_attributes: None,
details: None,
production_access_enabled: true,
},
import_jobs: BTreeMap::new(),
export_jobs: BTreeMap::new(),
tenants: BTreeMap::new(),
tenant_resource_associations: BTreeMap::new(),
reputation_entities: BTreeMap::new(),
receipt_rule_sets: BTreeMap::new(),
active_receipt_rule_set: None,
receipt_filters: BTreeMap::new(),
inbound_emails: Vec::new(),
smtp_submissions: Vec::new(),
event_destination_dispatches: Vec::new(),
deliverability_dashboard: DeliverabilityDashboard::default(),
deliverability_test_reports: BTreeMap::new(),
vdm_recommendations: Vec::new(),
suppressed_drops_total: 0,
}
}
pub fn reset(&mut self) {
let account_id = std::mem::take(&mut self.account_id);
let region = std::mem::take(&mut self.region);
*self = Self::new(&account_id, ®ion);
}
pub fn effective_suppressed_reasons(&self, config_set_name: Option<&str>) -> Vec<String> {
if let Some(name) = config_set_name {
if let Some(cs) = self.configuration_sets.get(name) {
if !cs.suppressed_reasons.is_empty() {
return cs.suppressed_reasons.clone();
}
}
}
if !self.account_settings.suppressed_reasons.is_empty() {
return self.account_settings.suppressed_reasons.clone();
}
vec!["BOUNCE".to_string(), "COMPLAINT".to_string()]
}
pub fn suppressed_match(
&self,
address: &str,
config_set_name: Option<&str>,
) -> Option<&SuppressedDestination> {
let key = address.trim().to_ascii_lowercase();
let entry = self.suppressed_destinations.iter().find_map(|(k, v)| {
if k.trim().eq_ignore_ascii_case(&key) {
Some(v)
} else {
None
}
})?;
let reasons = self.effective_suppressed_reasons(config_set_name);
if reasons.iter().any(|r| r == &entry.reason) {
Some(entry)
} else {
None
}
}
}
pub type SharedSesState = Arc<RwLock<fakecloud_core::multi_account::MultiAccountState<SesState>>>;
impl fakecloud_core::multi_account::AccountState for SesState {
fn new_for_account(account_id: &str, region: &str, _endpoint: &str) -> Self {
Self::new(account_id, region)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_initializes_defaults() {
let state = SesState::new("123456789012", "us-east-1");
assert_eq!(state.account_id, "123456789012");
assert_eq!(state.region, "us-east-1");
assert!(state.identities.is_empty());
assert!(state.configuration_sets.is_empty());
assert!(state.account_settings.sending_enabled);
}
#[test]
fn new_initializes_introspection_buffers_empty() {
let state = SesState::new("123456789012", "us-east-1");
assert!(state.smtp_submissions.is_empty());
assert!(state.event_destination_dispatches.is_empty());
}
#[test]
fn smtp_submission_round_trips_through_state() {
let mut state = SesState::new("123456789012", "us-east-1");
state.smtp_submissions.push(SmtpSubmission {
message_id: "smtp-1".to_string(),
from: "src@example.com".to_string(),
to: vec!["dst@example.com".to_string()],
subject: Some("hi".to_string()),
raw_size_bytes: 42,
received_at: Utc::now(),
auth_user: "user".to_string(),
});
assert_eq!(state.smtp_submissions.len(), 1);
assert_eq!(state.smtp_submissions[0].auth_user, "user");
state.reset();
assert!(state.smtp_submissions.is_empty());
}
#[test]
fn event_destination_dispatch_round_trips() {
let mut state = SesState::new("123456789012", "us-east-1");
state
.event_destination_dispatches
.push(EventDestinationDispatch {
destination_name: "fh".to_string(),
destination_type: "firehose".to_string(),
event_type: "SEND".to_string(),
message_id: "msg-1".to_string(),
dispatched_at: Utc::now(),
target_arn: "arn:aws:firehose:us-east-1:123456789012:deliverystream/ds1"
.to_string(),
});
assert_eq!(state.event_destination_dispatches.len(), 1);
assert_eq!(
state.event_destination_dispatches[0].destination_type,
"firehose"
);
}
#[test]
fn reset_preserves_account_region() {
let mut state = SesState::new("123456789012", "eu-west-1");
state.account_settings.sending_enabled = false;
state.reset();
assert_eq!(state.account_id, "123456789012");
assert_eq!(state.region, "eu-west-1");
assert!(state.account_settings.sending_enabled);
}
}