1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//! RFC 8621 §6 Identity object.
//!
//! Provides [`Identity`] — stores information about an email address or domain
//! the user may send from.
use crate::email::EmailAddress;
use jmap_types::Id;
use serde::{Deserialize, Serialize};
/// An RFC 8621 §6 Identity object.
///
/// Stores information about an email address or domain the user may send from.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Identity {
/// The id of the Identity (immutable; server-set).
pub id: Id,
/// The "From" name the client SHOULD use when creating a new Email
/// from this Identity. Defaults to `""`.
pub name: String,
/// The "From" email address the client MUST use (immutable).
pub email: String,
/// The Reply-To value the client SHOULD set. `null` if not set.
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to: Option<Vec<EmailAddress>>,
/// The Bcc value the client SHOULD set. `null` if not set.
#[serde(skip_serializing_if = "Option::is_none")]
pub bcc: Option<Vec<EmailAddress>>,
/// Plaintext signature. Defaults to `""`.
pub text_signature: String,
/// HTML snippet signature. Defaults to `""`.
pub html_signature: String,
/// Whether the user may delete this Identity (server-set).
pub may_delete: bool,
/// Catch-all for vendor / site / private extension fields not covered
/// by the typed fields above. Preserves unknown fields across
/// deserialize/serialize round-trip per workspace extras-preservation
/// policy (see workspace AGENTS.md).
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl Identity {
/// Construct an [`Identity`] from its three identifying fields.
///
/// `name`, `text_signature`, and `html_signature` default to `""`.
/// `reply_to` and `bcc` default to `None`.
pub fn new(id: Id, email: impl Into<String>, may_delete: bool) -> Self {
Self {
id,
email: email.into(),
may_delete,
name: String::new(),
reply_to: None,
bcc: None,
text_signature: String::new(),
html_signature: String::new(),
extra: serde_json::Map::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
// ── Extras-preservation policy tests (JMAP-lbdy.2) ───────────────────
/// `Identity.extra` captures vendor fields and preserves them across
/// deserialize/serialize round-trip.
#[test]
fn identity_preserves_vendor_extras() {
let raw = json!({
"id": "i1",
"name": "Alice",
"email": "alice@example.com",
"textSignature": "-- Alice",
"htmlSignature": "<p>-- Alice</p>",
"mayDelete": true,
"acmeCorpDepartment": "engineering"
});
let id: Identity = serde_json::from_value(raw).unwrap();
assert_eq!(
id.extra.get("acmeCorpDepartment").and_then(|v| v.as_str()),
Some("engineering")
);
let back = serde_json::to_value(&id).unwrap();
assert_eq!(back["acmeCorpDepartment"], "engineering");
}
}