email.message_from_binary_file() Silently Returns Empty on None (tested)
A non-blocking raw stream signals "no data ready yet" by returning None from readinto() -- distinct from the empty read that means real end-of-stream. We measured that email.message_from_binary_file() mishandles this signal the same silent way shutil.copyfileobj() and TextIOWrapper.readline() already do: a single transient None, even with a complete, well-formed email waiting on the very next read call, produces a completely empty Message object -- no Subject, no body, and critically, no exception at all.
1. A real, valid email behind one transient None
import io, email
class FlakyRaw(io.RawIOBase):
def __init__(self, payload):
self.calls = 0
self.sent = False
self.payload = payload
def readable(self):
return True
def readinto(self, b):
self.calls += 1
if self.calls == 1:
return None # transient: no data ready yet
if not self.sent:
self.sent = True
n = len(self.payload)
b[:n] = self.payload
return n # the real, complete email
return 0 # real EOF after that
payload = b"Subject: hello\n\nbody text\n"
raw = FlakyRaw(payload)
br = io.BufferedReader(raw)
msg = email.message_from_binary_file(br)
print("subject:", msg.get("Subject"))
print("payload:", repr(msg.get_payload()))
print("raw calls made:", raw.calls)
subject: None
payload: ''
raw calls made: 1
Measured: no exception is raised anywhere. message_from_binary_file() made exactly one read call, got None, and silently handed back a valid-looking but completely empty Message object -- indistinguishable, at the call site, from parsing a genuinely empty file. The real Subject: hello header and body are never read at all.
2. Contrast: plain BufferedReader.read() on the identical signal
raw0 = FlakyRaw(b"irrelevant for this check")
br0 = io.BufferedReader(raw0)
print("plain read():", repr(br0.read()))
plain read(): None
Measured: the io layer preserves the None/EOF distinction correctly; email.message_from_binary_file()'s own internal line-reading loop is what silently collapses that None into "nothing more to parse" -- the same root mechanism already confirmed in shutil.copyfileobj() and TextIOWrapper.readline().
3. The fix: drain into a real buffer first, retrying on None
def drain_retrying_on_none(raw_stream, chunk_size=65536):
out = io.BytesIO()
br = io.BufferedReader(raw_stream)
while True:
chunk = br.read(chunk_size)
if chunk is None:
continue # retry: not ready yet, not EOF
if not chunk:
break # real EOF
out.write(chunk)
out.seek(0)
return out
raw2 = FlakyRaw(payload)
safe_buf = drain_retrying_on_none(raw2)
msg2 = email.message_from_binary_file(safe_buf)
print("safe subject:", msg2.get("Subject"))
print("safe payload:", repr(msg2.get_payload()))
safe subject: hello
safe payload: 'body text\n'
Measured: resolving the None/EOF distinction before the bytes reach message_from_binary_file() parses the header and body correctly.
Frequently asked questions
Is this documented behavior?
The email module's documentation for message_from_binary_file() does not mention non-blocking streams or a None read result at all; it assumes a blocking file object that returns either data or b'' for EOF. We measured that a None-returning source produces a silently empty result with no error of any kind.
Why is there no exception at all here, unlike gzip or ElementTree?
We measured that the email parser's internal line-reading loop checks for a falsy result to decide a message has ended, the same way shutil.copyfileobj() and TextIOWrapper.readline() do -- there's no decode or length operation downstream that a None could break, so the parser just quietly concludes the message is over with zero content.
Is this related to the other None/EOF findings?
Yes -- same family already confirmed in shutil.copyfileobj, gzip/bz2/lzma, tarfile/zipfile, TextIOWrapper.readline(), and ElementTree.parse(): a correctly-returned non-blocking None gets mishandled one layer above io itself. This is the second confirmed case (after copyfileobj and readline()) where the failure is completely silent rather than a crash or misleading error.
Written with AI assistance. Measured on 2026-10-09 with Python 3.13.5's built-in email.message_from_binary_file(), reading a genuinely valid, complete email through an io.BufferedReader over a custom non-blocking RawIOBase whose readinto() returns None once before returning the real message bytes. PlainHeaders is our extension; Python's email and io modules are built in.