eternaltwin_core/mailer/store/
mod.rs

1use crate::core::{FinitePeriod, Handler, Instant, Listing};
2use crate::digest::{DigestSha2_256, DigestSha3_256};
3use crate::email::EmailAddress;
4use crate::mailer::store::acquire_outbound_email_request::{
5  AcquireOutboundEmailRequest, AcquireOutboundEmailRequestError,
6};
7use crate::mailer::store::create_outbound_email::{CreateOutboundEmail, CreateOutboundEmailError};
8use crate::mailer::store::get_outbound_email_requests::{GetOutboundEmailRequests, GetOutboundEmailRequestsError};
9use crate::mailer::store::get_outbound_emails::{GetOutboundEmails, GetOutboundEmailsError};
10use crate::mailer::store::release_outbound_email_request::{
11  ReleaseOutboundEmailRequest, ReleaseOutboundEmailRequestError,
12};
13use crate::user::UserIdRef;
14use async_trait::async_trait;
15#[cfg(feature = "serde")]
16use eternaltwin_serde_tools::{Deserialize, Serialize};
17use std::ops::Deref;
18
19pub mod acquire_outbound_email_request;
20pub mod create_outbound_email;
21pub mod get_outbound_email_requests;
22pub mod get_outbound_emails;
23pub mod release_outbound_email_request;
24
25declare_new_uuid! {
26  pub struct OutboundEmailId(Uuid);
27  pub type ParseError = OuboundEmailIdParseError;
28  const SQL_NAME = "outbound_email_id";
29}
30
31#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
32#[cfg_attr(feature = "serde", serde(tag = "type", rename = "User"))]
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
34pub struct OutboundEmailIdRef {
35  pub id: OutboundEmailId,
36}
37
38impl OutboundEmailIdRef {
39  pub const fn new(id: OutboundEmailId) -> Self {
40    Self { id }
41  }
42}
43
44impl From<OutboundEmailId> for OutboundEmailIdRef {
45  fn from(id: OutboundEmailId) -> Self {
46    Self::new(id)
47  }
48}
49
50declare_new_uuid! {
51  pub struct OutboundEmailRequestId(Uuid);
52  pub type ParseError = OutboundEmailRequestIdParseError;
53  const SQL_NAME = "outbound_email_request_id";
54}
55
56#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
57#[cfg_attr(feature = "serde", serde(tag = "type", rename = "User"))]
58#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
59pub struct OutboundEmailRequestIdRef {
60  pub id: OutboundEmailRequestId,
61}
62
63impl OutboundEmailRequestIdRef {
64  pub const fn new(id: OutboundEmailRequestId) -> Self {
65    Self { id }
66  }
67}
68
69impl From<OutboundEmailRequestId> for OutboundEmailRequestIdRef {
70  fn from(id: OutboundEmailRequestId) -> Self {
71    Self::new(id)
72  }
73}
74
75#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
76#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub struct OutboundEmail<Opaque> {
78  pub id: OutboundEmailId,
79  pub submitted_at: Instant,
80  pub created_by: UserIdRef,
81  pub deadline: Instant,
82  pub sender: EmailAddress,
83  pub recipient: EmailAddress,
84  pub payload: EmailContentPayload<Opaque>,
85  pub read_at: Option<Instant>,
86}
87
88declare_new_enum!(
89  pub enum EmailDeliveryStatus {
90    #[str("Pending")]
91    Pending,
92    #[str("Cancelled")]
93    Cancelled,
94    #[str("Sent")]
95    Sent,
96    #[str("Acknowledged")]
97    Acknowledged,
98  }
99  pub type ParseError = EmailDeliveryStatusParseError;
100  const SQL_NAME = "email_delivery_status";
101);
102
103declare_new_enum!(
104  pub enum EmailPayloadKind {
105    #[str("Marktwin")]
106    Marktwin,
107    #[str("ResetPassword")]
108    ResetPassword,
109    #[str("VerifyEmail")]
110    VerifyEmail,
111  }
112  pub type ParseError = EmailPayloadKindParseError;
113  const SQL_NAME = "email_payload_kind";
114);
115
116#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
117#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
118pub struct EmailContentPayload<Opaque> {
119  pub kind: EmailPayloadKind,
120  pub version: u8,
121  pub data: Opaque,
122  pub text: EmailContentSummary,
123  pub html: EmailContentSummary,
124}
125
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
128pub struct EmailContentSummary {
129  pub size: u16,
130  pub sha2_256: DigestSha2_256,
131  pub sha3_256: DigestSha3_256,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, thiserror::Error)]
135pub enum EmailContentSummaryError {
136  #[error("the provided data is too large: actual size = {0}, max size = {1}")]
137  Size(usize, usize),
138}
139
140impl EmailContentSummary {
141  /// Email content summary for empty data.
142  ///
143  /// ```
144  /// use eternaltwin_core::mailer::store::EmailContentSummary;
145  ///
146  /// assert_eq!(EmailContentSummary::EMPTY, EmailContentSummary::new(&[]).expect("empty content is valid"));
147  /// ```
148  pub const EMPTY: Self = Self {
149    size: 0,
150    #[rustfmt::skip]
151    sha2_256: DigestSha2_256::from_array([
152      0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
153      0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
154      0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
155      0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
156    ]),
157    #[rustfmt::skip]
158    sha3_256: DigestSha3_256::from_array([
159      0xa7, 0xff, 0xc6, 0xf8, 0xbf, 0x1e, 0xd7, 0x66,
160      0x51, 0xc1, 0x47, 0x56, 0xa0, 0x61, 0xd6, 0x62,
161      0xf5, 0x80, 0xff, 0x4d, 0xe4, 0x3b, 0x49, 0xfa,
162      0x82, 0xd8, 0x0a, 0x4b, 0x80, 0xf8, 0x43, 0x4a,
163    ]),
164  };
165
166  pub fn new(data: &[u8]) -> Result<Self, EmailContentSummaryError> {
167    let size =
168      u16::try_from(data.len()).map_err(|_| EmailContentSummaryError::Size(data.len(), usize::from(u16::MAX)))?;
169    let sha2_256 = DigestSha2_256::digest(data);
170    let sha3_256 = DigestSha3_256::digest(data);
171    Ok(Self {
172      size,
173      sha2_256,
174      sha3_256,
175    })
176  }
177}
178
179declare_new_enum!(
180  pub enum EmailRequestStatus {
181    #[str("Pending")]
182    Pending,
183    #[str("Cancelled")]
184    Cancelled,
185    #[str("Timeout")]
186    Timeout,
187    #[str("Ok")]
188    Ok,
189    #[str("Error")]
190    Error,
191  }
192  pub type ParseError = EmailRequestStatusParseError;
193  const SQL_NAME = "email_request_status";
194);
195
196#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
197#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub struct OutboundEmailRequest {
199  pub id: OutboundEmailRequestId,
200  pub email: OutboundEmailIdRef,
201  pub period: FinitePeriod,
202  pub status: EmailRequestStatus,
203}
204
205#[async_trait]
206pub trait MailerStore<Opaque>:
207  Send
208  + Sync
209  + Handler<CreateOutboundEmail<Opaque>>
210  + Handler<AcquireOutboundEmailRequest>
211  + Handler<ReleaseOutboundEmailRequest>
212  + Handler<GetOutboundEmails<Opaque>>
213  + Handler<GetOutboundEmailRequests>
214where
215  Opaque: Send + Sync + 'static,
216{
217  async fn create_outbound_email(
218    &self,
219    cmd: CreateOutboundEmail<Opaque>,
220  ) -> Result<OutboundEmail<Opaque>, CreateOutboundEmailError> {
221    Handler::<CreateOutboundEmail<Opaque>>::handle(self, cmd).await
222  }
223
224  async fn acquire_outbound_email_request(
225    &self,
226    cmd: AcquireOutboundEmailRequest,
227  ) -> Result<OutboundEmailRequest, AcquireOutboundEmailRequestError> {
228    Handler::<AcquireOutboundEmailRequest>::handle(self, cmd).await
229  }
230
231  async fn release_outbound_email_request(
232    &self,
233    cmd: ReleaseOutboundEmailRequest,
234  ) -> Result<OutboundEmailRequest, ReleaseOutboundEmailRequestError> {
235    Handler::<ReleaseOutboundEmailRequest>::handle(self, cmd).await
236  }
237
238  async fn get_outbound_emails(
239    &self,
240    query: GetOutboundEmails<Opaque>,
241  ) -> Result<Listing<OutboundEmail<Opaque>>, GetOutboundEmailsError> {
242    Handler::<GetOutboundEmails<Opaque>>::handle(self, query).await
243  }
244
245  async fn get_outbound_email_requests(
246    &self,
247    query: GetOutboundEmailRequests,
248  ) -> Result<Listing<OutboundEmailRequest>, GetOutboundEmailRequestsError> {
249    Handler::<GetOutboundEmailRequests>::handle(self, query).await
250  }
251}
252
253impl<T, Opaque> MailerStore<Opaque> for T
254where
255  T: Send
256    + Sync
257    + Handler<CreateOutboundEmail<Opaque>>
258    + Handler<AcquireOutboundEmailRequest>
259    + Handler<ReleaseOutboundEmailRequest>
260    + Handler<GetOutboundEmails<Opaque>>
261    + Handler<GetOutboundEmailRequests>,
262  Opaque: Send + Sync + 'static,
263{
264}
265
266/// Like [`Deref`], but the target has the bound [`MailerStore`]
267pub trait MailerStoreRef<Opaque>: Send + Sync
268where
269  Opaque: Send + Sync + 'static,
270{
271  type MailerStore: MailerStore<Opaque> + ?Sized;
272
273  fn mailer_store(&self) -> &Self::MailerStore;
274}
275
276impl<TyRef, Opaque> MailerStoreRef<Opaque> for TyRef
277where
278  TyRef: Deref + Send + Sync,
279  TyRef::Target: MailerStore<Opaque>,
280  Opaque: Send + Sync + 'static,
281{
282  type MailerStore = TyRef::Target;
283
284  fn mailer_store(&self) -> &Self::MailerStore {
285    self.deref()
286  }
287}