Skip to content

Commit

Permalink
Check parenthesis
Browse files Browse the repository at this point in the history
  • Loading branch information
vstinner committed Nov 30, 2023
1 parent 12f82df commit cda8ed3
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 12 deletions.
45 changes: 34 additions & 11 deletions Lib/email/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,28 +104,37 @@ def formataddr(pair, charset='utf-8'):
return address


def _iter_escaped_chars(addr):
pos = 0
escape = False
for pos, ch in enumerate(addr):
if ch == '\\' and not escape:
escape = True
else:
if escape:
yield (pos, '\\' + ch)
else:
yield (pos, ch)
escape = False
if escape:
yield (pos, '\\')


def _strip_quoted_realnames(addr):
"""Strip real names between quotes."""
if '"' not in addr:
# Fast path
return addr

pos = 0
start = None
remove = []
previous = None
while pos < len(addr):
ch = addr[pos]
if ch == '"' and previous != '\\':
for pos, ch in _iter_escaped_chars(addr):
if ch == '"':
if start is None:
start = pos
else:
remove.append((start, pos))
start = None
elif ch == '\\' and previous == '\\':
previous = None
previous = ch
pos += 1

result = []
pos = 0
Expand Down Expand Up @@ -180,11 +189,25 @@ def getaddresses(fieldvalues, *, strict=True):
return result


def _check_parenthesis(addr):
# Ignore parenthesis in quoted real names.
addr = _strip_quoted_realnames(addr)

opens = 0
for pos, ch in _iter_escaped_chars(addr):
if ch == '(':
opens += 1
elif ch == ')':
opens -= 1
if opens < 0:
return False
return (opens == 0)


def _pre_parse_validation(email_header_fields):
accepted_values = []
for v in email_header_fields:
s = v.replace('\\(', '').replace('\\)', '')
if s.count('(') != s.count(')'):
if not _check_parenthesis(v):
v = "('', '')"
accepted_values.append(v)

Expand Down
25 changes: 24 additions & 1 deletion Lib/test/test_email/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -3638,6 +3638,19 @@ def test_mime_classes_policy_argument(self):
m = cls(*constructor, policy=email.policy.default)
self.assertIs(m.policy, email.policy.default)

def test_iter_escaped_chars(self):
self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
[(0, 'a'),
(2, '\\\\'),
(3, 'b'),
(5, '\\"'),
(6, 'c'),
(8, '\\\\'),
(9, '"'),
(10, 'd')])
self.assertEqual(list(utils._iter_escaped_chars('a\\')),
[(0, 'a'), (1, '\\')])

def test_strip_quoted_realnames(self):
def check(addr, expected):
self.assertEqual(utils._strip_quoted_realnames(addr), expected)
Expand All @@ -3646,9 +3659,19 @@ def check(addr, expected):
'Jane Doe <[email protected]>, John Doe <[email protected]>')
check('"Jane Doe" <[email protected]>, "John Doe" <[email protected]>',
' <[email protected]>, <[email protected]>')
check(r'"Jane \"Doe"." <[email protected]>',
check(r'"Jane \"Doe\"." <[email protected]>',
' <[email protected]>')

def test_check_parenthesis(self):
addr = '[email protected]'
self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))

# Ignore real name between quotes
self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))


# Test the iterator/generators
class TestIterators(TestEmailBase):
Expand Down

0 comments on commit cda8ed3

Please sign in to comment.