Skip to content

Commit 6d95ba9

Browse files
Merge branch 'develop'
2 parents 0d91968 + fa73e07 commit 6d95ba9

31 files changed

+788
-142
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This workflows will upload a Python Package using Twine when a release is created
2+
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3+
4+
name: Upload Python Package
5+
6+
on:
7+
release:
8+
types: [created]
9+
10+
jobs:
11+
deploy:
12+
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v2
17+
- name: Set up Python
18+
uses: actions/setup-python@v2
19+
with:
20+
python-version: '3.x'
21+
- name: Install dependencies
22+
run: |
23+
python -m pip install --upgrade pip
24+
pip install setuptools wheel twine
25+
- name: Build and publish
26+
env:
27+
TWINE_USERNAME: __token__
28+
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
29+
run: |
30+
python setup.py sdist bdist_wheel
31+
twine upload dist/*

MANIFEST.in

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
include LICENSE.txt
22
include MANIFEST.in
33
include README.rst
4-
include requirements.txt

README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Sifter3 - Sieve email filter (RFC 5228)
2+
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>
4+
5+
![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)
7+
[![CodeFactor](https://www.codefactor.io/repository/github/manfred-kaiser/sifter3/badge)](https://www.codefactor.io/repository/github/manfred-kaiser/sifter3)
8+
[![Github version](https://img.shields.io/github/v/release/manfred-kaiser/sifter3?label=github&logo=github)](https://github.com/manfred-kaiser/sifter3/releases)
9+
[![PyPI version](https://img.shields.io/pypi/v/sifter3.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/sifter3/)
10+
[![Supported Python versions](https://img.shields.io/pypi/pyversions/sifter3.svg?logo=python&logoColor=FFE873)](https://pypi.org/project/sifter3/)
11+
[![PyPI downloads](https://pepy.tech/badge/sifter3/month)](https://pepy.tech/project/sifter3/month)
12+
[![GitHub](https://img.shields.io/github/license/manfred-kaiser/sifter3.svg)](LICENSE)
13+
14+
15+
16+
FEATURES
17+
========
18+
19+
- Supports all of the base Sieve spec from RFC 5228, except for
20+
features still listed under TODO below
21+
- Extensions supported:
22+
- regex (draft-ietf-sieve-regex-01)
23+
24+
EXAMPLE
25+
=======
26+
27+
import email
28+
import sifter.parser
29+
rules = sifter.parser.parse_file(open('my_rules.sieve'))
30+
msg = email.message_from_file(open('an_email_to_me.eml'))
31+
msg_actions = rules.evaluate(msg)
32+
33+
In the above example, `msg_actions` is a list of actions to apply to the
34+
email message. Each action is a tuple consisting of the action name and
35+
action-specific arguments. It is up to the caller to manipulate the
36+
message and message store based on the actions returned.
37+
38+
WARNINGS
39+
========
40+
41+
- No thought has been given yet to hardening against malicious user
42+
input. The current implementation is aimed at users that are running
43+
their own sieve scripts.
44+
- The current implementation is not optimized for performance, though
45+
hopefully it's not too slow for normal inputs.
46+
47+
TODO
48+
====
49+
50+
In rough order of importance:
51+
52+
- An example adaptor that provides Unix LDA behavior using sieve for
53+
filtering
54+
- Base spec features not yet implemented:
55+
- encoded characters (section 2.4.2.4)
56+
- multi-line strings (section 2.4.2)
57+
- bracketed comments (section 2.3)
58+
- message uniqueness (section 2.10.3)
59+
- envelope test (section 5.4)
60+
- handle message loops (section 10)
61+
- limit abuse of redirect action (section 10)
62+
- address test should limit allowed headers to those that contain
63+
addresses (section 5.1)
64+
- Make sure character sets are actually handled according to the spec
65+
- Make string parsing comply with the grammar in section 8.1 and the
66+
features described in section 2.4.2
67+
- Check that python's `email.message` implements header comparisons
68+
the same way as the sieve spec
69+
- Make sure regular expressions are actually handled according to the
70+
extension spec
71+
- Add support for various extensions:
72+
- variables (RFC 5229)
73+
- externally stored lists (draft-melnikov-sieve-external-lists)
74+
- body (RFC 5173)
75+
- relational (RFC 5231)
76+
- subaddress (RFC 5233)
77+
- copy (RFC 3894)
78+
- environment (RFC 5183)
79+
- date and index (RFC 5260)
80+
- editheader (RFC 5293)
81+
- ihave (RFC 5463)
82+
- mailbox metadata (RFC 5490)
83+
- notifications (RFC 5435), mailto notifications (RFC 5436), xmpp
84+
notifications (RFC 5437)

README.rst

Lines changed: 0 additions & 83 deletions
This file was deleted.

requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

setup.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,28 @@
55
# read the contents of your README file
66
from os import path
77
this_directory = path.abspath(path.dirname(__file__))
8-
with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f:
8+
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
99
long_description = f.read()
1010

11-
with open('requirements.txt') as f:
12-
requirements = f.read().splitlines()
13-
1411
setup(
15-
name="sifter",
12+
name="sifter3",
1613
version="0.2.0",
17-
author="Gary Peck, Manfred Kaiser",
18-
author_email="gary@realify.com, manfred.kaiser@logfile.at",
14+
author="Manfred Kaiser, Gary Peck",
15+
author_email="manfred.kaiser@logfile.at, gary@realify.com",
1916
url="https://github.com/garyp/sifter",
2017
license="BSD",
18+
description='Parser/evaluator for the Sieve filtering language (RFC 5228) - Python3 version',
2119
long_description=long_description,
22-
long_description_content_type='text/x-rst',
20+
long_description_content_type='text/markdown',
21+
keywords="sieve email filter parser",
2322
project_urls={
24-
'Source': 'https://github.com/garyp/sifter',
25-
'Tracker': 'https://github.com/garyp/sifter/issues',
23+
'Source': 'https://github.com/manfred-kaiser/sifter3',
24+
'Tracker': 'https://github.com/manfred-kaiser/sifter3/issues',
2625
},
2726
python_requires='>= 3.6',
28-
install_requires=requirements,
27+
install_requires=[
28+
'ply'
29+
],
2930
classifiers=[
3031
"Programming Language :: Python",
3132
"Programming Language :: Python :: 3",
@@ -46,6 +47,9 @@
4647
"sifter": ['py.typed']
4748
},
4849
entry_points={
50+
'console_scripts': [
51+
'sifter = sifter.cli:main'
52+
],
4953
'sifter_extensions': [
5054
# sifter commands
5155
'discard = sifter.commands.discard:CommandDiscard',
@@ -54,11 +58,14 @@
5458
'elseif = sifter.commands.if_cmd:CommandElsIf',
5559
'else = sifter.commands.if_cmd:CommandElse',
5660
'keep = sifter.commands.keep:CommandKeep',
61+
'notify = sifter.commands.notify:CommandNotify',
5762
'redirect = sifter.commands.redirect:CommandRedirect',
5863
'require = sifter.commands.require:CommandRequire',
64+
'set = sifter.commands.variables:CommandSet',
5965
'stop = sifter.commands.stop:CommandStop',
6066
# sifter tests
6167
'address = sifter.tests.address:TestAddress',
68+
'body = sifter.tests.body:TestBody',
6269
'allof = sifter.tests.allof:TestAllOf',
6370
'anyof = sifter.tests.anyof:TestAnyOf',
6471
'exists = sifter.tests.exists:TestExists',
@@ -67,9 +74,14 @@
6774
'not_test = sifter.tests.not_test:TestNot',
6875
'size = sifter.tests.size:TestSize',
6976
'true = sifter.tests.true:TestTrue',
77+
'valid_notify_method = sifter.tests.notify:TestValidNotifyMethod',
78+
'notify_method_capability = sifter.tests.notify:TestValidNotifyMethod',
7079
# sifter comparators
7180
'ascii_casemap = sifter.comparators.ascii_casemap:ComparatorASCIICasemap',
72-
'octed = sifter.comparators.octet:ComparatorOctet'
73-
],
81+
'ascii_casemap_noi = sifter.comparators.ascii_casemap:ComparatorASCIICasemapnoi',
82+
'octed = sifter.comparators.octet:ComparatorOctet',
83+
# notification methods
84+
'mailto = sifter.notificationmethods.mailto:MailtoNotificationMethod',
85+
]
7486
}
7587
)

sifter/cli.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from argparse import ArgumentParser
2+
import email
3+
import os
4+
import logging
5+
import sys
6+
7+
import sifter.parser
8+
9+
10+
def main() -> None:
11+
arg_parser = ArgumentParser()
12+
arg_parser.add_argument('rulefile')
13+
arg_parser.add_argument('messagefile')
14+
args = arg_parser.parse_args()
15+
16+
if not os.path.isfile(args.rulefile):
17+
logging.error("rulefile '%s' does not exist", args.rulefile)
18+
sys.exit(1)
19+
20+
if not os.path.isfile(args.messagefile):
21+
logging.error("mail message '%s' does not exist", args.messagefile)
22+
sys.exit(1)
23+
24+
rules = sifter.parser.parse_file(open(args.rulefile))
25+
msg = email.message_from_file(open(args.messagefile))
26+
msg_actions = rules.evaluate(msg)
27+
print(msg_actions)

sifter/commands/fileinto.py

Lines changed: 5 additions & 2 deletions
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)) # type: ignore
2024

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

sifter/commands/imap4flags.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from email.message import Message
2+
from typing import Optional
3+
4+
from sifter.grammar.command import Command
5+
from sifter.validators.stringlist import StringList
6+
from sifter.grammar.string import expand_variables
7+
from sifter.grammar.state import EvaluationState
8+
from sifter.grammar.actions import Actions
9+
10+
# This implements the RFC5232 imap4flags extension
11+
# commands: addflag, removeflag, setflag
12+
# tests: :hasflag
13+
# tagged arguments: :flag to 'fileinto'
14+
15+
16+
class CommandSetFlag(Command):
17+
18+
RULE_IDENTIFIER = 'SETFLAG'
19+
POSITIONAL_ARGS = [StringList()]
20+
21+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
22+
state.check_required_extension('imap4flags', 'imapflags')
23+
flag_list = self.positional_args[0]
24+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
25+
state.actions.append('setflag', flag_list)
26+
return None
27+
28+
29+
class CommandRemoveFlag(Command):
30+
31+
RULE_IDENTIFIER = 'REMOVEFLAG'
32+
POSITIONAL_ARGS = [StringList()]
33+
34+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
35+
state.check_required_extension('imap4flags', 'imapflags')
36+
flag_list = self.positional_args[0]
37+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
38+
state.actions.append('removeflag', flag_list)
39+
return None
40+
41+
42+
class CommandAddFlag(Command):
43+
44+
RULE_IDENTIFIER = 'ADDFLAG'
45+
POSITIONAL_ARGS = [StringList()]
46+
47+
def evaluate(self, message: Message, state: EvaluationState) -> Optional[Actions]:
48+
state.check_required_extension('imap4flags', 'imapflags')
49+
flag_list = self.positional_args[0]
50+
flag_list = list(map(lambda s: expand_variables(s, state), flag_list)) # type: ignore
51+
state.actions.append('addflag', flag_list)
52+
return None

0 commit comments

Comments
 (0)