Skip to main content

sieve/compiler/grammar/tests/
test_date.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 mail_parser::HeaderName;
8
9use crate::compiler::{
10    grammar::{instruction::CompilerState, Capability, Comparator},
11    lexer::{word::Word, StringConstant, Token},
12    CompileError, ErrorType, Number, Value,
13};
14
15use crate::compiler::grammar::{test::Test, MatchType};
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[cfg_attr(
19    any(test, feature = "serde"),
20    derive(serde::Serialize, serde::Deserialize)
21)]
22#[cfg_attr(
23    feature = "rkyv",
24    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
25)]
26pub(crate) struct TestDate {
27    pub header_name: Value,
28    pub key_list: Vec<Value>,
29    pub match_type: MatchType,
30    pub comparator: Comparator,
31    pub index: Option<i32>,
32    pub zone: Zone,
33    pub date_part: DatePart,
34    pub mime_anychild: bool,
35    pub is_not: bool,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39#[cfg_attr(
40    any(test, feature = "serde"),
41    derive(serde::Serialize, serde::Deserialize)
42)]
43#[cfg_attr(
44    feature = "rkyv",
45    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
46)]
47pub(crate) struct TestCurrentDate {
48    pub zone: Option<i64>,
49    pub match_type: MatchType,
50    pub comparator: Comparator,
51    pub date_part: DatePart,
52    pub key_list: Vec<Value>,
53    pub is_not: bool,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57#[cfg_attr(
58    any(test, feature = "serde"),
59    derive(serde::Serialize, serde::Deserialize)
60)]
61#[cfg_attr(
62    feature = "rkyv",
63    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
64)]
65pub(crate) enum Zone {
66    Time(i64),
67    Original,
68    Local,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[cfg_attr(
73    any(test, feature = "serde"),
74    derive(serde::Serialize, serde::Deserialize)
75)]
76#[cfg_attr(
77    feature = "rkyv",
78    derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)
79)]
80pub(crate) enum DatePart {
81    Year,
82    Month,
83    Day,
84    Date,
85    Julian,
86    Hour,
87    Minute,
88    Second,
89    Time,
90    Iso8601,
91    Std11,
92    Zone,
93    Weekday,
94}
95
96impl CompilerState<'_> {
97    pub(crate) fn parse_test_date(&mut self) -> Result<Test, CompileError> {
98        let mut match_type = MatchType::Is;
99        let mut comparator = Comparator::AsciiCaseMap;
100        let mut header_name = None;
101        let mut key_list;
102        let mut index = None;
103        let mut index_last = false;
104        let mut zone = Zone::Local;
105        let mut date_part = None;
106
107        let mut mime = false;
108        let mut mime_anychild = false;
109
110        loop {
111            let token_info = self.tokens.unwrap_next()?;
112            match token_info.token {
113                Token::Tag(
114                    word @ (Word::Is
115                    | Word::Contains
116                    | Word::Matches
117                    | Word::Value
118                    | Word::Count
119                    | Word::Regex
120                    | Word::List),
121                ) => {
122                    self.validate_argument(
123                        1,
124                        match word {
125                            Word::Value | Word::Count => Capability::Relational.into(),
126                            Word::Regex => Capability::Regex.into(),
127                            Word::List => Capability::ExtLists.into(),
128                            _ => None,
129                        },
130                        token_info.line_num,
131                        token_info.line_pos,
132                    )?;
133
134                    match_type = self.parse_match_type(word)?;
135                }
136                Token::Tag(Word::Comparator) => {
137                    self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
138                    comparator = self.parse_comparator()?;
139                }
140                Token::Tag(Word::Index) => {
141                    self.validate_argument(
142                        3,
143                        Capability::Index.into(),
144                        token_info.line_num,
145                        token_info.line_pos,
146                    )?;
147                    index = (self.tokens.expect_number(u16::MAX as usize)? as i32).into();
148                }
149                Token::Tag(Word::Last) => {
150                    self.validate_argument(
151                        4,
152                        Capability::Index.into(),
153                        token_info.line_num,
154                        token_info.line_pos,
155                    )?;
156                    index_last = true;
157                }
158                Token::Tag(Word::Mime) => {
159                    self.validate_argument(
160                        5,
161                        Capability::Mime.into(),
162                        token_info.line_num,
163                        token_info.line_pos,
164                    )?;
165                    mime = true;
166                }
167                Token::Tag(Word::AnyChild) => {
168                    self.validate_argument(
169                        6,
170                        Capability::Mime.into(),
171                        token_info.line_num,
172                        token_info.line_pos,
173                    )?;
174                    mime_anychild = true;
175                }
176                Token::Tag(Word::OriginalZone) => {
177                    self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
178                    zone = Zone::Original;
179                }
180                Token::Tag(Word::Zone) => {
181                    self.validate_argument(7, None, token_info.line_num, token_info.line_pos)?;
182                    zone = Zone::Time(self.parse_timezone()?);
183                }
184                _ => {
185                    if header_name.is_none() {
186                        let header = self.parse_string_token(token_info)?;
187                        if let Value::Text(header_name) = &header {
188                            if HeaderName::parse(header_name.as_ref()).is_none() {
189                                return Err(self
190                                    .tokens
191                                    .unwrap_next()?
192                                    .custom(ErrorType::InvalidHeaderName));
193                            }
194                        }
195                        header_name = header.into();
196                    } else if date_part.is_none() {
197                        if let Token::StringConstant(string) = &token_info.token {
198                            if let Some(date_part_) =
199                                lookup_date_part(&string.to_string().to_ascii_lowercase())
200                            {
201                                date_part = date_part_.into();
202                                continue;
203                            }
204                        }
205                        return Err(token_info.expected("valid date part"));
206                    } else {
207                        key_list = self.parse_strings_token(token_info)?;
208                        break;
209                    }
210                }
211            }
212        }
213
214        if !mime && mime_anychild {
215            return Err(self.tokens.unwrap_next()?.missing_tag(":mime"));
216        }
217        self.validate_match(&match_type, &mut key_list)?;
218
219        Ok(Test::Date(TestDate {
220            header_name: header_name.unwrap(),
221            key_list,
222            date_part: date_part.unwrap(),
223            match_type,
224            comparator,
225            index: if index_last { index.map(|i| -i) } else { index },
226            zone,
227            mime_anychild,
228            is_not: false,
229        }))
230    }
231
232    pub(crate) fn parse_test_currentdate(&mut self) -> Result<Test, CompileError> {
233        let mut match_type = MatchType::Is;
234        let mut comparator = Comparator::AsciiCaseMap;
235        let mut key_list;
236        let mut zone = None;
237        let mut date_part = None;
238
239        loop {
240            let token_info = self.tokens.unwrap_next()?;
241            match token_info.token {
242                Token::Tag(
243                    word @ (Word::Is
244                    | Word::Contains
245                    | Word::Matches
246                    | Word::Value
247                    | Word::Count
248                    | Word::Regex
249                    | Word::List),
250                ) => {
251                    self.validate_argument(
252                        1,
253                        match word {
254                            Word::Value | Word::Count => Capability::Relational.into(),
255                            Word::Regex => Capability::Regex.into(),
256                            Word::List => Capability::ExtLists.into(),
257                            _ => None,
258                        },
259                        token_info.line_num,
260                        token_info.line_pos,
261                    )?;
262
263                    match_type = self.parse_match_type(word)?;
264                }
265                Token::Tag(Word::Comparator) => {
266                    self.validate_argument(2, None, token_info.line_num, token_info.line_pos)?;
267                    comparator = self.parse_comparator()?;
268                }
269                Token::Tag(Word::Zone) => {
270                    self.validate_argument(3, None, token_info.line_num, token_info.line_pos)?;
271                    zone = self.parse_timezone()?.into();
272                }
273                _ => {
274                    if date_part.is_none() {
275                        if let Token::StringConstant(string) = &token_info.token {
276                            if let Some(date_part_) =
277                                lookup_date_part(&string.to_string().to_ascii_lowercase())
278                            {
279                                date_part = date_part_.into();
280                                continue;
281                            }
282                        }
283                        return Err(token_info.expected("valid date part"));
284                    } else {
285                        key_list = self.parse_strings_token(token_info)?;
286                        break;
287                    }
288                }
289            }
290        }
291        self.validate_match(&match_type, &mut key_list)?;
292
293        Ok(Test::CurrentDate(TestCurrentDate {
294            key_list,
295            date_part: date_part.unwrap(),
296            match_type,
297            comparator,
298            zone,
299            is_not: false,
300        }))
301    }
302
303    pub(crate) fn parse_timezone(&mut self) -> Result<i64, CompileError> {
304        let token_info = self.tokens.unwrap_next()?;
305        if let Token::StringConstant(value) = &token_info.token {
306            let timezone = match value {
307                StringConstant::String(value) => value.parse::<i64>().unwrap_or(i64::MAX),
308                StringConstant::Number(Number::Integer(n)) => *n,
309                StringConstant::Number(Number::Float(n)) => *n as i64,
310            };
311
312            return match timezone {
313                0..=1400 => Ok((timezone / 100 * 3600) + (timezone % 100 * 60)),
314                -1200..=-1 => Ok((timezone / 100 * 3600) - (-timezone % 100 * 60)),
315                _ => Err(token_info.expected("invalid timezone")),
316            };
317        }
318        Err(token_info.expected("string containing time zone"))
319    }
320}
321
322/*
323     "year"      => the year, "0000" .. "9999".
324     "month"     => the month, "01" .. "12".
325     "day"       => the day, "01" .. "31".
326     "date"      => the date in "yyyy-mm-dd" format.
327     "julian"    => the Modified Julian Day, that is, the date
328                    expressed as an integer number of days since
329                    00:00 UTC on November 17, 1858 (using the Gregorian
330                    calendar).  This corresponds to the regular
331                    Julian Day minus 2400000.5.  Sample routines to
332                    convert to and from modified Julian dates are
333                    given in Appendix A.
334     "hour"      => the hour, "00" .. "23".
335     "minute"    => the minute, "00" .. "59".
336     "second"    => the second, "00" .. "60".
337     "time"      => the time in "hh:mm:ss" format.
338     "iso8601"   => the date and time in restricted ISO 8601 format.
339     "std11"     => the date and time in a format appropriate
340                    for use in a Date: header field [RFC2822].
341     "zone"      => the time zone in use.  If the user specified a
342                    time zone with ":zone", "zone" will
343                    contain that value.  If :originalzone is specified
344                    this value will be the original zone specified
345                    in the date-time value.  If neither argument is
346                    specified the value will be the server's default
347                    time zone in offset format "+hhmm" or "-hhmm".  An
348                    offset of 0 (Zulu) always has a positive sign.
349     "weekday"   => the day of the week expressed as an integer between
350                    "0" and "6". "0" is Sunday, "1" is Monday, etc.
351*/
352
353fn lookup_date_part(input: &str) -> Option<DatePart> {
354    hashify::tiny_map!(
355        input.as_bytes(),
356        "year" => DatePart::Year,
357        "month" => DatePart::Month,
358        "day" => DatePart::Day,
359        "date" => DatePart::Date,
360        "julian" => DatePart::Julian,
361        "hour" => DatePart::Hour,
362        "minute" => DatePart::Minute,
363        "second" => DatePart::Second,
364        "time" => DatePart::Time,
365        "iso8601" => DatePart::Iso8601,
366        "std11" => DatePart::Std11,
367        "zone" => DatePart::Zone,
368        "weekday" => DatePart::Weekday,
369    )
370}