⚡ Sangsakala Proxy 🏠 Home
Skip to content

gh-161284: Fix quadratic time in email.header when folding long headers - #161285

Open
thejesh23 wants to merge 2 commits into
python:mainfrom
thejesh23:email-header-accumulator
Open

thejesh23 wants to merge 2 commits into
python:mainfrom
thejesh23:email-header-accumulator

Conversation

@thejesh23

@thejesh23 thejesh23 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

email.header._Accumulator.__len__() added up the lengths of all the parts of the current line on every call, and _ValueFormatter._append_chunk() calls it after every word. When a line grows long, folding becomes O(N²). That is what happens by default in Message.as_string(), which does not fold headers (maxheaderlen=0, compat32 policy).

The fix keeps the total length up to date in push(), pop(), pop_from() and reset(), the only methods that change the accumulator, and __len__() returns it. Nothing else touches the list.

Message.as_string() with one long Subject: header:

words in header main this PR
1,000 13.3 ms 0.3 ms
2,000 52.0 ms 0.6 ms
4,000 206.7 ms 1.2 ms
8,000 815.0 ms 2.4 ms

Ordinary messages also get faster. For a message with a 20-address To: header, as_string() goes from 100 µs to 38 µs and as_bytes() (folded at 78) from 68 µs to 52 µs.

Testing:

  • New tests check that a long header is left unfolded with maxlinelen=0 and by Message.as_string(), and that len() of the accumulator is correct after each kind of change.

  • I compared the old and new code on 60,000 random Header.encode() calls and on 15,000 random messages through as_string() and as_bytes(). The calls varied:

    • the charsets, maxlinelen, splitchars, continuation_ws and linesep
    • the header names
    • the input text, which included non-ASCII text, encoded words, long words, tabs and folded input

    The output was identical.

  • test_email, test_mailbox, test_smtplib and the full test suite pass.

AI disclosure: prepared with the help of Claude Code; I reviewed the change and the test results.

_Accumulator.__len__() summed the lengths of all the parts of the
current line on every call, and it is called after every appended
word, so folding a header whose line grows long took O(N**2) time.
This is the case for Message.as_string(), which does not fold headers
by default.  Keep the total length up to date instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant