sieve/runtime/tests/
test_spamtest.rs

1/*
2 * Copyright (c) 2020-2023, Stalwart Labs Ltd.
3 *
4 * This file is part of the Stalwart Sieve Interpreter.
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, either version 3 of
9 * the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 * in the LICENSE file at the top-level directory of this distribution.
16 * You should have received a copy of the GNU Affero General Public License
17 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 *
19 * You can be released from the requirements of the AGPLv3 license by
20 * purchasing a commercial license. Please contact licensing@stalw.art
21 * for more details.
22*/
23
24use crate::{
25    compiler::{
26        grammar::{
27            tests::test_spamtest::{TestSpamTest, TestVirusTest},
28            MatchType,
29        },
30        Number,
31    },
32    runtime::Variable,
33    Context, SpamStatus, VirusStatus,
34};
35
36use super::TestResult;
37
38impl TestSpamTest {
39    pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
40        let status = if self.percent {
41            ctx.spam_status.as_percentage()
42        } else {
43            ctx.spam_status.as_number()
44        };
45        let value = ctx.eval_value(&self.value);
46        let mut captured_values = Vec::new();
47
48        let result = match &self.match_type {
49            MatchType::Is => self.comparator.is(&status, &value),
50            MatchType::Contains => self
51                .comparator
52                .contains(status.to_string().as_ref(), value.to_string().as_ref()),
53            MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
54            MatchType::Matches(capture_positions) => self.comparator.matches(
55                status.to_string().as_ref(),
56                value.to_string().as_ref(),
57                *capture_positions,
58                &mut captured_values,
59            ),
60            MatchType::Regex(capture_positions) => self.comparator.regex(
61                &self.value,
62                &value,
63                status.to_string().as_ref(),
64                *capture_positions,
65                &mut captured_values,
66            ),
67            MatchType::Count(rel_match) => rel_match.cmp(
68                &Number::from(if matches!(&ctx.spam_status, SpamStatus::Unknown) {
69                    0.0
70                } else {
71                    1.1
72                }),
73                &value.to_number(),
74            ),
75            MatchType::List => false,
76        };
77
78        if !captured_values.is_empty() {
79            ctx.set_match_variables(captured_values);
80        }
81
82        TestResult::Bool(result ^ self.is_not)
83    }
84}
85
86impl TestVirusTest {
87    pub(crate) fn exec(&self, ctx: &mut Context) -> TestResult {
88        let status = ctx.virus_status.as_number();
89        let value = ctx.eval_value(&self.value);
90        let mut captured_values = Vec::new();
91
92        let result = match &self.match_type {
93            MatchType::Is => self.comparator.is(&status, &value),
94            MatchType::Contains => self
95                .comparator
96                .contains(status.to_string().as_ref(), value.to_string().as_ref()),
97            MatchType::Value(rel_match) => self.comparator.relational(rel_match, &status, &value),
98            MatchType::Matches(capture_positions) => self.comparator.matches(
99                status.to_string().as_ref(),
100                value.to_string().as_ref(),
101                *capture_positions,
102                &mut captured_values,
103            ),
104            MatchType::Regex(capture_positions) => self.comparator.regex(
105                &self.value,
106                &value,
107                status.to_string().as_ref(),
108                *capture_positions,
109                &mut captured_values,
110            ),
111            MatchType::Count(rel_match) => rel_match.cmp(
112                &Number::from(if matches!(&ctx.virus_status, VirusStatus::Unknown) {
113                    0.0
114                } else {
115                    1.1
116                }),
117                &value.to_number(),
118            ),
119            MatchType::List => false,
120        };
121
122        if !captured_values.is_empty() {
123            ctx.set_match_variables(captured_values);
124        }
125
126        TestResult::Bool(result ^ self.is_not)
127    }
128}
129
130impl SpamStatus {
131    pub fn from_number(number: u32) -> Self {
132        match number {
133            1 => SpamStatus::Ham,
134            2..=9 => SpamStatus::MaybeSpam(number as f64 / 10.0),
135            10 => SpamStatus::Spam,
136            _ => SpamStatus::Unknown,
137        }
138    }
139
140    pub(crate) fn as_number(&self) -> Variable {
141        Variable::Integer(match self {
142            SpamStatus::Unknown => 0,
143            SpamStatus::Ham => 1,
144            SpamStatus::MaybeSpam(pct) => {
145                let n = (pct * 10.0) as i64;
146                if n < 2 {
147                    2
148                } else if n > 9 {
149                    9
150                } else {
151                    n
152                }
153            }
154            SpamStatus::Spam => 10,
155        })
156    }
157
158    pub(crate) fn as_percentage(&self) -> Variable {
159        Variable::Integer(match self {
160            SpamStatus::Unknown | SpamStatus::Ham => 0,
161            SpamStatus::MaybeSpam(pct) => {
162                let n = (pct * 100.0).ceil() as i64;
163                if n > 100 {
164                    100
165                } else if n < 1 {
166                    1
167                } else {
168                    n
169                }
170            }
171            SpamStatus::Spam => 100,
172        })
173    }
174}
175
176impl VirusStatus {
177    pub fn from_number(number: u32) -> Self {
178        match number {
179            1 => VirusStatus::Clean,
180            2 => VirusStatus::Replaced,
181            3 => VirusStatus::Cured,
182            4 => VirusStatus::MaybeVirus,
183            5 => VirusStatus::Virus,
184            _ => VirusStatus::Unknown,
185        }
186    }
187
188    pub(crate) fn as_number(&self) -> Variable {
189        Variable::Integer(match self {
190            VirusStatus::Unknown => 0,
191            VirusStatus::Clean => 1,
192            VirusStatus::Replaced => 2,
193            VirusStatus::Cured => 3,
194            VirusStatus::MaybeVirus => 4,
195            VirusStatus::Virus => 5,
196        })
197    }
198}
199
200impl From<u32> for SpamStatus {
201    fn from(number: u32) -> Self {
202        SpamStatus::from_number(number)
203    }
204}
205
206impl From<i32> for SpamStatus {
207    fn from(number: i32) -> Self {
208        SpamStatus::from_number(number as u32)
209    }
210}
211
212impl From<usize> for SpamStatus {
213    fn from(number: usize) -> Self {
214        SpamStatus::from_number(number as u32)
215    }
216}
217
218impl From<u32> for VirusStatus {
219    fn from(number: u32) -> Self {
220        VirusStatus::from_number(number)
221    }
222}
223
224impl From<i32> for VirusStatus {
225    fn from(number: i32) -> Self {
226        VirusStatus::from_number(number as u32)
227    }
228}
229
230impl From<usize> for VirusStatus {
231    fn from(number: usize) -> Self {
232        VirusStatus::from_number(number as u32)
233    }
234}