Skip to content

Commit 8b75a37

Browse files
added extensions from russell's fork https://github.com/russell/sifter
1 parent 8f0aaa8 commit 8b75a37

21 files changed

Lines changed: 454 additions & 18 deletions

File tree

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@
5454
'elseif = sifter.commands.if_cmd:CommandElsIf',
5555
'else = sifter.commands.if_cmd:CommandElse',
5656
'keep = sifter.commands.keep:CommandKeep',
57+
'notify = sifter.commands.notify:CommandNotify',
5758
'redirect = sifter.commands.redirect:CommandRedirect',
5859
'require = sifter.commands.require:CommandRequire',
60+
'set = sifter.commands.variables:CommandSet',
5961
'stop = sifter.commands.stop:CommandStop',
6062
# sifter tests
6163
'address = sifter.tests.address:TestAddress',
64+
'body = sifter.tests.body:TestBody',
6265
'allof = sifter.tests.allof:TestAllOf',
6366
'anyof = sifter.tests.anyof:TestAnyOf',
6467
'exists = sifter.tests.exists:TestExists',
@@ -67,6 +70,8 @@
6770
'not_test = sifter.tests.not_test:TestNot',
6871
'size = sifter.tests.size:TestSize',
6972
'true = sifter.tests.true:TestTrue',
73+
'valid_notify_method = sifter.tests.notify:TestValidNotifyMethod',
74+
'notify_method_capability = sifter.tests.notify:TestValidNotifyMethod',
7075
# sifter comparators
7176
'ascii_casemap = sifter.comparators.ascii_casemap:ComparatorASCIICasemap',
7277
'octed = sifter.comparators.octet:ComparatorOctet'

sifter/commands/fileinto.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
)
55

66
from sifter.grammar.command import Command
7+
from sifter.grammar.string import expand_variables
78
from sifter.validators.stringlist import StringList
89
from sifter.grammar.state import EvaluationState
910
from sifter.grammar.actions import Actions
@@ -16,9 +17,11 @@ class CommandFileInto(Command):
1617
POSITIONAL_ARGS = [StringList(length=1)]
1718

1819
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
20+
state.check_required_extension('fileinto', 'FILEINTO')
21+
1922
file_dest = self.positional_args[0]
23+
file_dest = list(map(lambda s: expand_variables(s, state), file_dest))
2024

21-
state.check_required_extension('fileinto', 'FILEINTO')
2225
state.actions.append('fileinto', file_dest) # type: ignore
2326
state.actions.cancel_implicit_keep()
2427
return None

sifter/commands/notify.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import re
2+
import sifter.grammar
3+
import sifter.validators
4+
import sifter.grammar.notificationmethod
5+
from sifter.grammar.command import Command
6+
from sifter.validators.stringlist import StringList
7+
from sifter.validators.tag import Tag
8+
9+
10+
# RFC 5435
11+
class CommandNotify(Command):
12+
13+
RULE_IDENTIFIER = 'NOTIFY'
14+
TAGGED_ARGS = {
15+
'from': Tag('FROM', (StringList(1),)),
16+
'importance': Tag('IMPORTANCE', (StringList(1),)),
17+
'options': Tag('OPTIONS', (StringList(),)),
18+
'message': Tag('MESSAGE', (StringList(1),)),
19+
}
20+
POSITIONAL_ARGS = [
21+
StringList(length=1)
22+
]
23+
24+
def __init__(self, arguments=None, tests=None, block=None):
25+
super(CommandNotify, self).__init__(arguments, tests, block)
26+
27+
self.notify_from = self.notify_importance = self.notify_message = None
28+
self.notify_options = []
29+
if 'from' in self.tagged_args:
30+
self.notify_from = self.tagged_args['from'][1][0]
31+
if 'importance' in self.tagged_args:
32+
self.notify_importance = self.tagged_args['importance'][1][0]
33+
if 'options' in self.tagged_args:
34+
self.notify_options = self.tagged_args['options'][1]
35+
if 'message' in self.tagged_args:
36+
self.notify_message = self.tagged_args['message'][1][0]
37+
self.notify_method = self.positional_args[0][0]
38+
39+
def evaluate(self, message, state):
40+
state.check_required_extension('enotify', 'NOTIFY')
41+
notify_from = sifter.grammar.string.expand_variables(self.notify_from, state)
42+
notify_importance = sifter.grammar.string.expand_variables(self.notify_importance, state)
43+
notify_options = map(lambda s: sifter.grammar.string.expand_variables(s, state), self.notify_options)
44+
notify_message = sifter.grammar.string.expand_variables(self.notify_message, state)
45+
notify_method = sifter.grammar.string.expand_variables(self.notify_method, state)
46+
47+
m = re.match('^([A-Za-z][A-Za-z0-9.+-]*):', notify_method)
48+
if not m:
49+
raise sifter.grammar.RuleSyntaxError("Notification method must be an URI, e.g. 'mailto:email@example.com'")
50+
if notify_importance and notify_importance not in ["1", "2", "3"]:
51+
raise sifter.grammar.RuleSyntaxError("Illegal notify importance '%s' encountered" % self.notify_importance)
52+
notify_method_cls = sifter.grammar.notificationmethod.get_cls(m.group(1).lower())
53+
if not notify_method_cls:
54+
raise sifter.grammar.RuleSyntaxError("Unsupported notification method '%s'" % m.group(1))
55+
(res, msg) = notify_method_cls.test_valid(notify_method)
56+
if not res:
57+
raise sifter.grammar.RuleSyntaxError(msg)
58+
59+
state.actions.append('notify', (notify_method, notify_from, notify_importance, notify_options, notify_message))

sifter/commands/redirect.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sifter.validators.stringlist import StringList
1616
from sifter.grammar.state import EvaluationState
1717
from sifter.grammar.actions import Actions
18+
from sifter.grammar.string import expand_variables
1819

1920
if TYPE_CHECKING:
2021
from sifter.grammar.tag import Tag as TagGrammar
@@ -49,6 +50,7 @@ def __init__(
4950
)
5051

5152
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
52-
state.actions.append('redirect', self.email_address)
53+
email_address = expand_variables(self.email_address, state)
54+
state.actions.append('redirect', email_address)
5355
state.actions.cancel_implicit_keep()
5456
return None

sifter/commands/variables.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import re
2+
import urllib
3+
from sifter.grammar.command import Command
4+
from sifter.grammar.rule import RuleSyntaxError
5+
from sifter.validators.stringlist import StringList
6+
from sifter.validators.tag import Tag
7+
from sifter.grammar.string import expand_variables
8+
9+
__all__ = ('CommandSet',)
10+
11+
12+
# RFC 5229
13+
class CommandSet(Command):
14+
15+
RULE_IDENTIFIER = 'SET'
16+
TAGGED_ARGS = {
17+
'lower': Tag('LOWER'),
18+
'upper': Tag('UPPER'),
19+
'lowerfirst': Tag('LOWERFIRST'),
20+
'upperfirst': Tag('UPPERFIRST'),
21+
'quotewildcard': Tag('QUOTEWILDCARD'),
22+
'quoteregex': Tag('QUOTEREGEX'),
23+
'encodeurl': Tag('ENCODEURL'),
24+
'length': Tag('LENGTH'),
25+
}
26+
POSITIONAL_ARGS = [
27+
StringList(length=1),
28+
StringList(length=1),
29+
]
30+
31+
def __init__(self, arguments=None, tests=None, block=None):
32+
super(CommandSet, self).__init__(arguments, tests, block)
33+
34+
self.variable_modifier = self.tagged_args
35+
self.variable_name = self.positional_args[0][0]
36+
if (not re.match(r'^[A-Za-z_][A-Za-z0-9_]*$', self.variable_name)):
37+
raise RuleSyntaxError("Illegal variable name '%s' encountered" % self.variable_name)
38+
self.variable_value = self.positional_args[1][0]
39+
40+
def evaluate(self, message, state):
41+
state.check_required_extension('variables', 'VARIABLES')
42+
variable_value = expand_variables(self.variable_value, state)
43+
if 'lower' in self.variable_modifier:
44+
variable_value = variable_value.lower()
45+
if 'upper' in self.variable_modifier:
46+
variable_value = variable_value.upper()
47+
if 'lowerfirst' in self.variable_modifier:
48+
variable_value = variable_value[:1].lower() + variable_value[1:]
49+
if 'upperfirst' in self.variable_modifier:
50+
variable_value = variable_value[:1].upper() + variable_value[1:]
51+
if 'quotewildcard' in self.variable_modifier:
52+
variable_value = variable_value.replace('*', '\\*')
53+
variable_value = variable_value.replace('?', '\\?')
54+
variable_value = variable_value.replace('\\', '\\\\')
55+
if 'quoteregex' in self.variable_modifier:
56+
variable_value = re.escape(variable_value)
57+
if 'encodeurl' in self.variable_modifier:
58+
try:
59+
variable_value = urllib.quote(variable_value, safe='-._~')
60+
except AttributeError:
61+
variable_value = urllib.parse.quote(variable_value, safe='-._~')
62+
if 'length' in self.variable_modifier:
63+
variable_value = "" + len(variable_value)
64+
state.named_variables[self.variable_name] = variable_value

sifter/comparator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ def get_match_fn(
1919
match_type: Optional[Union[Text, 'Tag']]
2020
) -> Tuple[Callable[[Text, Text, 'EvaluationState'], bool], Union[Text, 'Tag'], Union[Text, 'Tag']]:
2121
# section 2.7.3: default comparator is 'i;ascii-casemap'
22-
if comparator is None:
23-
comparator = 'i;ascii-casemap'
2422
# RFC 4790, section 3.1: the special identifier 'default' refers to the
2523
# implementation-defined default comparator
26-
elif comparator == 'default':
27-
comparator = 'i;ascii-casemap'
24+
if comparator is None or comparator == 'default':
25+
if match_type != 'REGEX':
26+
comparator = 'i;ascii-casemap'
27+
else:
28+
# 'i;ascii-casemap' uppercases test string but not pattern, which
29+
# is is very counter intuitive -> change default for regex
30+
comparator = 'i;octet'
2831

2932
# section 2.7.1: default match type is ":is"
3033
if match_type is None:

sifter/comparators/octet.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ def cmp_contains(cls, s: Text, substring: Text, state: EvaluationState) -> bool:
2424
@classmethod
2525
def cmp_matches(cls, s: Text, pattern: Text, state: EvaluationState) -> Optional[Match[Text]]:
2626
pattern = cls.sort_key(pattern)
27-
i, n = 0, len(pattern)
27+
i, g, n = 0, 0, len(pattern)
2828
re_pattern = []
2929
while i < n:
3030
c = pattern[i]
3131
i += 1
3232
if c == "*":
33-
re_pattern.append(".*")
33+
re_pattern.append("(.*?)")
34+
g += 1
3435
elif c == "?":
35-
re_pattern.append(".")
36+
re_pattern.append("(.)")
37+
g += 1
3638
elif c == "\\":
3739
if pattern[i:i + 1] in ("\\*", "\\?"):
3840
re_pattern.append(re.escape(pattern[i + 1]))
@@ -44,8 +46,14 @@ def cmp_matches(cls, s: Text, pattern: Text, state: EvaluationState) -> Optional
4446
re_pattern.append(r"\Z")
4547
# TODO: compile and cache pattern for more efficient execution across
4648
# multiple strings and messages
47-
return re.match(
49+
m = re.match(
4850
''.join(re_pattern),
4951
cls.sort_key(s),
5052
re.MULTILINE | re.DOTALL
5153
)
54+
state.match_variables = []
55+
if m and state.have_extension('variables'):
56+
# Get the matched ranges from the original string, not the case-corrected one
57+
for i in range(0, g + 1):
58+
state.match_variables.append(s[m.start(i):m.end(i)])
59+
return m

sifter/extensions/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ class ExtensionRegistry():
2626
'comparator-i;ascii-casemap',
2727
'comparator-i;octet',
2828
'fileinto',
29+
'body'
30+
'variables',
31+
'enotify'
2932
]
3033

3134
def __init__(self) -> None:

sifter/grammar/comparator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,10 @@ def cmp_regex(cls, s: Text, pattern: Pattern[Text], state: EvaluationState) -> O
4444
# boundaries and backreferences, 2) double-check that python supports
4545
# all ERE features.
4646
compiled_re = re.compile(pattern)
47-
return compiled_re.search(cls.sort_key(s))
47+
m = compiled_re.search(cls.sort_key(s))
48+
state.match_variables = []
49+
if m and state.have_extension('variables'):
50+
# Get the matched ranges from the original string, not the case-corrected one
51+
for i in range(0, len(m.groups()) + 1):
52+
state.match_variables.append(s[m.start(i):m.end(i)])
53+
return m

sifter/grammar/lexer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def input(self, data: Text) -> None:
4444

4545
# section 2.3
4646
def t_HASH_COMMENT(self, t: 'LexToken') -> Optional['LexToken']:
47-
r'\#.*\r\n'
47+
r'\#.*\r?\n'
4848
t.lexer.lineno += 1
4949
return None
5050

@@ -113,7 +113,7 @@ def t_NUMBER(self, t: 'LexToken') -> Optional['LexToken']:
113113
return t
114114

115115
def t_newline(self, t: 'LexToken') -> Optional['LexToken']:
116-
r'(\r\n)+'
116+
r'(\r?\n)+'
117117
t.lexer.lineno += t.value.count('\n')
118118
return None
119119

0 commit comments

Comments
 (0)