sieve/runtime/tests/
test_spamtest.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
5 */
6
7use crate::{
8    compiler::{
9        grammar::{
10            tests::test_spamtest::{TestSpamTest, TestVirusTest},
11            MatchType,
12        },
13        Number,
14    },
15    runtime::Variable,
16    Context, SpamStatus, VirusStatus,
17};
18
19use super::TestResult;
20
21impl TestSpamTest {
22    pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
23        let status = if self.percent {
24            ctx.spam_status.as_percentage()
25        } else {
26            ctx.spam_status.as_number()
27        };
28        let value = ctx.eval_value(&self.value);
29        let mut captured_values = Vec::new();
30
31        let result = match &self.match_type {
32            MatchType::Is => self.comparator.is(&status, &value),
33            MatchType::Contains => self
34                .comparator
35                .contains(status.to_string().as_ref(), value.to_string().as_ref()),
36            MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
37            MatchType::Matches(capture_positions) => self.comparator.matches(
38                status.to_string().as_ref(),
39                value.to_string().as_ref(),
40                *capture_positions,
41                &mut captured_values,
42            ),
43            MatchType::Regex(capture_positions) => self.comparator.regex(
44                &self.value,
45                &value,
46                status.to_string().as_ref(),
47                *capture_positions,
48                &mut captured_values,
49            ),
50            MatchType::Count(rel_match) => rel_match.cmp(
51                &Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
52                    0.0
53                } else {
54                    1.1
55                }),
56                &value.to_number(),
57            ),
58            MatchType::List => false,
59        };
60
61        if !captured_values.is_empty() {
62            ctx.set_match_variables(captured_values);
63        }
64
65        TestResult::Bool(result ^ self.is_not)
66    }
67}
68
69impl TestVirusTest {
70    pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
71        let status = ctx.virus_status.as_number();
72        let value = ctx.eval_value(&self.value);
73        let mut captured_values = Vec::new();
74
75        let result = match &self.match_type {
76            MatchType::Is => self.comparator.is(&status, &value),
77            MatchType::Contains => self
78                .comparator
79                .contains(status.to_string().as_ref(), value.to_string().as_ref()),
80            MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
81            MatchType::Matches(capture_positions) => self.comparator.matches(
82                status.to_string().as_ref(),
83                value.to_string().as_ref(),
84                *capture_positions,
85                &mut captured_values,
86            ),
87            MatchType::Regex(capture_positions) => self.comparator.regex(
88                &self.value,
89                &value,
90                status.to_string().as_ref(),
91                *capture_positions,
92                &mut captured_values,
93            ),
94            MatchType::Count(rel_match) => rel_match.cmp(
95                &Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
96                    0.0
97                } else {
98                    1.1
99                }),
100                &value.to_number(),
101            ),
102            MatchType::List => false,
103        };
104
105        if !captured_values.is_empty() {
106            ctx.set_match_variables(captured_values);
107        }
108
109        TestResult::Bool(result ^ self.is_not)
110    }
111}
112
113impl SpamStatus {
114    pub fn from_number(number: u32) -> Self {
115        match number {
116            1 => SpamStatus::Ham,
117            2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
118            10 => SpamStatus::Spam,
119            _ => SpamStatus::Unknown,
120        }
121    }
122
123    pub(crate) fn as_number(&self) -> Variable {
124        Variable::Integer(match self {
125            SpamStatus::Unknown => 0,
126            SpamStatus::Ham => 1,
127            SpamStatus::MaybeSpam(pct) => ((pct * 10.0) as i64).clamp(2, 9),
128            SpamStatus::Spam => 10,
129        })
130    }
131
132    pub(crate) fn as_percentage(&self) -> Variable {
133        Variable::Integer(match self {
134            SpamStatus::Unknown | SpamStatus::Ham => 0,
135            SpamStatus::MaybeSpam(pct) => ((pct * 100.0).ceil() as i64).clamp(1, 100),
136            SpamStatus::Spam => 100,
137        })
138    }
139}
140
141impl VirusStatus {
142    pub fn from_number(number: u32) -> Self {
143        match number {
144            1 => VirusStatus::Clean,
145            2 => VirusStatus::Replaced,
146            3 => VirusStatus::Cured,
147            4 => VirusStatus::MaybeVirus,
148            5 => VirusStatus::Virus,
149            _ => VirusStatus::Unknown,
150        }
151    }
152
153    pub(crate) fn as_number(&self) -> Variable {
154        Variable::Integer(match self {
155            VirusStatus::Unknown => 0,
156            VirusStatus::Clean => 1,
157            VirusStatus::Replaced => 2,
158            VirusStatus::Cured => 3,
159            VirusStatus::MaybeVirus => 4,
160            VirusStatus::Virus => 5,
161        })
162    }
163}
164
165impl From<u32> for SpamStatus {
166    fn from(number: u32) -> Self {
167        SpamStatus::from_number(number)
168    }
169}
170
171impl From<i32> for SpamStatus {
172    fn from(number: i32) -> Self {
173        SpamStatus::from_number(number as u32)
174    }
175}
176
177impl From<usize> for SpamStatus {
178    fn from(number: usize) -> Self {
179        SpamStatus::from_number(number as u32)
180    }
181}
182
183impl From<u32> for VirusStatus {
184    fn from(number: u32) -> Self {
185        VirusStatus::from_number(number)
186    }
187}
188
189impl From<i32> for VirusStatus {
190    fn from(number: i32) -> Self {
191        VirusStatus::from_number(number as u32)
192    }
193}
194
195impl From<usize> for VirusStatus {
196    fn from(number: usize) -> Self {
197        VirusStatus::from_number(number as u32)
198    }
199}