blob: 0647133c5d1cd153b6753ea458617f6bf676b01b [file] [log] [blame]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +01001<?php
2
3declare(strict_types=1);
4
5namespace Ddeboer\Imap\Message;
6
7use Ddeboer\Imap\Exception\ImapFetchbodyException;
8use Ddeboer\Imap\Exception\UnexpectedEncodingException;
9use Ddeboer\Imap\ImapResourceInterface;
10use Ddeboer\Imap\Message;
11
12/**
13 * A message part.
14 */
15abstract class AbstractPart implements PartInterface
16{
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020017 private const TYPES_MAP = [
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010018 \TYPETEXT => self::TYPE_TEXT,
19 \TYPEMULTIPART => self::TYPE_MULTIPART,
20 \TYPEMESSAGE => self::TYPE_MESSAGE,
21 \TYPEAPPLICATION => self::TYPE_APPLICATION,
22 \TYPEAUDIO => self::TYPE_AUDIO,
23 \TYPEIMAGE => self::TYPE_IMAGE,
24 \TYPEVIDEO => self::TYPE_VIDEO,
25 \TYPEMODEL => self::TYPE_MODEL,
26 \TYPEOTHER => self::TYPE_OTHER,
27 ];
28
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020029 private const ENCODINGS_MAP = [
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010030 \ENC7BIT => self::ENCODING_7BIT,
31 \ENC8BIT => self::ENCODING_8BIT,
32 \ENCBINARY => self::ENCODING_BINARY,
33 \ENCBASE64 => self::ENCODING_BASE64,
34 \ENCQUOTEDPRINTABLE => self::ENCODING_QUOTED_PRINTABLE,
35 ];
36
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020037 private const ATTACHMENT_KEYS = [
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010038 'name' => true,
39 'filename' => true,
40 'name*' => true,
41 'filename*' => true,
42 ];
43
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020044 protected ImapResourceInterface $resource;
45 private bool $structureParsed = false;
46 /**
47 * @var AbstractPart[]
48 */
49 private array $parts = [];
50 private string $partNumber;
51 private int $messageNumber;
52 private \stdClass $structure;
53 private Parameters $parameters;
54 private ?string $type = null;
55 private ?string $subtype = null;
56 private ?string $encoding = null;
57 private ?string $disposition = null;
58 private ?string $description = null;
59 /** @var null|int|string */
60 private $bytes;
61 private ?string $lines = null;
62 private ?string $content = null;
63 private ?string $decodedContent = null;
64 private int $key = 0;
65
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010066 /**
67 * Constructor.
68 *
69 * @param ImapResourceInterface $resource IMAP resource
70 * @param int $messageNumber Message number
71 * @param string $partNumber Part number
72 * @param \stdClass $structure Part structure
73 */
74 public function __construct(
75 ImapResourceInterface $resource,
76 int $messageNumber,
77 string $partNumber,
78 \stdClass $structure
79 ) {
80 $this->resource = $resource;
81 $this->messageNumber = $messageNumber;
82 $this->partNumber = $partNumber;
83 $this->setStructure($structure);
84 }
85
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +010086 final public function getNumber(): int
87 {
88 $this->assertMessageExists($this->messageNumber);
89
90 return $this->messageNumber;
91 }
92
93 /**
94 * Ensure message exists.
95 */
96 protected function assertMessageExists(int $messageNumber): void
97 {
98 }
99
100 /**
101 * @param \stdClass $structure Part structure
102 */
103 final protected function setStructure(\stdClass $structure): void
104 {
105 $this->structure = $structure;
106 }
107
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100108 final public function getStructure(): \stdClass
109 {
110 $this->lazyLoadStructure();
111
112 return $this->structure;
113 }
114
115 /**
116 * Lazy load structure.
117 */
118 protected function lazyLoadStructure(): void
119 {
120 }
121
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100122 final public function getParameters(): Parameters
123 {
124 $this->lazyParseStructure();
125
126 return $this->parameters;
127 }
128
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100129 final public function getCharset(): ?string
130 {
131 $this->lazyParseStructure();
132
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200133 $charset = $this->parameters->get('charset');
134 \assert(null === $charset || \is_string($charset));
135
136 return '' !== $charset ? $charset : null;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100137 }
138
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100139 final public function getType(): ?string
140 {
141 $this->lazyParseStructure();
142
143 return $this->type;
144 }
145
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100146 final public function getSubtype(): ?string
147 {
148 $this->lazyParseStructure();
149
150 return $this->subtype;
151 }
152
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100153 final public function getEncoding(): ?string
154 {
155 $this->lazyParseStructure();
156
157 return $this->encoding;
158 }
159
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100160 final public function getDisposition(): ?string
161 {
162 $this->lazyParseStructure();
163
164 return $this->disposition;
165 }
166
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100167 final public function getDescription(): ?string
168 {
169 $this->lazyParseStructure();
170
171 return $this->description;
172 }
173
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100174 final public function getBytes()
175 {
176 $this->lazyParseStructure();
177
178 return $this->bytes;
179 }
180
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100181 final public function getLines(): ?string
182 {
183 $this->lazyParseStructure();
184
185 return $this->lines;
186 }
187
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100188 final public function getContent(): string
189 {
190 if (null === $this->content) {
191 $this->content = $this->doGetContent($this->getContentPartNumber());
192 }
193
194 return $this->content;
195 }
196
197 /**
198 * Get content part number.
199 */
200 protected function getContentPartNumber(): string
201 {
202 return $this->partNumber;
203 }
204
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100205 final public function getPartNumber(): string
206 {
207 return $this->partNumber;
208 }
209
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100210 final public function getDecodedContent(): string
211 {
212 if (null === $this->decodedContent) {
213 if (self::ENCODING_UNKNOWN === $this->getEncoding()) {
214 throw new UnexpectedEncodingException('Cannot decode a content with an uknown encoding');
215 }
216
217 $content = $this->getContent();
218 if (self::ENCODING_BASE64 === $this->getEncoding()) {
219 $content = \base64_decode($content, false);
220 } elseif (self::ENCODING_QUOTED_PRINTABLE === $this->getEncoding()) {
221 $content = \quoted_printable_decode($content);
222 }
223
224 if (false === $content) {
225 throw new UnexpectedEncodingException('Cannot decode content');
226 }
227
228 // If this part is a text part, convert its charset to UTF-8.
229 // We don't want to decode an attachment's charset.
230 if (!$this instanceof Attachment && null !== $this->getCharset() && self::TYPE_TEXT === $this->getType()) {
231 $content = Transcoder::decode($content, $this->getCharset());
232 }
233
234 $this->decodedContent = $content;
235 }
236
237 return $this->decodedContent;
238 }
239
240 /**
241 * Get raw message content.
242 */
243 final protected function doGetContent(string $partNumber): string
244 {
245 $return = \imap_fetchbody(
246 $this->resource->getStream(),
247 $this->getNumber(),
248 $partNumber,
249 \FT_UID | \FT_PEEK
250 );
251
252 if (false === $return) {
253 throw new ImapFetchbodyException('imap_fetchbody failed');
254 }
255
256 return $return;
257 }
258
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100259 final public function getParts(): array
260 {
261 $this->lazyParseStructure();
262
263 return $this->parts;
264 }
265
266 /**
267 * Get current child part.
268 *
269 * @return mixed
270 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100271 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100272 final public function current()
273 {
274 $this->lazyParseStructure();
275
276 return $this->parts[$this->key];
277 }
278
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100279 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100280 final public function getChildren()
281 {
282 return $this->current();
283 }
284
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100285 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100286 final public function hasChildren()
287 {
288 $this->lazyParseStructure();
289
290 return \count($this->parts) > 0;
291 }
292
293 /**
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100294 * @return int
295 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100296 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100297 final public function key()
298 {
299 return $this->key;
300 }
301
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100302 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100303 final public function next()
304 {
305 ++$this->key;
306 }
307
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100308 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100309 final public function rewind()
310 {
311 $this->key = 0;
312 }
313
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100314 #[\ReturnTypeWillChange]
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100315 final public function valid()
316 {
317 $this->lazyParseStructure();
318
319 return isset($this->parts[$this->key]);
320 }
321
322 /**
323 * Parse part structure.
324 */
325 private function lazyParseStructure(): void
326 {
327 if (true === $this->structureParsed) {
328 return;
329 }
330 $this->structureParsed = true;
331
332 $this->lazyLoadStructure();
333
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200334 $this->type = self::TYPES_MAP[$this->structure->type] ?? self::TYPE_UNKNOWN;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100335
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100336 // In our context, \ENCOTHER is as useful as an unknown encoding
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200337 $this->encoding = self::ENCODINGS_MAP[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100338 if (isset($this->structure->subtype)) {
339 $this->subtype = $this->structure->subtype;
340 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100341
342 if (isset($this->structure->bytes)) {
343 $this->bytes = $this->structure->bytes;
344 }
345 if ($this->structure->ifdisposition) {
346 $this->disposition = $this->structure->disposition;
347 }
348 if ($this->structure->ifdescription) {
349 $this->description = $this->structure->description;
350 }
351
352 $this->parameters = new Parameters();
353 if ($this->structure->ifparameters) {
354 $this->parameters->add($this->structure->parameters);
355 }
356
357 if ($this->structure->ifdparameters) {
358 $this->parameters->add($this->structure->dparameters);
359 }
360
361 // When the message is not multipart and the body is the attachment content
362 // Prevents infinite recursion
363 if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
364 $this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
365 }
366
367 if (isset($this->structure->parts)) {
368 $parts = $this->structure->parts;
369 // https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
370 if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
371 $parts = $parts[0]->parts;
372 }
373 foreach ($parts as $key => $partStructure) {
374 $partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
375 $partNumber .= (string) ($key + 1);
376
377 $newPartClass = self::isAttachment($partStructure)
378 ? Attachment::class
379 : SimplePart::class
380 ;
381
382 $this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
383 }
384 }
385 }
386
387 /**
388 * Check if the given part is an attachment.
389 */
390 private static function isAttachment(\stdClass $part): bool
391 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200392 if (isset(self::TYPES_MAP[$part->type]) && self::TYPE_MULTIPART === self::TYPES_MAP[$part->type]) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100393 return false;
394 }
395
396 // Attachment with correct Content-Disposition header
397 if ($part->ifdisposition) {
398 if ('attachment' === \strtolower($part->disposition)) {
399 return true;
400 }
401
402 if (
403 'inline' === \strtolower($part->disposition)
404 && self::SUBTYPE_PLAIN !== \strtoupper($part->subtype)
405 && self::SUBTYPE_HTML !== \strtoupper($part->subtype)
406 ) {
407 return true;
408 }
409 }
410
411 // Attachment without Content-Disposition header
412 if ($part->ifparameters) {
413 foreach ($part->parameters as $parameter) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200414 if (isset(self::ATTACHMENT_KEYS[\strtolower($parameter->attribute)])) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100415 return true;
416 }
417 }
418 }
419
420 /*
421 if ($part->ifdparameters) {
422 foreach ($part->dparameters as $parameter) {
423 if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
424 return true;
425 }
426 }
427 }
428 */
429
430 if (self::SUBTYPE_RFC822 === \strtoupper($part->subtype)) {
431 return true;
432 }
433
434 return false;
435 }
436}