blob: 0ab0ca584832169123a840b470b9313baa13c1af [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 */
271 final public function current()
272 {
273 $this->lazyParseStructure();
274
275 return $this->parts[$this->key];
276 }
277
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100278 final public function getChildren()
279 {
280 return $this->current();
281 }
282
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100283 final public function hasChildren()
284 {
285 $this->lazyParseStructure();
286
287 return \count($this->parts) > 0;
288 }
289
290 /**
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100291 * @return int
292 */
293 final public function key()
294 {
295 return $this->key;
296 }
297
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100298 final public function next()
299 {
300 ++$this->key;
301 }
302
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100303 final public function rewind()
304 {
305 $this->key = 0;
306 }
307
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100308 final public function valid()
309 {
310 $this->lazyParseStructure();
311
312 return isset($this->parts[$this->key]);
313 }
314
315 /**
316 * Parse part structure.
317 */
318 private function lazyParseStructure(): void
319 {
320 if (true === $this->structureParsed) {
321 return;
322 }
323 $this->structureParsed = true;
324
325 $this->lazyLoadStructure();
326
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200327 $this->type = self::TYPES_MAP[$this->structure->type] ?? self::TYPE_UNKNOWN;
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100328
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100329 // In our context, \ENCOTHER is as useful as an unknown encoding
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200330 $this->encoding = self::ENCODINGS_MAP[$this->structure->encoding] ?? self::ENCODING_UNKNOWN;
Matthias Andreas Benkarde39c4f82021-01-06 17:59:39 +0100331 if (isset($this->structure->subtype)) {
332 $this->subtype = $this->structure->subtype;
333 }
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100334
335 if (isset($this->structure->bytes)) {
336 $this->bytes = $this->structure->bytes;
337 }
338 if ($this->structure->ifdisposition) {
339 $this->disposition = $this->structure->disposition;
340 }
341 if ($this->structure->ifdescription) {
342 $this->description = $this->structure->description;
343 }
344
345 $this->parameters = new Parameters();
346 if ($this->structure->ifparameters) {
347 $this->parameters->add($this->structure->parameters);
348 }
349
350 if ($this->structure->ifdparameters) {
351 $this->parameters->add($this->structure->dparameters);
352 }
353
354 // When the message is not multipart and the body is the attachment content
355 // Prevents infinite recursion
356 if (self::isAttachment($this->structure) && !$this instanceof Attachment) {
357 $this->parts[] = new Attachment($this->resource, $this->getNumber(), '1', $this->structure);
358 }
359
360 if (isset($this->structure->parts)) {
361 $parts = $this->structure->parts;
362 // https://secure.php.net/manual/en/function.imap-fetchbody.php#89002
363 if ($this instanceof Attachment && $this->isEmbeddedMessage() && 1 === \count($parts) && \TYPEMULTIPART === $parts[0]->type) {
364 $parts = $parts[0]->parts;
365 }
366 foreach ($parts as $key => $partStructure) {
367 $partNumber = (!$this instanceof Message) ? $this->partNumber . '.' : '';
368 $partNumber .= (string) ($key + 1);
369
370 $newPartClass = self::isAttachment($partStructure)
371 ? Attachment::class
372 : SimplePart::class
373 ;
374
375 $this->parts[] = new $newPartClass($this->resource, $this->getNumber(), $partNumber, $partStructure);
376 }
377 }
378 }
379
380 /**
381 * Check if the given part is an attachment.
382 */
383 private static function isAttachment(\stdClass $part): bool
384 {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200385 if (isset(self::TYPES_MAP[$part->type]) && self::TYPE_MULTIPART === self::TYPES_MAP[$part->type]) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100386 return false;
387 }
388
389 // Attachment with correct Content-Disposition header
390 if ($part->ifdisposition) {
391 if ('attachment' === \strtolower($part->disposition)) {
392 return true;
393 }
394
395 if (
396 'inline' === \strtolower($part->disposition)
397 && self::SUBTYPE_PLAIN !== \strtoupper($part->subtype)
398 && self::SUBTYPE_HTML !== \strtoupper($part->subtype)
399 ) {
400 return true;
401 }
402 }
403
404 // Attachment without Content-Disposition header
405 if ($part->ifparameters) {
406 foreach ($part->parameters as $parameter) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200407 if (isset(self::ATTACHMENT_KEYS[\strtolower($parameter->attribute)])) {
Matthias Andreas Benkardb382b102021-01-02 15:32:21 +0100408 return true;
409 }
410 }
411 }
412
413 /*
414 if ($part->ifdparameters) {
415 foreach ($part->dparameters as $parameter) {
416 if (isset(self::$attachmentKeys[\strtolower($parameter->attribute)])) {
417 return true;
418 }
419 }
420 }
421 */
422
423 if (self::SUBTYPE_RFC822 === \strtoupper($part->subtype)) {
424 return true;
425 }
426
427 return false;
428 }
429}