From e9de7103b9c20ea9b024b028d2231516ff2901f4 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Tue, 10 Jun 2025 18:12:07 -0400 Subject: [PATCH 1/8] enhance email content handling: allow infinite line length in encoding --- Lib/email/contentmanager.py | 11 +++++--- Lib/test/test_email/test_message.py | 28 +++++++++++++++++++ ...-06-10-18-02-29.gh-issue-135307.fXGrcK.rst | 2 ++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst diff --git a/Lib/email/contentmanager.py b/Lib/email/contentmanager.py index b4f5830beada4a..506e99f6838468 100644 --- a/Lib/email/contentmanager.py +++ b/Lib/email/contentmanager.py @@ -1,3 +1,4 @@ +import sys import binascii import email.charset import email.message @@ -142,13 +143,15 @@ def _encode_base64(data, max_line_length): def _encode_text(string, charset, cte, policy): + # max_line_length 0/None means no limit, ie: infinitely long. + maxlen = policy.max_line_length or sys.maxsize lines = string.encode(charset).splitlines() linesep = policy.linesep.encode('ascii') def embedded_body(lines): return linesep.join(lines) + linesep def normal_body(lines): return b'\n'.join(lines) + b'\n' if cte is None: # Use heuristics to decide on the "best" encoding. - if max((len(x) for x in lines), default=0) <= policy.max_line_length: + if max((len(x) for x in lines), default=0) <= maxlen: try: return '7bit', normal_body(lines).decode('ascii') except UnicodeDecodeError: @@ -157,7 +160,7 @@ def normal_body(lines): return b'\n'.join(lines) + b'\n' return '8bit', normal_body(lines).decode('ascii', 'surrogateescape') sniff = embedded_body(lines[:10]) sniff_qp = quoprimime.body_encode(sniff.decode('latin-1'), - policy.max_line_length) + maxlen) sniff_base64 = binascii.b2a_base64(sniff) # This is a little unfair to qp; it includes lineseps, base64 doesn't. if len(sniff_qp) > len(sniff_base64): @@ -172,9 +175,9 @@ def normal_body(lines): return b'\n'.join(lines) + b'\n' data = normal_body(lines).decode('ascii', 'surrogateescape') elif cte == 'quoted-printable': data = quoprimime.body_encode(normal_body(lines).decode('latin-1'), - policy.max_line_length) + maxlen) elif cte == 'base64': - data = _encode_base64(embedded_body(lines), policy.max_line_length) + data = _encode_base64(embedded_body(lines), maxlen) else: raise ValueError("Unknown content transfer encoding {}".format(cte)) return cte, data diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 23c39775a8b2e5..614f693c991f53 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1004,6 +1004,34 @@ def test_folding_with_long_nospace_http_policy_1(self): parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default) self.assertEqual(parsed_msg['Message-ID'], m['Message-ID']) + def test_no_wrapping_with_zero_max_line_length(self): + pol = policy.default.clone(max_line_length=0) + subj = "S" * 100 + msg = EmailMessage(policy=pol) + msg["From"] = "a@ex.com" + msg["To"] = "b@ex.com" + msg["Subject"] = subj + + raw = msg.as_bytes() + self.assertNotIn(b"\r\n ", raw, "Found fold indicator; wrapping not disabled") + + parsed = message_from_bytes(raw, policy=policy.default) + self.assertEqual(parsed["Subject"], subj) + + def test_no_wrapping_with_none_max_line_length(self): + pol = policy.default.clone(max_line_length=None) + subj = "S" * 100 + body = "B" * 100 + msg = EmailMessage(policy=pol) + msg["From"] = "a@ex.com" + msg["To"] = "b@ex.com" + msg["Subject"] = subj + msg.set_content(body) + + parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) + self.assertEqual(parsed["Subject"], subj) + self.assertEqual(parsed.get_body().get_content().rstrip('\n'), body) + def test_invalid_header_names(self): invalid_headers = [ ('Invalid Header', 'contains space'), diff --git a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst new file mode 100644 index 00000000000000..0f66b4e1d5b98a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst @@ -0,0 +1,2 @@ +:mod:`email`: Ensure policy accepts unlimited line lengths by +treating 0 or :const:`None` as :data:`sys.maxsize` From 0a5f37921223070bf4c24835f2d1c0208c4a284d Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Tue, 10 Jun 2025 18:20:32 -0400 Subject: [PATCH 2/8] Fix lint --- .../next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst index 0f66b4e1d5b98a..d4c963b7749a83 100644 --- a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst +++ b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst @@ -1,2 +1,2 @@ -:mod:`email`: Ensure policy accepts unlimited line lengths by +:mod:`email`: Ensure policy accepts unlimited line lengths by treating 0 or :const:`None` as :data:`sys.maxsize` From d0dc3c08aa3e3e991990d279d6cca03aa2067b18 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Tue, 10 Jun 2025 19:50:12 -0400 Subject: [PATCH 3/8] fix: correct formatting in email test cases and NEWS entry --- Lib/test/test_email/test_message.py | 4 ++-- .../Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 614f693c991f53..6f7fb5f727e688 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1009,7 +1009,7 @@ def test_no_wrapping_with_zero_max_line_length(self): subj = "S" * 100 msg = EmailMessage(policy=pol) msg["From"] = "a@ex.com" - msg["To"] = "b@ex.com" + msg["To"] = "b@ex.com" msg["Subject"] = subj raw = msg.as_bytes() @@ -1024,7 +1024,7 @@ def test_no_wrapping_with_none_max_line_length(self): body = "B" * 100 msg = EmailMessage(policy=pol) msg["From"] = "a@ex.com" - msg["To"] = "b@ex.com" + msg["To"] = "b@ex.com" msg["Subject"] = subj msg.set_content(body) diff --git a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst index d4c963b7749a83..2eed86d88d202b 100644 --- a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst +++ b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst @@ -1,2 +1,2 @@ :mod:`email`: Ensure policy accepts unlimited line lengths by -treating 0 or :const:`None` as :data:`sys.maxsize` +treating 0 or :const:`None` as :data:`sys.maxsize`. From 77ea7983d7704a7e6ab2ef90b2cdd28c47fbf6f2 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Thu, 19 Jun 2025 10:47:29 -0400 Subject: [PATCH 4/8] Update NEWS entry, test case enhance, lint code --- Lib/email/contentmanager.py | 5 +- Lib/test/test_email/test_message.py | 46 ++++++++----------- ...-06-10-18-02-29.gh-issue-135307.fXGrcK.rst | 2 +- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/Lib/email/contentmanager.py b/Lib/email/contentmanager.py index 506e99f6838468..3b50b6b0f48a85 100644 --- a/Lib/email/contentmanager.py +++ b/Lib/email/contentmanager.py @@ -1,9 +1,9 @@ -import sys import binascii import email.charset import email.message import email.errors from email import quoprimime +import sys class ContentManager: @@ -159,8 +159,7 @@ def normal_body(lines): return b'\n'.join(lines) + b'\n' if policy.cte_type == '8bit': return '8bit', normal_body(lines).decode('ascii', 'surrogateescape') sniff = embedded_body(lines[:10]) - sniff_qp = quoprimime.body_encode(sniff.decode('latin-1'), - maxlen) + sniff_qp = quoprimime.body_encode(sniff.decode('latin-1'), maxlen) sniff_base64 = binascii.b2a_base64(sniff) # This is a little unfair to qp; it includes lineseps, base64 doesn't. if len(sniff_qp) > len(sniff_base64): diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 6f7fb5f727e688..d5ace9c0beba6a 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1004,33 +1004,25 @@ def test_folding_with_long_nospace_http_policy_1(self): parsed_msg = message_from_bytes(m.as_bytes(), policy=policy.default) self.assertEqual(parsed_msg['Message-ID'], m['Message-ID']) - def test_no_wrapping_with_zero_max_line_length(self): - pol = policy.default.clone(max_line_length=0) - subj = "S" * 100 - msg = EmailMessage(policy=pol) - msg["From"] = "a@ex.com" - msg["To"] = "b@ex.com" - msg["Subject"] = subj - - raw = msg.as_bytes() - self.assertNotIn(b"\r\n ", raw, "Found fold indicator; wrapping not disabled") - - parsed = message_from_bytes(raw, policy=policy.default) - self.assertEqual(parsed["Subject"], subj) - - def test_no_wrapping_with_none_max_line_length(self): - pol = policy.default.clone(max_line_length=None) - subj = "S" * 100 - body = "B" * 100 - msg = EmailMessage(policy=pol) - msg["From"] = "a@ex.com" - msg["To"] = "b@ex.com" - msg["Subject"] = subj - msg.set_content(body) - - parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) - self.assertEqual(parsed["Subject"], subj) - self.assertEqual(parsed.get_body().get_content().rstrip('\n'), body) + def test_no_wrapping_max_line_length(self): + def do_test_no_wrapping_max_line_length(n): + pol = policy.default.clone(max_line_length=n) + subj = "S" * 100 + body = "B" * 100 + msg = EmailMessage(policy=pol) + msg["From"] = "a@ex.com" + msg["To"] = "b@ex.com" + msg["Subject"] = subj + msg.set_content(body) + + raw = msg.as_bytes() + self.assertNotIn(b"\r\n ", raw, "Found fold indicator; wrapping not disabled") + + parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) + self.assertEqual(parsed["Subject"], subj) + self.assertEqual(parsed.get_body().get_content().rstrip('\n'), body) + do_test_no_wrapping_max_line_length(None) + do_test_no_wrapping_max_line_length(0) def test_invalid_header_names(self): invalid_headers = [ diff --git a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst index 2eed86d88d202b..930b5813770c23 100644 --- a/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst +++ b/Misc/NEWS.d/next/Library/2025-06-10-18-02-29.gh-issue-135307.fXGrcK.rst @@ -1,2 +1,2 @@ :mod:`email`: Ensure policy accepts unlimited line lengths by -treating 0 or :const:`None` as :data:`sys.maxsize`. +treating falsey values as :data:`sys.maxsize`. From 52e590c1def409fd907542a45b7e9da34d895ff1 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Thu, 19 Jun 2025 13:08:34 -0400 Subject: [PATCH 5/8] resort import; restructure testcase --- Lib/email/contentmanager.py | 2 +- Lib/test/test_email/test_message.py | 41 ++++++++++++++++------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Lib/email/contentmanager.py b/Lib/email/contentmanager.py index 3b50b6b0f48a85..7c65b2b95a56cc 100644 --- a/Lib/email/contentmanager.py +++ b/Lib/email/contentmanager.py @@ -2,8 +2,8 @@ import email.charset import email.message import email.errors -from email import quoprimime import sys +from email import quoprimime class ContentManager: diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index d5ace9c0beba6a..5ce48da97fa02c 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1005,24 +1005,29 @@ def test_folding_with_long_nospace_http_policy_1(self): self.assertEqual(parsed_msg['Message-ID'], m['Message-ID']) def test_no_wrapping_max_line_length(self): - def do_test_no_wrapping_max_line_length(n): - pol = policy.default.clone(max_line_length=n) - subj = "S" * 100 - body = "B" * 100 - msg = EmailMessage(policy=pol) - msg["From"] = "a@ex.com" - msg["To"] = "b@ex.com" - msg["Subject"] = subj - msg.set_content(body) - - raw = msg.as_bytes() - self.assertNotIn(b"\r\n ", raw, "Found fold indicator; wrapping not disabled") - - parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) - self.assertEqual(parsed["Subject"], subj) - self.assertEqual(parsed.get_body().get_content().rstrip('\n'), body) - do_test_no_wrapping_max_line_length(None) - do_test_no_wrapping_max_line_length(0) + # Test that falsey 'max_line_length' are converted to sys.maxsize. + for n in [0, None]: + with self.subTest(max_line_length=n): + self.do_test_no_wrapping_max_line_length(n) + + def do_test_no_wrapping_max_line_length(self, n): + pol = policy.default.clone(max_line_length=n) + subj = "S" * 100 + body = "B" * 100 + msg = EmailMessage(policy=pol) + msg["From"] = "a@ex.com" + msg["To"] = "b@ex.com" + msg["Subject"] = subj + msg.set_content(body) + + raw = msg.as_bytes() + self.assertNotIn(b"\r\n ", raw, + "Found fold indicator; wrapping not disabled") + + parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) + self.assertEqual(parsed["Subject"], subj) + parsed_body = parsed.get_body().get_content().rstrip('\n') + self.assertEqual(parsed_body, body) def test_invalid_header_names(self): invalid_headers = [ From 603ff7492008512fe5950d326ef1ac3c8d97b6b4 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Thu, 19 Jun 2025 13:29:12 -0400 Subject: [PATCH 6/8] tiny enhance varaible --- Lib/test/test_email/test_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index 5ce48da97fa02c..c3ad451ceb97d3 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1024,7 +1024,7 @@ def do_test_no_wrapping_max_line_length(self, n): self.assertNotIn(b"\r\n ", raw, "Found fold indicator; wrapping not disabled") - parsed = message_from_bytes(msg.as_bytes(), policy=policy.default) + parsed = message_from_bytes(raw, policy=policy.default) self.assertEqual(parsed["Subject"], subj) parsed_body = parsed.get_body().get_content().rstrip('\n') self.assertEqual(parsed_body, body) From 38e63813d2431e6b8d231a14669fcac5a6267866 Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Sat, 21 Jun 2025 17:04:53 -0400 Subject: [PATCH 7/8] Tiny update for test case for better readability --- Lib/test/test_email/test_message.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index c3ad451ceb97d3..f1675ec6beaee5 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1010,8 +1010,9 @@ def test_no_wrapping_max_line_length(self): with self.subTest(max_line_length=n): self.do_test_no_wrapping_max_line_length(n) - def do_test_no_wrapping_max_line_length(self, n): - pol = policy.default.clone(max_line_length=n) + def do_test_no_wrapping_max_line_length(self, falsey): + self.assertFalse(falsey) + pol = policy.default.clone(max_line_length=falsey) subj = "S" * 100 body = "B" * 100 msg = EmailMessage(policy=pol) From fa173acf06e64935b68c37326fe9f1f273ec923b Mon Sep 17 00:00:00 2001 From: Jiucheng Zang Date: Sat, 12 Jul 2025 13:36:08 -0400 Subject: [PATCH 8/8] test: update test assertion for line wrapping in email messages --- Lib/email/contentmanager.py | 2 +- Lib/test/test_email/test_message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/email/contentmanager.py b/Lib/email/contentmanager.py index 7c65b2b95a56cc..56cdd639c629c3 100644 --- a/Lib/email/contentmanager.py +++ b/Lib/email/contentmanager.py @@ -143,7 +143,7 @@ def _encode_base64(data, max_line_length): def _encode_text(string, charset, cte, policy): - # max_line_length 0/None means no limit, ie: infinitely long. + # If max_line_length is 0 or None, there is no limit. maxlen = policy.max_line_length or sys.maxsize lines = string.encode(charset).splitlines() linesep = policy.linesep.encode('ascii') diff --git a/Lib/test/test_email/test_message.py b/Lib/test/test_email/test_message.py index f1675ec6beaee5..6d2e2a082a29c5 100644 --- a/Lib/test/test_email/test_message.py +++ b/Lib/test/test_email/test_message.py @@ -1022,7 +1022,7 @@ def do_test_no_wrapping_max_line_length(self, falsey): msg.set_content(body) raw = msg.as_bytes() - self.assertNotIn(b"\r\n ", raw, + self.assertNotIn(b"=\n", raw, "Found fold indicator; wrapping not disabled") parsed = message_from_bytes(raw, policy=policy.default) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy