1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum HashAlgorithm {
23 SHA256,
25 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct TimestampToken {
62 pub tsa: String,
64 pub timestamp: u64,
66 pub message_imprint: String,
68 pub serial_number: String,
70 pub hash_algorithm: HashAlgorithm,
72 pub signature: String,
74 pub nonce: Option<u64>,
76}
77
78impl TimestampToken {
79 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 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 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 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 pub fn verify_content(&self, content_hash: &str) -> bool {
124 self.verify() && self.message_imprint == content_hash
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ArchiveRecord {
131 pub message_id: String,
133 pub timestamp_token: String,
135 pub content_hash: String,
137 pub hash_algorithm: HashAlgorithm,
139 pub archived_at: String,
141 pub retention_until: String,
143 pub legal_hold: bool,
145 pub compliance_tags: Vec<String>,
147 pub hash_chain: Vec<String>,
149 pub metadata: HashMap<String, String>,
151}
152
153impl ArchiveRecord {
154 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 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 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 pub fn add_to_chain(&mut self, hash: String) {
198 self.hash_chain.push(hash);
199 }
200
201 pub fn verify_chain(&self) -> bool {
203 if self.hash_chain.is_empty() {
204 return false;
205 }
206 true
209 }
210
211 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 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#[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#[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#[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#[derive(Debug, Clone)]
267pub struct LegalisConfig {
268 pub tsa_url: String,
270 pub tsa_certificate: Option<PathBuf>,
272 pub enabled: bool,
274 pub hash_algorithm: HashAlgorithm,
276 pub archive_storage: PathBuf,
278 pub retention_days: u32,
280 pub require_timestamp: bool,
282 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, require_timestamp: false,
296 timeout_secs: 30,
297 }
298 }
299}
300
301pub struct LegalisService {
303 config: LegalisConfig,
304 #[allow(dead_code)]
305 client: reqwest::Client,
306}
307
308impl LegalisService {
309 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 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 Ok(TimestampToken::new_mock(
332 message_hash,
333 self.config.hash_algorithm,
334 ))
335 }
336
337 pub fn compute_hash(&self, mail: &Mail) -> Result<String, LegalisError> {
339 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 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 self.add_compliance_tags(mail, &mut record);
366
367 record.add_to_chain(content_hash);
369
370 self.store_record(&record).await?;
372
373 Ok(record)
374 }
375
376 fn add_compliance_tags(&self, mail: &Mail, record: &mut ArchiveRecord) {
378 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 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 record.add_compliance_tag("gdpr".to_string());
411 }
412
413 async fn store_record(&self, record: &ArchiveRecord) -> Result<(), StorageError> {
415 if record.message_id.is_empty() {
418 return Err(StorageError::WriteFailed("Message ID is empty".to_string()));
419 }
420 Ok(())
421 }
422
423 pub async fn retrieve_record(&self, message_id: &str) -> Result<ArchiveRecord, StorageError> {
425 Err(StorageError::NotFound(message_id.to_string()))
427 }
428
429 pub async fn apply_legal_hold(&self, message_id: &str) -> Result<(), StorageError> {
431 tracing::info!("Applied legal hold to message: {}", message_id);
433 Ok(())
434 }
435
436 pub async fn remove_legal_hold(&self, message_id: &str) -> Result<(), StorageError> {
438 tracing::info!("Removed legal hold from message: {}", message_id);
440 Ok(())
441 }
442
443 pub fn verify_timestamp(&self, token: &TimestampToken, content_hash: &str) -> bool {
445 token.verify_content(content_hash)
446 }
447}
448
449pub struct LegalisMailet {
451 name: String,
452 service: Option<LegalisService>,
453 config: LegalisConfig,
454}
455
456impl LegalisMailet {
457 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 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 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 let content_hash = service.compute_hash(mail).map_err(|e| {
539 tracing::error!("Failed to compute message hash: {}", e);
540 e
541 })?;
542
543 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 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 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 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 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); }
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); }
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, );
700
701 std::thread::sleep(std::time::Duration::from_millis(10));
703 assert!(record.is_expired());
704
705 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()); 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); }
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 let result = service.apply_legal_hold("msg-123").await;
959 assert!(result.is_ok());
960
961 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 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 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 token.message_imprint = String::new();
1017 assert!(!token.verify());
1018
1019 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 assert_ne!(hash1, hash3);
1039 }
1040}