blob: ab13eb0295033acafa52d9a19dc4b829062d2800 [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{
17 /**
18 * @var ImapResourceInterface
19 */
20 protected $resource;
21
22 /**
23 * @var bool
24 */
25 private $structureParsed = false;
26
27 /**
28 * @var array
29 */
30 private $parts = [];
31
32 /**
33 * @var string
34 */
35 private $partNumber;
36
37 /**
38 * @var int
39 */
40 private $messageNumber;
41
42 /**
43 * @var \stdClass
44 */
45 private $structure;
46
47 /**
48 * @var Parameters
49 */
50 private $parameters;
51
52 /**
53 * @var null|string
54 */
55 private $type;
56
57 /**
58 * @var null|string
59 */
60 private $subtype;
61
62 /**
63 * @var null|string
64 */
65 private $encoding;
66
67 /**
68 * @var null|string
69 */
70 private $disposition;
71
72 /**
73 * @var null|string
74 */
75 private $description;
76
77 /**
78 * @var null|string
79 */
80 private $bytes;
81
82 /**
83 * @var null|string
84 */
85 private $lines;
86
87 /**
88 * @var null|string
89 */
90 private $content;
91
92 /**
93 * @var null|string
94 */
95 private $decodedContent;
96
97 /**
98 * @var int
99 */
100 private $key = 0;
101
102 /**
103 * @var array
104 */
105 private static $typesMap = [
106 \TYPETEXT => self::TYPE_TEXT,
107 \TYPEMULTIPART => self::TYPE_MULTIPART,
108 \TYPEMESSAGE => self::TYPE_MESSAGE,
109 \TYPEAPPLICATION => self::TYPE_APPLICATION,
110 \TYPEAUDIO => self::TYPE_AUDIO,
111 \TYPEIMAGE => self::TYPE_IMAGE,
112 \TYPEVIDEO => self::TYPE_VIDEO,
113 \TYPEMODEL => self::TYPE_MODEL,
114 \TYPEOTHER => self::TYPE_OTHER,
115 ];
116
117 /**
118 * @var array
119 */
120 private static $encodingsMap = [
121 \ENC7BIT => self::ENCODING_7BIT,
122 \ENC8BIT => self::ENCODING_8BIT,
123 \ENCBINARY => self::ENCODING_BINARY,
124 \ENCBASE64 => self::ENCODING_BASE64,
125 \ENCQUOTEDPRINTABLE => self::ENCODING_QUOTED_PRINTABLE,
126 ];
127
128 /**
129 * @var array
130 */
131 private static $attachmentKeys = [
132 'name' => true,
133 'filename' => true,
134 'name*' => true,
135 'filename*' => true,
136 ];
137
138 /**
139 * Constructor.
140 *
141 * @param ImapResourceInterface $resource IMAP resource
142 * @param int $messageNumber Message number
143 * @param string $partNumber Part number
144 * @param \stdClass $structure Part structure
145 */
146 public function __construct(
147 ImapResourceInterface $resource,
148 int $messageNumber,
149 string $partNumber,
150 \stdClass $structure
151 ) {
152 $this->resource = $resource;
153 $this->messageNumber = $messageNumber;
154 $this->partNumber = $partNumber;
155 $this->setStructure($structure);
156 }
157
158 /**
159 * Get message number (from headers).
160 */
161 final public function getNumber(): int
162 {
163 $this->assertMessageExists($this->messageNumber);
164
165 return $this->messageNumber;
166 }
167
168 /**
169 * Ensure message exists.
170 */
171 protected function assertMessageExists(int $messageNumber): void
172 {
173 }
174
175 /**
176 * @param \stdClass $structure Part structure
177 */
178 final protected function setStructure(\stdClass $structure): void
179 {
180 $this->structure = $structure;
181 }
182
183 /**
184 * Part structure.
185 */
186 final public function getStructure(): \stdClass
187 {
188 $this->lazyLoadStructure();
189
190 return $this->structure;
191 }
192
193 /**
194 * Lazy load structure.
195 */
196 protected function lazyLoadStructure(): void
197 {
198 }
199
200 /**
201 * Part parameters.
202 */
203 final public function getParameters(): Parameters
204 {
205 $this->lazyParseStructure();
206
207 return $this->parameters;
208 }
209
210 /**
211 * Part charset.
212 */
213 final public function getCharset(): ?string
214 {
215 $this->lazyParseStructure();
216
217 return $this->parameters->get('charset') ?: null;
218 }
219
220 /**
221 * Part type.
222 */
223 final public function getType(): ?string
224 {
225 $this->lazyParseStructure();
226
227 return $this->type;
228 }
229
230 /**
231 * Part subtype.
232 */
233 final public function getSubtype(): ?string
234 {
235 $this->lazyParseStructure();
236
237 return $this->subtype;
238 }
239
240 /**
241 * Part encoding.
242 */
243 final public function getEncoding(): ?string
244 {
245 $this->lazyParseStructure();
246
247 return $this->encoding;
248 }
249
250 /**
251 * Part disposition.
252 */
253 final public function getDisposition(): ?string
254 {
255 $this->lazyParseStructure();
256
257 return $this->disposition;
258 }
259
260 /**
261 * Part description.
262 */
263 final public function getDescription(): ?string
264 {
265 $this->lazyParseStructure();
266
267 return $this->description;
268 }
269
270 /**
271 * Part bytes.
272 *
273 * @return null|int|string
274 */
275 final public function getBytes()
276 {
277 $this->lazyParseStructure();
278
279 return $this->bytes;
280 }
281
282 /**
283 * Part lines.
284 */
285 final public function getLines(): ?string
286 {
287 $this->lazyParseStructure();
288
289 return $this->lines;
290 }
291
292 /**
293 * Get raw part content.
294 */
295 final public function getContent(): string
296 {
297 if (null === $this->content) {
298 $this->content = $this->doGetContent($this->getContentPartNumber());
299 }
300
301 return $this->content;
302 }
303
304 /**
305 * Get content part number.
306 */
307 protected function getContentPartNumber(): string
308 {
309 return $this->partNumber;
310 }
311
312 /**
313 * Get part number.
314 */
315 final public function getPartNumber(): string
316 {
317 return $this->partNumber;
318 }
319
320 /**
321 * Get decoded part content.
322 */
323 final public function getDecodedContent(): string
324 {
325 if (null === $this->decodedContent) {
326 if (self::ENCODING_UNKNOWN === $this->getEncoding()) {
327 throw new UnexpectedEncodingException('Cannot decode a content with an uknown encoding');
328 }
329
330 $content = $this->getContent();
331 if (self::ENCODING_BASE64 === $this->getEncoding()) {
332 $content = \base64_decode($content, false);
333 } elseif (self::ENCODING_QUOTED_PRINTABLE === $this->getEncoding()) {
334 $content = \quoted_printable_decode($content);
335 }
336
337 if (false === $content) {
338 throw new UnexpectedEncodingException('Cannot decode content');
339 }
340
341 // If this part is a text part, convert its charset to UTF-8.
342 // We don't want to decode an attachment's charset.
343 if (!$this instanceof Attachment && null !== $this->getCharset() && self::TYPE_TEXT === $this->getType()) {
344 $content = Transcoder::decode($content, $this->getCharset());
345 }
346
347 $this->decodedContent = $content;
348 }
349
350 return $this->decodedContent;
351 }
352
353 /**
354 * Get raw message content.
355 */
356 final protected function doGetContent(string $partNumber): string
357 {
358 $return = \imap_fetchbody(
359 $this->resource->getStream(),
360 $this->getNumber(),
361 $partNumber,
362 \FT_UID | \FT_PEEK
363 );
364
365 if (false === $return) {
366 throw new ImapFetchbodyException('imap_fetchbody failed');
367 }
368
369 return $return;
370 }
371
372 /**
373 * Get an array of all parts for this message.
374 *
375 * @return PartInterface[]
376 */
377 final public function getParts(): array
378 {
379 $this->lazyParseStructure();
380
381 return $this->parts;
382 }
383
384 /**
385 * Get current child part.
386 *
387 * @return mixed
388 */
389 final public function current()
390 {
391 $this->lazyParseStructure();
392
393 return $this->parts[$this->key];
394 }
395
396 /**
397 * Get current child part.
398 *
399 * @return \RecursiveIterator
400 */
401 final public function getChildren()
402 {
403 return $this->current();
404 }
405
406 /**
407 * Get current child part.
408 *
409 * @return bool
410 */
411 final public function hasChildren()
412 {
413 $this->lazyParseStructure();
414
415 return \count($this->parts) > 0;
416 }
417
418 /**
419 * Get current part key.
420 *
421 * @return int
422 */
423 final public function key()
424 {
425 return $this->key;
426 }
427
428 /**
429 * Move to next part.
430 *
431 * @return void
432 */
433 final public function next()
434 {
435 ++$this->key;
436 }
437
438 /**
439 * Reset part key.
440 *
441 * @return void
442 */
443 final public function rewind()
444 {
445 $this->key = 0;
446 }
447
448 /**
449 * Check if current part is a valid one.
450 *
451 * @return bool
452 */
453 final public function valid()
454 {
455 $this->lazyParseStructure();
456
457 return isset($this->parts[$this->key]);
458 }
459
460 /**
461 * Parse part structure.
462 */
463 private function lazyParseStructure(): void
464 {
465 if (true === $this->structureParsed) {
466 return;
467 }
468 $this->structureParsed = true;
469
470 $this->lazyLoadStructure();
471
472 $this->type = self::$typesMap[$this->structure->type] ?? self::TYPE_UNKNOWN;
473
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100474 // In our context, \ENCOTHER is as useful as an unknown encoding
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100475 $this->encoding = self::$encodingsMap[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100476 if (isset($this->structure->subtype)) {
477 $this->subtype = $this->structure->subtype;
478 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100479
480 if (isset($this->structure->bytes)) {
481 $this->bytes = $this->structure->bytes;
482 }
483 if ($this->structure->ifdisposition) {
484 $this->disposition = $this->structure->disposition;
485 }
486 if ($this->structure->ifdescription) {
487 $this->description = $this->structure->description;
488 }
489
490 $this->parameters = new Parameters();
491 if ($this->structure->ifparameters) {
492 $this->parameters->add($this->structure->parameters);
493 }
494
495 if ($this->structure->ifdparameters) {
496 $this->parameters->add($this->structure->dparameters);
497 }
498
499 // When the message is not multipart and the body is the attachment content
500 // Prevents infinite recursion
501 if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
502 $this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
503 }
504
505 if (isset($this->structure->parts)) {
506 $parts = $this->structure->parts;
507 // https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
508 if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
509 $parts = $parts[0]->parts;
510 }
511 foreach ($parts as $key => $partStructure) {
512 $partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
513 $partNumber .= (string) ($key + 1);
514
515 $newPartClass = self::isAttachment($partStructure)
516 ? Attachment::class
517 : SimplePart::class
518 ;
519
520 $this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
521 }
522 }
523 }
524
525 /**
526 * Check if the given part is an attachment.
527 */
528 private static function isAttachment(\stdClass $part): bool
529 {
530 if (isset(self::$typesMap[$part->type]) && self::TYPE_MULTIPART === self::$typesMap[$part->type]) {
531 return false;
532 }
533
534 // Attachment with correct Content-Disposition header
535 if ($part->ifdisposition) {
536 if ('attachment' === \strtolower($part->disposition)) {
537 return true;
538 }
539
540 if (
541 'inline' === \strtolower($part->disposition)
542 && self::SUBTYPE_PLAIN !== \strtoupper($part->subtype)
543 && self::SUBTYPE_HTML !== \strtoupper($part->subtype)
544 ) {
545 return true;
546 }
547 }
548
549 // Attachment without Content-Disposition header
550 if ($part->ifparameters) {
551 foreach ($part->parameters as $parameter) {
552 if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
553 return true;
554 }
555 }
556 }
557
558 /*
559 if ($part->ifdparameters) {
560 foreach ($part->dparameters as $parameter) {
561 if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
562 return true;
563 }
564 }
565 }
566 */
567
568 if (self::SUBTYPE_RFC822 === \strtoupper($part->subtype)) {
569 return true;
570 }
571
572 return false;
573 }
574}