1use 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
322fn 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}