Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions Lib/email/_header_value_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ class Token(TokenList):
token_type = 'token'
encode_as_ew = False

@property
def stripped_value(self):
for token in self:
if token.token_type == 'ttext':
return token.value


class EncodedWord(TokenList):
token_type = 'encoded-word'
Expand Down Expand Up @@ -734,7 +740,7 @@ def stripped_value(self):
if token.token_type == 'cfws':
token = self[1]
if token.token_type.endswith(
('quoted-string', 'attribute', 'extended-attribute')):
('quoted-string', 'attribute', 'extended-attribute', 'token')):
return token.stripped_value
return self.value

Expand Down Expand Up @@ -2458,8 +2464,8 @@ def get_section(value):
return section, value


def get_value(value):
""" quoted-string / attribute
def get_value(value, *, extended=False):
""" quoted-string / token / extended-attribute

"""
v = Value()
Expand All @@ -2473,8 +2479,10 @@ def get_value(value):
"only {}".format(leader))
if value[0] == '"':
token, value = get_quoted_string(value)
else:
elif extended:
token, value = get_extended_attribute(value)
else:
token, value = get_token(value)
if leader is not None:
token[:0] = [leader]
v.append(token)
Expand Down Expand Up @@ -2560,7 +2568,13 @@ def get_parameter(value):
if value and value[0] == "'":
token = None
else:
token, value = get_value(value)
# Preserve legacy recovery for unencoded *0 continuations using the
# charset'language'value form. Unsectioned parameters are MIME tokens.
parse_as_extended = (
param.extended or
(param.sectioned and param.section_number == 0)
)
token, value = get_value(value, extended=parse_as_extended)
if not param.extended or param.section_number > 0:
if not value or value[0] != "'":
appendto.append(token)
Expand Down Expand Up @@ -2614,7 +2628,7 @@ def get_parameter(value):
v.append(token)
token = v
else:
token, value = get_value(value)
token, value = get_value(value, extended=True)
appendto.append(token)
if remainder is not None:
assert not value, value
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_email/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ def test_get_filename(self):
subpart = msg.get_payload(1)
eq(subpart.get_filename(), 'dingusfish.gif')

def test_get_filename_with_unquoted_apostrophe(self):
msg = email.message_from_string(
"Content-Disposition: attachment; filename=O'Brien.pdf\n",
policy=email.policy.default,
)
self.assertEqual(msg.get_filename(), "O'Brien.pdf")
self.assertDefectsEqual(msg['content-disposition'].defects, [])

def test_get_filename_with_name_parameter(self):
eq = self.assertEqual

Expand Down Expand Up @@ -1924,6 +1932,23 @@ def test_hierarchy(self):
self.assertFalse(m0.is_multipart())
self.assertFalse(m1.is_multipart())

def test_unquoted_apostrophe_in_boundary(self):
msg = email.message_from_string("""\
Content-Type: multipart/mixed; boundary=abc'def

--abc'def
Content-Type: text/plain

body
--abc'def--
""", policy=email.policy.default)
self.assertEqual(msg.get_boundary(), "abc'def")
self.assertTrue(msg.is_multipart())
self.assertEqual(len(msg.get_payload()), 1)
self.assertEqual(msg.get_payload(0).get_payload(), 'body')
self.assertDefectsEqual(msg['content-type'].defects, [])
self.assertDefectsEqual(msg.defects, [])

def test_empty_multipart_idempotent(self):
text = """\
Content-Type: multipart/mixed; boundary="BOUNDARY"
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_email/test_headerregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,26 @@ def content_type_as_value(self,
'text/plain; title="foo"',
),

'unquoted_param_value_with_apostrophe_and_asterisk': (
"text/plain; title=O'Brien*notes.txt",
'text/plain',
'text',
'plain',
{'title': "O'Brien*notes.txt"},
[],
'text/plain; title="O\'Brien*notes.txt"',
),

'unquoted_boundary_with_apostrophe': (
"multipart/mixed; boundary=abc'def",
'multipart/mixed',
'multipart',
'mixed',
{'boundary': "abc'def"},
[],
'multipart/mixed; boundary="abc\'def"',
),

'param_value_with_tspecials': (
'text/plain; title="(bar)foo blue"',
'text/plain',
Expand Down Expand Up @@ -934,6 +954,15 @@ def content_disp_as_value(self,
' filename*=utf-8\'\'Schulbesuchsbest%C3%A4ttigung.pdf\n'),
),

'unquoted_filename_with_apostrophe': (
"attachment; filename=O'Brien.pdf",
'attachment',
{'filename': "O'Brien.pdf"},
[],
'attachment; filename="O\'Brien.pdf"',
'Content-Disposition: attachment; filename="O\'Brien.pdf"\n',
),

'parameter_value_with_fws_between_tokens': (
'attachment; filename="File =?utf-8?q?Name?= With Spaces.pdf"',
'attachment',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :data:`email.policy.default` to parse apostrophes in unquoted MIME
parameter values as valid token characters. This restores attachment
filename and multipart boundary handling consistent with ``compat32``.
Loading