elif_email/
compression.rs

1use crate::{Attachment, EmailError, config::AttachmentConfig};
2
3/// Compression utilities for email attachments
4pub struct AttachmentCompressor;
5
6impl AttachmentCompressor {
7    /// Compress an attachment if beneficial
8    pub fn compress_if_beneficial(attachment: &mut Attachment, config: &AttachmentConfig) -> Result<(), EmailError> {
9        if !config.auto_compress || attachment.compressed {
10            return Ok(());
11        }
12        
13        if !attachment.can_compress() {
14            return Ok(());
15        }
16        
17        // For now, only implement basic text compression using gzip
18        // In a full implementation, you might want image compression, etc.
19        match attachment.content_type.as_str() {
20            "text/plain" | "text/html" | "text/css" | "text/javascript" 
21            | "application/json" | "application/xml" => {
22                Self::gzip_compress(attachment)?;
23            }
24            // Image compression would go here with libraries like image, mozjpeg, etc.
25            _ => {
26                // No compression for this type
27            }
28        }
29        
30        Ok(())
31    }
32    
33    /// Compress text content using gzip
34    fn gzip_compress(attachment: &mut Attachment) -> Result<(), EmailError> {
35        use std::io::Write;
36        use flate2::{Compression, write::GzEncoder};
37        
38        // Only compress if it will save significant space
39        if attachment.content.len() < 1024 {
40            return Ok(()); // Too small to benefit from compression
41        }
42        
43        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
44        encoder.write_all(&attachment.content)
45            .map_err(|e| EmailError::configuration(format!("Compression failed: {}", e)))?;
46        
47        let compressed = encoder.finish()
48            .map_err(|e| EmailError::configuration(format!("Compression failed: {}", e)))?;
49        
50        // Only use compressed version if it's actually smaller
51        if compressed.len() < attachment.content.len() {
52            attachment.content = compressed;
53            attachment.size = attachment.content.len();
54            attachment.compressed = true;
55            
56            // Update filename and content type to reflect compression in a standard way
57            attachment.filename = format!("{}.gz", attachment.filename);
58            attachment.content_type = "application/gzip".to_string();
59        }
60        
61        Ok(())
62    }
63    
64    /// Get compression ratio estimate for an attachment
65    pub fn estimate_compression_ratio(attachment: &Attachment) -> f64 {
66        // If already compressed (gzip), no further compression
67        if attachment.compressed || attachment.content_type == "application/gzip" {
68            return 1.0;
69        }
70        
71        if !attachment.can_compress() {
72            return 1.0;
73        }
74        
75        match attachment.content_type.as_str() {
76            "text/plain" => 0.3,  // Text compresses very well
77            "text/html" => 0.4,
78            "text/css" => 0.5,
79            "text/javascript" => 0.6,
80            "application/json" => 0.4,
81            "application/xml" => 0.5,
82            "image/png" => 0.9,   // Already compressed
83            "image/jpeg" => 0.95, // Already compressed
84            "image/gif" => 0.9,   // Already compressed
85            _ => 1.0,
86        }
87    }
88}
89
90/// Validate a collection of attachments
91pub fn validate_attachments(attachments: &[Attachment], config: &AttachmentConfig) -> Result<(), EmailError> {
92    // Check count
93    if attachments.len() > config.max_count {
94        return Err(EmailError::validation(
95            "attachment_count",
96            format!("Too many attachments: {} (max: {})", attachments.len(), config.max_count)
97        ));
98    }
99    
100    // Check total size
101    let total_size: usize = attachments.iter().map(|a| a.size).sum();
102    if total_size > config.max_total_size {
103        return Err(EmailError::validation(
104            "attachments_total_size", 
105            format!("Total attachments size too large: {} bytes (max: {} bytes)",
106                total_size, config.max_total_size)
107        ));
108    }
109    
110    // Validate each attachment
111    for attachment in attachments {
112        attachment.validate(config)?;
113    }
114    
115    Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    
122    #[test]
123    fn test_compression_ratio_estimates() {
124        let text_attachment = Attachment::new("test.txt", b"Hello World".to_vec());
125        assert_eq!(AttachmentCompressor::estimate_compression_ratio(&text_attachment), 0.3);
126        
127        let image_attachment = Attachment::new("test.jpg", b"JPEG data".to_vec());
128        assert_eq!(AttachmentCompressor::estimate_compression_ratio(&image_attachment), 0.95);
129    }
130    
131    #[test]
132    fn test_attachment_validation() {
133        let config = AttachmentConfig::default();
134        let attachments = vec![
135            Attachment::new("test.txt", b"Hello".to_vec()),
136        ];
137        
138        let result = validate_attachments(&attachments, &config);
139        assert!(result.is_ok());
140    }
141    
142    #[test]
143    fn test_attachment_validation_too_many() {
144        let mut config = AttachmentConfig::default();
145        config.max_count = 1;
146        
147        let attachments = vec![
148            Attachment::new("test1.txt", b"Hello".to_vec()),
149            Attachment::new("test2.txt", b"World".to_vec()),
150        ];
151        
152        let result = validate_attachments(&attachments, &config);
153        assert!(result.is_err());
154    }
155    
156    #[test]
157    fn test_compression_updates_filename_and_content_type() {
158        let mut attachment = Attachment::new("test.txt", b"Hello World! This is a test document that should compress well because it has repeating text. Hello World! This is a test document that should compress well because it has repeating text.".to_vec());
159        let config = AttachmentConfig {
160            auto_compress: true,
161            ..AttachmentConfig::default()
162        };
163        
164        let original_filename = attachment.filename.clone();
165        let original_content_type = attachment.content_type.clone();
166        
167        let result = AttachmentCompressor::compress_if_beneficial(&mut attachment, &config);
168        assert!(result.is_ok());
169        
170        // If compression occurred, filename and content type should be updated
171        if attachment.compressed {
172            assert_eq!(attachment.filename, format!("{}.gz", original_filename));
173            assert_eq!(attachment.content_type, "application/gzip");
174        } else {
175            // If no compression, should remain unchanged
176            assert_eq!(attachment.filename, original_filename);
177            assert_eq!(attachment.content_type, original_content_type);
178        }
179    }
180    
181    #[test]
182    fn test_compression_ratio_for_gzip_files() {
183        let mut attachment = Attachment::new("test.txt.gz", b"compressed data".to_vec());
184        attachment.content_type = "application/gzip".to_string();
185        attachment.compressed = true;
186        
187        // Should not attempt further compression
188        assert_eq!(AttachmentCompressor::estimate_compression_ratio(&attachment), 1.0);
189        
190        let text_attachment = Attachment::new("test.txt", b"plain text".to_vec());
191        assert_eq!(AttachmentCompressor::estimate_compression_ratio(&text_attachment), 0.3);
192    }
193}