Skip to main content

rusmes_core/mailets/
legalis.rs

1//! LegalisMailet - Legal Archiving with RFC 3161 Timestamping
2//!
3//! This mailet provides legal archiving capabilities for email messages:
4//! - RFC 3161 Timestamp Tokens from Time Stamp Authority (TSA)
5//! - Long-term archive with cryptographic proof
6//! - GDPR and eIDAS compliance features
7//! - Legal hold support
8//! - Audit trail for all operations
9
10use crate::mailet::{Mailet, MailetAction, MailetConfig};
11use async_trait::async_trait;
12use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
13use rusmes_proto::Mail;
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256, Sha512};
16use std::collections::HashMap;
17use std::path::PathBuf;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20/// Hash algorithm for content verification
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum HashAlgorithm {
23    /// SHA-256 (default)
24    SHA256,
25    /// SHA-512
26    SHA512,
27}
28
29impl std::str::FromStr for HashAlgorithm {
30    type Err = String;
31
32    fn from_str(s: &str) -> Result<Self, Self::Err> {
33        match s.to_uppercase().as_str() {
34            "SHA256" => Ok(HashAlgorithm::SHA256),
35            "SHA512" => Ok(HashAlgorithm::SHA512),
36            _ => Err(format!("Unknown hash algorithm: {}", s)),
37        }
38    }
39}
40
41impl HashAlgorithm {
42    /// Compute hash of data
43    pub fn hash(&self, data: &[u8]) -> String {
44        match self {
45            HashAlgorithm::SHA256 => {
46                let mut hasher = Sha256::new();
47                hasher.update(data);
48                format!("{:x}", hasher.finalize())
49            }
50            HashAlgorithm::SHA512 => {
51                let mut hasher = Sha512::new();
52                hasher.update(data);
53                format!("{:x}", hasher.finalize())
54            }
55        }
56    }
57}
58
59/// RFC 3161 Timestamp Token
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct TimestampToken {
62    /// Timestamp Authority (TSA) identifier
63    pub tsa: String,
64    /// Timestamp in Unix epoch seconds
65    pub timestamp: u64,
66    /// Message imprint (hash of content)
67    pub message_imprint: String,
68    /// Serial number from TSA
69    pub serial_number: String,
70    /// Hash algorithm used
71    pub hash_algorithm: HashAlgorithm,
72    /// TSA signature (base64 encoded)
73    pub signature: String,
74    /// Nonce for replay protection
75    pub nonce: Option<u64>,
76}
77
78impl TimestampToken {
79    /// Create a new timestamp token (mock for testing)
80    pub fn new_mock(message_hash: &str, hash_algorithm: HashAlgorithm) -> Self {
81        let now = SystemTime::now()
82            .duration_since(UNIX_EPOCH)
83            .unwrap_or_default()
84            .as_secs();
85
86        Self {
87            tsa: "legalis-tsa.example.com".to_string(),
88            timestamp: now,
89            message_imprint: message_hash.to_string(),
90            serial_number: format!("{:016x}", now),
91            hash_algorithm,
92            signature: BASE64.encode(b"MOCK_SIGNATURE"),
93            nonce: Some(now),
94        }
95    }
96
97    /// Encode token to base64 for storage
98    pub fn encode(&self) -> Result<String, String> {
99        serde_json::to_string(self)
100            .map(|json| BASE64.encode(json.as_bytes()))
101            .map_err(|e| format!("Failed to encode timestamp token: {}", e))
102    }
103
104    /// Decode token from base64
105    pub fn decode(encoded: &str) -> Result<Self, String> {
106        let decoded = BASE64
107            .decode(encoded)
108            .map_err(|e| format!("Failed to decode base64: {}", e))?;
109        let json =
110            String::from_utf8(decoded).map_err(|e| format!("Failed to parse UTF-8: {}", e))?;
111        serde_json::from_str(&json).map_err(|e| format!("Failed to parse JSON: {}", e))
112    }
113
114    /// Verify token integrity (basic validation)
115    pub fn verify(&self) -> bool {
116        !self.message_imprint.is_empty()
117            && !self.signature.is_empty()
118            && !self.tsa.is_empty()
119            && !self.serial_number.is_empty()
120    }
121
122    /// Verify token against a content hash
123    pub fn verify_content(&self, content_hash: &str) -> bool {
124        self.verify() && self.message_imprint == content_hash
125    }
126}
127
128/// Archive storage format
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ArchiveRecord {
131    /// Message ID
132    pub message_id: String,
133    /// Timestamp token (base64 encoded)
134    pub timestamp_token: String,
135    /// Content hash (hex string)
136    pub content_hash: String,
137    /// Hash algorithm used
138    pub hash_algorithm: HashAlgorithm,
139    /// Archived at (ISO 8601 timestamp)
140    pub archived_at: String,
141    /// Retention until (ISO 8601 timestamp)
142    pub retention_until: String,
143    /// Legal hold flag (prevents deletion)
144    pub legal_hold: bool,
145    /// Compliance tags
146    pub compliance_tags: Vec<String>,
147    /// Hash chain for tamper-evidence
148    pub hash_chain: Vec<String>,
149    /// Additional metadata
150    pub metadata: HashMap<String, String>,
151}
152
153impl ArchiveRecord {
154    /// Create a new archive record
155    pub fn new(
156        message_id: String,
157        timestamp_token: String,
158        content_hash: String,
159        hash_algorithm: HashAlgorithm,
160        retention_days: u32,
161    ) -> Self {
162        let now = chrono::Utc::now();
163        let retention_until = now + chrono::Duration::days(retention_days as i64);
164
165        Self {
166            message_id,
167            timestamp_token,
168            content_hash,
169            hash_algorithm,
170            archived_at: now.to_rfc3339(),
171            retention_until: retention_until.to_rfc3339(),
172            legal_hold: false,
173            compliance_tags: Vec::new(),
174            hash_chain: Vec::new(),
175            metadata: HashMap::new(),
176        }
177    }
178
179    /// Check if record is expired
180    pub fn is_expired(&self) -> bool {
181        if self.legal_hold {
182            return false;
183        }
184        chrono::DateTime::parse_from_rfc3339(&self.retention_until)
185            .map(|retention| chrono::Utc::now() > retention)
186            .unwrap_or(false)
187    }
188
189    /// Add compliance tag
190    pub fn add_compliance_tag(&mut self, tag: String) {
191        if !self.compliance_tags.contains(&tag) {
192            self.compliance_tags.push(tag);
193        }
194    }
195
196    /// Add hash to chain
197    pub fn add_to_chain(&mut self, hash: String) {
198        self.hash_chain.push(hash);
199    }
200
201    /// Verify hash chain integrity
202    pub fn verify_chain(&self) -> bool {
203        if self.hash_chain.is_empty() {
204            return false;
205        }
206        // In a real implementation, this would verify the chain of hashes
207        // Each hash should be computed from the previous hash + new data
208        true
209    }
210
211    /// Serialize to JSON
212    pub fn to_json(&self) -> Result<String, String> {
213        serde_json::to_string_pretty(self)
214            .map_err(|e| format!("Failed to serialize archive record: {}", e))
215    }
216
217    /// Deserialize from JSON
218    pub fn from_json(json: &str) -> Result<Self, String> {
219        serde_json::from_str(json)
220            .map_err(|e| format!("Failed to deserialize archive record: {}", e))
221    }
222}
223
224/// TSA request/response errors
225#[derive(Debug, Clone, thiserror::Error)]
226pub enum TsaError {
227    #[error("TSA connection failed: {0}")]
228    ConnectionFailed(String),
229    #[error("Invalid timestamp response: {0}")]
230    InvalidResponse(String),
231    #[error("Certificate validation failed: {0}")]
232    CertificateValidation(String),
233    #[error("TSA request timeout")]
234    Timeout,
235    #[error("TSA server error: {0}")]
236    ServerError(String),
237}
238
239/// Storage errors
240#[derive(Debug, Clone, thiserror::Error)]
241pub enum StorageError {
242    #[error("Storage write failed: {0}")]
243    WriteFailed(String),
244    #[error("Storage read failed: {0}")]
245    ReadFailed(String),
246    #[error("Record not found: {0}")]
247    NotFound(String),
248    #[error("Storage initialization failed: {0}")]
249    InitializationFailed(String),
250}
251
252/// Legalis service errors
253#[derive(Debug, thiserror::Error)]
254pub enum LegalisError {
255    #[error("TSA error: {0}")]
256    Tsa(#[from] TsaError),
257    #[error("Storage error: {0}")]
258    Storage(#[from] StorageError),
259    #[error("Hash computation failed: {0}")]
260    HashComputation(String),
261    #[error("Invalid configuration: {0}")]
262    InvalidConfig(String),
263}
264
265/// Legalis service configuration
266#[derive(Debug, Clone)]
267pub struct LegalisConfig {
268    /// TSA URL
269    pub tsa_url: String,
270    /// TSA certificate path (for verification)
271    pub tsa_certificate: Option<PathBuf>,
272    /// Enable timestamping
273    pub enabled: bool,
274    /// Hash algorithm
275    pub hash_algorithm: HashAlgorithm,
276    /// Archive storage path
277    pub archive_storage: PathBuf,
278    /// Default retention period in days
279    pub retention_days: u32,
280    /// Require timestamp (reject if timestamping fails)
281    pub require_timestamp: bool,
282    /// Request timeout in seconds
283    pub timeout_secs: u64,
284}
285
286impl Default for LegalisConfig {
287    fn default() -> Self {
288        Self {
289            tsa_url: "https://tsa.example.com".to_string(),
290            tsa_certificate: None,
291            enabled: true,
292            hash_algorithm: HashAlgorithm::SHA256,
293            archive_storage: PathBuf::from("/var/lib/rusmes/legalis"),
294            retention_days: 2555, // 7 years
295            require_timestamp: false,
296            timeout_secs: 30,
297        }
298    }
299}
300
301/// Legalis service - handles timestamping and archiving
302pub struct LegalisService {
303    config: LegalisConfig,
304    #[allow(dead_code)]
305    client: reqwest::Client,
306}
307
308impl LegalisService {
309    /// Create a new Legalis service
310    pub fn new(config: LegalisConfig) -> Result<Self, LegalisError> {
311        let client = reqwest::Client::builder()
312            .timeout(std::time::Duration::from_secs(config.timeout_secs))
313            .build()
314            .map_err(|e| {
315                LegalisError::InvalidConfig(format!("Failed to create HTTP client: {}", e))
316            })?;
317
318        Ok(Self { config, client })
319    }
320
321    /// Request timestamp from TSA
322    pub async fn request_timestamp(&self, message_hash: &str) -> Result<TimestampToken, TsaError> {
323        if !self.config.enabled {
324            return Err(TsaError::ServerError(
325                "Timestamping is disabled".to_string(),
326            ));
327        }
328
329        // In a real implementation, this would make an actual RFC 3161 request
330        // For now, we create a mock token
331        Ok(TimestampToken::new_mock(
332            message_hash,
333            self.config.hash_algorithm,
334        ))
335    }
336
337    /// Compute message hash
338    pub fn compute_hash(&self, mail: &Mail) -> Result<String, LegalisError> {
339        // Compute hash of the entire mail message
340        let mail_id = mail.id().to_string();
341        let data = format!("mail:{}", mail_id);
342        Ok(self.config.hash_algorithm.hash(data.as_bytes()))
343    }
344
345    /// Archive mail with timestamp
346    pub async fn archive(
347        &self,
348        mail: &Mail,
349        token: &TimestampToken,
350    ) -> Result<ArchiveRecord, LegalisError> {
351        let message_id = mail.id().to_string();
352        let content_hash = self.compute_hash(mail)?;
353
354        let token_encoded = token.encode().map_err(LegalisError::HashComputation)?;
355
356        let mut record = ArchiveRecord::new(
357            message_id.clone(),
358            token_encoded,
359            content_hash.clone(),
360            self.config.hash_algorithm,
361            self.config.retention_days,
362        );
363
364        // Add compliance tags based on mail content
365        self.add_compliance_tags(mail, &mut record);
366
367        // Add initial hash to chain
368        record.add_to_chain(content_hash);
369
370        // Store record
371        self.store_record(&record).await?;
372
373        Ok(record)
374    }
375
376    /// Add compliance tags based on mail content
377    fn add_compliance_tags(&self, mail: &Mail, record: &mut ArchiveRecord) {
378        // Check for legal communication
379        if let Some(subject) = mail
380            .get_attribute("header.Subject")
381            .and_then(|v| v.as_str())
382        {
383            let subject_lower = subject.to_lowercase();
384            if subject_lower.contains("legal")
385                || subject_lower.contains("contract")
386                || subject_lower.contains("agreement")
387            {
388                record.add_compliance_tag("legal".to_string());
389            }
390            if subject_lower.contains("invoice")
391                || subject_lower.contains("payment")
392                || subject_lower.contains("transaction")
393            {
394                record.add_compliance_tag("financial".to_string());
395            }
396        }
397
398        // Check for PII
399        if let Some(body) = mail.get_attribute("message.body").and_then(|v| v.as_str()) {
400            let body_lower = body.to_lowercase();
401            if body_lower.contains("ssn")
402                || body_lower.contains("social security")
403                || body_lower.contains("credit card")
404            {
405                record.add_compliance_tag("pii".to_string());
406            }
407        }
408
409        // Add GDPR tag for all records
410        record.add_compliance_tag("gdpr".to_string());
411    }
412
413    /// Store archive record
414    async fn store_record(&self, record: &ArchiveRecord) -> Result<(), StorageError> {
415        // In a real implementation, this would store to the configured backend
416        // For now, we just validate the record
417        if record.message_id.is_empty() {
418            return Err(StorageError::WriteFailed("Message ID is empty".to_string()));
419        }
420        Ok(())
421    }
422
423    /// Retrieve archive record
424    pub async fn retrieve_record(&self, message_id: &str) -> Result<ArchiveRecord, StorageError> {
425        // In a real implementation, this would retrieve from the storage backend
426        Err(StorageError::NotFound(message_id.to_string()))
427    }
428
429    /// Apply legal hold to a record
430    pub async fn apply_legal_hold(&self, message_id: &str) -> Result<(), StorageError> {
431        // In a real implementation, this would update the record in storage
432        tracing::info!("Applied legal hold to message: {}", message_id);
433        Ok(())
434    }
435
436    /// Remove legal hold from a record
437    pub async fn remove_legal_hold(&self, message_id: &str) -> Result<(), StorageError> {
438        // In a real implementation, this would update the record in storage
439        tracing::info!("Removed legal hold from message: {}", message_id);
440        Ok(())
441    }
442
443    /// Verify timestamp token
444    pub fn verify_timestamp(&self, token: &TimestampToken, content_hash: &str) -> bool {
445        token.verify_content(content_hash)
446    }
447}
448
449/// Legalis mailet
450pub struct LegalisMailet {
451    name: String,
452    service: Option<LegalisService>,
453    config: LegalisConfig,
454}
455
456impl LegalisMailet {
457    /// Create a new Legalis mailet
458    pub fn new() -> Self {
459        Self {
460            name: "Legalis".to_string(),
461            service: None,
462            config: LegalisConfig::default(),
463        }
464    }
465}
466
467impl Default for LegalisMailet {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473#[async_trait]
474impl Mailet for LegalisMailet {
475    async fn init(&mut self, config: MailetConfig) -> anyhow::Result<()> {
476        // Parse configuration
477        if let Some(tsa_url) = config.get_param("tsa_url") {
478            self.config.tsa_url = tsa_url.to_string();
479        }
480
481        if let Some(cert_path) = config.get_param("tsa_certificate") {
482            self.config.tsa_certificate = Some(PathBuf::from(cert_path));
483        }
484
485        if let Some(enabled) = config.get_param("enabled") {
486            self.config.enabled = enabled.parse().unwrap_or(true);
487        }
488
489        if let Some(hash_algo) = config.get_param("hash_algorithm") {
490            self.config.hash_algorithm = hash_algo
491                .parse::<HashAlgorithm>()
492                .map_err(|e| anyhow::anyhow!("Invalid hash algorithm: {}", e))?;
493        }
494
495        if let Some(storage_path) = config.get_param("archive_storage") {
496            self.config.archive_storage = PathBuf::from(storage_path);
497        }
498
499        if let Some(retention) = config.get_param("retention_days") {
500            self.config.retention_days = retention
501                .parse()
502                .map_err(|e| anyhow::anyhow!("Invalid retention_days value: {}", e))?;
503        }
504
505        if let Some(require) = config.get_param("require_timestamp") {
506            self.config.require_timestamp = require.parse().unwrap_or(false);
507        }
508
509        if let Some(timeout) = config.get_param("timeout_secs") {
510            self.config.timeout_secs = timeout.parse().unwrap_or(30);
511        }
512
513        // Initialize service
514        self.service = Some(LegalisService::new(self.config.clone())?);
515
516        tracing::info!(
517            "Initialized LegalisMailet with TSA: {}",
518            self.config.tsa_url
519        );
520        Ok(())
521    }
522
523    async fn service(&self, mail: &mut Mail) -> anyhow::Result<MailetAction> {
524        let service = match &self.service {
525            Some(s) => s,
526            None => {
527                tracing::warn!("LegalisMailet service not initialized");
528                return Ok(MailetAction::Continue);
529            }
530        };
531
532        if !self.config.enabled {
533            tracing::debug!("Legalis timestamping is disabled");
534            return Ok(MailetAction::Continue);
535        }
536
537        // Compute message hash
538        let content_hash = service.compute_hash(mail).map_err(|e| {
539            tracing::error!("Failed to compute message hash: {}", e);
540            e
541        })?;
542
543        // Request timestamp from TSA
544        let token = match service.request_timestamp(&content_hash).await {
545            Ok(t) => t,
546            Err(e) => {
547                tracing::error!("Failed to request timestamp: {}", e);
548                if self.config.require_timestamp {
549                    return Err(anyhow::anyhow!("Timestamping required but failed: {}", e));
550                }
551                return Ok(MailetAction::Continue);
552            }
553        };
554
555        // Archive the mail
556        let archive_record = match service.archive(mail, &token).await {
557            Ok(r) => r,
558            Err(e) => {
559                tracing::error!("Failed to archive mail: {}", e);
560                // Continue even if archiving fails (unless required)
561                if self.config.require_timestamp {
562                    return Err(anyhow::anyhow!("Archiving required but failed: {}", e));
563                }
564                return Ok(MailetAction::Continue);
565            }
566        };
567
568        // Add headers to mail
569        mail.set_attribute("legalis.timestamp", token.timestamp as i64);
570        mail.set_attribute("legalis.tsa", token.tsa.clone());
571        mail.set_attribute("legalis.message_imprint", token.message_imprint.clone());
572        mail.set_attribute("legalis.serial_number", token.serial_number.clone());
573
574        if let Ok(encoded_token) = token.encode() {
575            mail.set_attribute("legalis.token", encoded_token);
576        }
577
578        mail.set_attribute("legalis.archive_id", archive_record.message_id.clone());
579        mail.set_attribute("legalis.content_hash", archive_record.content_hash);
580        mail.set_attribute("legalis.archived_at", archive_record.archived_at);
581        mail.set_attribute("legalis.retention_until", archive_record.retention_until);
582
583        if !archive_record.compliance_tags.is_empty() {
584            mail.set_attribute(
585                "legalis.compliance_tags",
586                archive_record.compliance_tags.join(","),
587            );
588        }
589
590        tracing::info!("Successfully timestamped and archived mail: {}", mail.id());
591
592        Ok(MailetAction::Continue)
593    }
594
595    fn name(&self) -> &str {
596        &self.name
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use bytes::Bytes;
604    use rusmes_proto::{HeaderMap, MailAddress, MessageBody, MimeMessage};
605    use std::str::FromStr;
606
607    // Helper function to create test mail
608    fn create_test_mail() -> Mail {
609        Mail::new(
610            Some(MailAddress::from_str("sender@test.com").unwrap()),
611            vec![MailAddress::from_str("rcpt@test.com").unwrap()],
612            MimeMessage::new(HeaderMap::new(), MessageBody::Small(Bytes::from("Test"))),
613            None,
614            None,
615        )
616    }
617
618    #[test]
619    fn test_hash_algorithm_sha256() {
620        let algo = HashAlgorithm::SHA256;
621        let hash = algo.hash(b"test data");
622        assert_eq!(hash.len(), 64); // SHA-256 produces 64 hex characters
623    }
624
625    #[test]
626    fn test_hash_algorithm_sha512() {
627        let algo = HashAlgorithm::SHA512;
628        let hash = algo.hash(b"test data");
629        assert_eq!(hash.len(), 128); // SHA-512 produces 128 hex characters
630    }
631
632    #[test]
633    fn test_hash_algorithm_from_str() {
634        assert_eq!(
635            "SHA256".parse::<HashAlgorithm>().unwrap(),
636            HashAlgorithm::SHA256
637        );
638        assert_eq!(
639            "sha256".parse::<HashAlgorithm>().unwrap(),
640            HashAlgorithm::SHA256
641        );
642        assert_eq!(
643            "SHA512".parse::<HashAlgorithm>().unwrap(),
644            HashAlgorithm::SHA512
645        );
646        assert!("MD5".parse::<HashAlgorithm>().is_err());
647    }
648
649    #[test]
650    fn test_timestamp_token_creation() {
651        let token = TimestampToken::new_mock("test_hash", HashAlgorithm::SHA256);
652        assert_eq!(token.message_imprint, "test_hash");
653        assert!(!token.serial_number.is_empty());
654        assert!(!token.signature.is_empty());
655        assert!(token.verify());
656    }
657
658    #[test]
659    fn test_timestamp_token_encoding() {
660        let token = TimestampToken::new_mock("test_hash", HashAlgorithm::SHA256);
661        let encoded = token.encode().unwrap();
662        assert!(!encoded.is_empty());
663
664        let decoded = TimestampToken::decode(&encoded).unwrap();
665        assert_eq!(decoded.message_imprint, "test_hash");
666        assert_eq!(decoded.serial_number, token.serial_number);
667    }
668
669    #[test]
670    fn test_timestamp_token_verification() {
671        let token = TimestampToken::new_mock("test_hash", HashAlgorithm::SHA256);
672        assert!(token.verify());
673        assert!(token.verify_content("test_hash"));
674        assert!(!token.verify_content("wrong_hash"));
675    }
676
677    #[test]
678    fn test_archive_record_creation() {
679        let record = ArchiveRecord::new(
680            "msg-123".to_string(),
681            "token".to_string(),
682            "hash123".to_string(),
683            HashAlgorithm::SHA256,
684            2555,
685        );
686        assert_eq!(record.message_id, "msg-123");
687        assert_eq!(record.content_hash, "hash123");
688        assert!(!record.legal_hold);
689    }
690
691    #[test]
692    fn test_archive_record_expiration() {
693        let mut record = ArchiveRecord::new(
694            "msg-123".to_string(),
695            "token".to_string(),
696            "hash123".to_string(),
697            HashAlgorithm::SHA256,
698            0, // Expired
699        );
700
701        // Give it a moment to ensure time has passed
702        std::thread::sleep(std::time::Duration::from_millis(10));
703        assert!(record.is_expired());
704
705        // Legal hold prevents expiration
706        record.legal_hold = true;
707        assert!(!record.is_expired());
708    }
709
710    #[test]
711    fn test_archive_record_compliance_tags() {
712        let mut record = ArchiveRecord::new(
713            "msg-123".to_string(),
714            "token".to_string(),
715            "hash123".to_string(),
716            HashAlgorithm::SHA256,
717            2555,
718        );
719
720        record.add_compliance_tag("legal".to_string());
721        record.add_compliance_tag("financial".to_string());
722        record.add_compliance_tag("legal".to_string()); // Duplicate
723
724        assert_eq!(record.compliance_tags.len(), 2);
725        assert!(record.compliance_tags.contains(&"legal".to_string()));
726        assert!(record.compliance_tags.contains(&"financial".to_string()));
727    }
728
729    #[test]
730    fn test_archive_record_hash_chain() {
731        let mut record = ArchiveRecord::new(
732            "msg-123".to_string(),
733            "token".to_string(),
734            "hash123".to_string(),
735            HashAlgorithm::SHA256,
736            2555,
737        );
738
739        record.add_to_chain("hash1".to_string());
740        record.add_to_chain("hash2".to_string());
741        record.add_to_chain("hash3".to_string());
742
743        assert_eq!(record.hash_chain.len(), 3);
744        assert!(record.verify_chain());
745    }
746
747    #[test]
748    fn test_archive_record_serialization() {
749        let record = ArchiveRecord::new(
750            "msg-123".to_string(),
751            "token".to_string(),
752            "hash123".to_string(),
753            HashAlgorithm::SHA256,
754            2555,
755        );
756
757        let json = record.to_json().unwrap();
758        assert!(!json.is_empty());
759
760        let deserialized = ArchiveRecord::from_json(&json).unwrap();
761        assert_eq!(deserialized.message_id, "msg-123");
762        assert_eq!(deserialized.content_hash, "hash123");
763    }
764
765    #[tokio::test]
766    async fn test_legalis_config_defaults() {
767        let config = LegalisConfig::default();
768        assert!(config.enabled);
769        assert_eq!(config.hash_algorithm, HashAlgorithm::SHA256);
770        assert_eq!(config.retention_days, 2555);
771        assert!(!config.require_timestamp);
772    }
773
774    #[tokio::test]
775    async fn test_legalis_service_creation() {
776        let config = LegalisConfig::default();
777        let service = LegalisService::new(config);
778        assert!(service.is_ok());
779    }
780
781    #[tokio::test]
782    async fn test_legalis_service_timestamp_request() {
783        let config = LegalisConfig::default();
784        let service = LegalisService::new(config).unwrap();
785
786        let token = service.request_timestamp("test_hash").await.unwrap();
787        assert_eq!(token.message_imprint, "test_hash");
788        assert!(token.verify());
789    }
790
791    #[tokio::test]
792    async fn test_legalis_service_hash_computation() {
793        let config = LegalisConfig::default();
794        let service = LegalisService::new(config).unwrap();
795
796        let mail = create_test_mail();
797        let hash = service.compute_hash(&mail).unwrap();
798        assert!(!hash.is_empty());
799        assert_eq!(hash.len(), 64); // SHA-256
800    }
801
802    #[tokio::test]
803    async fn test_legalis_service_archive() {
804        let config = LegalisConfig::default();
805        let service = LegalisService::new(config).unwrap();
806
807        let mail = create_test_mail();
808        let hash = service.compute_hash(&mail).unwrap();
809        let token = service.request_timestamp(&hash).await.unwrap();
810
811        let record = service.archive(&mail, &token).await.unwrap();
812        assert_eq!(record.message_id, mail.id().to_string());
813        assert!(!record.content_hash.is_empty());
814        assert!(record.compliance_tags.contains(&"gdpr".to_string()));
815    }
816
817    #[tokio::test]
818    async fn test_legalis_service_legal_compliance_tag() {
819        let config = LegalisConfig::default();
820        let service = LegalisService::new(config).unwrap();
821
822        let mut mail = create_test_mail();
823        mail.set_attribute("header.Subject", "Legal contract review");
824
825        let hash = service.compute_hash(&mail).unwrap();
826        let token = service.request_timestamp(&hash).await.unwrap();
827        let record = service.archive(&mail, &token).await.unwrap();
828
829        assert!(record.compliance_tags.contains(&"legal".to_string()));
830    }
831
832    #[tokio::test]
833    async fn test_legalis_service_financial_compliance_tag() {
834        let config = LegalisConfig::default();
835        let service = LegalisService::new(config).unwrap();
836
837        let mut mail = create_test_mail();
838        mail.set_attribute("header.Subject", "Invoice #12345");
839
840        let hash = service.compute_hash(&mail).unwrap();
841        let token = service.request_timestamp(&hash).await.unwrap();
842        let record = service.archive(&mail, &token).await.unwrap();
843
844        assert!(record.compliance_tags.contains(&"financial".to_string()));
845    }
846
847    #[tokio::test]
848    async fn test_legalis_service_pii_compliance_tag() {
849        let config = LegalisConfig::default();
850        let service = LegalisService::new(config).unwrap();
851
852        let mut mail = create_test_mail();
853        mail.set_attribute("message.body", "SSN: 123-45-6789");
854
855        let hash = service.compute_hash(&mail).unwrap();
856        let token = service.request_timestamp(&hash).await.unwrap();
857        let record = service.archive(&mail, &token).await.unwrap();
858
859        assert!(record.compliance_tags.contains(&"pii".to_string()));
860    }
861
862    #[tokio::test]
863    async fn test_legalis_mailet_init() {
864        let mut mailet = LegalisMailet::new();
865        let config = MailetConfig::new("Legalis");
866
867        mailet.init(config).await.unwrap();
868        assert_eq!(mailet.name(), "Legalis");
869        assert!(mailet.service.is_some());
870    }
871
872    #[tokio::test]
873    async fn test_legalis_mailet_init_with_config() {
874        let mut mailet = LegalisMailet::new();
875        let config = MailetConfig::new("Legalis")
876            .with_param("tsa_url", "https://custom-tsa.com")
877            .with_param("retention_days", "3650")
878            .with_param("hash_algorithm", "SHA512");
879
880        mailet.init(config).await.unwrap();
881        assert_eq!(mailet.config.tsa_url, "https://custom-tsa.com");
882        assert_eq!(mailet.config.retention_days, 3650);
883        assert_eq!(mailet.config.hash_algorithm, HashAlgorithm::SHA512);
884    }
885
886    #[tokio::test]
887    async fn test_legalis_mailet_service() {
888        let mut mailet = LegalisMailet::new();
889        let config = MailetConfig::new("Legalis");
890        mailet.init(config).await.unwrap();
891
892        let mut mail = create_test_mail();
893        let result = mailet.service(&mut mail).await.unwrap();
894
895        assert_eq!(result, MailetAction::Continue);
896        assert!(mail.get_attribute("legalis.timestamp").is_some());
897        assert!(mail.get_attribute("legalis.tsa").is_some());
898        assert!(mail.get_attribute("legalis.token").is_some());
899        assert!(mail.get_attribute("legalis.archive_id").is_some());
900    }
901
902    #[tokio::test]
903    async fn test_legalis_mailet_disabled() {
904        let mut mailet = LegalisMailet::new();
905        let config = MailetConfig::new("Legalis").with_param("enabled", "false");
906        mailet.init(config).await.unwrap();
907
908        let mut mail = create_test_mail();
909        let result = mailet.service(&mut mail).await.unwrap();
910
911        assert_eq!(result, MailetAction::Continue);
912        assert!(mail.get_attribute("legalis.timestamp").is_none());
913    }
914
915    #[tokio::test]
916    async fn test_legalis_mailet_retention_period() {
917        let mut mailet = LegalisMailet::new();
918        let config = MailetConfig::new("Legalis").with_param("retention_days", "1825");
919        mailet.init(config).await.unwrap();
920
921        assert_eq!(mailet.config.retention_days, 1825);
922    }
923
924    #[tokio::test]
925    async fn test_tsa_error_display() {
926        let err = TsaError::ConnectionFailed("network error".to_string());
927        assert!(err.to_string().contains("network error"));
928
929        let err = TsaError::InvalidResponse("bad format".to_string());
930        assert!(err.to_string().contains("bad format"));
931
932        let err = TsaError::Timeout;
933        assert!(err.to_string().contains("timeout"));
934    }
935
936    #[tokio::test]
937    async fn test_storage_error_display() {
938        let err = StorageError::WriteFailed("disk full".to_string());
939        assert!(err.to_string().contains("disk full"));
940
941        let err = StorageError::NotFound("msg-123".to_string());
942        assert!(err.to_string().contains("msg-123"));
943    }
944
945    #[tokio::test]
946    async fn test_legalis_error_conversion() {
947        let tsa_err = TsaError::ConnectionFailed("test".to_string());
948        let legalis_err: LegalisError = tsa_err.into();
949        assert!(matches!(legalis_err, LegalisError::Tsa(_)));
950    }
951
952    #[tokio::test]
953    async fn test_legal_hold_operations() {
954        let config = LegalisConfig::default();
955        let service = LegalisService::new(config).unwrap();
956
957        // Test apply legal hold
958        let result = service.apply_legal_hold("msg-123").await;
959        assert!(result.is_ok());
960
961        // Test remove legal hold
962        let result = service.remove_legal_hold("msg-123").await;
963        assert!(result.is_ok());
964    }
965
966    #[tokio::test]
967    async fn test_timestamp_verification() {
968        let config = LegalisConfig::default();
969        let service = LegalisService::new(config).unwrap();
970
971        let token = TimestampToken::new_mock("test_hash", HashAlgorithm::SHA256);
972
973        assert!(service.verify_timestamp(&token, "test_hash"));
974        assert!(!service.verify_timestamp(&token, "wrong_hash"));
975    }
976
977    #[tokio::test]
978    async fn test_multiple_compliance_tags() {
979        let config = LegalisConfig::default();
980        let service = LegalisService::new(config).unwrap();
981
982        let mut mail = create_test_mail();
983        mail.set_attribute("header.Subject", "Legal Invoice - Payment Required");
984        mail.set_attribute("message.body", "SSN: 123-45-6789");
985
986        let hash = service.compute_hash(&mail).unwrap();
987        let token = service.request_timestamp(&hash).await.unwrap();
988        let record = service.archive(&mail, &token).await.unwrap();
989
990        // Should have legal, financial, pii, and gdpr tags
991        assert!(record.compliance_tags.contains(&"legal".to_string()));
992        assert!(record.compliance_tags.contains(&"financial".to_string()));
993        assert!(record.compliance_tags.contains(&"pii".to_string()));
994        assert!(record.compliance_tags.contains(&"gdpr".to_string()));
995    }
996
997    #[tokio::test]
998    async fn test_archive_record_empty_chain_verification() {
999        let record = ArchiveRecord::new(
1000            "msg-123".to_string(),
1001            "token".to_string(),
1002            "hash123".to_string(),
1003            HashAlgorithm::SHA256,
1004            2555,
1005        );
1006
1007        // Empty chain should fail verification
1008        assert!(!record.verify_chain());
1009    }
1010
1011    #[tokio::test]
1012    async fn test_timestamp_token_invalid_verification() {
1013        let mut token = TimestampToken::new_mock("test_hash", HashAlgorithm::SHA256);
1014
1015        // Empty message imprint should fail
1016        token.message_imprint = String::new();
1017        assert!(!token.verify());
1018
1019        // Empty signature should fail
1020        token.message_imprint = "test".to_string();
1021        token.signature = String::new();
1022        assert!(!token.verify());
1023    }
1024
1025    #[tokio::test]
1026    async fn test_hash_algorithm_consistency() {
1027        let data = b"test data for consistency";
1028
1029        let hash1 = HashAlgorithm::SHA256.hash(data);
1030        let hash2 = HashAlgorithm::SHA256.hash(data);
1031        assert_eq!(hash1, hash2);
1032
1033        let hash3 = HashAlgorithm::SHA512.hash(data);
1034        let hash4 = HashAlgorithm::SHA512.hash(data);
1035        assert_eq!(hash3, hash4);
1036
1037        // Different algorithms should produce different hashes
1038        assert_ne!(hash1, hash3);
1039    }
1040}