jmap_mail_types/submission.rs
1//! [`EmailSubmission`] and related types for RFC 8621 §7.
2//!
3//! Covers the SMTP envelope ([`Envelope`], [`Address`]), per-recipient delivery
4//! status ([`DeliveryStatus`], [`Delivered`], [`Displayed`]), undo tracking
5//! ([`UndoStatus`]), and the [`EmailSubmission`] object itself.
6//!
7//! Also defines [`EmailSubmissionFilterCondition`] for EmailSubmission/query
8//! (RFC 8621 §7.3); the `EmailSubmissionFilter` type alias lives in
9//! [`crate::query`].
10
11use std::collections::HashMap;
12
13use jmap_types::{Id, UTCDate};
14use serde::{Deserialize, Serialize};
15
16/// SMTP envelope address with optional MAIL FROM / RCPT TO parameters (RFC 8621 §7).
17///
18/// Used in both `mailFrom` and the elements of `rcptTo` within an [`Envelope`].
19#[non_exhaustive]
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct Address {
23 /// The email address (Mailbox as per RFC 5321 Reverse-path / Forward-path).
24 pub email: String,
25 /// Optional SMTP parameters (mail-parameter or rcpt-parameter per RFC 5321).
26 ///
27 /// Each key is a parameter name; the value is the parameter value string, or
28 /// `None` if the parameter takes no value. xtext / unitext encodings are
29 /// stripped; JSON string encoding applies.
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub parameters: Option<HashMap<String, Option<String>>>,
32}
33
34impl Address {
35 /// Construct an [`Address`] with no SMTP parameters.
36 pub fn new(email: impl Into<String>) -> Self {
37 Self {
38 email: email.into(),
39 parameters: None,
40 }
41 }
42}
43
44/// SMTP envelope for an [`EmailSubmission`] (RFC 8621 §7).
45///
46/// Carries the return address and recipient list used in the SMTP dialogue.
47/// If omitted on creation the server derives it from the Email headers.
48#[non_exhaustive]
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct Envelope {
52 /// Return address for the SMTP MAIL FROM command.
53 pub mail_from: Address,
54 /// Recipient addresses for SMTP RCPT TO commands.
55 pub rcpt_to: Vec<Address>,
56}
57
58impl Envelope {
59 /// Construct an [`Envelope`] from a return address and recipient list.
60 pub fn new(mail_from: Address, rcpt_to: Vec<Address>) -> Self {
61 Self { mail_from, rcpt_to }
62 }
63}
64
65/// Delivery status of a message to a recipient (RFC 8621 §7, `delivered` field).
66#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67#[non_exhaustive]
68pub enum Delivered {
69 /// The message is in a local mail queue and the status is not yet known.
70 Queued,
71 /// The message was successfully delivered to the mail store of the recipient.
72 Yes,
73 /// Delivery failed; the `smtp_reply` field contains the failure reason.
74 No,
75 /// The final delivery status is unknown.
76 Unknown,
77 /// An unrecognised value was received from the server.
78 ///
79 /// The inner string retains the original value so this variant round-trips correctly.
80 Other(String),
81}
82
83impl_string_enum!(Delivered, "a delivery status string",
84 "queued" => Queued,
85 "yes" => Yes,
86 "no" => No,
87 "unknown" => Unknown,
88);
89
90/// Display status of a message to a recipient (RFC 8621 §7, `displayed` field).
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub enum Displayed {
94 /// The display status is unknown.
95 Unknown,
96 /// The message has been displayed to the recipient at least once.
97 Yes,
98 /// An unrecognised value was received from the server.
99 ///
100 /// The inner string retains the original value so this variant round-trips correctly.
101 Other(String),
102}
103
104impl_string_enum!(Displayed, "a display status string",
105 "unknown" => Unknown,
106 "yes" => Yes,
107);
108
109/// Whether an [`EmailSubmission`] may still be canceled (RFC 8621 §7).
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111#[non_exhaustive]
112pub enum UndoStatus {
113 /// The message has not yet been relayed; cancellation may be possible.
114 Pending,
115 /// The message has been relayed to at least one recipient and cannot be recalled.
116 Final,
117 /// The submission was canceled and will not be delivered to any recipient.
118 Canceled,
119 /// An unrecognised value was received from the server.
120 ///
121 /// The inner string retains the original value so this variant round-trips correctly.
122 Other(String),
123}
124
125impl_string_enum!(UndoStatus, "an undo status string",
126 "pending" => Pending,
127 "final" => Final,
128 "canceled" => Canceled,
129);
130
131/// Per-recipient delivery status for an [`EmailSubmission`] (RFC 8621 §7).
132#[non_exhaustive]
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct DeliveryStatus {
136 /// The SMTP reply string returned when the server last attempted relay,
137 /// or from a later DSN (RFC 3464). Multi-line responses are concatenated
138 /// into a single string.
139 pub smtp_reply: String,
140 /// Whether the message reached the recipient's mail store.
141 pub delivered: Delivered,
142 /// Whether the message has been displayed to the recipient.
143 pub displayed: Displayed,
144}
145
146impl DeliveryStatus {
147 /// Construct a [`DeliveryStatus`] from its three required fields.
148 pub fn new(smtp_reply: impl Into<String>, delivered: Delivered, displayed: Displayed) -> Self {
149 Self {
150 smtp_reply: smtp_reply.into(),
151 delivered,
152 displayed,
153 }
154 }
155}
156
157/// Represents the submission of an Email for delivery (RFC 8621 §7).
158#[non_exhaustive]
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct EmailSubmission {
162 /// Server-assigned immutable identifier for this submission.
163 pub id: Id,
164 /// Id of the Identity used to send this submission.
165 pub identity_id: Id,
166 /// Id of the Email being submitted.
167 pub email_id: Id,
168 /// Thread id of the submitted Email (server-set).
169 pub thread_id: Id,
170 /// SMTP envelope; server-derived from Email headers when absent on creation.
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub envelope: Option<Envelope>,
173 /// UTC timestamp when the submission was / will be released for delivery.
174 pub send_at: UTCDate,
175 /// Whether the submission may still be canceled.
176 pub undo_status: UndoStatus,
177 /// Per-recipient delivery status, keyed by recipient email address.
178 ///
179 /// `None` when the server does not support delivery-status tracking.
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub delivery_status: Option<HashMap<String, DeliveryStatus>>,
182 /// Blob ids of DSN messages (RFC 3464) received for this submission.
183 ///
184 /// Always present in serialized output (empty array when no DSN has been received);
185 /// RFC 8621 §7 requires these fields in responses. Do not add `skip_serializing_if`.
186 pub dsn_blob_ids: Vec<Id>,
187 /// Blob ids of MDN messages (RFC 8098) received for this submission.
188 ///
189 /// Always present in serialized output; same rationale as `dsn_blob_ids`.
190 pub mdn_blob_ids: Vec<Id>,
191}
192
193impl EmailSubmission {
194 /// Construct an [`EmailSubmission`] from its required fields.
195 ///
196 /// `envelope` and `delivery_status` default to `None`.
197 /// `dsn_blob_ids` and `mdn_blob_ids` default to empty.
198 pub fn new(
199 id: Id,
200 identity_id: Id,
201 email_id: Id,
202 thread_id: Id,
203 send_at: UTCDate,
204 undo_status: UndoStatus,
205 ) -> Self {
206 Self {
207 id,
208 identity_id,
209 email_id,
210 thread_id,
211 envelope: None,
212 send_at,
213 undo_status,
214 delivery_status: None,
215 dsn_blob_ids: Vec::new(),
216 mdn_blob_ids: Vec::new(),
217 }
218 }
219}
220
221// ---------------------------------------------------------------------------
222// EmailSubmission/query filter (RFC 8621 §7.3)
223// ---------------------------------------------------------------------------
224
225/// Filter condition for EmailSubmission/query (RFC 8621 §7.3).
226///
227/// All fields are optional. If zero properties are specified, the condition
228/// evaluates to `true` for every submission.
229///
230/// RFC 8621 §7.3 uses the standard `/query` mechanism (RFC 8620 §5.5), so
231/// `EmailSubmissionFilterCondition` can be used inside a
232/// `Filter<EmailSubmissionFilterCondition>` to combine conditions with
233/// logical operators.
234#[non_exhaustive]
235#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
236#[serde(rename_all = "camelCase")]
237pub struct EmailSubmissionFilterCondition {
238 /// The submission's `identityId` must be in this list.
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub identity_ids: Option<Vec<Id>>,
241
242 /// The submission's `emailId` must be in this list.
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub email_ids: Option<Vec<Id>>,
245
246 /// The submission's `threadId` must be in this list.
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub thread_ids: Option<Vec<Id>>,
249
250 /// The submission's `undoStatus` must equal this value.
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub undo_status: Option<UndoStatus>,
253
254 /// The `sendAt` of the submission must be before this date-time.
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub before: Option<UTCDate>,
257
258 /// The `sendAt` of the submission must be on or after this date-time.
259 #[serde(skip_serializing_if = "Option::is_none")]
260 pub after: Option<UTCDate>,
261}