elif_email/
compression.rs1use crate::{Attachment, EmailError, config::AttachmentConfig};
2
3pub struct AttachmentCompressor;
5
6impl AttachmentCompressor {
7 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 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 _ => {
26 }
28 }
29
30 Ok(())
31 }
32
33 fn gzip_compress(attachment: &mut Attachment) -> Result<(), EmailError> {
35 use std::io::Write;
36 use flate2::{Compression, write::GzEncoder};
37
38 if attachment.content.len() < 1024 {
40 return Ok(()); }
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 if compressed.len() < attachment.content.len() {
52 attachment.content = compressed;
53 attachment.size = attachment.content.len();
54 attachment.compressed = true;
55
56 attachment.filename = format!("{}.gz", attachment.filename);
58 attachment.content_type = "application/gzip".to_string();
59 }
60
61 Ok(())
62 }
63
64 pub fn estimate_compression_ratio(attachment: &Attachment) -> f64 {
66 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/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, "image/jpeg" => 0.95, "image/gif" => 0.9, _ => 1.0,
86 }
87 }
88}
89
90pub fn validate_attachments(attachments: &[Attachment], config: &AttachmentConfig) -> Result<(), EmailError> {
92 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 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 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 attachment.compressed {
172 assert_eq!(attachment.filename, format!("{}.gz", original_filename));
173 assert_eq!(attachment.content_type, "application/gzip");
174 } else {
175 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 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}