Skip to content

[2.x] Add partial body fetching and chunked attachment streams - #189

Merged
stevebauman merged 1 commit into
v2.0from
feature/v2-partial-body-fetching
Sep 28, 2026
Merged

stevebauman merged 1 commit into
v2.0from
feature/v2-partial-body-fetching

Conversation

@stevebauman

Copy link
Copy Markdown
Member

Fetching a small part of a large message or saving an attachment currently requires downloading the entire body section into memory.

This PR adds partial body fetching and chunked attachment streams so applications can retrieve only the bytes they need and save large attachments without holding the complete contents in memory.

Partial body fetching

Body fetch items accept an immutable partial() modifier, which can be combined with peek():

use DirectoryTree\ImapEngine\MessageData;

$messages = $folder->messages()
    ->with(MessageData::text()->partial(0, 4096)->peek())
    ->get();

$preview = $messages->first()->data()->get('BODY[TEXT]<0>');

Individual message sections can also be fetched directly:

$chunk = $message->bodyPart('2', offset: 65536, length: 65536);

Offsets are zero-based and refer to transfer-encoded bytes, before base64 or quoted-printable decoding. Responses retain their offsets, and partial fetches do not replace cached complete body parts. An empty string indicates the end of a section; a missing message or section returns null.

Attachment streaming

Attachments obtained through attachments(fetch: true) now fetch their contents in 64 KiB chunks as the stream is read. Base64 and quoted-printable decoding preserves state across chunk boundaries. Decoded content is cached in a temporary stream that spills to disk beyond 2 MiB, allowing seeks and repeated reads without fetching the same bytes again.

Attachment::save() copies stream chunks directly to the destination:

$attachment = $message->attachments(fetch: true)[0];
$attachment->save('/path/to/attachment.pdf');

Calling contents() or casting a stream to a string still loads the requested contents into memory. The decoded stream size remains unknown until the section has been fully read. Missing body parts throw rather than silently producing a truncated attachment.

Validated with the full unit suite: 847 tests passing. Formatting and diff checks pass. Live-server integration validation is blocked because the local IMAP server at localhost:3143 refuses connections.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Expanding the public interface method signature breaks existing third-party implementations on the 2.x release line.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds ranged IMAP body fetching and disk-backed, incrementally decoded attachment streams.

Changes:

  • Adds immutable partial body fetch modifiers and direct ranged section retrieval.
  • Streams and decodes attachments in 64 KiB chunks.
  • Adds unit, integration, and usage documentation coverage.
File Description
src/​Attachment.php Streams attachments when saving.
src/​FileMessage.php Updates body-part method signature.
src/​Message.php Implements ranged body fetching.
src/​MessageData/​Body.php Adds partial fetch modifiers.
src/​MessageInterface.php Expands the body-part API.
src/​Support/​LazyBodyPartStream.php Adds chunked fetching and decoding.
src/​Testing/​FakeMessage.php Aligns the test double signature.
tests/​Integration/​MessagesTest.php Tests live partial fetching.
tests/​Unit/​MessageTest.php Updates attachment fetch expectations.
tests/​Unit/​PartialBodyTest.php Tests ranges, streaming, decoding, and saving.
readme.md Documents partial fetching and streaming.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/MessageInterface.php Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The core behavior depends on real IMAP partial-response semantics, but live-server integration validation remains unavailable.

Review effort: Balanced
Findings: None

Resolved since last review (1)

@stevebauman
stevebauman merged commit b31bed2 into v2.0 Sep 28, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants