use chrono::Utc;
use http::StatusCode;
use serde_json::{json, Value};
use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
use crate::state::EmailIdentity;
use crate::state::SesState;
use super::SesV2Service;
pub(crate) fn mail_from_dns_records(domain: &str, region: &str) -> Vec<Value> {
if domain.is_empty() {
return Vec::new();
}
vec![
json!({
"Name": domain,
"Type": "MX",
"Value": format!("10 feedback-smtp.{region}.amazonses.com"),
}),
json!({
"Name": domain,
"Type": "TXT",
"Value": "\"v=spf1 include:amazonses.com ~all\"",
}),
]
}
impl SesV2Service {
pub(super) fn create_email_identity(
&self,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let identity_name = match body["EmailIdentity"].as_str() {
Some(name) => name.to_string(),
None => {
return Ok(Self::json_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"EmailIdentity is required",
));
}
};
if identity_name.is_empty() {
return Ok(Self::json_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"EmailIdentity must not be empty",
));
}
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
if state.identities.contains_key(&identity_name) {
return Ok(Self::json_error(
StatusCode::CONFLICT,
"AlreadyExistsException",
&format!("Identity {} already exists", identity_name),
));
}
let identity_type = if identity_name.contains('@') {
"EMAIL_ADDRESS"
} else {
"DOMAIN"
};
let configuration_set_name = body["ConfigurationSetName"].as_str().map(|s| s.to_string());
let (priv_pem, pub_b64) = crate::dkim::generate_easy_dkim_keypair();
let identity = EmailIdentity {
identity_name: identity_name.clone(),
identity_type: identity_type.to_string(),
verified: true,
created_at: Utc::now(),
dkim_signing_enabled: true,
dkim_signing_attributes_origin: "AWS_SES".to_string(),
dkim_domain_signing_private_key: Some(priv_pem),
dkim_domain_signing_selector: Some("fakecloudses".to_string()),
dkim_next_signing_key_length: Some("RSA_2048_BIT".to_string()),
dkim_public_key_b64: Some(pub_b64),
email_forwarding_enabled: true,
mail_from_domain: None,
mail_from_behavior_on_mx_failure: "USE_DEFAULT_VALUE".to_string(),
mail_from_domain_status: "NotStarted".to_string(),
configuration_set_name,
bounce_topic: None,
complaint_topic: None,
delivery_topic: None,
verification_token: None,
};
state.identities.insert(identity_name.clone(), identity);
let arn = format!(
"arn:aws:ses:{}:{}:identity/{}",
req.region, req.account_id, identity_name
);
if let Some(tags_arr) = body["Tags"].as_array() {
let mut tag_map = std::collections::BTreeMap::new();
for tag in tags_arr {
if let (Some(k), Some(v)) = (tag["Key"].as_str(), tag["Value"].as_str()) {
tag_map.insert(k.to_string(), v.to_string());
}
}
state.tags.insert(arn, tag_map);
} else {
state.tags.remove(&arn);
}
let response = json!({
"IdentityType": identity_type,
"VerifiedForSendingStatus": true,
"DkimAttributes": {
"SigningEnabled": true,
"Status": "SUCCESS",
"Tokens": [
"token1",
"token2",
"token3",
],
},
});
Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
}
pub(super) fn list_email_identities(
&self,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let accounts = self.state.read();
let empty = SesState::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
let identities: Vec<Value> = state
.identities
.values()
.map(|id| {
json!({
"IdentityType": id.identity_type,
"IdentityName": id.identity_name,
"SendingEnabled": true,
})
})
.collect();
let response = json!({
"EmailIdentities": identities,
});
Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
}
pub(super) fn get_email_identity(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let region = state.region.clone();
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
let mail_from_domain = identity.mail_from_domain.clone().unwrap_or_default();
if identity.mail_from_domain_status == "Pending" && !mail_from_domain.is_empty() {
identity.mail_from_domain_status = "Success".to_string();
}
if mail_from_domain.is_empty() {
identity.mail_from_domain_status = "NotStarted".to_string();
}
let mail_from_status = identity.mail_from_domain_status.clone();
let behavior = identity.mail_from_behavior_on_mx_failure.clone();
let mail_from_dns = mail_from_dns_records(&mail_from_domain, ®ion);
let mut dkim_attrs = json!({
"SigningEnabled": identity.dkim_signing_enabled,
"Status": "SUCCESS",
"SigningAttributesOrigin": identity.dkim_signing_attributes_origin,
"Tokens": ["token1", "token2", "token3"],
});
if let Some(ref selector) = identity.dkim_domain_signing_selector {
dkim_attrs["LastKeyGenerationTimestamp"] = json!(identity.created_at.timestamp());
dkim_attrs["CurrentSigningKeyLength"] = json!(identity
.dkim_next_signing_key_length
.as_deref()
.unwrap_or("RSA_2048_BIT"));
dkim_attrs["NextSigningKeyLength"] = json!(identity
.dkim_next_signing_key_length
.as_deref()
.unwrap_or("RSA_2048_BIT"));
dkim_attrs["DomainSigningSelector"] = json!(selector);
}
let mut response = json!({
"IdentityType": identity.identity_type,
"VerifiedForSendingStatus": true,
"FeedbackForwardingStatus": identity.email_forwarding_enabled,
"DkimAttributes": dkim_attrs,
"MailFromAttributes": {
"MailFromDomain": mail_from_domain,
"MailFromDomainStatus": mail_from_status,
"BehaviorOnMxFailure": behavior,
},
"Tags": [],
});
if !mail_from_dns.is_empty() {
response["MailFromAttributes"]["MailFromDomainDnsRecords"] = json!(mail_from_dns);
}
let configuration_set_name = identity.configuration_set_name.clone();
let _ = identity;
if let Some(cs) = configuration_set_name {
response["ConfigurationSetName"] = json!(cs);
}
let arn = format!(
"arn:aws:ses:{}:{}:identity/{}",
req.region, req.account_id, identity_name
);
if let Some(tag_map) = state.tags.get(&arn) {
response["Tags"] =
Value::Array(fakecloud_core::tags::tags_to_json(tag_map, "Key", "Value"));
}
Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
}
pub(super) fn delete_email_identity(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
if state.identities.remove(identity_name).is_none() {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
let arn = format!(
"arn:aws:ses:{}:{}:identity/{}",
req.region, req.account_id, identity_name
);
state.tags.remove(&arn);
state.identity_policies.remove(identity_name);
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn create_email_identity_policy(
&self,
identity_name: &str,
policy_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
Self::require_nonempty("EmailIdentity", identity_name)?;
Self::require_nonempty("PolicyName", policy_name)?;
if policy_name.starts_with('{') && policy_name.ends_with('}') {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"PolicyName is required",
));
}
if policy_name.is_empty() || policy_name.len() > 64 {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"PolicyName length must be between 1 and 64",
));
}
if !policy_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"PolicyName must match pattern [a-zA-Z0-9_-]+",
));
}
let body: Value = Self::parse_body(req)?;
let policy = match body["Policy"].as_str() {
Some(p) => p.to_string(),
None => {
return Ok(Self::json_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"Policy is required",
));
}
};
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
if !state.identities.contains_key(identity_name) {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
let policies = state
.identity_policies
.entry(identity_name.to_string())
.or_default();
if policies.contains_key(policy_name) {
return Ok(Self::json_error(
StatusCode::CONFLICT,
"AlreadyExistsException",
&format!("Policy {} already exists", policy_name),
));
}
policies.insert(policy_name.to_string(), policy);
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn get_email_identity_policies(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let accounts = self.state.read();
let empty = SesState::new(&req.account_id, &req.region);
let state = accounts.get(&req.account_id).unwrap_or(&empty);
if !state.identities.contains_key(identity_name) {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
let policies = state
.identity_policies
.get(identity_name)
.cloned()
.unwrap_or_default();
let policies_json: Value = policies
.into_iter()
.map(|(k, v)| (k, Value::String(v)))
.collect::<serde_json::Map<String, Value>>()
.into();
let response = json!({
"Policies": policies_json,
});
Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
}
pub(super) fn update_email_identity_policy(
&self,
identity_name: &str,
policy_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let policy = match body["Policy"].as_str() {
Some(p) => p.to_string(),
None => {
return Ok(Self::json_error(
StatusCode::BAD_REQUEST,
"BadRequestException",
"Policy is required",
));
}
};
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
if !state.identities.contains_key(identity_name) {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
let policies = state
.identity_policies
.entry(identity_name.to_string())
.or_default();
if !policies.contains_key(policy_name) {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Policy {} does not exist", policy_name),
));
}
policies.insert(policy_name.to_string(), policy);
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn delete_email_identity_policy(
&self,
identity_name: &str,
policy_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
if !state.identities.contains_key(identity_name) {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
let policies = state
.identity_policies
.entry(identity_name.to_string())
.or_default();
if policies.remove(policy_name).is_none() {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Policy {} does not exist", policy_name),
));
}
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn put_email_identity_dkim_attributes(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
if let Some(enabled) = body["SigningEnabled"].as_bool() {
identity.dkim_signing_enabled = enabled;
}
if identity.dkim_signing_enabled && identity.dkim_domain_signing_private_key.is_none() {
let (priv_pem, pub_b64) = crate::dkim::generate_easy_dkim_keypair();
identity.dkim_domain_signing_private_key = Some(priv_pem);
identity.dkim_public_key_b64 = Some(pub_b64);
if identity.dkim_domain_signing_selector.is_none() {
identity.dkim_domain_signing_selector = Some("fakecloudses".to_string());
}
if identity.dkim_next_signing_key_length.is_none() {
identity.dkim_next_signing_key_length = Some("RSA_2048_BIT".to_string());
}
}
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn put_email_identity_dkim_signing_attributes(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
let origin = body["SigningAttributesOrigin"]
.as_str()
.unwrap_or(&identity.dkim_signing_attributes_origin)
.to_string();
identity.dkim_signing_attributes_origin = origin.clone();
if let Some(attrs) = body.get("SigningAttributes") {
if let Some(key) = attrs["DomainSigningPrivateKey"].as_str() {
identity.dkim_domain_signing_private_key = Some(key.to_string());
identity.dkim_public_key_b64 = None;
}
if let Some(selector) = attrs["DomainSigningSelector"].as_str() {
identity.dkim_domain_signing_selector = Some(selector.to_string());
}
if let Some(length) = attrs["NextSigningKeyLength"].as_str() {
identity.dkim_next_signing_key_length = Some(length.to_string());
}
}
if origin == "AWS_SES" && identity.dkim_domain_signing_private_key.is_none() {
let (priv_pem, pub_b64) = crate::dkim::generate_easy_dkim_keypair();
identity.dkim_domain_signing_private_key = Some(priv_pem);
identity.dkim_public_key_b64 = Some(pub_b64);
if identity.dkim_domain_signing_selector.is_none() {
identity.dkim_domain_signing_selector = Some("fakecloudses".to_string());
}
}
let response = json!({
"DkimStatus": "SUCCESS",
"DkimTokens": ["token1", "token2", "token3"],
});
Ok(AwsResponse::json(StatusCode::OK, response.to_string()))
}
pub(super) fn put_email_identity_feedback_attributes(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
identity.email_forwarding_enabled =
body["EmailForwardingEnabled"].as_bool().unwrap_or(false);
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn put_email_identity_mail_from_attributes(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
if let Some(domain) = body["MailFromDomain"].as_str() {
let trimmed = domain.trim();
if trimmed.is_empty() {
identity.mail_from_domain = None;
identity.mail_from_domain_status = "NotStarted".to_string();
} else {
identity.mail_from_domain = Some(trimmed.to_string());
identity.mail_from_domain_status = "Pending".to_string();
}
}
if let Some(behavior) = body["BehaviorOnMxFailure"].as_str() {
identity.mail_from_behavior_on_mx_failure = behavior.to_string();
}
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
pub(super) fn put_email_identity_configuration_set_attributes(
&self,
identity_name: &str,
req: &AwsRequest,
) -> Result<AwsResponse, AwsServiceError> {
let body: Value = Self::parse_body(req)?;
let mut accounts = self.state.write();
let state = accounts.get_or_create(&req.account_id);
let identity = match state.identities.get_mut(identity_name) {
Some(id) => id,
None => {
return Ok(Self::json_error(
StatusCode::NOT_FOUND,
"NotFoundException",
&format!("Identity {} does not exist", identity_name),
));
}
};
identity.configuration_set_name =
body["ConfigurationSetName"].as_str().map(|s| s.to_string());
Ok(AwsResponse::json(StatusCode::OK, "{}"))
}
}