use std::collections::HashMap;
use jmap_types::{impl_string_enum, Id, UTCDate};
use serde::{Deserialize, Serialize};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Address {
pub email: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<HashMap<String, Option<String>>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl Address {
pub fn new(email: impl Into<String>) -> Self {
Self {
email: email.into(),
parameters: None,
extra: serde_json::Map::new(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Envelope {
pub mail_from: Address,
pub rcpt_to: Vec<Address>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl Envelope {
pub fn new(mail_from: Address, rcpt_to: Vec<Address>) -> Self {
Self {
mail_from,
rcpt_to,
extra: serde_json::Map::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Delivered {
Queued,
Yes,
No,
Unknown,
Other(String),
}
impl_string_enum!(Delivered, "a delivery status string",
"queued" => Queued,
"yes" => Yes,
"no" => No,
"unknown" => Unknown,
);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Displayed {
Unknown,
Yes,
Other(String),
}
impl_string_enum!(Displayed, "a display status string",
"unknown" => Unknown,
"yes" => Yes,
);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UndoStatus {
Pending,
Final,
Canceled,
Other(String),
}
impl_string_enum!(UndoStatus, "an undo status string",
"pending" => Pending,
"final" => Final,
"canceled" => Canceled,
);
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeliveryStatus {
pub smtp_reply: String,
pub delivered: Delivered,
pub displayed: Displayed,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl DeliveryStatus {
pub fn new(smtp_reply: impl Into<String>, delivered: Delivered, displayed: Displayed) -> Self {
Self {
smtp_reply: smtp_reply.into(),
delivered,
displayed,
extra: serde_json::Map::new(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmailSubmission {
pub id: Id,
pub identity_id: Id,
pub email_id: Id,
pub thread_id: Id,
#[serde(skip_serializing_if = "Option::is_none")]
pub envelope: Option<Envelope>,
pub send_at: UTCDate,
pub undo_status: UndoStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub delivery_status: Option<HashMap<String, DeliveryStatus>>,
pub dsn_blob_ids: Vec<Id>,
pub mdn_blob_ids: Vec<Id>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl EmailSubmission {
pub fn new(
id: Id,
identity_id: Id,
email_id: Id,
thread_id: Id,
send_at: UTCDate,
undo_status: UndoStatus,
) -> Self {
Self {
id,
identity_id,
email_id,
thread_id,
envelope: None,
send_at,
undo_status,
delivery_status: None,
dsn_blob_ids: Vec::new(),
mdn_blob_ids: Vec::new(),
extra: serde_json::Map::new(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EmailSubmissionFilterCondition {
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_ids: Option<Vec<Id>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_ids: Option<Vec<Id>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_ids: Option<Vec<Id>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub undo_status: Option<UndoStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<UTCDate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<UTCDate>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn address_preserves_vendor_extras() {
let raw = json!({
"email": "alice@example.com",
"acmeCorpRouting": "us-east"
});
let addr: Address = serde_json::from_value(raw).unwrap();
assert_eq!(
addr.extra.get("acmeCorpRouting").and_then(|v| v.as_str()),
Some("us-east")
);
let back = serde_json::to_value(&addr).unwrap();
assert_eq!(back["acmeCorpRouting"], "us-east");
}
#[test]
fn envelope_preserves_vendor_extras() {
let raw = json!({
"mailFrom": {"email": "a@b"},
"rcptTo": [{"email": "c@d"}],
"acmeCorpSubmissionPath": "smarthost-3"
});
let env: Envelope = serde_json::from_value(raw).unwrap();
assert_eq!(
env.extra
.get("acmeCorpSubmissionPath")
.and_then(|v| v.as_str()),
Some("smarthost-3")
);
let back = serde_json::to_value(&env).unwrap();
assert_eq!(back["acmeCorpSubmissionPath"], "smarthost-3");
}
#[test]
fn delivery_status_preserves_vendor_extras() {
let raw = json!({
"smtpReply": "250 OK",
"delivered": "yes",
"displayed": "unknown",
"acmeCorpDeliveryTimeMs": 120
});
let st: DeliveryStatus = serde_json::from_value(raw).unwrap();
assert_eq!(
st.extra
.get("acmeCorpDeliveryTimeMs")
.and_then(|v| v.as_u64()),
Some(120)
);
let back = serde_json::to_value(&st).unwrap();
assert_eq!(back["acmeCorpDeliveryTimeMs"], 120);
}
#[test]
fn email_submission_preserves_vendor_extras() {
let raw = json!({
"id": "es1",
"identityId": "i1",
"emailId": "e1",
"threadId": "t1",
"sendAt": "2024-06-01T00:00:00Z",
"undoStatus": "pending",
"dsnBlobIds": [],
"mdnBlobIds": [],
"acmeCorpSubmissionTag": "campaign-42"
});
let sub: EmailSubmission = serde_json::from_value(raw).unwrap();
assert_eq!(
sub.extra
.get("acmeCorpSubmissionTag")
.and_then(|v| v.as_str()),
Some("campaign-42")
);
let back = serde_json::to_value(&sub).unwrap();
assert_eq!(back["acmeCorpSubmissionTag"], "campaign-42");
}
}