FileFormatUtil.DetectFileFormat allocates 1.85 GiB for a 373-byte .eml

Product: Aspose.Email for .NET
Versions affected: 26.3.0 and 24.9.0 (identical behaviour on both)
Platform: .NET 8.0.28, win-x64, Windows Server, Workstation GC, licensed

Summary

Aspose.Email.Tools.FileFormatUtil.DetectFileFormat(Stream) allocates roughly 1.85 GiB for a 373-byte message stream. MailMessage.Load() on the identical stream allocates 142 KB — so format detection costs roughly 14,000x more memory than actually parsing the message.

The allocated size has no relation to the input size. Across five samples it takes one of exactly two values, and which one appears to be decided solely by whether the message has a Received: header:

sample size Received: header DetectFileFormat allocates
minimal.eml 237 B no 540,800,635 bytes (516 MB)
3001.eml 6.99 KB no 540,972,851 bytes (516 MB)
with-received.eml 373 B yes 1,986,721,987 bytes (1.85 GiB)
3020.eml 28.31 KB yes 1,986,948,556 bytes (1.85 GiB)
3021.eml 76.52 KB yes 1,987,260,733 bytes (1.85 GiB)

A 237-byte file and a 6.99 KB file both allocate 516 MB. A 373-byte file and a 76.52 KB file both allocate 1.85 GiB. Adding a single Received: header to minimal.eml — a 136-byte change — moves it from 516 MB to 1.85 GiB.

Reproduction

The two synthetic samples are attached (.eml is not an allowed attachment type here, so they are zipped). Both are entirely synthetic; their full contents are below so the case can be reproduced without the attachment.

with-received.eml (373 bytes) — allocates 1.85 GiB:

Received: from MAIL01.example.com (192.0.2.1) by MAIL02.example.com with
 Microsoft SMTP Server; Thu, 4 Sep 2026 10:00:00 +0000
From: sender@example.com
To: recipient@example.com
Subject: Test with Received header
Date: Thu, 04 Sep 2026 10:00:00 +0000
Message-ID: <received@example.com>
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii

Hello world.

minimal.eml (237 bytes) — identical but with the Received: header removed, allocates 516 MB:

From: sender@example.com
To: recipient@example.com
Subject: Minimal test message
Date: Thu, 04 Sep 2026 10:00:00 +0000
Message-ID: <minimal@example.com>
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii

Hello world.
byte[] bytes = File.ReadAllBytes("with-received.eml");

// Detection
using (MemoryStream stream = new MemoryStream(bytes, writable: false))
{
    long before = GC.GetTotalAllocatedBytes(precise: true);
    _ = FileFormatUtil.DetectFileFormat(stream).FileFormatType;
    Console.WriteLine($"DetectFileFormat : {GC.GetTotalAllocatedBytes(precise: true) - before:N0} bytes");
}

// Parsing the same stream, for comparison
using (MemoryStream stream = new MemoryStream(bytes, writable: false))
{
    long before = GC.GetTotalAllocatedBytes(precise: true);
    _ = MailMessage.Load(stream);
    Console.WriteLine($"MailMessage.Load : {GC.GetTotalAllocatedBytes(precise: true) - before:N0} bytes");
}

Timings

Each stage runs once un-measured (to absorb JIT) then three measured runs:

sample stage warm ms
with-received.eml (373 B) DetectFileFormat 12 / 1691 / 1889
with-received.eml (373 B) MailMessage.Load 1 / 1 / 5
3020.eml (28 KB) DetectFileFormat 23 / 1521 / 1975
3020.eml (28 KB) MailMessage.Load 6 / 6 / 6

The cost grows as the process accumulates these arrays. The first such allocation takes ~12-23 ms (the OS hands out zero pages lazily). Once a few exist, the same call takes 1.5-2.0 seconds.

Call stack

Captured from a production process dump (obfuscated frames are Aspose internals):

Interop.Kernel32.ReadFile(SafeHandle, Byte*, Int32, Int32 ByRef, NativeOverlapped*)
System.IO.Strategies.OSFileStreamStrategy.Read(Span<Byte>)
System.IO.Strategies.BufferedFileStreamStrategy.ReadSpan(Span<Byte>, ArraySegment<Byte>)
System.IO.FileStream.Read(Span<Byte>)
System.IO.BinaryReader.ReadBytes(Int32)          <-- allocates new byte[count] up front
XX.v()
UX..ctor(XX)
Aspose.Email.Tools.FileFormatUtil.c(Stream)
Aspose.Email.Tools.FileFormatUtil.DetectFileFormat(Stream)

BinaryReader.ReadBytes(int count) allocates new byte[count] before reading anything, then trims the result if fewer bytes were available. So passing a count of ~1.9e9 allocates the full 1.85 GiB regardless of how short the stream actually is. It looks like the count is not clamped to the remaining stream length.

In the dump the single byte[] is exactly 1,986,618,747 bytes, which appeared five times over.

Impact

In our service this runs per message during text extraction. Each .eml puts a ~1.85 GiB array on its own dedicated Large Object Heap segment. Under sustained load the process plateaus at ~11.8 GB (six such segments), and 20 TB was allocated over 5.5 hours to process ~10,400 small messages.

A dump taken mid-load showed 5 of these arrays live — 9.93 GB of a 12.05 GB managed heap in five objects — with 4 of the 5 already unreachable. The rest of the application accounted for ~130 MB.

The GC does keep up, so this is a peak/throughput problem rather than a leak, but the plateau starves everything else on the machine.

Questions

  1. Is the read length in DetectFileFormat intended to be unbounded, or should it be clamped to the remaining stream length? A short stream should not require a ~1.9e9-element buffer.
  2. Why would the presence of a Received: header change the buffer size from 516 MB to 1.85 GiB?
  3. Can a fix be scheduled? This is currently a live production issue for us.

Thanks.
aspose-email-detectfileformat-repro.zip (76.2 KB)

@hemalp
Thanks for the detailed report.
We have opened the following new ticket(s) in our internal issue tracking system and will deliver their fixes according to the terms mentioned in Free Support Policies.

Issue ID(s): EMAILNET-41834

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.

Hi @hemalp,
After reviewing, we found that this bug has been fixed in version 26.6 AsposeEmail. Please check and provide feedback.

Thanks — confirmed fixed in 26.6.

I verified it by swapping only Aspose.Email.dll and calling FileFormatUtil.DetectFileFormat directly on a MemoryStream, so nothing else in our stack is involved. Same 373-byte with-received.eml from the original report:

version released DetectFileFormat allocates
26.3.0 27 Mar 2026 1,986,721,987 bytes
26.4.0 08 Apr 2026 1,986,721,987 bytes
26.5.0 29 May 2026 1,986,721,987 bytes
26.6.0 29 Jun 2026 99,860 bytes
26.7.0 30 Jul 2026 99,871 bytes

Byte-identical on 26.3 / 26.4 / 26.5, then a discrete ~19,900x drop at 26.6. Our real-world sample behaves the same way: 3020.eml (28.31 KB) goes from 1,986,960,886 bytes on 26.3.0 to 289,147 bytes on 26.6.0. So EMAILNET-41834 is genuinely fixed, and the fix is effective.

One question. Our licence allows free upgrades until 23 May 2026, and 26.6.0 was released on 29 Jun 2026, so it is rejected:

That makes 26.4.0 the newest version we can currently run, and 26.4.0 still has the bug.

Is a backport of EMAILNET-41834 to a 26.3.x or 26.4.x hotfix build possible?

Happy to verify any build you produce — our harness swaps a single DLL and measures, so additional builds are cheap for us to test.

Thanks.

Hello @hemalp,

Thanks for the detailed verification.

Regarding the backport to 26.3.x/26.4.x, I’ll check this with our developers. Since the fix is already available in 26.6, we need to confirm whether it can be safely backported to an older branch and provided as a hotfix build.

I’ll get back to you once I have an update from the developers.