Skip to content

Commit 3689445

Browse files
authored
Merge pull request #144 from DirectoryTree/bodystructure
Bodystructure Support
2 parents 8a109e0 + 3ed6940 commit 3689445

16 files changed

+1081
-8
lines changed

src/BodyStructureCollection.php

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
<?php
2+
3+
namespace DirectoryTree\ImapEngine;
4+
5+
use Countable;
6+
use DirectoryTree\ImapEngine\Connection\Responses\Data\ListData;
7+
use DirectoryTree\ImapEngine\Connection\Tokens\Nil;
8+
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
9+
use Illuminate\Contracts\Support\Arrayable;
10+
use IteratorAggregate;
11+
use JsonSerializable;
12+
use Traversable;
13+
14+
/**
15+
* @implements IteratorAggregate<int, BodyStructurePart|BodyStructureCollection>
16+
*/
17+
class BodyStructureCollection implements Arrayable, Countable, IteratorAggregate, JsonSerializable
18+
{
19+
/**
20+
* Constructor.
21+
*
22+
* @param array<BodyStructurePart|BodyStructureCollection> $parts
23+
*/
24+
public function __construct(
25+
protected string $subtype = 'mixed',
26+
protected array $parameters = [],
27+
protected array $parts = [],
28+
) {}
29+
30+
/**
31+
* Parse a multipart BODYSTRUCTURE ListData into a BodyStructureCollection.
32+
*/
33+
public static function fromListData(ListData $data, ?string $partNumber = null): static
34+
{
35+
$tokens = $data->tokens();
36+
37+
$parts = [];
38+
$childIndex = 1;
39+
$subtypeIndex = null;
40+
41+
foreach ($tokens as $index => $token) {
42+
if ($token instanceof Token && ! $token instanceof Nil) {
43+
$subtypeIndex = $index;
44+
45+
break;
46+
}
47+
48+
if (! $token instanceof ListData) {
49+
continue;
50+
}
51+
52+
$childPartNumber = $partNumber ? "{$partNumber}.{$childIndex}" : (string) $childIndex;
53+
54+
$parts[] = static::isMultipart($token)
55+
? static::fromListData($token, $childPartNumber)
56+
: BodyStructurePart::fromListData($token, $childPartNumber);
57+
58+
$childIndex++;
59+
}
60+
61+
$parameters = [];
62+
63+
if ($subtypeIndex) {
64+
foreach (array_slice($tokens, $subtypeIndex + 1) as $token) {
65+
if ($token instanceof ListData && ! static::isDispositionList($token)) {
66+
$parameters = $token->toKeyValuePairs();
67+
68+
break;
69+
}
70+
}
71+
}
72+
73+
return new static(
74+
$subtypeIndex ? strtolower($tokens[$subtypeIndex]->value) : 'mixed',
75+
$parameters,
76+
$parts
77+
);
78+
}
79+
80+
/**
81+
* Determine if a ListData represents a multipart structure.
82+
*/
83+
protected static function isMultipart(ListData $data): bool
84+
{
85+
return head($data->tokens()) instanceof ListData;
86+
}
87+
88+
/**
89+
* Determine if a ListData represents a disposition (INLINE or ATTACHMENT).
90+
*/
91+
protected static function isDispositionList(ListData $data): bool
92+
{
93+
$tokens = $data->tokens();
94+
95+
if (count($tokens) < 2 || ! isset($tokens[0]) || ! $tokens[0] instanceof Token) {
96+
return false;
97+
}
98+
99+
return in_array(strtoupper($tokens[0]->value), ['INLINE', 'ATTACHMENT']);
100+
}
101+
102+
/**
103+
* Get the multipart subtype (mixed, alternative, related, etc.).
104+
*/
105+
public function subtype(): string
106+
{
107+
return $this->subtype;
108+
}
109+
110+
/**
111+
* Get the content type.
112+
*/
113+
public function contentType(): string
114+
{
115+
return "multipart/{$this->subtype}";
116+
}
117+
118+
/**
119+
* Get the parameters (e.g., boundary).
120+
*/
121+
public function parameters(): array
122+
{
123+
return $this->parameters;
124+
}
125+
126+
/**
127+
* Get the boundary parameter.
128+
*/
129+
public function boundary(): ?string
130+
{
131+
return $this->parameters['boundary'] ?? null;
132+
}
133+
134+
/**
135+
* Get the direct child parts.
136+
*
137+
* @return array<BodyStructurePart|BodyStructureCollection>
138+
*/
139+
public function parts(): array
140+
{
141+
return $this->parts;
142+
}
143+
144+
/**
145+
* Get all parts flattened (including nested parts).
146+
*
147+
* @return BodyStructurePart[]
148+
*/
149+
public function flatten(): array
150+
{
151+
$flattened = [];
152+
153+
foreach ($this->parts as $part) {
154+
if ($part instanceof self) {
155+
$flattened = array_merge($flattened, $part->flatten());
156+
} else {
157+
$flattened[] = $part;
158+
}
159+
}
160+
161+
return $flattened;
162+
}
163+
164+
/**
165+
* Find a part by its part number.
166+
*/
167+
public function find(string $partNumber): BodyStructurePart|BodyStructureCollection|null
168+
{
169+
foreach ($this->parts as $part) {
170+
if ($part instanceof self) {
171+
if ($found = $part->find($partNumber)) {
172+
return $found;
173+
}
174+
} elseif ($part->partNumber() === $partNumber) {
175+
return $part;
176+
}
177+
}
178+
179+
return null;
180+
}
181+
182+
/**
183+
* Get the text/plain part if available.
184+
*/
185+
public function text(): ?BodyStructurePart
186+
{
187+
foreach ($this->flatten() as $part) {
188+
if ($part->isText()) {
189+
return $part;
190+
}
191+
}
192+
193+
return null;
194+
}
195+
196+
/**
197+
* Get the text/html part if available.
198+
*/
199+
public function html(): ?BodyStructurePart
200+
{
201+
foreach ($this->flatten() as $part) {
202+
if ($part->isHtml()) {
203+
return $part;
204+
}
205+
}
206+
207+
return null;
208+
}
209+
210+
/**
211+
* Get all attachment parts.
212+
*
213+
* @return BodyStructurePart[]
214+
*/
215+
public function attachments(): array
216+
{
217+
return array_values(array_filter(
218+
$this->flatten(),
219+
fn (BodyStructurePart $part) => $part->isAttachment()
220+
));
221+
}
222+
223+
/**
224+
* Determine if the collection has attachments.
225+
*/
226+
public function hasAttachments(): bool
227+
{
228+
return count($this->attachments()) > 0;
229+
}
230+
231+
/**
232+
* Get the count of attachments.
233+
*/
234+
public function attachmentCount(): int
235+
{
236+
return count($this->attachments());
237+
}
238+
239+
/**
240+
* Get the count of parts.
241+
*/
242+
public function count(): int
243+
{
244+
return count($this->parts);
245+
}
246+
247+
/**
248+
* Get an iterator for the parts.
249+
*/
250+
public function getIterator(): Traversable
251+
{
252+
yield from $this->parts;
253+
}
254+
255+
/**
256+
* Get the array representation.
257+
*/
258+
public function toArray(): array
259+
{
260+
return [
261+
'subtype' => $this->subtype,
262+
'parameters' => $this->parameters,
263+
'content_type' => $this->contentType(),
264+
'parts' => array_map(fn (Arrayable $part) => $part->toArray(), $this->parts),
265+
];
266+
}
267+
268+
/**
269+
* Get the JSON representation.
270+
*/
271+
public function jsonSerialize(): array
272+
{
273+
return $this->toArray();
274+
}
275+
}

0 commit comments

Comments
 (0)