1use std::fmt::Display;
8
9use mail_parser::HeaderName;
10
11use crate::{
12 compiler::{
13 grammar::{
14 expr::{self},
15 instruction::CompilerState,
16 AddressPart,
17 },
18 ContentTypePart, ErrorType, HeaderPart, HeaderVariable, MessagePart, Number,
19 ReceivedHostname, ReceivedPart, Value, VariableType,
20 },
21 runtime::eval::IntoString,
22 Envelope, MAX_MATCH_VARIABLES,
23};
24
25enum State {
26 None,
27 Variable,
28 Encoded {
29 is_unicode: bool,
30 initial_buf_size: usize,
31 },
32}
33
34impl CompilerState<'_> {
35 pub(crate) fn tokenize_string(
36 &mut self,
37 bytes: &[u8],
38 parse_decoded: bool,
39 ) -> Result<Value, ErrorType> {
40 let mut state = State::None;
41 let mut items = Vec::with_capacity(3);
42 let mut last_ch = 0;
43
44 let mut var_start_pos = usize::MAX;
45 let mut var_is_number = true;
46 let mut var_has_namespace = false;
47
48 let mut text_has_digits = true;
49 let mut text_has_dots = false;
50
51 let mut hex_start = usize::MAX;
52 let mut decode_buf = Vec::with_capacity(bytes.len());
53
54 for (pos, &ch) in bytes.iter().enumerate() {
55 let mut is_var_error = false;
56
57 match state {
58 State::None => match ch {
59 b'{' if last_ch == b'$' => {
60 decode_buf.pop();
61 var_start_pos = pos + 1;
62 var_is_number = true;
63 var_has_namespace = false;
64 state = State::Variable;
65 }
66 b'.' => {
67 if text_has_dots {
68 text_has_digits = false;
69 } else {
70 text_has_dots = true;
71 }
72 decode_buf.push(ch);
73 }
74 b'0'..=b'9' => {
75 decode_buf.push(ch);
76 }
77 _ => {
78 text_has_digits = false;
79 decode_buf.push(ch);
80 }
81 },
82 State::Variable => match ch {
83 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'[' | b']' | b'*' | b'-' => {
84 var_is_number = false;
85 }
86 b'.' => {
87 var_is_number = false;
88 var_has_namespace = true;
89 }
90 b'0'..=b'9' => {}
91 b'}' => {
92 if pos > var_start_pos {
93 if !decode_buf.is_empty() {
95 self.add_value(
96 &mut items,
97 &decode_buf,
98 parse_decoded,
99 text_has_digits,
100 text_has_dots,
101 )?;
102 decode_buf.clear();
103 text_has_digits = true;
104 text_has_dots = false;
105 }
106
107 let var_name = std::str::from_utf8(&bytes[var_start_pos..pos]).unwrap();
109 let var_type = if !var_is_number {
110 self.parse_variable(var_name, var_has_namespace)
111 } else {
112 self.parse_match_variable(var_name)
113 };
114
115 match var_type {
116 Ok(Some(var)) => items.push(Value::Variable(var)),
117 Ok(None) => {}
118 Err(
119 ErrorType::InvalidNamespace(_) | ErrorType::InvalidEnvelope(_),
120 ) => {
121 is_var_error = true;
122 }
123 Err(e) => return Err(e),
124 }
125
126 state = State::None;
127 } else {
128 is_var_error = true;
129 }
130 }
131 b':' => {
132 if parse_decoded && !var_has_namespace {
133 match bytes.get(var_start_pos..pos) {
134 Some(enc) if enc.eq_ignore_ascii_case(b"hex") => {
135 state = State::Encoded {
136 is_unicode: false,
137 initial_buf_size: decode_buf.len(),
138 };
139 }
140 Some(enc) if enc.eq_ignore_ascii_case(b"unicode") => {
141 state = State::Encoded {
142 is_unicode: true,
143 initial_buf_size: decode_buf.len(),
144 };
145 }
146 _ => {
147 is_var_error = true;
148 }
149 }
150 } else if var_has_namespace {
151 var_is_number = false;
152 } else {
153 is_var_error = true;
154 }
155 }
156 _ => {
157 is_var_error = true;
158 }
159 },
160
161 State::Encoded {
162 is_unicode,
163 initial_buf_size,
164 } => match ch {
165 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
166 if hex_start == usize::MAX {
167 hex_start = pos;
168 }
169 }
170 b' ' | b'\t' | b'\r' | b'\n' | b'}' => {
171 if hex_start != usize::MAX {
172 let code = std::str::from_utf8(&bytes[hex_start..pos]).unwrap();
173 hex_start = usize::MAX;
174
175 if !is_unicode {
176 if let Ok(ch) = u8::from_str_radix(code, 16) {
177 decode_buf.push(ch);
178 } else {
179 is_var_error = true;
180 }
181 } else if let Ok(ch) = u32::from_str_radix(code, 16) {
182 let mut buf = [0; 4];
183 decode_buf.extend_from_slice(
184 char::from_u32(ch)
185 .ok_or(ErrorType::InvalidUnicodeSequence(ch))?
186 .encode_utf8(&mut buf)
187 .as_bytes(),
188 );
189 } else {
190 is_var_error = true;
191 }
192 }
193 if ch == b'}' {
194 if decode_buf.len() != initial_buf_size {
195 state = State::None;
196 } else {
197 is_var_error = true;
198 }
199 }
200 }
201 _ => {
202 is_var_error = true;
203 }
204 },
205 }
206
207 if is_var_error {
208 if let State::Encoded {
209 initial_buf_size, ..
210 } = state
211 {
212 if initial_buf_size != decode_buf.len() {
213 decode_buf.truncate(initial_buf_size);
214 }
215 }
216 decode_buf.extend_from_slice(&bytes[var_start_pos - 2..pos + 1]);
217 hex_start = usize::MAX;
218 state = State::None;
219 }
220
221 last_ch = ch;
222 }
223
224 match state {
225 State::Variable => {
226 decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
227 }
228 State::Encoded {
229 initial_buf_size, ..
230 } => {
231 if initial_buf_size != decode_buf.len() {
232 decode_buf.truncate(initial_buf_size);
233 }
234 decode_buf.extend_from_slice(&bytes[var_start_pos - 2..bytes.len()]);
235 }
236 State::None => (),
237 }
238
239 if !decode_buf.is_empty() {
240 self.add_value(
241 &mut items,
242 &decode_buf,
243 parse_decoded,
244 text_has_digits,
245 text_has_dots,
246 )?;
247 }
248
249 Ok(match items.len() {
250 1 => items.pop().unwrap(),
251 0 => Value::Text(String::new().into()),
252 _ => Value::List(items),
253 })
254 }
255
256 fn parse_match_variable(&mut self, var_name: &str) -> Result<Option<VariableType>, ErrorType> {
257 let num = var_name
258 .parse()
259 .map_err(|_| ErrorType::InvalidNumber(var_name.to_string()))?;
260 if num < MAX_MATCH_VARIABLES as usize {
261 if self.register_match_var(num) {
262 let total_vars = num + 1;
263 if total_vars > self.vars_match_max {
264 self.vars_match_max = total_vars;
265 }
266 Ok(Some(VariableType::Match(num)))
267 } else {
268 Ok(None)
269 }
270 } else {
271 Err(ErrorType::InvalidMatchVariable(num))
272 }
273 }
274
275 pub fn parse_variable(
276 &self,
277 var_name: &str,
278 maybe_namespace: bool,
279 ) -> Result<Option<VariableType>, ErrorType> {
280 if !maybe_namespace {
281 if self.is_var_global(var_name) {
282 Ok(Some(VariableType::Global(var_name.to_string())))
283 } else if let Some(var_id) = self.get_local_var(var_name) {
284 Ok(Some(VariableType::Local(var_id)))
285 } else {
286 Ok(None)
287 }
288 } else {
289 let var = match var_name.to_lowercase().split_once('.') {
290 Some(("global" | "t", var_name)) if !var_name.is_empty() => {
291 VariableType::Global(var_name.to_string())
292 }
293 Some(("env", var_name)) if !var_name.is_empty() => {
294 VariableType::Environment(var_name.to_string())
295 }
296 Some(("envelope", var_name)) if !var_name.is_empty() => {
297 let envelope = match var_name {
298 "from" => Envelope::From,
299 "to" => Envelope::To,
300 "by_time_absolute" => Envelope::ByTimeAbsolute,
301 "by_time_relative" => Envelope::ByTimeRelative,
302 "by_mode" => Envelope::ByMode,
303 "by_trace" => Envelope::ByTrace,
304 "notify" => Envelope::Notify,
305 "orcpt" => Envelope::Orcpt,
306 "ret" => Envelope::Ret,
307 "envid" => Envelope::Envid,
308 _ => {
309 return Err(ErrorType::InvalidEnvelope(var_name.to_string()));
310 }
311 };
312 VariableType::Envelope(envelope)
313 }
314 Some(("header", var_name)) if !var_name.is_empty() => {
315 self.parse_header_variable(var_name)?
316 }
317 Some(("body", var_name)) if !var_name.is_empty() => match var_name {
318 "text" => VariableType::Part(MessagePart::TextBody(false)),
319 "html" => VariableType::Part(MessagePart::HtmlBody(false)),
320 "to_text" => VariableType::Part(MessagePart::TextBody(true)),
321 "to_html" => VariableType::Part(MessagePart::HtmlBody(true)),
322 _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
323 },
324 Some(("part", var_name)) if !var_name.is_empty() => match var_name {
325 "text" => VariableType::Part(MessagePart::Contents),
326 "raw" => VariableType::Part(MessagePart::Raw),
327 _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
328 },
329 None => {
330 if self.is_var_global(var_name) {
331 VariableType::Global(var_name.to_string())
332 } else if let Some(var_id) = self.get_local_var(var_name) {
333 VariableType::Local(var_id)
334 } else {
335 return Ok(None);
336 }
337 }
338 _ => return Err(ErrorType::InvalidNamespace(var_name.to_string())),
339 };
340
341 Ok(Some(var))
342 }
343 }
344
345 fn parse_header_variable(&self, var_name: &str) -> Result<VariableType, ErrorType> {
346 #[derive(Debug)]
347 enum State {
348 Name,
349 Index,
350 Part,
351 PartIndex,
352 }
353 let mut name = vec![];
354 let mut has_name = false;
355 let mut has_wildcard = false;
356 let mut hdr_name = String::new();
357 let mut hdr_index = String::new();
358 let mut part = String::new();
359 let mut part_index = String::new();
360 let mut state = State::Name;
361
362 for ch in var_name.chars() {
363 match state {
364 State::Name => match ch {
365 '[' => {
366 state = if hdr_index.is_empty() {
367 State::Index
368 } else if part.is_empty() {
369 State::PartIndex
370 } else {
371 return Err(ErrorType::InvalidExpression(var_name.to_string()));
372 };
373 has_name = true;
374 }
375 '.' => {
376 state = State::Part;
377 has_name = true;
378 }
379 ' ' | '\t' | '\r' | '\n' => {}
380 '*' if !has_wildcard && hdr_name.is_empty() && name.is_empty() => {
381 has_wildcard = true;
382 }
383 ':' if !hdr_name.is_empty() && !has_wildcard => {
384 name.push(
385 HeaderName::parse(std::mem::take(&mut hdr_name)).ok_or_else(|| {
386 ErrorType::InvalidExpression(var_name.to_string())
387 })?,
388 );
389 }
390 _ if !has_name && !has_wildcard => {
391 hdr_name.push(ch);
392 }
393 _ => {
394 return Err(ErrorType::InvalidExpression(var_name.to_string()));
395 }
396 },
397 State::Index => match ch {
398 ']' => {
399 state = State::Name;
400 }
401 ' ' | '\t' | '\r' | '\n' => {}
402 _ => {
403 hdr_index.push(ch);
404 }
405 },
406 State::Part => match ch {
407 '[' => {
408 state = State::PartIndex;
409 }
410 ' ' | '\t' | '\r' | '\n' => {}
411 _ => {
412 part.push(ch);
413 }
414 },
415 State::PartIndex => match ch {
416 ']' => {
417 state = State::Name;
418 }
419 ' ' | '\t' | '\r' | '\n' => {}
420 _ => {
421 part_index.push(ch);
422 }
423 },
424 }
425 }
426
427 if !hdr_name.is_empty() {
428 name.push(
429 HeaderName::parse(hdr_name)
430 .ok_or_else(|| ErrorType::InvalidExpression(var_name.to_string()))?,
431 );
432 }
433
434 if !name.is_empty() || has_wildcard {
435 Ok(VariableType::Header(HeaderVariable {
436 name,
437 part: HeaderPart::try_from(part.as_str())
438 .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
439 index_hdr: match hdr_index.as_str() {
440 "" => {
441 if !has_wildcard {
442 -1
443 } else {
444 0
445 }
446 }
447 "*" => 0,
448 _ => hdr_index
449 .parse()
450 .map(|v| if v == 0 { 1 } else { v })
451 .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
452 },
453 index_part: match part_index.as_str() {
454 "" => {
455 if !has_wildcard {
456 -1
457 } else {
458 0
459 }
460 }
461 "*" => 0,
462 _ => part_index
463 .parse()
464 .map(|v| if v == 0 { 1 } else { v })
465 .map_err(|_| ErrorType::InvalidExpression(var_name.to_string()))?,
466 },
467 }))
468 } else {
469 Err(ErrorType::InvalidExpression(var_name.to_string()))
470 }
471 }
472
473 pub fn parse_expr_fnc_or_var(
474 &self,
475 var_name: &str,
476 maybe_namespace: bool,
477 ) -> Result<expr::Token, String> {
478 match self.parse_variable(var_name, maybe_namespace) {
479 Ok(Some(var)) => Ok(expr::Token::Variable(var)),
480 _ => {
481 if let Some((id, num_args)) = self.compiler.functions.get(var_name) {
482 Ok(expr::Token::Function {
483 name: var_name.to_string(),
484 id: *id,
485 num_args: *num_args,
486 })
487 } else {
488 Err(format!("Invalid variable or function name {var_name:?}"))
489 }
490 }
491 }
492 }
493
494 #[inline(always)]
495 fn add_value(
496 &mut self,
497 items: &mut Vec<Value>,
498 buf: &[u8],
499 parse_decoded: bool,
500 has_digits: bool,
501 has_dots: bool,
502 ) -> Result<(), ErrorType> {
503 if !parse_decoded {
504 items.push(if has_digits {
505 if has_dots {
506 match std::str::from_utf8(buf)
507 .ok()
508 .and_then(|v| (v, v.parse::<f64>().ok()?).into())
509 {
510 Some((v, n)) if n.to_string() == v => Value::Number(Number::Float(n)),
511 _ => Value::Text(buf.to_vec().into_string().into()),
512 }
513 } else {
514 match std::str::from_utf8(buf)
515 .ok()
516 .and_then(|v| (v, v.parse::<i64>().ok()?).into())
517 {
518 Some((v, n)) if n.to_string() == v => Value::Number(Number::Integer(n)),
519 _ => Value::Text(buf.to_vec().into_string().into()),
520 }
521 }
522 } else {
523 Value::Text(buf.to_vec().into_string().into())
524 });
525 } else {
526 match self.tokenize_string(buf, false)? {
527 Value::List(new_items) => items.extend(new_items),
528 item => items.push(item),
529 }
530 }
531
532 Ok(())
533 }
534}
535
536impl TryFrom<&str> for HeaderPart {
537 type Error = ();
538
539 fn try_from(value: &str) -> Result<Self, Self::Error> {
540 let (value, subvalue) = value.split_once('.').unwrap_or((value, ""));
541 Ok(match value {
542 "" | "text" => HeaderPart::Text,
543 "name" => HeaderPart::Address(AddressPart::Name),
545 "addr" => {
546 if !subvalue.is_empty() {
547 HeaderPart::Address(AddressPart::try_from(subvalue)?)
548 } else {
549 HeaderPart::Address(AddressPart::All)
550 }
551 }
552
553 "type" => HeaderPart::ContentType(ContentTypePart::Type),
555 "subtype" => HeaderPart::ContentType(ContentTypePart::Subtype),
556 "attr" if !subvalue.is_empty() => {
557 HeaderPart::ContentType(ContentTypePart::Attribute(subvalue.to_string()))
558 }
559
560 "rcvd" => {
562 if !subvalue.is_empty() {
563 HeaderPart::Received(ReceivedPart::try_from(subvalue)?)
564 } else {
565 HeaderPart::Text
566 }
567 }
568
569 "id" => HeaderPart::Id,
571
572 "raw" => HeaderPart::Raw,
574 "raw_name" => HeaderPart::RawName,
575
576 "date" => HeaderPart::Date,
578
579 "exists" => HeaderPart::Exists,
581
582 _ => {
583 return Err(());
584 }
585 })
586 }
587}
588
589impl TryFrom<&str> for ReceivedPart {
590 type Error = ();
591
592 fn try_from(value: &str) -> Result<Self, Self::Error> {
593 Ok(match value {
594 "from" => ReceivedPart::From(ReceivedHostname::Any),
596 "from.name" => ReceivedPart::From(ReceivedHostname::Name),
597 "from.ip" => ReceivedPart::From(ReceivedHostname::Ip),
598 "ip" => ReceivedPart::FromIp,
599 "iprev" => ReceivedPart::FromIpRev,
600 "by" => ReceivedPart::By(ReceivedHostname::Any),
601 "by.name" => ReceivedPart::By(ReceivedHostname::Name),
602 "by.ip" => ReceivedPart::By(ReceivedHostname::Ip),
603 "for" => ReceivedPart::For,
604 "with" => ReceivedPart::With,
605 "tls" => ReceivedPart::TlsVersion,
606 "cipher" => ReceivedPart::TlsCipher,
607 "id" => ReceivedPart::Id,
608 "ident" => ReceivedPart::Ident,
609 "date" => ReceivedPart::Date,
610 "date.raw" => ReceivedPart::DateRaw,
611 _ => return Err(()),
612 })
613 }
614}
615
616impl TryFrom<&str> for AddressPart {
617 type Error = ();
618
619 fn try_from(value: &str) -> Result<Self, Self::Error> {
620 Ok(match value {
621 "name" => AddressPart::Name,
622 "addr" | "all" => AddressPart::All,
623 "addr.domain" => AddressPart::Domain,
624 "addr.local" => AddressPart::LocalPart,
625 "addr.user" => AddressPart::User,
626 "addr.detail" => AddressPart::Detail,
627 _ => return Err(()),
628 })
629 }
630}
631
632impl Display for Value {
633 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
634 match self {
635 Value::Text(t) => f.write_str(t),
636 Value::List(l) => {
637 for i in l {
638 i.fmt(f)?;
639 }
640 Ok(())
641 }
642 Value::Number(n) => n.fmt(f),
643 Value::Variable(v) => v.fmt(f),
644 Value::Regex(r) => f.write_str(&r.expr),
645 }
646 }
647}
648
649impl Display for VariableType {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 match self {
652 VariableType::Local(v) => write!(f, "${{{v}}}"),
653 VariableType::Match(v) => write!(f, "${{{v}}}"),
654 VariableType::Global(v) => write!(f, "${{global.{v}}}"),
655 VariableType::Environment(v) => write!(f, "${{env.{v}}}"),
656
657 VariableType::Envelope(env) => f.write_str(match env {
658 Envelope::From => "${{envelope.from}}",
659 Envelope::To => "${{envelope.to}}",
660 Envelope::ByTimeAbsolute => "${{envelope.by_time_absolute}}",
661 Envelope::ByTimeRelative => "${{envelope.by_time_relative}}",
662 Envelope::ByMode => "${{envelope.by_mode}}",
663 Envelope::ByTrace => "${{envelope.by_trace}}",
664 Envelope::Notify => "${{envelope.notify}}",
665 Envelope::Orcpt => "${{envelope.orcpt}}",
666 Envelope::Ret => "${{envelope.ret}}",
667 Envelope::Envid => "${{envelope.envit}}",
668 }),
669
670 VariableType::Header(hdr) => {
671 write!(
672 f,
673 "${{header.{}",
674 hdr.name.first().map(|h| h.as_str()).unwrap_or_default()
675 )?;
676 if hdr.index_hdr != 0 {
677 write!(f, "[{}]", hdr.index_hdr)?;
678 } else {
679 f.write_str("[*]")?;
680 }
681 if hdr.index_part != 0 {
695 write!(f, "[{}]", hdr.index_part)?;
696 } else {
697 f.write_str("[*]")?;
698 }
699 f.write_str("}")
700 }
701 VariableType::Part(part) => {
702 write!(
703 f,
704 "${{{}",
705 match part {
706 MessagePart::TextBody(true) => "body.to_text",
707 MessagePart::TextBody(false) => "body.text",
708 MessagePart::HtmlBody(true) => "body.to_html",
709 MessagePart::HtmlBody(false) => "body.html",
710 MessagePart::Contents => "part.text",
711 MessagePart::Raw => "part.raw",
712 }
713 )?;
714 f.write_str("}")
715 }
716 }
717 }
718}
719
720#[cfg(test)]
721mod tests {
722
723 use mail_parser::HeaderName;
724
725 use super::Value;
726 use crate::compiler::grammar::instruction::{Block, CompilerState, Instruction, MAX_PARAMS};
727 use crate::compiler::grammar::test::Test;
728 use crate::compiler::grammar::tests::test_string::TestString;
729 use crate::compiler::grammar::{Comparator, MatchType};
730 use crate::compiler::lexer::tokenizer::Tokenizer;
731 use crate::compiler::lexer::word::Word;
732 use crate::compiler::{AddressPart, HeaderPart, HeaderVariable, VariableType};
733 use crate::{AHashSet, Compiler};
734
735 #[test]
736 fn tokenize_string() {
737 let c = Compiler::new();
738 let mut block = Block::new(Word::Not);
739 block.match_test_pos.push(0);
740 let mut compiler = CompilerState {
741 compiler: &c,
742 instructions: vec![Instruction::Test(Test::String(TestString {
743 match_type: MatchType::Regex(u64::MAX),
744 comparator: Comparator::AsciiCaseMap,
745 source: vec![Value::Variable(VariableType::Local(0))],
746 key_list: vec![Value::Variable(VariableType::Local(0))],
747 is_not: false,
748 }))],
749 block_stack: Vec::new(),
750 block,
751 last_block_type: Word::Not,
752 vars_global: AHashSet::new(),
753 vars_num: 0,
754 vars_num_max: 0,
755 vars_local: 0,
756 tokens: Tokenizer::new(&c, b""),
757 vars_match_max: usize::MAX,
758 param_check: [false; MAX_PARAMS],
759 includes_num: 0,
760 };
761
762 for (input, expected_result) in [
763 ("$${hex:24 24}", Value::Text("$$$".to_string().into())),
764 ("$${hex:40}", Value::Text("$@".to_string().into())),
765 ("${hex: 40 }", Value::Text("@".to_string().into())),
766 ("${HEX: 40}", Value::Text("@".to_string().into())),
767 ("${hex:40", Value::Text("${hex:40".to_string().into())),
768 ("${hex:400}", Value::Text("${hex:400}".to_string().into())),
769 (
770 "${hex:4${hex:30}}",
771 Value::Text("${hex:40}".to_string().into()),
772 ),
773 ("${unicode:40}", Value::Text("@".to_string().into())),
774 (
775 "${ unicode:40}",
776 Value::Text("${ unicode:40}".to_string().into()),
777 ),
778 ("${UNICODE:40}", Value::Text("@".to_string().into())),
779 ("${UnICoDE:0000040}", Value::Text("@".to_string().into())),
780 ("${Unicode:40}", Value::Text("@".to_string().into())),
781 (
782 "${Unicode:40 40 ",
783 Value::Text("${Unicode:40 40 ".to_string().into()),
784 ),
785 (
786 "${Unicode:Cool}",
787 Value::Text("${Unicode:Cool}".to_string().into()),
788 ),
789 ("", Value::Text("".to_string().into())),
790 (
791 "${global.full}",
792 Value::Variable(VariableType::Global("full".to_string())),
793 ),
794 (
795 "${BAD${global.Company}",
796 Value::List(vec![
797 Value::Text("${BAD".to_string().into()),
798 Value::Variable(VariableType::Global("company".to_string())),
799 ]),
800 ),
801 (
802 "${President, ${global.Company} Inc.}",
803 Value::List(vec![
804 Value::Text("${President, ".to_string().into()),
805 Value::Variable(VariableType::Global("company".to_string())),
806 Value::Text(" Inc.}".to_string().into()),
807 ]),
808 ),
809 (
810 "dear${hex:20 24 7b}global.Name}",
811 Value::List(vec![
812 Value::Text("dear ".to_string().into()),
813 Value::Variable(VariableType::Global("name".to_string())),
814 ]),
815 ),
816 (
817 "INBOX.lists.${2}",
818 Value::List(vec![
819 Value::Text("INBOX.lists.".to_string().into()),
820 Value::Variable(VariableType::Match(2)),
821 ]),
822 ),
823 (
824 "Ein unerh${unicode:00F6}rt gro${unicode:00DF}er Test",
825 Value::Text("Ein unerhört großer Test".to_string().into()),
826 ),
827 ("&%${}!", Value::Text("&%${}!".to_string().into())),
828 ("${doh!}", Value::Text("${doh!}".to_string().into())),
829 (
830 "${hex: 20 }${global.hi}${hex: 20 }",
831 Value::List(vec![
832 Value::Text(" ".to_string().into()),
833 Value::Variable(VariableType::Global("hi".to_string())),
834 Value::Text(" ".to_string().into()),
835 ]),
836 ),
837 (
838 "${hex:20 24 7b z}${global.hi}${unicode:}${unicode: }${hex:20}",
839 Value::List(vec![
840 Value::Text("${hex:20 24 7b z}".to_string().into()),
841 Value::Variable(VariableType::Global("hi".to_string())),
842 Value::Text("${unicode:}${unicode: } ".to_string().into()),
843 ]),
844 ),
845 (
846 "${header.from}",
847 Value::Variable(VariableType::Header(HeaderVariable {
848 name: vec![HeaderName::From],
849 part: HeaderPart::Text,
850 index_hdr: -1,
851 index_part: -1,
852 })),
853 ),
854 (
855 "${header.from.addr}",
856 Value::Variable(VariableType::Header(HeaderVariable {
857 name: vec![HeaderName::From],
858 part: HeaderPart::Address(AddressPart::All),
859 index_hdr: -1,
860 index_part: -1,
861 })),
862 ),
863 (
864 "${header.from[1]}",
865 Value::Variable(VariableType::Header(HeaderVariable {
866 name: vec![HeaderName::From],
867 part: HeaderPart::Text,
868 index_hdr: 1,
869 index_part: -1,
870 })),
871 ),
872 (
873 "${header.from[*]}",
874 Value::Variable(VariableType::Header(HeaderVariable {
875 name: vec![HeaderName::From],
876 part: HeaderPart::Text,
877 index_hdr: 0,
878 index_part: -1,
879 })),
880 ),
881 (
882 "${header.from[20].name}",
883 Value::Variable(VariableType::Header(HeaderVariable {
884 name: vec![HeaderName::From],
885 part: HeaderPart::Address(AddressPart::Name),
886 index_hdr: 20,
887 index_part: -1,
888 })),
889 ),
890 (
891 "${header.from[*].addr}",
892 Value::Variable(VariableType::Header(HeaderVariable {
893 name: vec![HeaderName::From],
894 part: HeaderPart::Address(AddressPart::All),
895 index_hdr: 0,
896 index_part: -1,
897 })),
898 ),
899 (
900 "${header.from[-5].name[2]}",
901 Value::Variable(VariableType::Header(HeaderVariable {
902 name: vec![HeaderName::From],
903 part: HeaderPart::Address(AddressPart::Name),
904 index_hdr: -5,
905 index_part: 2,
906 })),
907 ),
908 (
909 "${header.from[*].raw[*]}",
910 Value::Variable(VariableType::Header(HeaderVariable {
911 name: vec![HeaderName::From],
912 part: HeaderPart::Raw,
913 index_hdr: 0,
914 index_part: 0,
915 })),
916 ),
917 ] {
918 assert_eq!(
919 compiler.tokenize_string(input.as_bytes(), true).unwrap(),
920 expected_result,
921 "Failed for {input}"
922 );
923 }
924
925 for input in ["${unicode:200000}", "${Unicode:DF01}"] {
926 assert!(compiler.tokenize_string(input.as_bytes(), true).is_err());
927 }
928 }
929}