Skip to content

Commit e5f7dd6

Browse files
Merge branch 'develop'
2 parents 53dc873 + 0bc5373 commit e5f7dd6

File tree

13 files changed

+114
-35
lines changed

13 files changed

+114
-35
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Sifter3 - Sieve email filter (RFC 5228)
22

3-
Sifter3 is a Python 3 implementation of the Sieve email filter language (RFC 5228) and is based on the Python 2 version from <https://github.com/garyp/sifter>
3+
Sifter3 is a Python 3 implementation of the Sieve email filter language (RFC 5228)
44

55
![Python package](https://github.com/manfred-kaiser/sifter3/workflows/Python%20package/badge.svg)
6-
[![Documentation Status](https://readthedocs.org/projects/sifter3/badge/?version=master)](https://sifter3.readthedocs.io/de/master/?badge=master)
6+
[![Documentation Status](https://readthedocs.org/projects/sifter3/badge/?version=latest)](https://sifter3.readthedocs.io/en/latest/?badge=latest)
77
[![CodeFactor](https://www.codefactor.io/repository/github/manfred-kaiser/sifter3/badge)](https://www.codefactor.io/repository/github/manfred-kaiser/sifter3)
88
[![Github version](https://img.shields.io/github/v/release/manfred-kaiser/sifter3?label=github&logo=github)](https://github.com/manfred-kaiser/sifter3/releases)
99
[![PyPI version](https://img.shields.io/pypi/v/sifter3.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/sifter3/)
@@ -18,6 +18,7 @@ FEATURES
1818

1919
- Supports all of the base Sieve spec from RFC 5228, except for
2020
features still listed under TODO below
21+
- multiline strings (since version 0.2.2)
2122
- Extensions supported:
2223
- regex (draft-ietf-sieve-regex-01)
2324
- body (RFC 5173)
@@ -45,6 +46,15 @@ email message. Each action is a tuple consisting of the action name and
4546
action-specific arguments. It is up to the caller to manipulate the
4647
message and message store based on the actions returned.
4748

49+
COMMAND LINE
50+
============
51+
52+
The output of the command line tool can be parsed as json.
53+
54+
$ sifter tests/evaluation_1.rules tests/evaluation_1.msg
55+
[['redirect', '[email protected]']]
56+
57+
4858
WARNINGS
4959
========
5060

docs/index.rst

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ FEATURES
99

1010
- Supports all of the base Sieve spec from RFC 5228, except for
1111
features still listed under TODO below
12+
13+
- multiline strings (since version 0.2.2)
14+
1215
- Extensions supported:
1316

1417
- regex (draft-ietf-sieve-regex-01)
@@ -45,6 +48,17 @@ consisting of the action name and action-specific arguments. It is up to
4548
the caller to manipulate the message and message store based on the
4649
actions returned.
4750

51+
COMMAND LINE
52+
------------
53+
54+
The output of the command line tool can be parsed as json.
55+
56+
.. code-block:: bash
57+
58+
$ sifter tests/evaluation_1.rules tests/evaluation_1.msg
59+
[['redirect', '[email protected]']]
60+
61+
4862
WARNINGS
4963
--------
5064

@@ -60,8 +74,8 @@ TODO
6074
- An example adaptor that provides Unix LDA behavior using sieve for
6175
filtering
6276
- Base spec features not yet implemented:
77+
6378
- encoded characters (section 2.4.2.4)
64-
- multi-line strings (section 2.4.2)
6579
- bracketed comments (section 2.3)
6680
- message uniqueness (section 2.10.3)
6781
- envelope test (section 5.4)

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name="sifter3",
13-
version="0.2.1",
13+
version="0.2.2",
1414
author="Manfred Kaiser, Gary Peck",
1515
1616
url="https://sifter3.readthedocs.io/en/latest/",
@@ -60,6 +60,7 @@
6060
'keep = sifter.commands.keep:CommandKeep',
6161
'notify = sifter.commands.notify:CommandNotify',
6262
'redirect = sifter.commands.redirect:CommandRedirect',
63+
'reject = sifter.commands.reject:CommandReject',
6364
'require = sifter.commands.require:CommandRequire',
6465
'set = sifter.commands.variables:CommandSet',
6566
'stop = sifter.commands.stop:CommandStop',

sifter/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import logging
55
import sys
6+
import json
67

78
import sifter.parser
89

@@ -24,4 +25,4 @@ def main() -> None:
2425
rules = sifter.parser.parse_file(open(args.rulefile))
2526
msg = email.message_from_file(open(args.messagefile))
2627
msg_actions = rules.evaluate(msg)
27-
print(msg_actions)
28+
print(json.dumps(msg_actions, indent=4))

sifter/commands/reject.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from email.message import Message
2+
from typing import (
3+
Text,
4+
Optional
5+
)
6+
7+
from sifter.grammar.command import Command
8+
from sifter.validators.stringlist import StringList
9+
from sifter.grammar.state import EvaluationState
10+
from sifter.grammar.actions import Actions
11+
12+
13+
# section 3.2
14+
class CommandReject(Command):
15+
16+
RULE_IDENTIFIER: Text = 'REJECT'
17+
POSITIONAL_ARGS = [StringList()]
18+
19+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
20+
print(self.positional_args[0][0])
21+
state.actions.append('reject', self.positional_args[0][0])
22+
state.actions.cancel_implicit_keep()
23+
return None

sifter/extensions/__init__.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@ class ExtensionRegistry():
3434
]
3535
] = {}
3636
DEFAULT_EXTENSION: List[Text] = [
37-
'regex',
37+
'body',
3838
'comparator-i;ascii-casemap',
3939
'comparator-i;octet',
40+
'enotify',
4041
'fileinto',
41-
'body'
42+
'imap4flags',
43+
'regex',
44+
'reject',
4245
'variables',
43-
'enotify',
44-
'imap4flags'
4546
]
4647

4748
def __init__(self) -> None:

sifter/grammar/grammar.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, debug: bool = False) -> None:
3232
def make_parser(mod: Any, debug: bool = False) -> 'LRParser':
3333
return yacc(
3434
module=mod,
35-
debug=debug,
35+
debug=True,
3636
write_tables=False,
3737
errorlog=NullLogger() if not debug else None
3838
)
@@ -167,6 +167,10 @@ def p_argument_tag(self, p: 'YaccProduction') -> None:
167167
"""argument : TAG"""
168168
p[0] = Tag(p[1])
169169

170+
def p_argument_multiline_string(self, p: 'YaccProduction') -> None:
171+
"""argument : MULTILINE_STRING"""
172+
p[0] = [p[1]]
173+
170174
def p_stringlist_error(self, p: 'YaccProduction') -> None:
171175
"""argument : '[' error ']'"""
172176
print("Syntax error in string list that starts on line %d" % p.lineno(1))
@@ -186,4 +190,9 @@ def p_string(self, p: 'YaccProduction') -> None:
186190
p[0] = String(p[1])
187191

188192
def p_error(self, p: 'YaccProduction') -> None:
189-
pass
193+
if p is None:
194+
token = "end of file"
195+
else:
196+
token = f"{p.type}({p.value}) on line {p.lineno}"
197+
198+
print(f"Syntax error: Unexpected {token}")

sifter/grammar/lexer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def t_BRACKET_COMMENT(self, t: 'LexToken') -> Optional['LexToken']:
5454

5555
# section 2.4.2
5656
def t_MULTILINE_STRING(self, t: 'LexToken') -> Optional['LexToken']:
57-
r'"@@@@@@@@@@@@@@@"'
57+
r'text:\s?(?:\#.*)\r?\n(?P<multilinetext>[\r\n\S\s.]*?\r?\n)\.\r?\n'
5858
# TODO: For entering larger amounts of text, such as an email message,
5959
# a multi-line form is allowed. It starts with the keyword "text:",
6060
# followed by a CRLF, and ends with the sequence of a CRLF, a single
@@ -68,7 +68,8 @@ def t_MULTILINE_STRING(self, t: 'LexToken') -> Optional['LexToken']:
6868
# that is, ".foo" is interpreted as ".foo". However, because this is
6969
# potentially ambiguous, scripts SHOULD be properly dot-stuffed so such
7070
# lines do not appear.
71-
return None
71+
t.value = t.lexer.lexmatch.group('multilinetext')
72+
return t
7273

7374
# section 2.4.2
7475
def t_QUOTED_STRING(self, t: 'LexToken') -> Optional['LexToken']:

sifter/tests/body.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,26 +65,26 @@ def evaluate(self, message: Message, state: EvaluationState) -> Optional[bool]:
6565
# if there are no headers, we match ^\n, which is guaranteed to be there
6666
(_, bodystr) = re.split(r'^\r?\n|\r?\n\r?\n', message.as_string(False), 1)
6767
return self.evaluate_part(bodystr, state)
68-
else:
69-
for msgpart in message.walk():
70-
if msgpart.is_multipart():
71-
# TODO: If "multipart/*" extract prologue and epilogue and make that searcheable
72-
# TODO: If "message/rfc822" extract headers and make that searchable
73-
# Insetad we skip multipart objects and descend into its children
74-
continue
75-
msgtxt = msgpart.get_payload()
76-
for mimetype in self.body_transform:
77-
if not mimetype: # empty body_transform matches all
78-
if self.evaluate_part(msgtxt, state):
79-
return True
80-
match = re.match(r'^([^/]+)(?:/([^/]+))?$', mimetype)
81-
if not match:
82-
continue # malformed body_transform is skipped
83-
(maintype, subtype) = match.groups()
84-
if maintype == msgpart.get_content_maintype() and (
85-
not subtype or subtype == msgpart.get_content_subtype()):
86-
if self.evaluate_part(msgtxt, state):
87-
return True
68+
69+
for msgpart in message.walk():
70+
if msgpart.is_multipart():
71+
# TODO: If "multipart/*" extract prologue and epilogue and make that searcheable
72+
# TODO: If "message/rfc822" extract headers and make that searchable
73+
# Insetad we skip multipart objects and descend into its children
74+
continue
75+
msgtxt = msgpart.get_payload()
76+
for mimetype in self.body_transform:
77+
if not mimetype: # empty body_transform matches all
78+
if self.evaluate_part(msgtxt, state):
79+
return True
80+
match = re.match(r'^([^/]+)(?:/([^/]+))?$', mimetype)
81+
if not match:
82+
continue # malformed body_transform is skipped
83+
(maintype, subtype) = match.groups()
84+
if maintype == msgpart.get_content_maintype() and (
85+
not subtype or subtype == msgpart.get_content_subtype()):
86+
if self.evaluate_part(msgtxt, state):
87+
return True
8888
return False
8989

9090
def evaluate_part(self, part_str: Text, state: EvaluationState) -> bool:

sifter/validators/tag.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,5 @@ def validate(
125125
raise ValueError('unexpected return value from StringList.validate')
126126
if content_args > 0:
127127
return validated_args + content_args
128-
else:
129-
raise RuleSyntaxError("body :content requires argument")
128+
raise RuleSyntaxError("body :content requires argument")
130129
return validated_args

0 commit comments

Comments
 (0)