use std::fmt;
use serde::{Deserialize, Deserializer, Serialize};
pub const MESSAGE_ID_PREFIX: &str = "m4a_";
pub const MESSAGE_ID_HEX_LEN: usize = 24;
pub const SELECTOR_MAX_BYTES: usize = 128;
pub const SUBJECT_MAX_BYTES: usize = 512;
pub const BODY_MAX_BYTES: usize = 65_536;
pub const REFS_MAX: usize = 8;
pub const REF_LOCATOR_MAX_BYTES: usize = 512;
pub const REF_DIGEST_MAX_CHARS: usize = 128;
pub const INBOX_LIMIT_MAX: u16 = 256;
pub const INBOX_LIMIT_DEFAULT: u16 = 50;
pub const INBOX_WAIT_SECS_MAX: u16 = 60;
fn default_inbox_limit() -> u16 {
INBOX_LIMIT_DEFAULT
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum MailError {
UnknownParticipant { participant: ParticipantId },
UnknownRoom { room: RoomId },
UnknownSession { session: SessionId },
SessionAccountMismatch { session: SessionId, expected: ParticipantId, presented: ParticipantId },
UnknownMessage { message_id: MessageId },
NotAddressedToYou { message_id: MessageId },
PermissionDenied { need: String },
Malformed { field: String, reason: String },
TooLarge { field: String, limit: usize, actual: usize },
StoreUnavailable { operation: String },
}
impl fmt::Display for MailError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownParticipant { participant } => {
write!(f, "unknown participant \"{participant}\"")
}
Self::UnknownRoom { room } => write!(f, "unknown room \"{room}\""),
Self::UnknownSession { session } => write!(f, "unknown session \"{session}\""),
Self::SessionAccountMismatch { session, expected, presented } => write!(
f,
"session \"{session}\" belongs to account \"{expected}\", not \"{presented}\""
),
Self::UnknownMessage { message_id } => write!(f, "unknown message \"{message_id}\""),
Self::NotAddressedToYou { message_id } => {
write!(f, "message \"{message_id}\" is not addressed to you")
}
Self::PermissionDenied { need } => write!(f, "permission denied: need \"{need}\""),
Self::Malformed { field, reason } => {
write!(f, "field \"{field}\" is malformed: {reason}")
}
Self::TooLarge { field, limit, actual } => {
write!(f, "field \"{field}\" is too large: limit {limit}, actual {actual}")
}
Self::StoreUnavailable { operation } => {
write!(f, "the store could not complete \"{operation}\"")
}
}
}
}
impl std::error::Error for MailError {}
fn validate_opaque_id(
label: &'static str,
value: &str,
prefix: &str,
min_hex_len: usize,
max_hex_len: usize,
) -> Result<(), MailError> {
let Some(hex) = value.strip_prefix(prefix) else {
return Err(MailError::Malformed {
field: label.to_string(),
reason: format!("must start with \"{prefix}\""),
});
};
if hex.len() < min_hex_len
|| hex.len() > max_hex_len
|| !hex.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
let width = if min_hex_len == max_hex_len {
min_hex_len.to_string()
} else {
format!("{min_hex_len}..={max_hex_len}")
};
return Err(MailError::Malformed {
field: label.to_string(),
reason: format!("body after \"{prefix}\" must be {width} lowercase hex characters"),
});
}
Ok(())
}
fn validate_selector(label: &'static str, value: &str) -> Result<(), MailError> {
if value.is_empty() {
return Err(MailError::Malformed {
field: label.to_string(),
reason: "must not be empty".to_string(),
});
}
if value.len() > SELECTOR_MAX_BYTES {
return Err(MailError::TooLarge {
field: label.to_string(),
limit: SELECTOR_MAX_BYTES,
actual: value.len(),
});
}
if !value.is_ascii()
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'@' | b'+'))
{
return Err(MailError::Malformed {
field: label.to_string(),
reason: "must be ASCII from the set [A-Za-z0-9._@+-]".to_string(),
});
}
Ok(())
}
fn validate_bounded_text(field: &'static str, value: &str) -> Result<(), MailError> {
if value.len() > SUBJECT_MAX_BYTES {
return Err(MailError::TooLarge {
field: field.to_string(),
limit: SUBJECT_MAX_BYTES,
actual: value.len(),
});
}
if value.chars().any(char::is_control) {
return Err(MailError::Malformed {
field: field.to_string(),
reason: "must not contain control characters".to_string(),
});
}
Ok(())
}
fn validate_subject(value: &str) -> Result<(), MailError> {
validate_bounded_text("subject", value)
}
fn validate_body(value: &str) -> Result<(), MailError> {
if value.len() > BODY_MAX_BYTES {
return Err(MailError::TooLarge {
field: "body".to_string(),
limit: BODY_MAX_BYTES,
actual: value.len(),
});
}
if value.chars().any(|character| character.is_control() && !matches!(character, '\n' | '\t')) {
return Err(MailError::Malformed {
field: "body".to_string(),
reason: "must not contain control characters other than newline and tab".to_string(),
});
}
Ok(())
}
fn validate_correlation(value: &str) -> Result<(), MailError> {
if value.len() > SUBJECT_MAX_BYTES {
return Err(MailError::TooLarge {
field: "correlation".to_string(),
limit: SUBJECT_MAX_BYTES,
actual: value.len(),
});
}
if value.is_empty() || value.chars().any(char::is_control) {
return Err(MailError::Malformed {
field: "correlation".to_string(),
reason: "must be non-empty and free of control characters".to_string(),
});
}
Ok(())
}
fn validate_refs(refs: &[MessageRef]) -> Result<(), MailError> {
if refs.len() > REFS_MAX {
return Err(MailError::TooLarge {
field: "refs".to_string(),
limit: REFS_MAX,
actual: refs.len(),
});
}
for reference in refs {
reference.validate()?;
}
Ok(())
}
macro_rules! opaque_id {
($name:ident, $prefix:expr, $label:literal, $min_hex:expr, $max_hex:expr, $doc:expr) => {
#[doc = $doc]
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub const PREFIX: &'static str = $prefix;
pub fn new(value: impl Into<String>) -> Result<Self, MailError> {
let value = value.into();
validate_opaque_id($label, &value, Self::PREFIX, $min_hex, $max_hex)?;
Ok(Self(value))
}
pub fn validate(&self) -> Result<(), MailError> {
validate_opaque_id($label, &self.0, Self::PREFIX, $min_hex, $max_hex)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple(stringify!($name)).field(&self.0).finish()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::str::FromStr for $name {
type Err = MailError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
};
}
macro_rules! selector_id {
($name:ident, $label:literal, $doc:expr) => {
#[doc = $doc]
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, MailError> {
let value = value.into();
validate_selector($label, &value)?;
Ok(Self(value))
}
pub fn validate(&self) -> Result<(), MailError> {
validate_selector($label, &self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple(stringify!($name)).field(&self.0).finish()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl std::str::FromStr for $name {
type Err = MailError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
};
}
opaque_id!(
MessageId,
MESSAGE_ID_PREFIX,
"message id",
MESSAGE_ID_HEX_LEN,
MESSAGE_ID_HEX_LEN,
"Opaque, prefixed, fixed-width hex id for a stored [`Message`]. Ported \
from `HarnessMailMessageId` (prefix `hmail_` there, `m4a_` here so a \
value can never be mistaken for a harness message id from the crate \
this was ported out of)."
);
pub const SESSION_ID_PREFIX: &str = "s-";
pub const SESSION_ID_HEX_MIN_CHARS: usize = 8;
pub const SESSION_ID_HEX_MAX_CHARS: usize = 64;
opaque_id!(
SessionId,
SESSION_ID_PREFIX,
"session id",
SESSION_ID_HEX_MIN_CHARS,
SESSION_ID_HEX_MAX_CHARS,
"Opaque id for one live session under a [`ParticipantId`] account. \
Derived by the caller from a process identity and handed to this \
crate already formed -- `MailboxEngine::ensure_session` registers one, \
it never invents one."
);
selector_id!(
ParticipantId,
"participant id",
"Addresses one participant directly. Distinct from [`RoomId`] on \
purpose: a room id must never be accepted where a participant id is \
meant, and a shared alias would let one slip into the other's slot."
);
selector_id!(
RoomId,
"room id",
"Addresses a named group of participants the mailbox itself tracks. \
Distinct from [`ParticipantId`] on purpose (see there)."
);
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum Address {
Direct { participant: ParticipantId },
Session { participant: ParticipantId, session: SessionId },
Room { room: RoomId },
}
impl Address {
pub fn validate(&self) -> Result<(), MailError> {
match self {
Self::Direct { participant } => participant.validate(),
Self::Session { participant, session } => {
participant.validate()?;
session.validate()
}
Self::Room { room } => room.validate(),
}
}
pub fn account(&self) -> Option<&ParticipantId> {
match self {
Self::Direct { participant } | Self::Session { participant, .. } => Some(participant),
Self::Room { .. } => None,
}
}
}
fn validate_participant_address(field: &'static str, address: &Address) -> Result<(), MailError> {
address.validate()?;
if address.account().is_none() {
return Err(MailError::Malformed {
field: field.to_string(),
reason: "must be a participant address (direct or session), not a room".to_string(),
});
}
Ok(())
}
impl fmt::Display for Address {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Direct { participant } => write!(f, "{participant}"),
Self::Session { participant, session } => write!(f, "{participant}/{session}"),
Self::Room { room } => write!(f, "#{room}"),
}
}
}
impl std::str::FromStr for Address {
type Err = MailError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if let Some(room) = value.strip_prefix('#') {
return Ok(Self::Room { room: RoomId::new(room)? });
}
match value.split_once('/') {
Some((participant, session)) => {
Ok(Self::Session { participant: ParticipantId::new(participant)?, session: SessionId::new(session)? })
}
None => Ok(Self::Direct { participant: ParticipantId::new(value)? }),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageRef {
pub kind: String,
pub locator: String,
pub digest: Option<String>,
}
impl MessageRef {
pub fn validate(&self) -> Result<(), MailError> {
validate_selector("ref kind", &self.kind)?;
if self.locator.len() > REF_LOCATOR_MAX_BYTES {
return Err(MailError::TooLarge {
field: "ref locator".to_string(),
limit: REF_LOCATOR_MAX_BYTES,
actual: self.locator.len(),
});
}
if self.locator.is_empty() || self.locator.chars().any(char::is_control) {
return Err(MailError::Malformed {
field: "ref locator".to_string(),
reason: "must be non-empty and free of control characters".to_string(),
});
}
if let Some(digest) = &self.digest {
if digest.len() > REF_DIGEST_MAX_CHARS {
return Err(MailError::TooLarge {
field: "ref digest".to_string(),
limit: REF_DIGEST_MAX_CHARS,
actual: digest.len(),
});
}
if digest.is_empty()
|| !digest.bytes().all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(MailError::Malformed {
field: "ref digest".to_string(),
reason: "must be non-empty lowercase hex".to_string(),
});
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Message {
pub message_id: MessageId,
pub from: Address,
pub to: Address,
pub subject: String,
pub body: String,
pub reply_to: Option<MessageId>,
pub correlation: Option<String>,
#[serde(default)]
pub refs: Vec<MessageRef>,
pub created_at_unix_ms: u64,
}
impl Message {
pub fn validate(&self) -> Result<(), MailError> {
self.message_id.validate()?;
validate_participant_address("from", &self.from)?;
self.to.validate()?;
validate_subject(&self.subject)?;
validate_body(&self.body)?;
if let Some(reply_to) = &self.reply_to {
reply_to.validate()?;
if reply_to == &self.message_id {
return Err(MailError::Malformed {
field: "reply_to".to_string(),
reason: "must not reference its own message_id".to_string(),
});
}
}
if let Some(correlation) = &self.correlation {
validate_correlation(correlation)?;
}
validate_refs(&self.refs)?;
if self.created_at_unix_ms == 0 {
return Err(MailError::Malformed {
field: "created_at_unix_ms".to_string(),
reason: "must not be zero".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Ack {
pub message_id: MessageId,
pub reader: Address,
pub acked_at_unix_ms: u64,
}
impl Ack {
pub fn validate(&self) -> Result<(), MailError> {
self.message_id.validate()?;
validate_participant_address("reader", &self.reader)?;
if self.acked_at_unix_ms == 0 {
return Err(MailError::Malformed {
field: "acked_at_unix_ms".to_string(),
reason: "must not be zero".to_string(),
});
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Declared<T>(T);
impl<T> Declared<T> {
pub fn new(value: T) -> Self {
Self(value)
}
pub fn into_inner(self) -> T {
self.0
}
pub fn inner_ref(&self) -> &T {
&self.0
}
}
impl<T: fmt::Display> fmt::Display for Declared<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionAttested {
pub pid: u32,
pub started_at_unix_ms: u64,
pub exe: Option<String>,
}
impl SessionAttested {
pub fn validate(&self) -> Result<(), MailError> {
if let Some(exe) = &self.exe {
validate_bounded_text("attested exe", exe)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionCorroborated {
pub provider_session_id: Option<Declared<String>>,
pub model: Option<Declared<String>>,
pub cwd: Option<Declared<String>>,
}
impl SessionCorroborated {
pub fn validate(&self) -> Result<(), MailError> {
if let Some(value) = &self.provider_session_id {
validate_bounded_text("corroborated provider_session_id", value.inner_ref())?;
}
if let Some(value) = &self.model {
validate_bounded_text("corroborated model", value.inner_ref())?;
}
if let Some(value) = &self.cwd {
validate_bounded_text("corroborated cwd", value.inner_ref())?;
}
Ok(())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionDeclared {
pub working_on: Option<String>,
pub role: Option<String>,
pub parent: Option<SessionId>,
}
impl SessionDeclared {
pub fn validate(&self) -> Result<(), MailError> {
if let Some(value) = &self.working_on {
validate_bounded_text("declared working_on", value)?;
}
if let Some(value) = &self.role {
validate_bounded_text("declared role", value)?;
}
if let Some(parent) = &self.parent {
parent.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionCard {
pub attested: SessionAttested,
pub corroborated: SessionCorroborated,
pub declared: SessionDeclared,
}
impl SessionCard {
pub fn validate(&self) -> Result<(), MailError> {
self.attested.validate()?;
self.corroborated.validate()?;
self.declared.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SessionEntry {
pub id: SessionId,
pub card: SessionCard,
pub last_seen_unix_ms: u64,
pub live: bool,
}
impl SessionEntry {
pub fn validate(&self) -> Result<(), MailError> {
self.id.validate()?;
self.card.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Participant {
pub id: ParticipantId,
pub label: Option<String>,
}
impl Participant {
pub fn validate(&self) -> Result<(), MailError> {
self.id.validate()?;
if let Some(label) = &self.label {
validate_label(label)?;
}
Ok(())
}
}
fn validate_label(value: &str) -> Result<(), MailError> {
validate_bounded_text("label", value)
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DirectoryEntry {
pub id: ParticipantId,
pub label: Option<String>,
#[serde(default)]
pub sessions: Vec<SessionEntry>,
}
impl DirectoryEntry {
pub fn validate(&self) -> Result<(), MailError> {
self.id.validate()?;
if let Some(label) = &self.label {
validate_label(label)?;
}
for session in &self.sessions {
session.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RoomEntry {
pub id: RoomId,
pub member: bool,
}
impl RoomEntry {
pub fn validate(&self) -> Result<(), MailError> {
self.id.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Directory {
pub participants: Vec<DirectoryEntry>,
pub rooms: Vec<RoomEntry>,
}
impl Directory {
pub fn validate(&self) -> Result<(), MailError> {
for participant in &self.participants {
participant.validate()?;
}
for room in &self.rooms {
room.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SendRequest {
pub to: Address,
pub subject: String,
pub body: String,
pub reply_to: Option<MessageId>,
pub correlation: Option<String>,
#[serde(default)]
pub refs: Vec<MessageRef>,
#[serde(default)]
pub idempotency_key: Option<String>,
}
impl SendRequest {
pub fn validate(&self) -> Result<(), MailError> {
self.to.validate()?;
validate_subject(&self.subject)?;
validate_body(&self.body)?;
if let Some(reply_to) = &self.reply_to {
reply_to.validate()?;
}
if let Some(correlation) = &self.correlation {
validate_correlation(correlation)?;
}
validate_refs(&self.refs)?;
if let Some(idempotency_key) = &self.idempotency_key {
validate_selector("idempotency_key", idempotency_key)?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InboxRequest {
pub since_unix_ms: Option<u64>,
#[serde(default = "default_inbox_limit")]
pub limit: u16,
#[serde(default)]
pub wait_secs: Option<u16>,
}
impl InboxRequest {
pub fn validate(&self) -> Result<(), MailError> {
if self.limit == 0 {
return Err(MailError::Malformed {
field: "limit".to_string(),
reason: "must be at least 1".to_string(),
});
}
if self.limit > INBOX_LIMIT_MAX {
return Err(MailError::TooLarge {
field: "limit".to_string(),
limit: usize::from(INBOX_LIMIT_MAX),
actual: usize::from(self.limit),
});
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AckRequest {
pub message_id: MessageId,
}
impl AckRequest {
pub fn validate(&self) -> Result<(), MailError> {
self.message_id.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MessageGetRequest {
pub message_id: MessageId,
}
impl MessageGetRequest {
pub fn validate(&self) -> Result<(), MailError> {
self.message_id.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UnreadCountRequest {
pub target: Address,
}
impl UnreadCountRequest {
pub fn validate(&self) -> Result<(), MailError> {
validate_participant_address("target", &self.target)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SendResponse {
pub message_id: MessageId,
pub from: Address,
}
impl SendResponse {
pub fn validate(&self) -> Result<(), MailError> {
self.message_id.validate()?;
validate_participant_address("from", &self.from)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct InboxPage {
pub messages: Vec<Message>,
pub unread: u32,
}
impl InboxPage {
pub fn validate(&self) -> Result<(), MailError> {
for message in &self.messages {
message.validate()?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AckResponse {
pub ack: Ack,
}
impl AckResponse {
pub fn validate(&self) -> Result<(), MailError> {
self.ack.validate()
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct UnreadCount {
pub target: Address,
pub unread: u32,
}
impl UnreadCount {
pub fn validate(&self) -> Result<(), MailError> {
validate_participant_address("target", &self.target)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DeliveryNotification {
pub account: ParticipantId,
pub to: Address,
pub message_id: MessageId,
pub from: Address,
}
impl DeliveryNotification {
pub fn validate(&self) -> Result<(), MailError> {
self.account.validate()?;
self.to.validate()?;
self.message_id.validate()?;
self.from.validate()
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_MESSAGE_ID: &str = "m4a_0123456789abcdef01234567";
fn participant(value: &str) -> ParticipantId {
ParticipantId::new(value).expect("test participant id is valid")
}
fn message_id() -> MessageId {
MessageId::new(VALID_MESSAGE_ID).expect("test message id is valid")
}
fn session_id(value: &str) -> SessionId {
SessionId::new(value).expect("test session id is valid")
}
fn sample_message() -> Message {
Message {
message_id: message_id(),
from: Address::Direct { participant: participant("alice") },
to: Address::Direct { participant: participant("bob") },
subject: "hi".to_string(),
body: "hi".to_string(),
reply_to: None,
correlation: None,
refs: Vec::new(),
created_at_unix_ms: 1,
}
}
#[test]
fn message_id_round_trips_through_display_from_str_and_serde() {
let id: MessageId = VALID_MESSAGE_ID.parse().expect("valid id parses");
assert_eq!(id.to_string(), VALID_MESSAGE_ID);
assert_eq!(id.as_str(), VALID_MESSAGE_ID);
let json = serde_json::to_string(&id).expect("id serializes");
assert_eq!(json, format!("\"{VALID_MESSAGE_ID}\""));
let decoded: MessageId = serde_json::from_str(&json).expect("id deserializes");
assert_eq!(decoded, id);
}
#[test]
fn message_id_rejects_wrong_prefix() {
let err = MessageId::new("wrong_0123456789abcdef01234567").expect_err("wrong prefix must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "message id"));
}
#[test]
fn message_id_rejects_short_body() {
let err = MessageId::new("m4a_0123456789abcdef").expect_err("short body must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "message id"));
}
#[test]
fn message_id_rejects_non_hex_body() {
let err = MessageId::new("m4a_0123456789abcdef0123456g").expect_err("non-hex body must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "message id"));
}
#[test]
fn participant_id_rejects_empty() {
let err = ParticipantId::new("").expect_err("empty selector must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "participant id"));
}
#[test]
fn participant_id_rejects_129_bytes() {
let value = "a".repeat(129);
let err = ParticipantId::new(value).expect_err("129-byte selector must be rejected");
assert!(matches!(err, MailError::TooLarge { field, limit, actual }
if field == "participant id" && limit == SELECTOR_MAX_BYTES && actual == 129));
}
#[test]
fn participant_id_accepts_exactly_128_bytes() {
let value = "a".repeat(SELECTOR_MAX_BYTES);
ParticipantId::new(value).expect("128-byte selector is accepted");
}
#[test]
fn participant_id_rejects_non_ascii() {
let err = ParticipantId::new("héllo").expect_err("non-ASCII selector must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "participant id"));
}
#[test]
fn participant_id_rejects_disallowed_characters() {
for value in ["a/b", "a b"] {
let err = ParticipantId::new(value).expect_err("disallowed character must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "participant id"));
}
}
#[test]
fn room_id_and_participant_id_are_distinct_types() {
let room = RoomId::new("shared-name").expect("valid room id");
let participant = ParticipantId::new("shared-name").expect("valid participant id");
assert_eq!(room.as_str(), participant.as_str());
}
#[test]
fn subject_is_accepted_at_exactly_the_byte_limit() {
let subject = "a".repeat(SUBJECT_MAX_BYTES);
validate_subject(&subject).expect("exactly at the limit is accepted");
}
#[test]
fn subject_is_rejected_one_byte_over_the_limit() {
let subject = "a".repeat(SUBJECT_MAX_BYTES + 1);
let err = validate_subject(&subject).expect_err("one byte over the limit must be rejected");
assert!(matches!(err, MailError::TooLarge { field, limit, actual }
if field == "subject" && limit == SUBJECT_MAX_BYTES && actual == SUBJECT_MAX_BYTES + 1));
}
#[test]
fn subject_byte_limit_counts_bytes_not_chars_for_multi_byte_utf8() {
let subject: String = "\u{1F600}".repeat(200);
assert!(subject.chars().count() < SUBJECT_MAX_BYTES, "under the byte bound counted as chars");
assert!(subject.len() > SUBJECT_MAX_BYTES, "over the byte bound counted as bytes");
let err = validate_subject(&subject).expect_err("byte length must govern, not char count");
assert!(matches!(err, MailError::TooLarge { field, .. } if field == "subject"));
}
#[test]
fn body_is_accepted_at_exactly_the_byte_limit() {
let body = "a".repeat(BODY_MAX_BYTES);
validate_body(&body).expect("exactly at the limit is accepted");
}
#[test]
fn body_is_rejected_one_byte_over_the_limit() {
let body = "a".repeat(BODY_MAX_BYTES + 1);
let err = validate_body(&body).expect_err("one byte over the limit must be rejected");
assert!(matches!(err, MailError::TooLarge { field, limit, actual }
if field == "body" && limit == BODY_MAX_BYTES && actual == BODY_MAX_BYTES + 1));
}
#[test]
fn body_byte_limit_counts_bytes_not_chars_for_multi_byte_utf8() {
let body: String = "\u{1F600}".repeat(20_000);
assert!(body.chars().count() < BODY_MAX_BYTES, "under the byte bound counted as chars");
assert!(body.len() > BODY_MAX_BYTES, "over the byte bound counted as bytes");
let err = validate_body(&body).expect_err("byte length must govern, not char count");
assert!(matches!(err, MailError::TooLarge { field, .. } if field == "body"));
}
#[test]
fn message_rejects_reply_to_referencing_its_own_message_id() {
let mut message = sample_message();
message.reply_to = Some(message.message_id.clone());
let err = message.validate().expect_err("self reply_to must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "reply_to"));
}
#[test]
fn message_rejects_more_refs_than_the_bound() {
let mut message = sample_message();
message.refs = (0..=REFS_MAX)
.map(|index| MessageRef {
kind: "note".to_string(),
locator: format!("loc-{index}"),
digest: None,
})
.collect();
let err = message.validate().expect_err("refs over the bound must be rejected");
assert!(matches!(err, MailError::TooLarge { field, limit, actual }
if field == "refs" && limit == REFS_MAX && actual == REFS_MAX + 1));
}
#[test]
fn message_accepts_exactly_refs_max_refs() {
let mut message = sample_message();
message.refs = (0..REFS_MAX)
.map(|index| MessageRef {
kind: "note".to_string(),
locator: format!("loc-{index}"),
digest: None,
})
.collect();
message.validate().expect("exactly the bound is accepted");
}
#[test]
fn message_json_without_refs_key_still_deserializes() {
let json = format!(
r#"{{
"message_id": "{VALID_MESSAGE_ID}",
"from": {{"kind": "direct", "participant": "alice"}},
"to": {{"kind": "direct", "participant": "bob"}},
"subject": "hi",
"body": "hi",
"reply_to": null,
"correlation": null,
"created_at_unix_ms": 1
}}"#
);
let message: Message = serde_json::from_str(&json).expect("old shape without refs must still deserialize");
assert!(message.refs.is_empty());
message.validate().expect("decoded message is otherwise valid");
}
#[test]
fn address_serialises_to_the_tagged_kind_form_and_round_trips() {
let direct = Address::Direct { participant: participant("alice") };
let json = serde_json::to_value(&direct).expect("direct address serializes");
assert_eq!(json, serde_json::json!({"kind": "direct", "participant": "alice"}));
let decoded: Address = serde_json::from_value(json).expect("direct address deserializes");
assert_eq!(decoded, direct);
let session = Address::Session { participant: participant("claude"), session: session_id("s-7f3a0000") };
let json = serde_json::to_value(&session).expect("session address serializes");
assert_eq!(
json,
serde_json::json!({"kind": "session", "participant": "claude", "session": "s-7f3a0000"})
);
let decoded: Address = serde_json::from_value(json).expect("session address deserializes");
assert_eq!(decoded, session);
let room = Address::Room { room: RoomId::new("room-1").expect("valid room id") };
let json = serde_json::to_value(&room).expect("room address serializes");
assert_eq!(json, serde_json::json!({"kind": "room", "room": "room-1"}));
let decoded: Address = serde_json::from_value(json).expect("room address deserializes");
assert_eq!(decoded, room);
}
#[test]
fn address_display_and_from_str_use_the_familiar_shape() {
let direct = Address::Direct { participant: participant("claude") };
assert_eq!(direct.to_string(), "claude");
assert_eq!("claude".parse::<Address>().expect("direct address parses"), direct);
let session = Address::Session { participant: participant("claude"), session: session_id("s-7f3a0000") };
assert_eq!(session.to_string(), "claude/s-7f3a0000");
assert_eq!("claude/s-7f3a0000".parse::<Address>().expect("session address parses"), session);
let room = Address::Room { room: RoomId::new("room-1").expect("valid room id") };
assert_eq!(room.to_string(), "#room-1");
assert_eq!("#room-1".parse::<Address>().expect("room address parses"), room);
}
#[test]
fn address_account_names_the_owning_account_and_none_for_a_room() {
let alice = participant("alice");
assert_eq!(Address::Direct { participant: alice.clone() }.account(), Some(&alice));
assert_eq!(
Address::Session { participant: alice.clone(), session: session_id("s-7f3a0000") }.account(),
Some(&alice)
);
assert_eq!(Address::Room { room: RoomId::new("room-1").expect("valid room id") }.account(), None);
}
#[test]
fn message_and_ack_and_send_response_refuse_a_room_as_the_participant_address() {
let room = Address::Room { room: RoomId::new("room-1").expect("valid room id") };
let mut message = sample_message();
message.from = room.clone();
let err = message.validate().expect_err("a room must not be accepted as `from`");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "from"));
let ack = Ack { message_id: message_id(), reader: room.clone(), acked_at_unix_ms: 1 };
let err = ack.validate().expect_err("a room must not be accepted as `reader`");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "reader"));
let response = SendResponse { message_id: message_id(), from: room };
let err = response.validate().expect_err("a room must not be accepted as `from`");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "from"));
}
#[test]
fn session_id_round_trips_and_rejects_a_short_body() {
let id: SessionId = "s-7f3a0000".parse().expect("valid session id parses");
assert_eq!(id.to_string(), "s-7f3a0000");
let err = SessionId::new("s-abc").expect_err("a body shorter than the minimum must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "session id"));
let err = SessionId::new("wrong-7f3a0000").expect_err("a wrong prefix must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "session id"));
}
#[test]
fn session_card_validates_every_group_and_rejects_a_control_character_anywhere() {
let mut card = SessionCard {
attested: SessionAttested { pid: 4242, started_at_unix_ms: 1, exe: Some("claude.exe".to_string()) },
corroborated: SessionCorroborated {
provider_session_id: Some(Declared::new("prov-1".to_string())),
model: Some(Declared::new("opus".to_string())),
cwd: None,
},
declared: SessionDeclared { working_on: Some("parity work".to_string()), role: None, parent: None },
};
card.validate().expect("a well-formed card validates");
card.corroborated.model = Some(Declared::new("bad\u{0007}model".to_string()));
let err = card.validate().expect_err("a control character in a corroborated field must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "corroborated model"));
}
#[test]
fn directory_entry_nests_its_sessions_and_defaults_to_none_on_decode() {
let entry = DirectoryEntry {
id: participant("claude"),
label: None,
sessions: vec![SessionEntry {
id: session_id("s-7f3a0000"),
card: SessionCard {
attested: SessionAttested { pid: 1, started_at_unix_ms: 1, exe: None },
corroborated: SessionCorroborated { provider_session_id: None, model: None, cwd: None },
declared: SessionDeclared::default(),
},
last_seen_unix_ms: 1,
live: true,
}],
};
entry.validate().expect("a well-formed entry with a session validates");
let json = serde_json::json!({"id": "claude", "label": null});
let decoded: DirectoryEntry = serde_json::from_value(json).expect("an entry without sessions still decodes");
assert!(decoded.sessions.is_empty());
}
#[test]
fn inbox_request_defaults_limit_when_json_omits_it() {
let request: InboxRequest = serde_json::from_str(r#"{"since_unix_ms": null}"#)
.expect("inbox request without limit still deserializes");
assert_eq!(request.limit, INBOX_LIMIT_DEFAULT);
request.validate().expect("default limit is valid");
}
#[test]
fn inbox_request_rejects_limit_over_the_max() {
let request = InboxRequest { since_unix_ms: None, limit: INBOX_LIMIT_MAX + 1, wait_secs: None };
let err = request.validate().expect_err("limit over the max must be rejected");
assert!(matches!(err, MailError::TooLarge { field, .. } if field == "limit"));
}
#[test]
fn inbox_request_rejects_zero_limit() {
let request = InboxRequest { since_unix_ms: None, limit: 0, wait_secs: None };
let err = request.validate().expect_err("zero limit must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "limit"));
}
#[test]
fn inbox_request_defaults_wait_secs_to_none_when_json_omits_it() {
let request: InboxRequest = serde_json::from_str(r#"{"since_unix_ms": null}"#)
.expect("inbox request without wait_secs still deserializes");
assert_eq!(request.wait_secs, None);
}
#[test]
fn inbox_request_accepts_wait_secs_over_the_cap_without_refusing() {
let request =
InboxRequest { since_unix_ms: None, limit: INBOX_LIMIT_DEFAULT, wait_secs: Some(INBOX_WAIT_SECS_MAX + 1) };
request.validate().expect("wait_secs over the cap is not a validation failure");
}
#[test]
fn delivery_notification_round_trips_and_never_carries_a_subject_or_body_field() {
let notification = DeliveryNotification {
account: participant("alice"),
to: Address::Direct { participant: participant("alice") },
message_id: message_id(),
from: Address::Direct { participant: participant("bob") },
};
notification.validate().expect("a well-formed notification validates");
let json = serde_json::to_value(¬ification).expect("notification serializes");
assert!(json.get("subject").is_none());
assert!(json.get("body").is_none());
let decoded: DeliveryNotification =
serde_json::from_value(json).expect("notification round-trips through serde");
assert_eq!(decoded, notification);
}
#[test]
fn message_ref_rejects_more_than_the_digest_bound() {
let reference = MessageRef {
kind: "note".to_string(),
locator: "loc".to_string(),
digest: Some("a".repeat(REF_DIGEST_MAX_CHARS + 1)),
};
let err = reference.validate().expect_err("digest over the bound must be rejected");
assert!(matches!(err, MailError::TooLarge { field, .. } if field == "ref digest"));
}
#[test]
fn a_send_request_carrying_only_its_required_fields_decodes() {
let json = r#"{"to":{"kind":"direct","participant":"bob"},"subject":"s","body":"b"}"#;
let req: SendRequest = serde_json::from_str(json).expect("minimal send request decodes");
assert!(req.reply_to.is_none());
assert!(req.correlation.is_none());
assert!(req.idempotency_key.is_none());
assert!(req.refs.is_empty());
req.validate().expect("and it validates");
}
#[test]
fn an_inbox_request_carrying_nothing_decodes_with_the_default_limit() {
let req: InboxRequest = serde_json::from_str("{}").expect("empty inbox request decodes");
assert!(req.since_unix_ms.is_none());
assert_eq!(req.limit, INBOX_LIMIT_DEFAULT);
}
#[test]
fn send_request_old_shape_without_idempotency_key_still_deserializes_and_validates() {
let json = serde_json::json!({
"to": {"kind": "direct", "participant": "bob"},
"subject": "hi",
"body": "hi",
"reply_to": null,
"correlation": null
});
let request: SendRequest = serde_json::from_value(json)
.expect("old shape without idempotency_key must still deserialize");
assert_eq!(request.idempotency_key, None);
request.validate().expect("decoded request is otherwise valid");
}
#[test]
fn directory_entry_serialises_with_id_label_and_sessions() {
let entry = DirectoryEntry { id: participant("alice"), label: Some("Alice".to_string()), sessions: Vec::new() };
let json = serde_json::to_value(&entry).expect("directory entry serializes");
assert_eq!(json, serde_json::json!({"id": "alice", "label": "Alice", "sessions": []}));
let decoded: DirectoryEntry = serde_json::from_value(json).expect("directory entry deserializes");
assert_eq!(decoded, entry);
}
#[test]
fn directory_entry_rejects_a_control_character_label() {
let entry =
DirectoryEntry { id: participant("alice"), label: Some("bad\u{0007}label".to_string()), sessions: Vec::new() };
let err = entry.validate().expect_err("control character in label must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "label"));
}
#[test]
fn room_entry_round_trips_its_member_flag() {
let entry = RoomEntry { id: RoomId::new("room-1").expect("valid room id"), member: true };
let json = serde_json::to_value(&entry).expect("room entry serializes");
assert_eq!(json, serde_json::json!({"id": "room-1", "member": true}));
let decoded: RoomEntry = serde_json::from_value(json).expect("room entry deserializes");
assert_eq!(decoded, entry);
}
#[test]
fn directory_validates_every_entry_it_carries() {
let directory = Directory {
participants: vec![DirectoryEntry { id: participant("alice"), label: None, sessions: Vec::new() }],
rooms: vec![RoomEntry { id: RoomId::new("room-1").expect("valid room id"), member: false }],
};
directory.validate().expect("a directory of otherwise-valid entries validates");
}
#[test]
fn message_ref_rejects_non_hex_digest() {
let reference = MessageRef {
kind: "note".to_string(),
locator: "loc".to_string(),
digest: Some("not-hex".to_string()),
};
let err = reference.validate().expect_err("non-hex digest must be rejected");
assert!(matches!(err, MailError::Malformed { field, .. } if field == "ref digest"));
}
}