Skip to main content

rustack_ses_core/
provider.rs

1//! Main SES provider implementing all operations across all 4 phases.
2//!
3//! Acts as the central coordinator that owns all stores (identity, email,
4//! template, configuration set, receipt rule, statistics) and implements
5//! each SES operation as a method.
6//!
7//! Operation methods take input structs by value (matching the SNS provider
8//! pattern) since callers always construct inputs immediately before calling.
9
10use 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
66/// Validate a slice of `MessageTag` values.
67///
68/// # Errors
69///
70/// Returns the first validation error encountered for any tag name or value.
71fn 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
79/// Convert a slice of `MessageTag` into `SentEmailTag` for retrospection storage.
80fn 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/// Main SES provider implementing all operations.
90#[derive(Debug)]
91pub struct RustackSes {
92    /// Identity store.
93    pub(crate) identities: Arc<IdentityStore>,
94    /// Email store for retrospection.
95    pub(crate) emails: Arc<EmailStore>,
96    /// Template store.
97    pub(crate) templates: Arc<TemplateStore>,
98    /// Configuration set store.
99    pub(crate) config_sets: Arc<ConfigurationSetStore>,
100    /// Receipt rule set store.
101    pub(crate) receipt_rules: Arc<ReceiptRuleSetStore>,
102    /// Send statistics.
103    pub(crate) statistics: Arc<SendStatistics>,
104    /// Service configuration.
105    pub(crate) config: Arc<SesConfig>,
106}
107
108#[allow(clippy::needless_pass_by_value)]
109impl RustackSes {
110    /// Create a new SES provider with the given configuration.
111    #[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    /// Get a reference to the email store for retrospection.
125    #[must_use]
126    pub fn email_store(&self) -> &Arc<EmailStore> {
127        &self.emails
128    }
129
130    /// Get a reference to the configuration.
131    #[must_use]
132    pub fn config(&self) -> &SesConfig {
133        &self.config
134    }
135
136    // ---------------------------------------------------------------
137    // Phase 0: Core Sending + Identities
138    // ---------------------------------------------------------------
139
140    /// Verify an email identity.
141    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    /// Verify a domain identity.
151    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    /// List all identities, optionally filtered by type.
163    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    /// Delete an identity.
175    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    /// Get verification attributes for identities.
185    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    /// Legacy API: verify an email address.
198    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    /// Legacy API: delete a verified email address.
205    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    /// Legacy API: list verified email addresses.
214    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    /// Send an email.
224    pub fn send_email(&self, input: SendEmailInput) -> Result<SendEmailResponse, SesError> {
225        validate_message_tags(&input.tags)?;
226
227        // Optionally validate source is verified
228        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    /// Send a raw MIME email.
267    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    /// Get send quota.
322    #[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    /// Get send statistics.
332    #[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    // ---------------------------------------------------------------
348    // Phase 1: Templates + Configuration Sets
349    // ---------------------------------------------------------------
350
351    /// Create a template.
352    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    /// Get a template.
362    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    /// Update a template.
370    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    /// Delete a template.
380    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    /// List templates.
390    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    /// Send a templated email.
402    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    /// Create a configuration set.
464    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    /// Delete a configuration set.
474    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    /// Describe a configuration set.
484    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    /// List configuration sets.
499    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    // ---------------------------------------------------------------
515    // Phase 2: Event Destinations + Receipt Rules
516    // ---------------------------------------------------------------
517
518    /// Create a configuration set event destination.
519    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    /// Update a configuration set event destination.
529    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    /// Delete a configuration set event destination.
539    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    /// Create a receipt rule set.
551    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    /// Delete a receipt rule set.
561    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    /// Create a receipt rule.
570    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    /// Delete a receipt rule.
580    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    /// Describe a receipt rule set.
590    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    /// Clone a receipt rule set.
605    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    /// Describe the active receipt rule set.
615    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    /// Set the active receipt rule set.
633    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    // ---------------------------------------------------------------
643    // Phase 3: Identity Configuration + Sending Authorization
644    // ---------------------------------------------------------------
645
646    /// Set identity notification topic.
647    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    /// Set identity feedback forwarding enabled.
660    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    /// Get identity notification attributes.
670    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    /// Verify domain DKIM.
683    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    /// Get identity DKIM attributes.
692    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    /// Set identity mail-from domain.
701    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    /// Get identity mail-from domain attributes.
714    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    /// Get identity policies.
727    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    /// Put (create or update) an identity policy.
738    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    /// Delete an identity policy.
748    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    /// List identity policy names.
758    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
767/// Extract the `From:` address from raw MIME data.
768///
769/// Uses case-insensitive matching for the header name per RFC 2822.
770fn 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            // Handle "Name <email>" format
776            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}