Skip to content

Commit c514b85

Browse files
author
Kien Pham
committed
init commit
1 parent 374f40b commit c514b85

File tree

10 files changed

+279
-3
lines changed

10 files changed

+279
-3
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pip-log.txt
2424

2525
# Unit test / coverage reports
2626
.coverage
27+
coverage/
2728
.tox
2829
nosetests.xml
2930

.travis.yml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
language: python
2+
3+
python:
4+
- "2.7"
5+
6+
install:
7+
- make install
8+
9+
# command to run tests
10+
script: make test

Makefile

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.PHONY: all clean cover doc test
2+
3+
help:
4+
@echo "Available commands:"
5+
@echo " clean remove temp files and static generated docs"
6+
@echo " cover generate test coverage"
7+
@echo " doc generate doc"
8+
@echo " test run all tests"
9+
10+
install: clean
11+
pip install -e .[dev]
12+
13+
clean:
14+
find ./ -type f -name '*.pyc' -exec rm -f {} \;
15+
rm -rf cover .coverage
16+
rm -rf docs/build dist eggs
17+
18+
cover:
19+
ENV=test nosetests -s -v --with-coverage --cover-html --cover-html-dir ./coverage
20+
21+
test:
22+
ENV=test nosetests -s --nologcapture
23+
24+
all: clean
25+
26+
doc: clean
27+
sphinx-build -b html doc/source doc/build/html
28+
@echo
29+
livereload -b doc/build/html -p 3001

README.md

+32-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,33 @@
1-
frink
2-
=====
1+
nibbler-python
2+
==============
3+
Better email parser for Python
34

4-
Better email parser
5+
[![Build Status](https://magnum.travis-ci.com/sendgridlabs/nibbler-python?token=Cxh5J57XgaqDXKj1xyma&branch=master)](https://magnum.travis-ci.com/sendgridlabs/nibbler-python)
6+
7+
8+
![nibbler](doc/_static/nibbler.gif)
9+
10+
Installation
11+
------------
12+
# Production: install from PyPI
13+
$ pip install nibbler-python
14+
15+
# Development: if you want to contribute to this project, clone it, install it from source
16+
$ make install
17+
18+
Usage
19+
-----
20+
Please see the [test/test_parser.py](test/test_parser.py) for more test cases
21+
22+
from nibbler.parser import parse_email
23+
24+
# valid email
25+
parse_email('"much.more unusual"@example.com')
26+
27+
# invalid email
28+
parse_email('Abc.example.com')
29+
30+
31+
Testing
32+
-------
33+
$ make test

doc/_static/nibbler.gif

12 KB
Loading

nibbler/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '0.0.1'

nibbler/parser.py

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import string
2+
3+
4+
class State(object):
5+
def __init__(self):
6+
self.in_quotes = False
7+
self.in_comment = False
8+
self.previous_dot = False
9+
self.previous_slash = False
10+
self.in_domain = False
11+
self.last_state = None
12+
13+
ATEXT = "!#$%&'*+-/=?^_`.{|}~@\"" + string.ascii_letters + string.digits
14+
SPECIAL = ['(', ')', ',', ':', ';', '<', '>', '[', '\\', ']', ' ']
15+
HOSTNAME = "-." + string.ascii_letters + string.digits
16+
17+
18+
def parse_email(email_str):
19+
20+
state = State()
21+
valid = True
22+
address = ''
23+
24+
for offset, character in enumerate(email_str):
25+
# Local part
26+
if not state.in_domain:
27+
if character == '\\':
28+
if state.in_quotes:
29+
# Check if slash was backslashed within quotes
30+
if state.previous_slash:
31+
state.previous_slash = False
32+
else:
33+
state.previous_slash = True
34+
# \ can only occur within slashes
35+
else:
36+
valid = False
37+
break
38+
elif character == '"':
39+
40+
if state.in_quotes:
41+
# Ignore if it was preceded by a backslash
42+
if not state.previous_slash:
43+
state.in_quotes = False
44+
else:
45+
state.previous_slash = False
46+
else:
47+
# Quotes must happen as the first character or after a dot
48+
if offset != 0 and not state.previous_dot:
49+
valid = False
50+
break
51+
state.in_quotes = True
52+
53+
elif character == '.':
54+
# We can't have two consecutive dots
55+
if state.previous_dot:
56+
break
57+
elif not state.in_quotes:
58+
state.previous_dot = True
59+
60+
elif character == '@' and not state.in_quotes:
61+
state.in_domain = True
62+
63+
# These characters must only occur in quotes
64+
if character in SPECIAL:
65+
if not state.in_quotes:
66+
valid = False
67+
break
68+
else:
69+
address += character
70+
else:
71+
if character not in ATEXT:
72+
valid = False
73+
break
74+
else:
75+
address += character
76+
77+
# Check states and clear them if necessary
78+
if state.previous_slash and character != '\\':
79+
state.previous_slash = False
80+
if state.previous_dot and character != '.':
81+
state.previous_dot = False
82+
83+
# Domain part, we don't allow ip addresses [x.x.x.x] only host names
84+
else:
85+
if character not in HOSTNAME:
86+
valid = False
87+
break
88+
address += character
89+
90+
if not state.in_domain:
91+
valid = False
92+
93+
return valid, address

setup.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""nibbler-python - Better email parser
2+
3+
nibbler-python helps you parse for valid email addresses in Python projects
4+
5+
Example Usage
6+
-------------
7+
from nibbler.parser import parse_email
8+
9+
# valid email
10+
parse_email('"much.more unusual"@example.com')
11+
12+
# invalid email
13+
parse_email('Abc.example.com')
14+
15+
Contribute
16+
----------
17+
This library is hosted on Github and you can contribute there:
18+
https://github.com/sendgridlabs/nibbler-python
19+
"""
20+
21+
from setuptools import setup, find_packages
22+
from nibbler import __version__
23+
24+
doc = __doc__.splitlines()
25+
26+
dev_requirements = [
27+
# tests:
28+
'nose',
29+
'coverage',
30+
31+
# docs:
32+
'Sphinx',
33+
'sphinx_rtd_theme',
34+
'sphinxcontrib-httpdomain',
35+
'livereload'
36+
]
37+
38+
setup(
39+
name='nibbler-python',
40+
description=doc[0],
41+
long_description='\n'.join(doc[2:]),
42+
version=__version__,
43+
packages=find_packages(),
44+
extras_require={
45+
'dev': dev_requirements
46+
},
47+
url="https://github.com/sendgridlabs/nibbler-python",
48+
keywords='email parser esp sendgrid',
49+
license="MIT",
50+
platforms="Posix; MacOS X; Windows",
51+
classifiers=["Development Status :: 5 - Production/Stable",
52+
"Intended Audience :: Developers",
53+
"License :: OSI Approved :: MIT License",
54+
"Operating System :: OS Independent",
55+
"Topic :: Internet",
56+
"Programming Language :: Python :: 2",
57+
"Programming Language :: Python :: 2.6",
58+
"Programming Language :: Python :: 2.7"],
59+
)

test/__init__.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import unittest
2+
3+
4+
class BaseTestCase(unittest.TestCase):
5+
6+
def setUp(self):
7+
# Valid email addresses:
8+
self.valid_addresses = [
9+
10+
11+
12+
13+
14+
'"much.more unusual"@example.com',
15+
'"[email protected]"@example.com',
16+
('"very.(),:;<>[]\\".VERY.\\"very@\\\\ \\"very\\".unusual"'
17+
'@strange.example.com'),
18+
'postbox@com',
19+
'admin@mailserver1',
20+
'!#$%&\'*+-/=?^_`{}|[email protected]',
21+
'"()<>[]:,;@\\\\\\"!#$%&\'*+-/=?^_`{}| ~.a"@example.org',
22+
'" "@example.org',
23+
'abc."defghi"[email protected]'
24+
]
25+
26+
# invalid email addresses:
27+
self.invalid_addresses = [
28+
'Abc.example.com',
29+
30+
'a"b(c)d,e:f;g<h>i[j\\k][email protected]',
31+
'just"not"[email protected]',
32+
'this is"not\\[email protected]',
33+
'this\\ still\\"not\\\\[email protected]',
34+
'abc"defghi"[email protected]'
35+
]

test/test_parser.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import unittest
2+
from nibbler.parser import parse_email
3+
from test import BaseTestCase
4+
from nose.tools import (assert_true, assert_false)
5+
6+
7+
class TestCase(BaseTestCase):
8+
def test_valid_email(self):
9+
for e in self.valid_addresses:
10+
valid, email_parsed = parse_email(e)
11+
assert_true(valid, 'Error: email %s should be valid' % e)
12+
13+
def test_invalid_email(self):
14+
for e in self.invalid_addresses:
15+
valid, email_parsed = parse_email(e)
16+
assert_false(valid, 'Error: email %s should be invalid' % e)
17+
18+
if __name__ == '__main__':
19+
unittest.main()

0 commit comments

Comments
 (0)