1use std::sync::Arc;
11
12use rustack_ses_model::{
13 error::{SesError, SesErrorCode},
14 input::{
15 CloneReceiptRuleSetInput, CreateConfigurationSetEventDestinationInput,
16 CreateConfigurationSetInput, CreateReceiptRuleInput, CreateReceiptRuleSetInput,
17 CreateTemplateInput, DeleteConfigurationSetEventDestinationInput,
18 DeleteConfigurationSetInput, DeleteIdentityInput, DeleteIdentityPolicyInput,
19 DeleteReceiptRuleInput, DeleteReceiptRuleSetInput, DeleteTemplateInput,
20 DeleteVerifiedEmailAddressInput, DescribeActiveReceiptRuleSetInput,
21 DescribeConfigurationSetInput, DescribeReceiptRuleSetInput, GetIdentityDkimAttributesInput,
22 GetIdentityMailFromDomainAttributesInput, GetIdentityNotificationAttributesInput,
23 GetIdentityPoliciesInput, GetIdentityVerificationAttributesInput, GetTemplateInput,
24 ListConfigurationSetsInput, ListIdentitiesInput, ListIdentityPoliciesInput,
25 ListTemplatesInput, PutIdentityPolicyInput, SendEmailInput, SendRawEmailInput,
26 SendTemplatedEmailInput, SetActiveReceiptRuleSetInput,
27 SetIdentityFeedbackForwardingEnabledInput, SetIdentityMailFromDomainInput,
28 SetIdentityNotificationTopicInput, UpdateConfigurationSetEventDestinationInput,
29 UpdateTemplateInput, VerifyDomainDkimInput, VerifyDomainIdentityInput,
30 VerifyEmailAddressInput, VerifyEmailIdentityInput,
31 },
32 output::{
33 CloneReceiptRuleSetResponse, CreateConfigurationSetEventDestinationResponse,
34 CreateConfigurationSetResponse, CreateReceiptRuleResponse, CreateReceiptRuleSetResponse,
35 CreateTemplateResponse, DeleteConfigurationSetEventDestinationResponse,
36 DeleteConfigurationSetResponse, DeleteIdentityPolicyResponse, DeleteIdentityResponse,
37 DeleteReceiptRuleResponse, DeleteReceiptRuleSetResponse, DeleteTemplateResponse,
38 DescribeActiveReceiptRuleSetResponse, DescribeConfigurationSetResponse,
39 DescribeReceiptRuleSetResponse, GetIdentityDkimAttributesResponse,
40 GetIdentityMailFromDomainAttributesResponse, GetIdentityNotificationAttributesResponse,
41 GetIdentityPoliciesResponse, GetIdentityVerificationAttributesResponse,
42 GetSendQuotaResponse, GetSendStatisticsResponse, GetTemplateResponse,
43 ListConfigurationSetsResponse, ListIdentitiesResponse, ListIdentityPoliciesResponse,
44 ListTemplatesResponse, ListVerifiedEmailAddressesResponse, PutIdentityPolicyResponse,
45 SendEmailResponse, SendRawEmailResponse, SendTemplatedEmailResponse,
46 SetActiveReceiptRuleSetResponse, SetIdentityFeedbackForwardingEnabledResponse,
47 SetIdentityMailFromDomainResponse, SetIdentityNotificationTopicResponse,
48 UpdateConfigurationSetEventDestinationResponse, UpdateTemplateResponse,
49 VerifyDomainDkimResponse, VerifyDomainIdentityResponse, VerifyEmailIdentityResponse,
50 },
51 types::{ConfigurationSet, IdentityType, MessageTag, ReceiptRuleSetMetadata, SendDataPoint},
52};
53use tracing::debug;
54
55use crate::{
56 config::SesConfig,
57 config_set::ConfigurationSetStore,
58 identity::IdentityStore,
59 receipt_rule::ReceiptRuleSetStore,
60 retrospection::{EmailStore, SentEmail, SentEmailBody, SentEmailDestination, SentEmailTag},
61 statistics::SendStatistics,
62 template::{TemplateStore, render_template},
63 validation::validate_tags,
64};
65
66fn validate_message_tags(tags: &[MessageTag]) -> Result<(), SesError> {
72 let pairs: Vec<(String, String)> = tags
73 .iter()
74 .map(|t| (t.name.clone(), t.value.clone()))
75 .collect();
76 validate_tags(&pairs)
77}
78
79fn convert_tags(tags: &[MessageTag]) -> Vec<SentEmailTag> {
81 tags.iter()
82 .map(|t| SentEmailTag {
83 name: t.name.clone(),
84 value: t.value.clone(),
85 })
86 .collect()
87}
88
89#[derive(Debug)]
91pub struct RustackSes {
92 pub(crate) identities: Arc<IdentityStore>,
94 pub(crate) emails: Arc<EmailStore>,
96 pub(crate) templates: Arc<TemplateStore>,
98 pub(crate) config_sets: Arc<ConfigurationSetStore>,
100 pub(crate) receipt_rules: Arc<ReceiptRuleSetStore>,
102 pub(crate) statistics: Arc<SendStatistics>,
104 pub(crate) config: Arc<SesConfig>,
106}
107
108#[allow(clippy::needless_pass_by_value)]
109impl RustackSes {
110 #[must_use]
112 pub fn new(config: SesConfig) -> Self {
113 Self {
114 identities: Arc::new(IdentityStore::new()),
115 emails: Arc::new(EmailStore::new()),
116 templates: Arc::new(TemplateStore::new()),
117 config_sets: Arc::new(ConfigurationSetStore::new()),
118 receipt_rules: Arc::new(ReceiptRuleSetStore::new()),
119 statistics: Arc::new(SendStatistics::new()),
120 config: Arc::new(config),
121 }
122 }
123
124 #[must_use]
126 pub fn email_store(&self) -> &Arc<EmailStore> {
127 &self.emails
128 }
129
130 #[must_use]
132 pub fn config(&self) -> &SesConfig {
133 &self.config
134 }
135
136 pub fn verify_email_identity(
142 &self,
143 input: VerifyEmailIdentityInput,
144 ) -> Result<VerifyEmailIdentityResponse, SesError> {
145 debug!(email = %input.email_address, "verify email identity");
146 let _ = self.identities.verify_email(&input.email_address);
147 Ok(VerifyEmailIdentityResponse {})
148 }
149
150 pub fn verify_domain_identity(
152 &self,
153 input: VerifyDomainIdentityInput,
154 ) -> Result<VerifyDomainIdentityResponse, SesError> {
155 debug!(domain = %input.domain, "verify domain identity");
156 let (_, token) = self.identities.verify_domain(&input.domain);
157 Ok(VerifyDomainIdentityResponse {
158 verification_token: token,
159 })
160 }
161
162 pub fn list_identities(
164 &self,
165 input: ListIdentitiesInput,
166 ) -> Result<ListIdentitiesResponse, SesError> {
167 let identities = self.identities.list(input.identity_type.as_ref());
168 Ok(ListIdentitiesResponse {
169 identities,
170 next_token: None,
171 })
172 }
173
174 pub fn delete_identity(
176 &self,
177 input: DeleteIdentityInput,
178 ) -> Result<DeleteIdentityResponse, SesError> {
179 debug!(identity = %input.identity, "delete identity");
180 self.identities.delete(&input.identity);
181 Ok(DeleteIdentityResponse {})
182 }
183
184 pub fn get_identity_verification_attributes(
186 &self,
187 input: GetIdentityVerificationAttributesInput,
188 ) -> Result<GetIdentityVerificationAttributesResponse, SesError> {
189 let verification_attributes = self
190 .identities
191 .get_verification_attributes(&input.identities);
192 Ok(GetIdentityVerificationAttributesResponse {
193 verification_attributes,
194 })
195 }
196
197 pub fn verify_email_address(&self, input: VerifyEmailAddressInput) -> Result<(), SesError> {
199 debug!(email = %input.email_address, "verify email address (legacy)");
200 let _ = self.identities.verify_email(&input.email_address);
201 Ok(())
202 }
203
204 pub fn delete_verified_email_address(
206 &self,
207 input: DeleteVerifiedEmailAddressInput,
208 ) -> Result<(), SesError> {
209 self.identities.delete(&input.email_address);
210 Ok(())
211 }
212
213 pub fn list_verified_email_addresses(
215 &self,
216 ) -> Result<ListVerifiedEmailAddressesResponse, SesError> {
217 let verified_email_addresses = self.identities.list(Some(&IdentityType::EmailAddress));
218 Ok(ListVerifiedEmailAddressesResponse {
219 verified_email_addresses,
220 })
221 }
222
223 pub fn send_email(&self, input: SendEmailInput) -> Result<SendEmailResponse, SesError> {
225 validate_message_tags(&input.tags)?;
226
227 if self.config.require_verified_identity && !self.identities.is_verified(&input.source) {
229 return Err(SesError::with_message(
230 SesErrorCode::MessageRejected,
231 format!(
232 "Email address is not verified. The following identities failed the check in \
233 region {}: {}",
234 self.config.default_region, input.source
235 ),
236 ));
237 }
238
239 let message_id = uuid::Uuid::new_v4().to_string();
240 let sent = SentEmail {
241 id: message_id.clone(),
242 region: self.config.default_region.clone(),
243 timestamp: chrono::Utc::now().to_rfc3339(),
244 source: input.source,
245 destination: SentEmailDestination {
246 to_addresses: input.destination.to_addresses,
247 cc_addresses: input.destination.cc_addresses,
248 bcc_addresses: input.destination.bcc_addresses,
249 },
250 subject: Some(input.message.subject.data.clone()),
251 body: Some(SentEmailBody {
252 text_part: input.message.body.text.as_ref().map(|t| t.data.clone()),
253 html_part: input.message.body.html.as_ref().map(|h| h.data.clone()),
254 }),
255 raw_data: None,
256 template: None,
257 template_data: None,
258 tags: convert_tags(&input.tags),
259 };
260 self.emails.capture(sent);
261 self.statistics.record_send();
262 debug!(message_id = %message_id, "email sent");
263 Ok(SendEmailResponse { message_id })
264 }
265
266 pub fn send_raw_email(
268 &self,
269 input: SendRawEmailInput,
270 ) -> Result<SendRawEmailResponse, SesError> {
271 validate_message_tags(&input.tags)?;
272
273 let source = input
274 .source
275 .clone()
276 .unwrap_or_else(|| extract_from_raw(&input.raw_message.data));
277
278 if source.is_empty() {
279 return Err(SesError::with_message(
280 SesErrorCode::MessageRejected,
281 "Could not determine source address from raw message.",
282 ));
283 }
284
285 if self.config.require_verified_identity && !self.identities.is_verified(&source) {
286 return Err(SesError::with_message(
287 SesErrorCode::MessageRejected,
288 format!(
289 "Email address is not verified. The following identities failed the check in \
290 region {}: {source}",
291 self.config.default_region
292 ),
293 ));
294 }
295
296 let message_id = uuid::Uuid::new_v4().to_string();
297 let raw_str = String::from_utf8_lossy(&input.raw_message.data).into_owned();
298 let sent = SentEmail {
299 id: message_id.clone(),
300 region: self.config.default_region.clone(),
301 timestamp: chrono::Utc::now().to_rfc3339(),
302 source,
303 destination: SentEmailDestination {
304 to_addresses: input.destinations,
305 cc_addresses: Vec::new(),
306 bcc_addresses: Vec::new(),
307 },
308 subject: None,
309 body: None,
310 raw_data: Some(raw_str),
311 template: None,
312 template_data: None,
313 tags: convert_tags(&input.tags),
314 };
315 self.emails.capture(sent);
316 self.statistics.record_send();
317 debug!(message_id = %message_id, "raw email sent");
318 Ok(SendRawEmailResponse { message_id })
319 }
320
321 #[allow(clippy::cast_precision_loss)]
323 pub fn get_send_quota(&self) -> Result<GetSendQuotaResponse, SesError> {
324 Ok(GetSendQuotaResponse {
325 max24_hour_send: Some(self.config.max_24_hour_send),
326 max_send_rate: Some(self.config.max_send_rate),
327 sent_last24_hours: Some(self.emails.total_sent() as f64),
328 })
329 }
330
331 #[allow(clippy::cast_possible_wrap)]
333 pub fn get_send_statistics(&self) -> Result<GetSendStatisticsResponse, SesError> {
334 let stats = self.statistics.get_stats();
335 let data_point = SendDataPoint {
336 delivery_attempts: Some(stats.delivery_attempts as i64),
337 bounces: Some(stats.bounce_count as i64),
338 complaints: Some(stats.complaint_count as i64),
339 rejects: Some(stats.reject_count as i64),
340 timestamp: Some(chrono::Utc::now()),
341 };
342 Ok(GetSendStatisticsResponse {
343 send_data_points: vec![data_point],
344 })
345 }
346
347 pub fn create_template(
353 &self,
354 input: CreateTemplateInput,
355 ) -> Result<CreateTemplateResponse, SesError> {
356 debug!(template = %input.template.template_name, "create template");
357 self.templates.create(input.template)?;
358 Ok(CreateTemplateResponse {})
359 }
360
361 pub fn get_template(&self, input: GetTemplateInput) -> Result<GetTemplateResponse, SesError> {
363 let template = self.templates.get(&input.template_name)?;
364 Ok(GetTemplateResponse {
365 template: Some(template),
366 })
367 }
368
369 pub fn update_template(
371 &self,
372 input: UpdateTemplateInput,
373 ) -> Result<UpdateTemplateResponse, SesError> {
374 debug!(template = %input.template.template_name, "update template");
375 self.templates.update(input.template)?;
376 Ok(UpdateTemplateResponse {})
377 }
378
379 pub fn delete_template(
381 &self,
382 input: DeleteTemplateInput,
383 ) -> Result<DeleteTemplateResponse, SesError> {
384 debug!(template = %input.template_name, "delete template");
385 self.templates.delete(&input.template_name);
386 Ok(DeleteTemplateResponse {})
387 }
388
389 pub fn list_templates(
391 &self,
392 _input: ListTemplatesInput,
393 ) -> Result<ListTemplatesResponse, SesError> {
394 let templates_metadata = self.templates.list();
395 Ok(ListTemplatesResponse {
396 templates_metadata,
397 next_token: None,
398 })
399 }
400
401 pub fn send_templated_email(
403 &self,
404 input: SendTemplatedEmailInput,
405 ) -> Result<SendTemplatedEmailResponse, SesError> {
406 validate_message_tags(&input.tags)?;
407
408 if self.config.require_verified_identity && !self.identities.is_verified(&input.source) {
409 return Err(SesError::with_message(
410 SesErrorCode::MessageRejected,
411 format!(
412 "Email address is not verified. The following identities failed the check in \
413 region {}: {}",
414 self.config.default_region, input.source
415 ),
416 ));
417 }
418
419 let template = self.templates.get(&input.template)?;
420 let rendered_subject = template
421 .subject_part
422 .as_deref()
423 .map(|s| render_template(s, &input.template_data))
424 .transpose()?;
425 let rendered_text = template
426 .text_part
427 .as_deref()
428 .map(|s| render_template(s, &input.template_data))
429 .transpose()?;
430 let rendered_html = template
431 .html_part
432 .as_deref()
433 .map(|s| render_template(s, &input.template_data))
434 .transpose()?;
435
436 let message_id = uuid::Uuid::new_v4().to_string();
437 let sent = SentEmail {
438 id: message_id.clone(),
439 region: self.config.default_region.clone(),
440 timestamp: chrono::Utc::now().to_rfc3339(),
441 source: input.source,
442 destination: SentEmailDestination {
443 to_addresses: input.destination.to_addresses,
444 cc_addresses: input.destination.cc_addresses,
445 bcc_addresses: input.destination.bcc_addresses,
446 },
447 subject: rendered_subject,
448 body: Some(SentEmailBody {
449 text_part: rendered_text,
450 html_part: rendered_html,
451 }),
452 raw_data: None,
453 template: Some(input.template),
454 template_data: Some(input.template_data),
455 tags: convert_tags(&input.tags),
456 };
457 self.emails.capture(sent);
458 self.statistics.record_send();
459 debug!(message_id = %message_id, "templated email sent");
460 Ok(SendTemplatedEmailResponse { message_id })
461 }
462
463 pub fn create_configuration_set(
465 &self,
466 input: CreateConfigurationSetInput,
467 ) -> Result<CreateConfigurationSetResponse, SesError> {
468 debug!(name = %input.configuration_set.name, "create configuration set");
469 self.config_sets.create(&input.configuration_set.name)?;
470 Ok(CreateConfigurationSetResponse {})
471 }
472
473 pub fn delete_configuration_set(
475 &self,
476 input: DeleteConfigurationSetInput,
477 ) -> Result<DeleteConfigurationSetResponse, SesError> {
478 debug!(name = %input.configuration_set_name, "delete configuration set");
479 self.config_sets.delete(&input.configuration_set_name)?;
480 Ok(DeleteConfigurationSetResponse {})
481 }
482
483 pub fn describe_configuration_set(
485 &self,
486 input: DescribeConfigurationSetInput,
487 ) -> Result<DescribeConfigurationSetResponse, SesError> {
488 let record = self.config_sets.describe(&input.configuration_set_name)?;
489 Ok(DescribeConfigurationSetResponse {
490 configuration_set: Some(ConfigurationSet { name: record.name }),
491 event_destinations: record.event_destinations,
492 reputation_options: None,
493 tracking_options: None,
494 delivery_options: None,
495 })
496 }
497
498 pub fn list_configuration_sets(
500 &self,
501 _input: ListConfigurationSetsInput,
502 ) -> Result<ListConfigurationSetsResponse, SesError> {
503 let names = self.config_sets.list();
504 let configuration_sets = names
505 .into_iter()
506 .map(|name| ConfigurationSet { name })
507 .collect();
508 Ok(ListConfigurationSetsResponse {
509 configuration_sets,
510 next_token: None,
511 })
512 }
513
514 pub fn create_configuration_set_event_destination(
520 &self,
521 input: CreateConfigurationSetEventDestinationInput,
522 ) -> Result<CreateConfigurationSetEventDestinationResponse, SesError> {
523 self.config_sets
524 .add_event_destination(&input.configuration_set_name, input.event_destination)?;
525 Ok(CreateConfigurationSetEventDestinationResponse {})
526 }
527
528 pub fn update_configuration_set_event_destination(
530 &self,
531 input: UpdateConfigurationSetEventDestinationInput,
532 ) -> Result<UpdateConfigurationSetEventDestinationResponse, SesError> {
533 self.config_sets
534 .update_event_destination(&input.configuration_set_name, input.event_destination)?;
535 Ok(UpdateConfigurationSetEventDestinationResponse {})
536 }
537
538 pub fn delete_configuration_set_event_destination(
540 &self,
541 input: DeleteConfigurationSetEventDestinationInput,
542 ) -> Result<DeleteConfigurationSetEventDestinationResponse, SesError> {
543 self.config_sets.delete_event_destination(
544 &input.configuration_set_name,
545 &input.event_destination_name,
546 )?;
547 Ok(DeleteConfigurationSetEventDestinationResponse {})
548 }
549
550 pub fn create_receipt_rule_set(
552 &self,
553 input: CreateReceiptRuleSetInput,
554 ) -> Result<CreateReceiptRuleSetResponse, SesError> {
555 debug!(name = %input.rule_set_name, "create receipt rule set");
556 self.receipt_rules.create_rule_set(&input.rule_set_name)?;
557 Ok(CreateReceiptRuleSetResponse {})
558 }
559
560 pub fn delete_receipt_rule_set(
562 &self,
563 input: DeleteReceiptRuleSetInput,
564 ) -> Result<DeleteReceiptRuleSetResponse, SesError> {
565 self.receipt_rules.delete_rule_set(&input.rule_set_name)?;
566 Ok(DeleteReceiptRuleSetResponse {})
567 }
568
569 pub fn create_receipt_rule(
571 &self,
572 input: CreateReceiptRuleInput,
573 ) -> Result<CreateReceiptRuleResponse, SesError> {
574 self.receipt_rules
575 .create_rule(&input.rule_set_name, input.rule, input.after.as_deref())?;
576 Ok(CreateReceiptRuleResponse {})
577 }
578
579 pub fn delete_receipt_rule(
581 &self,
582 input: DeleteReceiptRuleInput,
583 ) -> Result<DeleteReceiptRuleResponse, SesError> {
584 self.receipt_rules
585 .delete_rule(&input.rule_set_name, &input.rule_name)?;
586 Ok(DeleteReceiptRuleResponse {})
587 }
588
589 pub fn describe_receipt_rule_set(
591 &self,
592 input: DescribeReceiptRuleSetInput,
593 ) -> Result<DescribeReceiptRuleSetResponse, SesError> {
594 let record = self.receipt_rules.describe_rule_set(&input.rule_set_name)?;
595 Ok(DescribeReceiptRuleSetResponse {
596 metadata: Some(ReceiptRuleSetMetadata {
597 name: Some(record.name),
598 created_timestamp: Some(record.created_timestamp),
599 }),
600 rules: record.rules,
601 })
602 }
603
604 pub fn clone_receipt_rule_set(
606 &self,
607 input: CloneReceiptRuleSetInput,
608 ) -> Result<CloneReceiptRuleSetResponse, SesError> {
609 self.receipt_rules
610 .clone_rule_set(&input.original_rule_set_name, &input.rule_set_name)?;
611 Ok(CloneReceiptRuleSetResponse {})
612 }
613
614 pub fn describe_active_receipt_rule_set(
616 &self,
617 _input: DescribeActiveReceiptRuleSetInput,
618 ) -> Result<DescribeActiveReceiptRuleSetResponse, SesError> {
619 if let Some((metadata, rules)) = self.receipt_rules.get_active_rule_set() {
620 Ok(DescribeActiveReceiptRuleSetResponse {
621 metadata: Some(metadata),
622 rules,
623 })
624 } else {
625 Ok(DescribeActiveReceiptRuleSetResponse {
626 metadata: None,
627 rules: Vec::new(),
628 })
629 }
630 }
631
632 pub fn set_active_receipt_rule_set(
634 &self,
635 input: SetActiveReceiptRuleSetInput,
636 ) -> Result<SetActiveReceiptRuleSetResponse, SesError> {
637 self.receipt_rules
638 .set_active_rule_set(input.rule_set_name.as_deref())?;
639 Ok(SetActiveReceiptRuleSetResponse {})
640 }
641
642 pub fn set_identity_notification_topic(
648 &self,
649 input: SetIdentityNotificationTopicInput,
650 ) -> Result<SetIdentityNotificationTopicResponse, SesError> {
651 self.identities.set_notification_topic(
652 &input.identity,
653 &input.notification_type,
654 input.sns_topic,
655 );
656 Ok(SetIdentityNotificationTopicResponse {})
657 }
658
659 pub fn set_identity_feedback_forwarding_enabled(
661 &self,
662 input: SetIdentityFeedbackForwardingEnabledInput,
663 ) -> Result<SetIdentityFeedbackForwardingEnabledResponse, SesError> {
664 self.identities
665 .set_feedback_forwarding_enabled(&input.identity, input.forwarding_enabled);
666 Ok(SetIdentityFeedbackForwardingEnabledResponse {})
667 }
668
669 pub fn get_identity_notification_attributes(
671 &self,
672 input: GetIdentityNotificationAttributesInput,
673 ) -> Result<GetIdentityNotificationAttributesResponse, SesError> {
674 let notification_attributes = self
675 .identities
676 .get_notification_attributes(&input.identities);
677 Ok(GetIdentityNotificationAttributesResponse {
678 notification_attributes,
679 })
680 }
681
682 pub fn verify_domain_dkim(
684 &self,
685 input: VerifyDomainDkimInput,
686 ) -> Result<VerifyDomainDkimResponse, SesError> {
687 let dkim_tokens = self.identities.verify_domain_dkim(&input.domain);
688 Ok(VerifyDomainDkimResponse { dkim_tokens })
689 }
690
691 pub fn get_identity_dkim_attributes(
693 &self,
694 input: GetIdentityDkimAttributesInput,
695 ) -> Result<GetIdentityDkimAttributesResponse, SesError> {
696 let dkim_attributes = self.identities.get_dkim_attributes(&input.identities);
697 Ok(GetIdentityDkimAttributesResponse { dkim_attributes })
698 }
699
700 pub fn set_identity_mail_from_domain(
702 &self,
703 input: SetIdentityMailFromDomainInput,
704 ) -> Result<SetIdentityMailFromDomainResponse, SesError> {
705 self.identities.set_mail_from_domain(
706 &input.identity,
707 input.mail_from_domain,
708 input.behavior_on_mx_failure,
709 );
710 Ok(SetIdentityMailFromDomainResponse {})
711 }
712
713 pub fn get_identity_mail_from_domain_attributes(
715 &self,
716 input: GetIdentityMailFromDomainAttributesInput,
717 ) -> Result<GetIdentityMailFromDomainAttributesResponse, SesError> {
718 let mail_from_domain_attributes = self
719 .identities
720 .get_mail_from_domain_attributes(&input.identities);
721 Ok(GetIdentityMailFromDomainAttributesResponse {
722 mail_from_domain_attributes,
723 })
724 }
725
726 pub fn get_identity_policies(
728 &self,
729 input: GetIdentityPoliciesInput,
730 ) -> Result<GetIdentityPoliciesResponse, SesError> {
731 let policies = self
732 .identities
733 .get_policies(&input.identity, &input.policy_names);
734 Ok(GetIdentityPoliciesResponse { policies })
735 }
736
737 pub fn put_identity_policy(
739 &self,
740 input: PutIdentityPolicyInput,
741 ) -> Result<PutIdentityPolicyResponse, SesError> {
742 self.identities
743 .put_policy(&input.identity, &input.policy_name, &input.policy);
744 Ok(PutIdentityPolicyResponse {})
745 }
746
747 pub fn delete_identity_policy(
749 &self,
750 input: DeleteIdentityPolicyInput,
751 ) -> Result<DeleteIdentityPolicyResponse, SesError> {
752 self.identities
753 .delete_policy(&input.identity, &input.policy_name);
754 Ok(DeleteIdentityPolicyResponse {})
755 }
756
757 pub fn list_identity_policies(
759 &self,
760 input: ListIdentityPoliciesInput,
761 ) -> Result<ListIdentityPoliciesResponse, SesError> {
762 let policy_names = self.identities.list_policy_names(&input.identity);
763 Ok(ListIdentityPoliciesResponse { policy_names })
764 }
765}
766
767fn extract_from_raw(data: &[u8]) -> String {
771 let text = String::from_utf8_lossy(data);
772 for line in text.lines() {
773 if line.len() >= 5 && line[..5].eq_ignore_ascii_case("from:") {
774 let addr = line[5..].trim();
775 if let Some(start) = addr.find('<') {
777 if let Some(end) = addr.find('>') {
778 return addr[start + 1..end].to_owned();
779 }
780 }
781 return addr.to_owned();
782 }
783 }
784 String::new()
785}
786
787#[cfg(test)]
788mod tests {
789 use rustack_ses_model::types::{Body, Content, Destination, Message, Template};
790
791 use super::*;
792
793 fn make_provider() -> RustackSes {
794 RustackSes::new(SesConfig::default())
795 }
796
797 #[test]
798 fn test_should_verify_and_list_email() {
799 let p = make_provider();
800 p.verify_email_identity(VerifyEmailIdentityInput {
801 email_address: "test@example.com".to_owned(),
802 })
803 .unwrap_or_default();
804 let resp = p
805 .list_identities(ListIdentitiesInput::default())
806 .unwrap_or_default();
807 assert!(resp.identities.contains(&"test@example.com".to_owned()));
808 }
809
810 #[test]
811 fn test_should_verify_domain_and_return_token() {
812 let p = make_provider();
813 let resp = p
814 .verify_domain_identity(VerifyDomainIdentityInput {
815 domain: "example.com".to_owned(),
816 })
817 .unwrap_or_default();
818 assert!(!resp.verification_token.is_empty());
819 }
820
821 #[test]
822 fn test_should_send_email_and_capture() {
823 let p = make_provider();
824 let resp = p
825 .send_email(SendEmailInput {
826 source: "sender@example.com".to_owned(),
827 destination: Destination {
828 to_addresses: vec!["recipient@example.com".to_owned()],
829 ..Destination::default()
830 },
831 message: Message {
832 subject: Content {
833 data: "Test".to_owned(),
834 ..Content::default()
835 },
836 body: Body {
837 text: Some(Content {
838 data: "Hello".to_owned(),
839 ..Content::default()
840 }),
841 ..Body::default()
842 },
843 },
844 ..SendEmailInput::default()
845 })
846 .unwrap_or_default();
847 assert!(!resp.message_id.is_empty());
848 let emails = p.emails.query(None, None);
849 assert_eq!(emails.len(), 1);
850 assert_eq!(emails[0].subject.as_deref(), Some("Test"));
851 }
852
853 #[test]
854 fn test_should_reject_unverified_in_strict_mode() {
855 let p = RustackSes::new(SesConfig {
856 require_verified_identity: true,
857 ..SesConfig::default()
858 });
859 let result = p.send_email(SendEmailInput {
860 source: "unverified@example.com".to_owned(),
861 destination: Destination {
862 to_addresses: vec!["r@e.com".to_owned()],
863 ..Destination::default()
864 },
865 message: Message {
866 subject: Content {
867 data: "Test".to_owned(),
868 ..Content::default()
869 },
870 body: Body::default(),
871 },
872 ..SendEmailInput::default()
873 });
874 assert!(result.is_err());
875 }
876
877 #[test]
878 fn test_should_send_templated_email() {
879 let p = make_provider();
880 p.create_template(CreateTemplateInput {
881 template: Template {
882 template_name: "welcome".to_owned(),
883 subject_part: Some("Hello {{name}}".to_owned()),
884 text_part: Some("Welcome {{name}}!".to_owned()),
885 html_part: None,
886 },
887 })
888 .unwrap_or_default();
889 let resp = p
890 .send_templated_email(SendTemplatedEmailInput {
891 source: "s@e.com".to_owned(),
892 destination: Destination {
893 to_addresses: vec!["r@e.com".to_owned()],
894 ..Destination::default()
895 },
896 template: "welcome".to_owned(),
897 template_data: r#"{"name":"World"}"#.to_owned(),
898 ..SendTemplatedEmailInput::default()
899 })
900 .unwrap_or_default();
901 assert!(!resp.message_id.is_empty());
902 let emails = p.emails.query(None, None);
903 assert_eq!(emails[0].subject.as_deref(), Some("Hello World"));
904 }
905
906 #[test]
907 fn test_should_get_send_quota() {
908 let p = make_provider();
909 let resp = p.get_send_quota().unwrap_or_default();
910 assert!((resp.max24_hour_send.unwrap_or_default() - 200.0).abs() < f64::EPSILON);
911 }
912
913 #[test]
914 fn test_should_extract_from_raw() {
915 assert_eq!(
916 extract_from_raw(b"From: sender@example.com\r\nSubject: Test"),
917 "sender@example.com"
918 );
919 assert_eq!(
920 extract_from_raw(b"From: John Doe <john@example.com>\r\n"),
921 "john@example.com"
922 );
923 assert_eq!(extract_from_raw(b"Subject: No From"), "");
924 }
925
926 #[test]
927 fn test_should_extract_from_raw_case_insensitive() {
928 assert_eq!(
929 extract_from_raw(b"from: lower@example.com\r\nSubject: Test"),
930 "lower@example.com"
931 );
932 assert_eq!(
933 extract_from_raw(b"FROM: UPPER@example.com\r\nSubject: Test"),
934 "UPPER@example.com"
935 );
936 assert_eq!(
937 extract_from_raw(b"fRoM: mixed@example.com\r\nSubject: Test"),
938 "mixed@example.com"
939 );
940 }
941}