Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
061e549ad7 | ||
|
|
49d1a5d248 | ||
|
|
03d6b00db8 | ||
|
|
a2eb0f7201 | ||
|
|
5c71a0ca07 | ||
|
|
489d16fad9 | ||
|
|
a458707777 | ||
|
|
a1d0a86305 | ||
|
|
29f1d21be7 | ||
|
|
34c5b526c3 | ||
|
|
3edb6578ba | ||
|
|
984c036b6e | ||
|
|
a403ecb5c9 | ||
|
|
a44713409c | ||
|
|
567467b8ed | ||
|
|
139edd6104 | ||
|
|
e756d55abf | ||
|
|
015c8d2a78 | ||
|
|
5af846c13d | ||
|
|
e69a9c7a54 | ||
|
|
23cb2a9a53 | ||
|
|
b5e3397b88 | ||
|
|
5685a4055a | ||
|
|
97b72ef767 | ||
|
|
31489848be | ||
|
|
e5988d447b | ||
|
|
adfed748ce |
2
setup.py
2
setup.py
@@ -29,7 +29,7 @@ class InstallCommand(install):
|
|||||||
|
|
||||||
|
|
||||||
setup(name='talon',
|
setup(name='talon',
|
||||||
version='1.3.2',
|
version='1.3.4',
|
||||||
description=("Mailgun library "
|
description=("Mailgun library "
|
||||||
"to extract message quotations and signatures."),
|
"to extract message quotations and signatures."),
|
||||||
long_description=open("README.rst").read(),
|
long_description=open("README.rst").read(),
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ RE_ORIGINAL_MESSAGE = re.compile(u'[\s]*[-]+[ ]*({})[ ]*[-]+'.format(
|
|||||||
'Oprindelig meddelelse',
|
'Oprindelig meddelelse',
|
||||||
))), re.I)
|
))), re.I)
|
||||||
|
|
||||||
RE_FROM_COLON_OR_DATE_COLON = re.compile(u'(_+\r?\n)?[\s]*(:?[*]?{})[\s]?:[*]? .*'.format(
|
RE_FROM_COLON_OR_DATE_COLON = re.compile(u'(_+\r?\n)?[\s]*(:?[*]?{})[\s]?:[*]?.*'.format(
|
||||||
u'|'.join((
|
u'|'.join((
|
||||||
# "From" in different languages.
|
# "From" in different languages.
|
||||||
'From', 'Van', 'De', 'Von', 'Fra', u'Från',
|
'From', 'Van', 'De', 'Von', 'Fra', u'Från',
|
||||||
@@ -172,6 +172,9 @@ MAX_HTML_LEN = 2794202
|
|||||||
QUOT_PATTERN = re.compile('^>+ ?')
|
QUOT_PATTERN = re.compile('^>+ ?')
|
||||||
NO_QUOT_LINE = re.compile('^[^>].*[\S].*')
|
NO_QUOT_LINE = re.compile('^[^>].*[\S].*')
|
||||||
|
|
||||||
|
# Regular expression to identify if a line is a header.
|
||||||
|
RE_HEADER = re.compile(": ")
|
||||||
|
|
||||||
|
|
||||||
def extract_from(msg_body, content_type='text/plain'):
|
def extract_from(msg_body, content_type='text/plain'):
|
||||||
try:
|
try:
|
||||||
@@ -185,6 +188,19 @@ def extract_from(msg_body, content_type='text/plain'):
|
|||||||
return msg_body
|
return msg_body
|
||||||
|
|
||||||
|
|
||||||
|
def remove_initial_spaces_and_mark_message_lines(lines):
|
||||||
|
"""
|
||||||
|
Removes the initial spaces in each line before marking message lines.
|
||||||
|
|
||||||
|
This ensures headers can be identified if they are indented with spaces.
|
||||||
|
"""
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
lines[i] = lines[i].lstrip(' ')
|
||||||
|
i += 1
|
||||||
|
return mark_message_lines(lines)
|
||||||
|
|
||||||
|
|
||||||
def mark_message_lines(lines):
|
def mark_message_lines(lines):
|
||||||
"""Mark message lines with markers to distinguish quotation lines.
|
"""Mark message lines with markers to distinguish quotation lines.
|
||||||
|
|
||||||
@@ -287,9 +303,21 @@ def preprocess(msg_body, delimiter, content_type='text/plain'):
|
|||||||
|
|
||||||
Converts msg_body into a unicode.
|
Converts msg_body into a unicode.
|
||||||
"""
|
"""
|
||||||
# normalize links i.e. replace '<', '>' wrapping the link with some symbols
|
msg_body = _replace_link_brackets(msg_body)
|
||||||
# so that '>' closing the link couldn't be mistakenly taken for quotation
|
|
||||||
# marker.
|
msg_body = _wrap_splitter_with_newline(msg_body, delimiter, content_type)
|
||||||
|
|
||||||
|
return msg_body
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_link_brackets(msg_body):
|
||||||
|
"""
|
||||||
|
Normalize links i.e. replace '<', '>' wrapping the link with some symbols
|
||||||
|
so that '>' closing the link couldn't be mistakenly taken for quotation
|
||||||
|
marker.
|
||||||
|
|
||||||
|
Converts msg_body into a unicode
|
||||||
|
"""
|
||||||
if isinstance(msg_body, bytes):
|
if isinstance(msg_body, bytes):
|
||||||
msg_body = msg_body.decode('utf8')
|
msg_body = msg_body.decode('utf8')
|
||||||
|
|
||||||
@@ -301,7 +329,14 @@ def preprocess(msg_body, delimiter, content_type='text/plain'):
|
|||||||
return "@@%s@@" % link.group(1)
|
return "@@%s@@" % link.group(1)
|
||||||
|
|
||||||
msg_body = re.sub(RE_LINK, link_wrapper, msg_body)
|
msg_body = re.sub(RE_LINK, link_wrapper, msg_body)
|
||||||
|
return msg_body
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_splitter_with_newline(msg_body, delimiter, content_type='text/plain'):
|
||||||
|
"""
|
||||||
|
Splits line in two if splitter pattern preceded by some text on the same
|
||||||
|
line (done only for 'On <date> <person> wrote:' pattern.
|
||||||
|
"""
|
||||||
def splitter_wrapper(splitter):
|
def splitter_wrapper(splitter):
|
||||||
"""Wraps splitter with new line"""
|
"""Wraps splitter with new line"""
|
||||||
if splitter.start() and msg_body[splitter.start() - 1] != '\n':
|
if splitter.start() and msg_body[splitter.start() - 1] != '\n':
|
||||||
@@ -450,6 +485,82 @@ def _extract_from_html(msg_body):
|
|||||||
return html.tostring(html_tree_copy)
|
return html.tostring(html_tree_copy)
|
||||||
|
|
||||||
|
|
||||||
|
def split_emails(msg):
|
||||||
|
"""
|
||||||
|
Given a message (which may consist of an email conversation thread with
|
||||||
|
multiple emails), mark the lines to identify split lines, content lines and
|
||||||
|
empty lines.
|
||||||
|
|
||||||
|
Correct the split line markers inside header blocks. Header blocks are
|
||||||
|
identified by the regular expression RE_HEADER.
|
||||||
|
|
||||||
|
Return the corrected markers
|
||||||
|
"""
|
||||||
|
msg_body = _replace_link_brackets(msg)
|
||||||
|
|
||||||
|
# don't process too long messages
|
||||||
|
lines = msg_body.splitlines()[:MAX_LINES_COUNT]
|
||||||
|
markers = remove_initial_spaces_and_mark_message_lines(lines)
|
||||||
|
|
||||||
|
markers = _mark_quoted_email_splitlines(markers, lines)
|
||||||
|
|
||||||
|
# we don't want splitlines in header blocks
|
||||||
|
markers = _correct_splitlines_in_headers(markers, lines)
|
||||||
|
|
||||||
|
return markers
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_quoted_email_splitlines(markers, lines):
|
||||||
|
"""
|
||||||
|
When there are headers indented with '>' characters, this method will
|
||||||
|
attempt to identify if the header is a splitline header. If it is, then we
|
||||||
|
mark it with 's' instead of leaving it as 'm' and return the new markers.
|
||||||
|
"""
|
||||||
|
# Create a list of markers to easily alter specific characters
|
||||||
|
markerlist = list(markers)
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if markerlist[i] != 'm':
|
||||||
|
continue
|
||||||
|
for pattern in SPLITTER_PATTERNS:
|
||||||
|
matcher = re.search(pattern, line)
|
||||||
|
if matcher:
|
||||||
|
markerlist[i] = 's'
|
||||||
|
break
|
||||||
|
|
||||||
|
return "".join(markerlist)
|
||||||
|
|
||||||
|
|
||||||
|
def _correct_splitlines_in_headers(markers, lines):
|
||||||
|
"""
|
||||||
|
Corrects markers by removing splitlines deemed to be inside header blocks.
|
||||||
|
"""
|
||||||
|
updated_markers = ""
|
||||||
|
i = 0
|
||||||
|
in_header_block = False
|
||||||
|
|
||||||
|
for m in markers:
|
||||||
|
# Only set in_header_block flag when we hit an 's' and line is a header
|
||||||
|
if m == 's':
|
||||||
|
if not in_header_block:
|
||||||
|
if bool(re.search(RE_HEADER, lines[i])):
|
||||||
|
in_header_block = True
|
||||||
|
else:
|
||||||
|
if QUOT_PATTERN.match(lines[i]):
|
||||||
|
m = 'm'
|
||||||
|
else:
|
||||||
|
m = 't'
|
||||||
|
|
||||||
|
# If the line is not a header line, set in_header_block false.
|
||||||
|
if not bool(re.search(RE_HEADER, lines[i])):
|
||||||
|
in_header_block = False
|
||||||
|
|
||||||
|
# Add the marker to the new updated markers string.
|
||||||
|
updated_markers += m
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return updated_markers
|
||||||
|
|
||||||
|
|
||||||
def _readable_text_empty(html_tree):
|
def _readable_text_empty(html_tree):
|
||||||
return not bool(html_tree_to_text(html_tree).strip())
|
return not bool(html_tree_to_text(html_tree).strip())
|
||||||
|
|
||||||
|
|||||||
@@ -696,3 +696,52 @@ def test_standard_replies():
|
|||||||
"'%(reply)s' != %(stripped)s for %(fn)s" % \
|
"'%(reply)s' != %(stripped)s for %(fn)s" % \
|
||||||
{'reply': reply_text, 'stripped': stripped_text,
|
{'reply': reply_text, 'stripped': stripped_text,
|
||||||
'fn': filename}
|
'fn': filename}
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_email():
|
||||||
|
msg = """From: Mr. X
|
||||||
|
Date: 24 February 2016
|
||||||
|
To: Mr. Y
|
||||||
|
Subject: Hi
|
||||||
|
Attachments: none
|
||||||
|
Goodbye.
|
||||||
|
From: Mr. Y
|
||||||
|
To: Mr. X
|
||||||
|
Date: 24 February 2016
|
||||||
|
Subject: Hi
|
||||||
|
Attachments: none
|
||||||
|
|
||||||
|
Hello.
|
||||||
|
|
||||||
|
On 24th February 2016 at 09.32am, Conal wrote:
|
||||||
|
|
||||||
|
Hey!
|
||||||
|
|
||||||
|
On Mon, 2016-10-03 at 09:45 -0600, Stangel, Dan wrote:
|
||||||
|
> Mohan,
|
||||||
|
>
|
||||||
|
> We have not yet migrated the systems.
|
||||||
|
>
|
||||||
|
> Dan
|
||||||
|
>
|
||||||
|
> > -----Original Message-----
|
||||||
|
> > Date: Mon, 2 Apr 2012 17:44:22 +0400
|
||||||
|
> > Subject: Test
|
||||||
|
> > From: bob@xxx.mailgun.org
|
||||||
|
> > To: xxx@gmail.com; xxx@hotmail.com; xxx@yahoo.com; xxx@aol.com; xxx@comcast.net; xxx@nyc.rr.com
|
||||||
|
> >
|
||||||
|
> > Hi
|
||||||
|
> >
|
||||||
|
> > > From: bob@xxx.mailgun.org
|
||||||
|
> > > To: xxx@gmail.com; xxx@hotmail.com; xxx@yahoo.com; xxx@aol.com; xxx@comcast.net; xxx@nyc.rr.com
|
||||||
|
> > > Date: Mon, 2 Apr 2012 17:44:22 +0400
|
||||||
|
> > > Subject: Test
|
||||||
|
> > > Hi
|
||||||
|
> > >
|
||||||
|
> >
|
||||||
|
>
|
||||||
|
>
|
||||||
|
"""
|
||||||
|
expected_markers = "stttttsttttetesetesmmmmmmssmmmmmmsmmmmmmmm"
|
||||||
|
markers = quotations.split_emails(msg)
|
||||||
|
eq_(markers, expected_markers)
|
||||||
|
|||||||
Reference in New Issue
Block a user