1#[derive(Debug, Clone, PartialEq)]
5pub enum SieveValue {
6 String(String),
7 StringList(Vec<String>),
8 Number(i64),
9 Tag(String),
10}
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum SieveTest {
15 True,
17 False,
19 Header {
21 comparator: Option<String>,
22 match_type: String,
23 headers: Vec<String>,
24 keys: Vec<String>,
25 },
26 Address {
28 comparator: Option<String>,
29 match_type: String,
30 headers: Vec<String>,
31 keys: Vec<String>,
32 },
33 Envelope {
35 comparator: Option<String>,
36 match_type: String,
37 parts: Vec<String>,
38 keys: Vec<String>,
39 },
40 Exists(Vec<String>),
42 Size { over: bool, limit: i64 },
44 AllOf(Vec<SieveTest>),
46 AnyOf(Vec<SieveTest>),
48 Not(Box<SieveTest>),
50}
51
52#[derive(Debug, Clone, PartialEq)]
54pub enum SieveCommand {
55 Keep,
57 Fileinto(String),
59 Redirect(String),
61 Discard,
63 Stop,
65 If {
67 test: SieveTest,
68 then_commands: Vec<SieveCommand>,
69 elsif_branches: Vec<(SieveTest, Vec<SieveCommand>)>,
70 else_commands: Option<Vec<SieveCommand>>,
71 },
72 Require(Vec<String>),
74 Set { name: String, value: String },
76 Vacation {
78 days: Option<i64>,
79 subject: Option<String>,
80 from: Option<String>,
81 addresses: Vec<String>,
82 message: String,
83 },
84}
85
86#[derive(Debug, Clone)]
88pub struct SieveScript {
89 pub commands: Vec<SieveCommand>,
91 pub requires: Vec<String>,
93}
94
95impl SieveScript {
96 pub fn new() -> Self {
98 Self {
99 commands: Vec::new(),
100 requires: Vec::new(),
101 }
102 }
103
104 pub fn parse(script: &str) -> Result<Self, String> {
106 let mut parser = Parser::new(script);
107 parser.parse()
108 }
109
110 pub fn add_command(&mut self, command: SieveCommand) {
112 if let SieveCommand::Require(exts) = &command {
113 self.requires.extend(exts.clone());
114 }
115 self.commands.push(command);
116 }
117
118 pub fn validate(&self) -> Result<(), String> {
120 let known_extensions = ["fileinto", "envelope", "variables", "vacation"];
122 for ext in &self.requires {
123 if !known_extensions.contains(&ext.as_str()) {
124 return Err(format!("Unknown extension: {}", ext));
125 }
126 }
127 Ok(())
128 }
129}
130
131impl Default for SieveScript {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137struct Parser {
139 input: String,
140 pos: usize,
141}
142
143impl Parser {
144 fn new(input: &str) -> Self {
145 Self {
146 input: input.to_string(),
147 pos: 0,
148 }
149 }
150
151 fn parse(&mut self) -> Result<SieveScript, String> {
152 let mut script = SieveScript::new();
153
154 self.skip_whitespace();
155 while self.pos < self.input.len() {
156 let cmd = self.parse_command()?;
157 script.add_command(cmd);
158 self.skip_whitespace();
159 }
160
161 Ok(script)
162 }
163
164 fn parse_command(&mut self) -> Result<SieveCommand, String> {
165 self.skip_whitespace();
166 let word = self.parse_word()?;
167
168 match word.as_str() {
169 "require" => self.parse_require(),
170 "if" => self.parse_if(),
171 "keep" => {
172 self.expect(";")?;
173 Ok(SieveCommand::Keep)
174 }
175 "discard" => {
176 self.expect(";")?;
177 Ok(SieveCommand::Discard)
178 }
179 "stop" => {
180 self.expect(";")?;
181 Ok(SieveCommand::Stop)
182 }
183 "fileinto" => self.parse_fileinto(),
184 "redirect" => self.parse_redirect(),
185 "set" => self.parse_set(),
186 "vacation" => self.parse_vacation(),
187 _ => Err(format!("Unknown command: {}", word)),
188 }
189 }
190
191 fn parse_require(&mut self) -> Result<SieveCommand, String> {
192 self.skip_whitespace();
193 let extensions = if self.peek_char() == Some('"') {
194 vec![self.parse_string()?]
195 } else if self.peek_char() == Some('[') {
196 self.parse_string_list()?
197 } else {
198 return Err("Expected string or string list after 'require'".to_string());
199 };
200 self.expect(";")?;
201 Ok(SieveCommand::Require(extensions))
202 }
203
204 fn parse_if(&mut self) -> Result<SieveCommand, String> {
205 self.skip_whitespace();
206 let test = self.parse_test()?;
207 self.skip_whitespace();
208 let then_commands = self.parse_block()?;
209
210 let mut elsif_branches = Vec::new();
211 let mut else_commands = None;
212
213 loop {
214 self.skip_whitespace();
215 if self.peek_word() == Some("elsif".to_string()) {
216 self.parse_word()?; self.skip_whitespace();
218 let elsif_test = self.parse_test()?;
219 self.skip_whitespace();
220 let elsif_commands = self.parse_block()?;
221 elsif_branches.push((elsif_test, elsif_commands));
222 } else if self.peek_word() == Some("else".to_string()) {
223 self.parse_word()?; self.skip_whitespace();
225 else_commands = Some(self.parse_block()?);
226 break;
227 } else {
228 break;
229 }
230 }
231
232 Ok(SieveCommand::If {
233 test,
234 then_commands,
235 elsif_branches,
236 else_commands,
237 })
238 }
239
240 fn parse_fileinto(&mut self) -> Result<SieveCommand, String> {
241 self.skip_whitespace();
242 let mailbox = self.parse_string()?;
243 self.expect(";")?;
244 Ok(SieveCommand::Fileinto(mailbox))
245 }
246
247 fn parse_redirect(&mut self) -> Result<SieveCommand, String> {
248 self.skip_whitespace();
249 let address = self.parse_string()?;
250 self.expect(";")?;
251 Ok(SieveCommand::Redirect(address))
252 }
253
254 fn parse_set(&mut self) -> Result<SieveCommand, String> {
255 self.skip_whitespace();
256 let name = self.parse_string()?;
257 self.skip_whitespace();
258 let value = self.parse_string()?;
259 self.expect(";")?;
260 Ok(SieveCommand::Set { name, value })
261 }
262
263 fn parse_vacation(&mut self) -> Result<SieveCommand, String> {
264 self.skip_whitespace();
265
266 let mut days = None;
267 let mut subject = None;
268 let mut from = None;
269 let mut addresses = Vec::new();
270
271 while self.peek_char() == Some(':') {
273 let tag = self.parse_tag()?;
274 self.skip_whitespace();
275
276 match tag.as_str() {
277 ":days" => {
278 days = Some(self.parse_number()?);
279 }
280 ":subject" => {
281 subject = Some(self.parse_string()?);
282 }
283 ":from" => {
284 from = Some(self.parse_string()?);
285 }
286 ":addresses" => {
287 addresses = self.parse_string_list()?;
288 }
289 _ => return Err(format!("Unknown vacation tag: {}", tag)),
290 }
291 self.skip_whitespace();
292 }
293
294 let message = self.parse_string()?;
295 self.expect(";")?;
296
297 Ok(SieveCommand::Vacation {
298 days,
299 subject,
300 from,
301 addresses,
302 message,
303 })
304 }
305
306 fn parse_test(&mut self) -> Result<SieveTest, String> {
307 self.skip_whitespace();
308 let word = self.parse_word()?;
309
310 match word.as_str() {
311 "true" => Ok(SieveTest::True),
312 "false" => Ok(SieveTest::False),
313 "header" => self.parse_header_test(),
314 "address" => self.parse_address_test(),
315 "envelope" => self.parse_envelope_test(),
316 "exists" => self.parse_exists_test(),
317 "size" => self.parse_size_test(),
318 "allof" => self.parse_allof_test(),
319 "anyof" => self.parse_anyof_test(),
320 "not" => self.parse_not_test(),
321 _ => Err(format!("Unknown test: {}", word)),
322 }
323 }
324
325 fn parse_header_test(&mut self) -> Result<SieveTest, String> {
326 self.skip_whitespace();
327
328 let mut comparator = None;
329 let mut match_type = "is".to_string();
330
331 while self.peek_char() == Some(':') {
333 let tag = self.parse_tag()?;
334 self.skip_whitespace();
335
336 match tag.as_str() {
337 ":comparator" => {
338 comparator = Some(self.parse_string()?);
339 self.skip_whitespace();
340 }
341 ":is" | ":contains" | ":matches" => {
342 match_type = tag[1..].to_string();
343 }
344 _ => return Err(format!("Unknown header test tag: {}", tag)),
345 }
346 }
347
348 let headers = self.parse_string_or_list()?;
349 self.skip_whitespace();
350 let keys = self.parse_string_or_list()?;
351
352 Ok(SieveTest::Header {
353 comparator,
354 match_type,
355 headers,
356 keys,
357 })
358 }
359
360 fn parse_address_test(&mut self) -> Result<SieveTest, String> {
361 self.skip_whitespace();
362
363 let mut comparator = None;
364 let mut match_type = "is".to_string();
365
366 while self.peek_char() == Some(':') {
367 let tag = self.parse_tag()?;
368 self.skip_whitespace();
369
370 match tag.as_str() {
371 ":comparator" => {
372 comparator = Some(self.parse_string()?);
373 self.skip_whitespace();
374 }
375 ":is" | ":contains" | ":matches" => {
376 match_type = tag[1..].to_string();
377 }
378 _ => {}
379 }
380 }
381
382 let headers = self.parse_string_or_list()?;
383 self.skip_whitespace();
384 let keys = self.parse_string_or_list()?;
385
386 Ok(SieveTest::Address {
387 comparator,
388 match_type,
389 headers,
390 keys,
391 })
392 }
393
394 fn parse_envelope_test(&mut self) -> Result<SieveTest, String> {
395 self.skip_whitespace();
396
397 let mut comparator = None;
398 let mut match_type = "is".to_string();
399
400 while self.peek_char() == Some(':') {
401 let tag = self.parse_tag()?;
402 self.skip_whitespace();
403
404 match tag.as_str() {
405 ":comparator" => {
406 comparator = Some(self.parse_string()?);
407 self.skip_whitespace();
408 }
409 ":is" | ":contains" | ":matches" => {
410 match_type = tag[1..].to_string();
411 }
412 _ => {}
413 }
414 }
415
416 let parts = self.parse_string_or_list()?;
417 self.skip_whitespace();
418 let keys = self.parse_string_or_list()?;
419
420 Ok(SieveTest::Envelope {
421 comparator,
422 match_type,
423 parts,
424 keys,
425 })
426 }
427
428 fn parse_exists_test(&mut self) -> Result<SieveTest, String> {
429 self.skip_whitespace();
430 let headers = self.parse_string_or_list()?;
431 Ok(SieveTest::Exists(headers))
432 }
433
434 fn parse_size_test(&mut self) -> Result<SieveTest, String> {
435 self.skip_whitespace();
436 let tag = self.parse_tag()?;
437 let over = match tag.as_str() {
438 ":over" => true,
439 ":under" => false,
440 _ => return Err(format!("Expected :over or :under, got {}", tag)),
441 };
442 self.skip_whitespace();
443 let limit = self.parse_number()?;
444 Ok(SieveTest::Size { over, limit })
445 }
446
447 fn parse_allof_test(&mut self) -> Result<SieveTest, String> {
448 self.skip_whitespace();
449 self.expect("(")?;
450 let mut tests = Vec::new();
451 loop {
452 self.skip_whitespace();
453 if self.peek_char() == Some(')') {
454 break;
455 }
456 tests.push(self.parse_test()?);
457 self.skip_whitespace();
458 if self.peek_char() == Some(',') {
459 self.advance();
460 }
461 }
462 self.expect(")")?;
463 Ok(SieveTest::AllOf(tests))
464 }
465
466 fn parse_anyof_test(&mut self) -> Result<SieveTest, String> {
467 self.skip_whitespace();
468 self.expect("(")?;
469 let mut tests = Vec::new();
470 loop {
471 self.skip_whitespace();
472 if self.peek_char() == Some(')') {
473 break;
474 }
475 tests.push(self.parse_test()?);
476 self.skip_whitespace();
477 if self.peek_char() == Some(',') {
478 self.advance();
479 }
480 }
481 self.expect(")")?;
482 Ok(SieveTest::AnyOf(tests))
483 }
484
485 fn parse_not_test(&mut self) -> Result<SieveTest, String> {
486 self.skip_whitespace();
487 let test = self.parse_test()?;
488 Ok(SieveTest::Not(Box::new(test)))
489 }
490
491 fn parse_block(&mut self) -> Result<Vec<SieveCommand>, String> {
492 self.expect("{")?;
493 let mut commands = Vec::new();
494 loop {
495 self.skip_whitespace();
496 if self.peek_char() == Some('}') {
497 break;
498 }
499 commands.push(self.parse_command()?);
500 }
501 self.expect("}")?;
502 Ok(commands)
503 }
504
505 fn parse_string_or_list(&mut self) -> Result<Vec<String>, String> {
506 self.skip_whitespace();
507 if self.peek_char() == Some('"') {
508 Ok(vec![self.parse_string()?])
509 } else if self.peek_char() == Some('[') {
510 self.parse_string_list()
511 } else {
512 Err("Expected string or string list".to_string())
513 }
514 }
515
516 fn parse_string_list(&mut self) -> Result<Vec<String>, String> {
517 self.expect("[")?;
518 let mut strings = Vec::new();
519 loop {
520 self.skip_whitespace();
521 if self.peek_char() == Some(']') {
522 break;
523 }
524 strings.push(self.parse_string()?);
525 self.skip_whitespace();
526 if self.peek_char() == Some(',') {
527 self.advance();
528 }
529 }
530 self.expect("]")?;
531 Ok(strings)
532 }
533
534 fn parse_string(&mut self) -> Result<String, String> {
535 self.skip_whitespace();
536 if self.peek_char() != Some('"') {
537 return Err("Expected string".to_string());
538 }
539 self.advance(); let mut result = String::new();
542 let mut escaped = false;
543
544 while self.pos < self.input.len() {
545 let ch = match self.current_char() {
546 Some(c) => c,
547 None => break,
548 };
549 self.advance();
550
551 if escaped {
552 result.push(ch);
553 escaped = false;
554 } else if ch == '\\' {
555 escaped = true;
556 } else if ch == '"' {
557 return Ok(result);
558 } else {
559 result.push(ch);
560 }
561 }
562
563 Err("Unterminated string".to_string())
564 }
565
566 fn parse_number(&mut self) -> Result<i64, String> {
567 self.skip_whitespace();
568 let mut num_str = String::new();
569
570 while self.pos < self.input.len() {
571 let ch = match self.current_char() {
572 Some(c) => c,
573 None => break,
574 };
575 if ch.is_ascii_digit() {
576 num_str.push(ch);
577 self.advance();
578 } else {
579 break;
580 }
581 }
582
583 if num_str.is_empty() {
584 return Err("Expected number".to_string());
585 }
586
587 if let Some(ch) = self.current_char() {
589 if ch == 'K' || ch == 'M' || ch == 'G' {
590 self.advance();
591 let multiplier = match ch {
592 'K' => 1024,
593 'M' => 1024 * 1024,
594 'G' => 1024 * 1024 * 1024,
595 _ => 1,
596 };
597 let base: i64 = num_str
598 .parse()
599 .map_err(|e| format!("Invalid number: {}", e))?;
600 return Ok(base * multiplier);
601 }
602 }
603
604 num_str
605 .parse()
606 .map_err(|e| format!("Invalid number: {}", e))
607 }
608
609 fn parse_tag(&mut self) -> Result<String, String> {
610 self.skip_whitespace();
611 if self.current_char() != Some(':') {
612 return Err("Expected tag starting with ':'".to_string());
613 }
614 self.advance(); let mut tag = String::from(":");
617 while self.pos < self.input.len() {
618 let ch = match self.current_char() {
619 Some(c) => c,
620 None => break,
621 };
622 if ch.is_alphanumeric() || ch == '_' || ch == '-' {
623 tag.push(ch);
624 self.advance();
625 } else {
626 break;
627 }
628 }
629
630 if tag.len() == 1 {
631 return Err("Empty tag".to_string());
632 }
633
634 Ok(tag)
635 }
636
637 fn parse_word(&mut self) -> Result<String, String> {
638 self.skip_whitespace();
639 let mut word = String::new();
640
641 while self.pos < self.input.len() {
642 let ch = match self.current_char() {
643 Some(c) => c,
644 None => break,
645 };
646 if ch.is_alphanumeric() || ch == '_' || ch == '-' {
647 word.push(ch);
648 self.advance();
649 } else {
650 break;
651 }
652 }
653
654 if word.is_empty() {
655 return Err("Expected word".to_string());
656 }
657
658 Ok(word)
659 }
660
661 fn peek_word(&mut self) -> Option<String> {
662 let saved_pos = self.pos;
663 let result = self.parse_word().ok();
664 self.pos = saved_pos;
665 result
666 }
667
668 fn expect(&mut self, s: &str) -> Result<(), String> {
669 self.skip_whitespace();
670 for expected_ch in s.chars() {
671 if self.current_char() != Some(expected_ch) {
672 return Err(format!(
673 "Expected '{}', got '{:?}'",
674 expected_ch,
675 self.current_char()
676 ));
677 }
678 self.advance();
679 }
680 Ok(())
681 }
682
683 fn skip_whitespace(&mut self) {
684 while self.pos < self.input.len() {
685 let ch = match self.input.chars().nth(self.pos) {
686 Some(c) => c,
687 None => break,
688 };
689 if ch.is_whitespace() {
690 self.pos += 1;
691 } else if ch == '#' {
692 while self.pos < self.input.len() {
694 let c = match self.input.chars().nth(self.pos) {
695 Some(c) => c,
696 None => break,
697 };
698 self.pos += 1;
699 if c == '\n' {
700 break;
701 }
702 }
703 } else {
704 break;
705 }
706 }
707 }
708
709 fn current_char(&self) -> Option<char> {
710 if self.pos < self.input.len() {
711 self.input.chars().nth(self.pos)
712 } else {
713 None
714 }
715 }
716
717 fn peek_char(&self) -> Option<char> {
718 self.current_char()
719 }
720
721 fn advance(&mut self) {
722 if self.pos < self.input.len() {
723 self.pos += 1;
724 }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn test_parse_simple_keep() {
734 let script = "keep;";
735 let parsed = SieveScript::parse(script).unwrap();
736 assert_eq!(parsed.commands.len(), 1);
737 assert_eq!(parsed.commands[0], SieveCommand::Keep);
738 }
739
740 #[test]
741 fn test_parse_fileinto() {
742 let script = r#"fileinto "INBOX.Spam";"#;
743 let parsed = SieveScript::parse(script).unwrap();
744 assert_eq!(parsed.commands.len(), 1);
745 assert_eq!(
746 parsed.commands[0],
747 SieveCommand::Fileinto("INBOX.Spam".to_string())
748 );
749 }
750
751 #[test]
752 fn test_parse_redirect() {
753 let script = r#"redirect "user@example.com";"#;
754 let parsed = SieveScript::parse(script).unwrap();
755 assert_eq!(parsed.commands.len(), 1);
756 assert_eq!(
757 parsed.commands[0],
758 SieveCommand::Redirect("user@example.com".to_string())
759 );
760 }
761
762 #[test]
763 fn test_parse_discard() {
764 let script = "discard;";
765 let parsed = SieveScript::parse(script).unwrap();
766 assert_eq!(parsed.commands.len(), 1);
767 assert_eq!(parsed.commands[0], SieveCommand::Discard);
768 }
769
770 #[test]
771 fn test_parse_require() {
772 let script = r#"require "fileinto";"#;
773 let parsed = SieveScript::parse(script).unwrap();
774 assert_eq!(parsed.requires, vec!["fileinto"]);
775 }
776
777 #[test]
778 fn test_parse_if_header() {
779 let script = r#"
780 if header :contains "Subject" "spam" {
781 discard;
782 }
783 "#;
784 let parsed = SieveScript::parse(script).unwrap();
785 assert_eq!(parsed.commands.len(), 1);
786 }
787
788 #[test]
789 fn test_parse_if_else() {
790 let script = r#"
791 if false {
792 discard;
793 } else {
794 keep;
795 }
796 "#;
797 let parsed = SieveScript::parse(script).unwrap();
798 assert_eq!(parsed.commands.len(), 1);
799 }
800
801 #[test]
802 fn test_parse_size_test() {
803 let script = r#"
804 if size :over 100K {
805 discard;
806 }
807 "#;
808 let parsed = SieveScript::parse(script).unwrap();
809 assert_eq!(parsed.commands.len(), 1);
810 }
811
812 #[test]
813 fn test_parse_exists_test() {
814 let script = r#"
815 if exists "X-Spam-Flag" {
816 fileinto "Spam";
817 }
818 "#;
819 let parsed = SieveScript::parse(script).unwrap();
820 assert_eq!(parsed.commands.len(), 1);
821 }
822
823 #[test]
824 fn test_parse_allof() {
825 let script = r#"
826 if allof(true, true) {
827 keep;
828 }
829 "#;
830 let parsed = SieveScript::parse(script).unwrap();
831 assert_eq!(parsed.commands.len(), 1);
832 }
833
834 #[test]
835 fn test_parse_comment() {
836 let script = r#"
837 # This is a comment
838 keep; # Another comment
839 "#;
840 let parsed = SieveScript::parse(script).unwrap();
841 assert_eq!(parsed.commands.len(), 1);
842 }
843}