use crate::types::Time;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::net::SocketAddr;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub mod client;
pub mod encryption;
pub mod quota;
pub mod relay;
pub mod storage;
pub use client::MailboxClient;
pub use encryption::{ChunkNonce, EncryptedChunk, MailboxKey};
pub use quota::{QuotaManager, QuotaPolicy, QuotaUsage};
pub use relay::{RelayClient, RelayMessage, RelayProtocol, RelayResponse};
pub use storage::{MailboxEntry, MailboxStorage, TransferState};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MailboxTransferId(pub [u8; 16]);
impl MailboxTransferId {
pub fn new() -> Self {
let mut bytes = [0u8; 16];
getrandom::fill(&mut bytes).expect("OS entropy unavailable for mailbox transfer id");
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
Self(bytes)
}
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(bytes)
}
pub fn to_bytes(self) -> [u8; 16] {
self.0
}
}
impl Default for MailboxTransferId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for MailboxTransferId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let b = self.0;
write!(
f,
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
b[0],
b[1],
b[2],
b[3],
b[4],
b[5],
b[6],
b[7],
b[8],
b[9],
b[10],
b[11],
b[12],
b[13],
b[14],
b[15]
)
}
}
impl Serialize for MailboxTransferId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for MailboxTransferId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
parse_transfer_id(&raw).map_err(serde::de::Error::custom)
}
}
fn parse_transfer_id(raw: &str) -> Result<MailboxTransferId, String> {
let compact = raw.replace('-', "");
if compact.len() != 32 {
return Err(format!(
"mailbox transfer id must contain 32 hex digits, got {}",
compact.len()
));
}
let decoded =
hex::decode(&compact).map_err(|err| format!("invalid mailbox transfer id hex: {err}"))?;
let mut bytes = [0u8; 16];
bytes.copy_from_slice(&decoded);
Ok(MailboxTransferId(bytes))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PeerId(pub String);
impl PeerId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for PeerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone)]
pub struct MailboxConfig {
pub local_peer_id: PeerId,
pub relay_endpoint: SocketAddr,
pub encryption_key: MailboxKey,
pub quota_limit: u64,
pub default_retention: Duration,
pub operation_timeout: Duration,
pub max_chunk_size: usize,
pub tamper_detection: bool,
}
impl Default for MailboxConfig {
fn default() -> Self {
let encryption_key = MailboxKey::generate();
let local_peer_id = derive_peer_id_from_key(&encryption_key);
Self {
local_peer_id,
relay_endpoint: "127.0.0.1:8080".parse().unwrap(),
encryption_key,
quota_limit: 100_000_000, default_retention: Duration::from_secs(7 * 24 * 3600), operation_timeout: Duration::from_secs(30),
max_chunk_size: 1024 * 1024, tamper_detection: true,
}
}
}
fn derive_peer_id_from_key(key: &MailboxKey) -> PeerId {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(key.as_bytes());
PeerId::new(format!("peer-{}", hex::encode(&digest[..8])))
}
pub(crate) fn mailbox_time_now() -> Time {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.min(u128::from(u64::MAX)) as u64;
Time::from_nanos(nanos)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MailboxTransferMetadata {
pub transfer_id: MailboxTransferId,
pub destination_peer: PeerId,
pub created_at: Time,
pub expires_at: Time,
pub total_size: u64,
pub chunk_count: u32,
pub encrypted_metadata: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MailboxOperationResult {
pub success: bool,
pub transfer_id: Option<MailboxTransferId>,
pub quota_usage: QuotaUsage,
pub duration_ms: u64,
pub messages: Vec<String>,
pub relay_receipt: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MailboxEvent {
TransferUploadStarted {
transfer_id: MailboxTransferId,
destination: PeerId,
total_size: u64,
},
ChunkUploaded {
transfer_id: MailboxTransferId,
chunk_index: u32,
encrypted_size: usize,
},
TransferUploadCompleted {
transfer_id: MailboxTransferId,
duration_ms: u64,
total_chunks: u32,
},
TransferDownloadStarted {
transfer_id: MailboxTransferId,
sender: PeerId,
},
ChunkDownloaded {
transfer_id: MailboxTransferId,
chunk_index: u32,
decrypted_size: usize,
},
TransferDownloadCompleted {
transfer_id: MailboxTransferId,
duration_ms: u64,
verification_status: String,
},
QuotaWarning {
current_usage: u64,
quota_limit: u64,
utilization_percent: f64,
},
TamperDetected {
transfer_id: MailboxTransferId,
tamper_type: String,
evidence: String,
},
CleanupPerformed {
expired_transfers: u32,
bytes_freed: u64,
},
}
#[derive(Debug, thiserror::Error)]
pub enum MailboxError {
#[error("Relay communication error: {message}")]
RelayError { message: String },
#[error("Cryptographic error: {operation}")]
CryptoError { operation: String },
#[error("Quota exceeded: {usage} / {limit} bytes")]
QuotaExceeded { usage: u64, limit: u64 },
#[error("Transfer not found: {transfer_id}")]
TransferNotFound { transfer_id: MailboxTransferId },
#[error("Transfer expired: {transfer_id}, expired at {expired_at:?}")]
TransferExpired {
transfer_id: MailboxTransferId,
expired_at: Time,
},
#[error("Tamper detected in {transfer_id}: {evidence}")]
TamperDetected {
transfer_id: MailboxTransferId,
evidence: String,
},
#[error("Invalid mailbox configuration: {details}")]
ConfigurationError { details: String },
#[error("Network error: {details}")]
NetworkError { details: String },
}
pub type MailboxResult<T> = Result<T, MailboxError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mailbox_transfer_id_generation() {
let id1 = MailboxTransferId::new();
let id2 = MailboxTransferId::new();
assert_ne!(id1, id2);
let bytes = id1.to_bytes();
let reconstructed = MailboxTransferId::from_bytes(bytes);
assert_eq!(id1, reconstructed);
}
#[test]
fn test_peer_id_creation() {
let peer = PeerId::new("test-peer-123");
assert_eq!(peer.as_str(), "test-peer-123");
}
#[test]
fn test_mailbox_config_defaults() {
let config = MailboxConfig::default();
assert_eq!(config.quota_limit, 100_000_000);
assert_eq!(config.max_chunk_size, 1024 * 1024);
assert!(config.tamper_detection);
assert_eq!(config.operation_timeout, Duration::from_secs(30));
}
#[test]
fn test_mailbox_event_serialization() {
let event = MailboxEvent::TransferUploadStarted {
transfer_id: MailboxTransferId::new(),
destination: PeerId::new("peer-123"),
total_size: 1024,
};
let serialized = serde_json::to_string(&event).unwrap();
let deserialized: MailboxEvent = serde_json::from_str(&serialized).unwrap();
match (event, deserialized) {
(
MailboxEvent::TransferUploadStarted { total_size: s1, .. },
MailboxEvent::TransferUploadStarted { total_size: s2, .. },
) => {
assert_eq!(s1, s2);
}
_ => panic!("Event type mismatch after serialization"),
}
}
#[test]
fn test_mailbox_error_display() {
let error = MailboxError::QuotaExceeded {
usage: 1500,
limit: 1000,
};
let display = format!("{}", error);
assert!(display.contains("Quota exceeded"));
assert!(display.contains("1500"));
assert!(display.contains("1000"));
}
fn fixed_transfer_id() -> MailboxTransferId {
MailboxTransferId::from_bytes([
0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4,
0x30, 0xc8,
])
}
#[test]
fn golden_mailbox_transfer_id_serialization() {
let transfer_id = fixed_transfer_id();
insta::assert_json_snapshot!(transfer_id, @r###"
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
"###);
}
#[test]
fn golden_peer_id_serialization() {
let peer_id = PeerId::new("peer-atp-node-f3c4d5e6");
insta::assert_json_snapshot!(peer_id, @r###"
"peer-atp-node-f3c4d5e6"
"###);
}
#[test]
fn golden_mailbox_transfer_metadata_serialization() {
const CREATED_AT_NANOS: u64 = 1_640_995_200_000_000_000;
const WEEK_NANOS: u64 = 604_800_000_000_000;
let metadata = MailboxTransferMetadata {
transfer_id: fixed_transfer_id(),
destination_peer: PeerId::new("peer-destination-node"),
created_at: Time::from_nanos(CREATED_AT_NANOS), expires_at: Time::from_nanos(CREATED_AT_NANOS + WEEK_NANOS), total_size: 2048576, chunk_count: 4,
encrypted_metadata: vec![0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe],
};
insta::assert_json_snapshot!(metadata, @r###"
{
"transfer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"destination_peer": "peer-destination-node",
"created_at": 1640995200000000000,
"expires_at": 1641600000000000000,
"total_size": 2048576,
"chunk_count": 4,
"encrypted_metadata": [
222,
173,
190,
239,
202,
254,
186,
190
]
}
"###);
}
#[test]
fn golden_mailbox_operation_result_serialization() {
use crate::atp::mailbox::quota::QuotaUsage;
use std::time::UNIX_EPOCH;
let result = MailboxOperationResult {
success: true,
transfer_id: Some(fixed_transfer_id()),
quota_usage: QuotaUsage {
bytes_used: 1048576, active_transfers: 3,
total_transfers: 15,
last_updated: UNIX_EPOCH + std::time::Duration::from_secs(1640995200), },
duration_ms: 1234,
messages: vec![
"Transfer initiated successfully".to_string(),
"Encryption completed".to_string(),
],
relay_receipt: Some("receipt-abc123def456".to_string()),
};
insta::assert_json_snapshot!(result, @r###"
{
"success": true,
"transfer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"quota_usage": {
"bytes_used": 1048576,
"active_transfers": 3,
"total_transfers": 15,
"last_updated": {
"secs_since_epoch": 1640995200,
"nanos_since_epoch": 0
}
},
"duration_ms": 1234,
"messages": [
"Transfer initiated successfully",
"Encryption completed"
],
"relay_receipt": "receipt-abc123def456"
}
"###);
}
#[test]
fn golden_mailbox_event_transfer_upload_started_serialization() {
let event = MailboxEvent::TransferUploadStarted {
transfer_id: fixed_transfer_id(),
destination: PeerId::new("peer-upload-target"),
total_size: 3145728, };
insta::assert_json_snapshot!(event, @r###"
{
"TransferUploadStarted": {
"transfer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"destination": "peer-upload-target",
"total_size": 3145728
}
}
"###);
}
#[test]
fn golden_mailbox_event_quota_warning_serialization() {
let event = MailboxEvent::QuotaWarning {
current_usage: 85000000, quota_limit: 100000000, utilization_percent: 85.0,
};
insta::assert_json_snapshot!(event, @r###"
{
"QuotaWarning": {
"current_usage": 85000000,
"quota_limit": 100000000,
"utilization_percent": 85.0
}
}
"###);
}
#[test]
fn golden_mailbox_event_tamper_detected_serialization() {
let event = MailboxEvent::TamperDetected {
transfer_id: fixed_transfer_id(),
tamper_type: "checksum_mismatch".to_string(),
evidence: "expected_hash=abc123, actual_hash=def456".to_string(),
};
insta::assert_json_snapshot!(event, @r###"
{
"TamperDetected": {
"transfer_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"tamper_type": "checksum_mismatch",
"evidence": "expected_hash=abc123, actual_hash=def456"
}
}
"###);
}
#[test]
fn golden_mailbox_event_cleanup_performed_serialization() {
let event = MailboxEvent::CleanupPerformed {
expired_transfers: 7,
bytes_freed: 15728640, };
insta::assert_json_snapshot!(event, @r###"
{
"CleanupPerformed": {
"expired_transfers": 7,
"bytes_freed": 15728640
}
}
"###);
}
}