generated from Nodarx/template
feat: implement download
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use DateTimeImmutable;
|
||||
use Generator;
|
||||
use KTXM\ProviderImap\Client\Client;
|
||||
use KTXM\ProviderImap\Client\Command\FetchManyCommand;
|
||||
use KTXM\ProviderImap\Client\Command\FetchOneCommand;
|
||||
use KTXM\ProviderImap\Client\Command\ExpungeCommand;
|
||||
use KTXM\ProviderImap\Client\Command\ListCommand;
|
||||
use KTXM\ProviderImap\Client\Command\SearchCommand;
|
||||
@@ -45,6 +46,7 @@ use KTXF\Resource\Range\IRangeTally;
|
||||
use KTXF\Resource\Range\RangeAnchorType;
|
||||
use KTXF\Resource\Range\RangeTally;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
use KTXF\Mail\Service\DownloadResult;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
|
||||
@@ -295,6 +297,116 @@ class RemoteMailService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream the raw bytes of a message or a specific MIME part without buffering.
|
||||
*
|
||||
* When $partId is given, first fetches BODYSTRUCTURE to determine the
|
||||
* correct filename and MIME type, then starts the streaming body fetch.
|
||||
*
|
||||
* @param string $collection Mailbox name
|
||||
* @param int $uid Message UID
|
||||
* @param string|null $partId MIME section (e.g. '1', '1.2'); null = full RFC 822
|
||||
*/
|
||||
public function entityDownload(string $collection, int $uid, ?string $partId = null): DownloadResult
|
||||
{
|
||||
$this->client->perform(new SelectCommand($collection, true));
|
||||
|
||||
$encoding = null;
|
||||
|
||||
if ($partId === null) {
|
||||
$filename = 'message.eml';
|
||||
$mimeType = 'message/rfc822';
|
||||
} else {
|
||||
// Fetch BODYSTRUCTURE first to determine metadata (no body bytes transferred)
|
||||
$message = $this->client->perform(new FetchOneCommand(
|
||||
FetchTarget::uid(SequenceSet::items($uid)),
|
||||
FetchOptions::of('BODYSTRUCTURE'),
|
||||
));
|
||||
$bodyStructure = $message->bodyStructure();
|
||||
$part = $bodyStructure !== null ? $this->findBodyPart($bodyStructure, $partId) : null;
|
||||
$mimeType = $part?->mimeType() ?? 'application/octet-stream';
|
||||
$partData = $part?->toArray() ?? [];
|
||||
$filename = isset($partData['name']) && $partData['name'] !== ''
|
||||
? $partData['name']
|
||||
: "attachment-{$partId}";
|
||||
$encoding = $part?->encoding();
|
||||
}
|
||||
|
||||
// Start download stream
|
||||
$stream = $this->decodeStream(
|
||||
$this->client->download(FetchTarget::uid(SequenceSet::items($uid)), $partId ?? ''),
|
||||
$encoding
|
||||
);
|
||||
|
||||
return new DownloadResult($filename, $mimeType, $stream);
|
||||
}
|
||||
|
||||
private function findBodyPart(MessagePart $root, string $partId): ?MessagePart
|
||||
{
|
||||
if ($root->partId() === $partId) {
|
||||
return $root;
|
||||
}
|
||||
foreach ($root->parts() as $child) {
|
||||
$found = $this->findBodyPart($child, $partId);
|
||||
if ($found !== null) {
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a raw IMAP body stream with a transfer-encoding decoder.
|
||||
*
|
||||
* IMAP BODY[n] literals are delivered in the transfer encoding declared
|
||||
* by BODYSTRUCTURE (typically base64 or quoted-printable for attachments).
|
||||
* 7bit / 8bit / binary sections pass through unchanged.
|
||||
*/
|
||||
private function decodeStream(\Generator $stream, ?string $encoding): \Generator
|
||||
{
|
||||
return match (strtolower($encoding ?? '')) {
|
||||
'base64' => $this->decodeBase64Stream($stream),
|
||||
'quoted-printable' => $this->decodeQpStream($stream),
|
||||
default => $stream,
|
||||
};
|
||||
}
|
||||
|
||||
private function decodeBase64Stream(\Generator $source): \Generator
|
||||
{
|
||||
$buffer = '';
|
||||
foreach ($source as $chunk) {
|
||||
// IMAP folds base64 at 76 chars with CRLF — strip all whitespace
|
||||
$buffer .= preg_replace('/\s+/', '', $chunk);
|
||||
// Decode complete 4-character groups; keep any partial tail
|
||||
$remainder = strlen($buffer) % 4;
|
||||
$complete = strlen($buffer) - $remainder;
|
||||
if ($complete > 0) {
|
||||
yield base64_decode(substr($buffer, 0, $complete), true);
|
||||
$buffer = substr($buffer, $complete);
|
||||
}
|
||||
}
|
||||
// Flush remainder (handles padded or stripped trailing '=')
|
||||
if ($buffer !== '') {
|
||||
yield base64_decode($buffer, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function decodeQpStream(\Generator $source): \Generator
|
||||
{
|
||||
// Buffer until we have complete lines so soft-line-breaks are intact
|
||||
$buffer = '';
|
||||
foreach ($source as $chunk) {
|
||||
$buffer .= $chunk;
|
||||
while (($pos = strpos($buffer, "\n")) !== false) {
|
||||
yield quoted_printable_decode(substr($buffer, 0, $pos + 1));
|
||||
$buffer = substr($buffer, $pos + 1);
|
||||
}
|
||||
}
|
||||
if ($buffer !== '') {
|
||||
yield quoted_printable_decode($buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a raw RFC 822 message to a mailbox and return the assigned UID.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user