cull_gmail/eol_action.rs
1//! # End-of-Life Action Module
2//!
3//! This module defines the actions that can be performed on Gmail messages
4//! when they reach their end-of-life criteria based on configured rules.
5//!
6//! ## Overview
7//!
8//! The `EolAction` enum specifies how messages should be handled when they
9//! meet the criteria for removal from a Gmail account. The module provides
10//! two primary actions:
11//!
12//! - **Trash**: Moves messages to the trash folder (reversible)
13//! - **Delete**: Permanently deletes messages (irreversible)
14//!
15//! ## Safety Considerations
16//!
17//! - **Trash** action allows message recovery from Gmail's trash folder
18//! - **Delete** action permanently removes messages and cannot be undone
19//! - Always test rules carefully before applying delete actions
20//!
21//! ## Usage Examples
22//!
23//! ### Basic Usage
24//!
25//! ```rust
26//! use cull_gmail::EolAction;
27//!
28//! // Default action is Trash (safer option)
29//! let action = EolAction::default();
30//! assert_eq!(action, EolAction::Trash);
31//!
32//! // Parse from string
33//! let delete_action = EolAction::parse("delete").unwrap();
34//! assert_eq!(delete_action, EolAction::Delete);
35//!
36//! // Display as string
37//! println!("Action: {}", delete_action); // Prints: "Action: delete"
38//! ```
39//!
40//! ### Integration with Rules
41//!
42//! ```rust,no_run
43//! use cull_gmail::EolAction;
44//!
45//! fn configure_rule_action(action_str: &str) -> Option<EolAction> {
46//! match EolAction::parse(action_str) {
47//! Some(action) => {
48//! println!("Configured action: {}", action);
49//! Some(action)
50//! }
51//! None => {
52//! eprintln!("Invalid action: {}", action_str);
53//! None
54//! }
55//! }
56//! }
57//! ```
58//!
59//! ## String Representation
60//!
61//! The enum implements both parsing from strings and display formatting:
62//!
63//! | Variant | String | Description |
64//! |---------|--------|--------------|
65//! | `Trash` | "trash" | Move to trash (recoverable) |
66//! | `Delete` | "delete" | Permanent deletion |
67//!
68//! Parsing is case-insensitive, so "TRASH", "Trash", and "trash" are all valid.
69
70use std::fmt;
71
72/// Represents the action to take on Gmail messages that meet end-of-life criteria.
73///
74/// This enum defines the two possible actions for handling messages when they
75/// reach the end of their lifecycle based on configured retention rules.
76///
77/// # Variants
78///
79/// - [`Trash`](EolAction::Trash) - Move messages to Gmail's trash folder (default, reversible)
80/// - [`Delete`](EolAction::Delete) - Permanently delete messages (irreversible)
81///
82/// # Default Behavior
83///
84/// The default action is [`Trash`](EolAction::Trash), which provides a safety net
85/// by allowing message recovery from the trash folder.
86///
87/// # Examples
88///
89/// ```rust
90/// use cull_gmail::EolAction;
91///
92/// // Using the default (Trash)
93/// let safe_action = EolAction::default();
94/// assert_eq!(safe_action, EolAction::Trash);
95///
96/// // Comparing actions
97/// let delete = EolAction::Delete;
98/// let trash = EolAction::Trash;
99/// assert_ne!(delete, trash);
100///
101/// // Converting to string for logging/display
102/// println!("Action: {}", delete); // Prints: "delete"
103/// ```
104#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
105pub enum EolAction {
106 /// Move the message to Gmail's trash folder.
107 ///
108 /// This is the default and safer option as it allows message recovery.
109 /// Messages in the trash are automatically deleted by Gmail after 30 days.
110 ///
111 /// # Safety
112 ///
113 /// This action is reversible - messages can be recovered from the trash folder
114 /// until they are automatically purged or manually deleted from trash.
115 #[default]
116 Trash,
117
118 /// Permanently delete the message immediately.
119 ///
120 /// This action bypasses the trash folder and permanently removes the message.
121 ///
122 /// # Warning
123 ///
124 /// This action is **irreversible**. Once deleted, messages cannot be recovered.
125 /// Use with extreme caution and thorough testing of rule criteria.
126 ///
127 /// # Use Cases
128 ///
129 /// - Sensitive data that should not remain in trash
130 /// - Storage optimization where trash recovery is not needed
131 /// - Automated cleanup of known disposable messages
132 Delete,
133}
134
135impl fmt::Display for EolAction {
136 /// Formats the `EolAction` as a lowercase string.
137 ///
138 /// This implementation provides a consistent string representation
139 /// for logging, configuration, and user interfaces.
140 ///
141 /// # Returns
142 ///
143 /// - `"trash"` for [`EolAction::Trash`]
144 /// - `"delete"` for [`EolAction::Delete`]
145 ///
146 /// # Examples
147 ///
148 /// ```rust
149 /// use cull_gmail::EolAction;
150 ///
151 /// assert_eq!(EolAction::Trash.to_string(), "trash");
152 /// assert_eq!(EolAction::Delete.to_string(), "delete");
153 ///
154 /// // Useful for logging
155 /// let action = EolAction::default();
156 /// println!("Performing action: {}", action);
157 /// ```
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 EolAction::Trash => write!(f, "trash"),
161 EolAction::Delete => write!(f, "delete"),
162 }
163 }
164}
165
166impl EolAction {
167 /// Parses a string into an `EolAction` variant.
168 ///
169 /// This method provides case-insensitive parsing from string representations
170 /// to `EolAction` variants. It's useful for configuration file parsing,
171 /// command-line arguments, and user input validation.
172 ///
173 /// # Arguments
174 ///
175 /// * `input` - A string slice to parse. Case is ignored.
176 ///
177 /// # Returns
178 ///
179 /// - `Some(EolAction)` if the string matches a valid variant
180 /// - `None` if the string is not recognized
181 ///
182 /// # Valid Input Strings
183 ///
184 /// - `"trash"`, `"Trash"`, `"TRASH"` → [`EolAction::Trash`]
185 /// - `"delete"`, `"Delete"`, `"DELETE"` → [`EolAction::Delete`]
186 ///
187 /// # Examples
188 ///
189 /// ```rust
190 /// use cull_gmail::EolAction;
191 ///
192 /// // Valid parsing (case-insensitive)
193 /// assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash));
194 /// assert_eq!(EolAction::parse("TRASH"), Some(EolAction::Trash));
195 /// assert_eq!(EolAction::parse("Delete"), Some(EolAction::Delete));
196 ///
197 /// // Invalid input
198 /// assert_eq!(EolAction::parse("invalid"), None);
199 /// assert_eq!(EolAction::parse(""), None);
200 /// ```
201 ///
202 /// # Use Cases
203 ///
204 /// ```rust
205 /// use cull_gmail::EolAction;
206 ///
207 /// fn parse_user_action(input: &str) -> Result<EolAction, String> {
208 /// EolAction::parse(input)
209 /// .ok_or_else(|| format!("Invalid action: '{}'. Use 'trash' or 'delete'.", input))
210 /// }
211 ///
212 /// assert!(parse_user_action("trash").is_ok());
213 /// assert!(parse_user_action("invalid").is_err());
214 /// ```
215 pub fn parse(input: &str) -> Option<Self> {
216 match input.trim().to_lowercase().as_str() {
217 "trash" => Some(EolAction::Trash),
218 "delete" => Some(EolAction::Delete),
219 _ => None,
220 }
221 }
222
223 /// Returns `true` if the action is reversible (can be undone).
224 ///
225 /// This method helps determine if an action allows for message recovery,
226 /// which is useful for safety checks and user confirmations.
227 ///
228 /// # Returns
229 ///
230 /// - `true` for [`EolAction::Trash`] (messages can be recovered from trash)
231 /// - `false` for [`EolAction::Delete`] (messages are permanently deleted)
232 ///
233 /// # Examples
234 ///
235 /// ```rust
236 /// use cull_gmail::EolAction;
237 ///
238 /// assert!(EolAction::Trash.is_reversible());
239 /// assert!(!EolAction::Delete.is_reversible());
240 ///
241 /// // Use in safety checks
242 /// let action = EolAction::Delete;
243 /// if !action.is_reversible() {
244 /// println!("Warning: This action cannot be undone!");
245 /// }
246 /// ```
247 pub fn is_reversible(&self) -> bool {
248 match self {
249 EolAction::Trash => true,
250 EolAction::Delete => false,
251 }
252 }
253
254 /// Returns all possible `EolAction` variants.
255 ///
256 /// This method is useful for generating help text, validation lists,
257 /// or iterating over all possible actions.
258 ///
259 /// # Returns
260 ///
261 /// An array containing all `EolAction` variants in declaration order.
262 ///
263 /// # Examples
264 ///
265 /// ```rust
266 /// use cull_gmail::EolAction;
267 ///
268 /// let all_actions = EolAction::variants();
269 /// assert_eq!(all_actions.len(), 2);
270 /// assert_eq!(all_actions[0], EolAction::Trash);
271 /// assert_eq!(all_actions[1], EolAction::Delete);
272 ///
273 /// // Generate help text
274 /// println!("Available actions:");
275 /// for action in EolAction::variants() {
276 /// println!(" {} - {}", action,
277 /// if action.is_reversible() { "reversible" } else { "irreversible" });
278 /// }
279 /// ```
280 pub fn variants() -> &'static [EolAction] {
281 &[EolAction::Trash, EolAction::Delete]
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_default_action_is_trash() {
291 let action = EolAction::default();
292 assert_eq!(action, EolAction::Trash);
293 }
294
295 #[test]
296 fn test_copy_and_equality() {
297 let trash1 = EolAction::Trash;
298 let trash2 = trash1; // Copy semantics
299 assert_eq!(trash1, trash2);
300
301 let delete1 = EolAction::Delete;
302 let delete2 = delete1; // Copy semantics
303 assert_eq!(delete1, delete2);
304
305 assert_ne!(trash1, delete1);
306 }
307
308 #[test]
309 fn test_debug_formatting() {
310 assert_eq!(format!("{:?}", EolAction::Trash), "Trash");
311 assert_eq!(format!("{:?}", EolAction::Delete), "Delete");
312 }
313
314 #[test]
315 fn test_display_formatting() {
316 assert_eq!(EolAction::Trash.to_string(), "trash");
317 assert_eq!(EolAction::Delete.to_string(), "delete");
318 assert_eq!(format!("{}", EolAction::Trash), "trash");
319 assert_eq!(format!("{}", EolAction::Delete), "delete");
320 }
321
322 #[test]
323 fn test_parse_valid_inputs() {
324 // Test lowercase
325 assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash));
326 assert_eq!(EolAction::parse("delete"), Some(EolAction::Delete));
327
328 // Test uppercase
329 assert_eq!(EolAction::parse("TRASH"), Some(EolAction::Trash));
330 assert_eq!(EolAction::parse("DELETE"), Some(EolAction::Delete));
331
332 // Test mixed case
333 assert_eq!(EolAction::parse("Trash"), Some(EolAction::Trash));
334 assert_eq!(EolAction::parse("Delete"), Some(EolAction::Delete));
335 assert_eq!(EolAction::parse("TrAsH"), Some(EolAction::Trash));
336 assert_eq!(EolAction::parse("dElEtE"), Some(EolAction::Delete));
337
338 // Test with whitespace
339 assert_eq!(EolAction::parse(" trash "), Some(EolAction::Trash));
340 assert_eq!(EolAction::parse("\tdelete\n"), Some(EolAction::Delete));
341 assert_eq!(EolAction::parse(" TRASH "), Some(EolAction::Trash));
342 }
343
344 #[test]
345 fn test_parse_invalid_inputs() {
346 // Invalid strings
347 assert_eq!(EolAction::parse("invalid"), None);
348 assert_eq!(EolAction::parse("remove"), None);
349 assert_eq!(EolAction::parse("destroy"), None);
350 assert_eq!(EolAction::parse("archive"), None);
351
352 // Empty and whitespace
353 assert_eq!(EolAction::parse(""), None);
354 assert_eq!(EolAction::parse(" "), None);
355 assert_eq!(EolAction::parse("\t\n"), None);
356
357 // Partial matches
358 assert_eq!(EolAction::parse("tras"), None);
359 assert_eq!(EolAction::parse("delet"), None);
360 assert_eq!(EolAction::parse("trashh"), None);
361 assert_eq!(EolAction::parse("deletee"), None);
362
363 // Special characters
364 assert_eq!(EolAction::parse("trash!"), None);
365 assert_eq!(EolAction::parse("delete?"), None);
366 assert_eq!(EolAction::parse("trash-delete"), None);
367 }
368
369 #[test]
370 fn test_parse_edge_cases() {
371 // Unicode variations
372 assert_eq!(EolAction::parse("trash"), Some(EolAction::Trash)); // Unicode 't'
373
374 // Numbers and symbols
375 assert_eq!(EolAction::parse("trash123"), None);
376 assert_eq!(EolAction::parse("123delete"), None);
377 assert_eq!(EolAction::parse("t@rash"), None);
378 }
379
380 #[test]
381 fn test_is_reversible() {
382 assert!(EolAction::Trash.is_reversible());
383 assert!(!EolAction::Delete.is_reversible());
384 }
385
386 #[test]
387 fn test_variants() {
388 let variants = EolAction::variants();
389 assert_eq!(variants.len(), 2);
390 assert_eq!(variants[0], EolAction::Trash);
391 assert_eq!(variants[1], EolAction::Delete);
392
393 // Ensure all enum variants are included
394 assert!(variants.contains(&EolAction::Trash));
395 assert!(variants.contains(&EolAction::Delete));
396 }
397
398 #[test]
399 fn test_variants_completeness() {
400 // Verify that variants() returns all possible enum values
401 let variants = EolAction::variants();
402
403 // Test that we can parse back to all variants
404 for variant in variants {
405 let string_repr = variant.to_string();
406 let parsed = EolAction::parse(&string_repr);
407 assert_eq!(parsed, Some(*variant));
408 }
409 }
410
411 #[test]
412 fn test_hash_trait() {
413 use std::collections::HashMap;
414
415 let mut map = HashMap::new();
416 map.insert(EolAction::Trash, "safe");
417 map.insert(EolAction::Delete, "dangerous");
418
419 assert_eq!(map.get(&EolAction::Trash), Some(&"safe"));
420 assert_eq!(map.get(&EolAction::Delete), Some(&"dangerous"));
421 }
422
423 #[test]
424 fn test_round_trip_conversion() {
425 // Test that display -> parse -> display is consistent
426 let actions = [EolAction::Trash, EolAction::Delete];
427
428 for action in actions {
429 let string_repr = action.to_string();
430 let parsed = EolAction::parse(&string_repr).expect("Should parse successfully");
431 assert_eq!(action, parsed);
432 assert_eq!(string_repr, parsed.to_string());
433 }
434 }
435
436 #[test]
437 fn test_safety_properties() {
438 // Verify safety properties are as expected
439 assert!(
440 EolAction::Trash.is_reversible(),
441 "Trash should be reversible for safety"
442 );
443 assert!(
444 !EolAction::Delete.is_reversible(),
445 "Delete should be irreversible"
446 );
447 assert_eq!(
448 EolAction::default(),
449 EolAction::Trash,
450 "Default should be the safer option"
451 );
452 }
453
454 #[test]
455 fn test_string_case_insensitive_parsing() {
456 let test_cases = [
457 ("trash", Some(EolAction::Trash)),
458 ("TRASH", Some(EolAction::Trash)),
459 ("Trash", Some(EolAction::Trash)),
460 ("TrAsH", Some(EolAction::Trash)),
461 ("delete", Some(EolAction::Delete)),
462 ("DELETE", Some(EolAction::Delete)),
463 ("Delete", Some(EolAction::Delete)),
464 ("DeLeTe", Some(EolAction::Delete)),
465 ("invalid", None),
466 ("INVALID", None),
467 ("", None),
468 ];
469
470 for (input, expected) in test_cases {
471 assert_eq!(
472 EolAction::parse(input),
473 expected,
474 "Failed for input: '{input}'"
475 );
476 }
477 }
478
479 #[test]
480 fn test_practical_usage_scenarios() {
481 // Test common usage patterns
482
483 // Configuration parsing scenario
484 let config_value = "delete";
485 let action = EolAction::parse(config_value).unwrap_or_default();
486 assert_eq!(action, EolAction::Delete);
487
488 // Invalid config falls back to default (safe)
489 let invalid_config = "invalid_action";
490 let safe_action = EolAction::parse(invalid_config).unwrap_or_default();
491 assert_eq!(safe_action, EolAction::Trash);
492
493 // Logging/display scenario
494 let action = EolAction::Delete;
495 let log_message = format!("Executing {action} action");
496 assert_eq!(log_message, "Executing delete action");
497
498 // Safety check scenario
499 let dangerous_action = EolAction::Delete;
500 if !dangerous_action.is_reversible() {
501 // This would prompt user confirmation in real usage
502 // Test that we can detect dangerous actions
503 }
504 }
505
506 #[test]
507 fn test_error_handling_patterns() {
508 // Test error handling patterns that might be used with this enum
509
510 fn parse_with_error(input: &str) -> Result<EolAction, String> {
511 EolAction::parse(input)
512 .ok_or_else(|| format!("Invalid action: '{input}'. Valid options: trash, delete"))
513 }
514
515 // Valid cases
516 assert!(parse_with_error("trash").is_ok());
517 assert!(parse_with_error("delete").is_ok());
518
519 // Error cases
520 let error = parse_with_error("invalid").unwrap_err();
521 assert!(error.contains("Invalid action: 'invalid'"));
522 assert!(error.contains("trash, delete"));
523 }
524}