Skip to main content

rusmes_core/matchers/
has_attachment.rs

1//! Matcher for messages with attachments
2
3use crate::matcher::Matcher;
4use async_trait::async_trait;
5use rusmes_proto::{Mail, MailAddress};
6
7/// Matches messages that have attachments
8pub struct HasAttachmentMatcher;
9
10impl HasAttachmentMatcher {
11    /// Create a new HasAttachment matcher
12    pub fn new() -> Self {
13        Self
14    }
15}
16
17impl Default for HasAttachmentMatcher {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23#[async_trait]
24impl Matcher for HasAttachmentMatcher {
25    async fn match_mail(&self, mail: &Mail) -> anyhow::Result<Vec<MailAddress>> {
26        // Check MIME headers for attachments
27        let has_attachment = Self::detect_attachment(mail);
28
29        if has_attachment {
30            Ok(mail.recipients().to_vec())
31        } else {
32            Ok(Vec::new())
33        }
34    }
35
36    fn name(&self) -> &str {
37        "HasAttachment"
38    }
39}
40
41impl HasAttachmentMatcher {
42    /// Detect if a mail has attachments by checking MIME headers
43    fn detect_attachment(mail: &Mail) -> bool {
44        let headers = mail.message().headers();
45
46        // Check Content-Type for multipart
47        if let Some(content_type) = headers.get_first("Content-Type") {
48            let content_type_lower = content_type.to_lowercase();
49
50            // Multipart messages (except multipart/alternative which is just text+html)
51            if content_type_lower.contains("multipart/mixed")
52                || content_type_lower.contains("multipart/related")
53            {
54                return true;
55            }
56        }
57
58        // Check Content-Disposition for attachment
59        if let Some(disposition) = headers.get_first("Content-Disposition") {
60            if disposition.to_lowercase().contains("attachment") {
61                return true;
62            }
63        }
64
65        // Check for common attachment indicators in headers
66        // Files like image/*, application/*, etc. (but not text/plain or text/html)
67        if let Some(content_type) = headers.get_first("Content-Type") {
68            let content_type_lower = content_type.to_lowercase();
69            if (content_type_lower.starts_with("image/")
70                || content_type_lower.starts_with("application/")
71                || content_type_lower.starts_with("audio/")
72                || content_type_lower.starts_with("video/"))
73                && !content_type_lower.contains("multipart/alternative")
74            {
75                // Additional check: must have filename parameter or be disposition: attachment
76                if content_type_lower.contains("name=")
77                    || headers
78                        .get_first("Content-Disposition")
79                        .map(|d| d.to_lowercase().contains("attachment"))
80                        .unwrap_or(false)
81                {
82                    return true;
83                }
84            }
85        }
86
87        false
88    }
89}