Completed
Push — master ( cc370f...f8592e )
by Frederik
02:45 queued 01:07
created

MessageBodyCollection::getEmbeddedImages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail;
5
6
use Genkgo\Mail\Header\Cc;
7
use Genkgo\Mail\Header\ContentTransferEncoding;
8
use Genkgo\Mail\Header\ContentType;
9
use Genkgo\Mail\Header\GenericHeader;
10
use Genkgo\Mail\Header\HeaderName;
11
use Genkgo\Mail\Header\ParsedHeader;
12
use Genkgo\Mail\Header\Subject;
13
use Genkgo\Mail\Header\To;
14
use Genkgo\Mail\Mime\Boundary;
15
use Genkgo\Mail\Mime\EmbeddedImage;
16
use Genkgo\Mail\Mime\HtmlPart;
17
use Genkgo\Mail\Mime\MultiPart;
18
use Genkgo\Mail\Mime\MultiPartInterface;
19
use Genkgo\Mail\Mime\PartInterface;
20
use Genkgo\Mail\Mime\PlainTextPart;
21
use Genkgo\Mail\Mime\ResourceAttachment;
22
use Genkgo\Mail\Stream\Base64DecodedStream;
23
use Genkgo\Mail\Stream\MimeBodyDecodedStream;
24
use Genkgo\Mail\Stream\QuotedPrintableDecodedStream;
25
26
final class MessageBodyCollection
27
{
28
    /**
29
     * @var string
30
     */
31
    private $html = '';
32
33
    /**
34
     * @var AlternativeText
35
     */
36
    private $text;
37
38
    /**
39
     * @var array|PartInterface[]
40
     */
41
    private $attachments = [];
42
43
    /**
44
     * @var array|PartInterface[]
45
     */
46
    private $embedImages = [];
47
48
    /**
49
     * @param string $html
50
     */
51 37
    public function __construct(string $html = '')
52
    {
53 37
        $this->html = $html;
54 37
        $this->text = AlternativeText::fromHtml($html);
55 37
    }
56
57
    /**
58
     * @param string $html
59
     * @return MessageBodyCollection
60
     */
61 6
    public function withHtml(string $html): self
62
    {
63 6
        $clone = clone $this;
64 6
        $clone->html = $html;
65 6
        $clone->text = AlternativeText::fromHtml($html);
66 6
        return $clone;
67
    }
68
69
    /**
70
     * @param string $html
71
     * @return MessageBodyCollection
72
     */
73 16
    public function withHtmlAndNoGeneratedAlternativeText(string $html): self
74
    {
75 16
        $clone = clone $this;
76 16
        $clone->html = $html;
77 16
        return $clone;
78
    }
79
80
    /**
81
     * @param AlternativeText $text
82
     * @return MessageBodyCollection
83
     */
84 17
    public function withAlternativeText(AlternativeText $text): self
85
    {
86 17
        $clone = clone $this;
87 17
        $clone->text = $text;
88 17
        return $clone;
89
    }
90
91
    /**
92
     * @param PartInterface $part
93
     * @return MessageBodyCollection
94
     */
95 10
    public function withAttachment(PartInterface $part): self
96
    {
97
        try {
98 10
            $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
99 9
            if ($disposition !== 'attachment') {
100 1
                throw new \InvalidArgumentException(
101 9
                    'An attachment must have Content-Disposition header with value `attachment`'
102
                );
103
            }
104 2
        } catch (\UnexpectedValueException $e) {
105 1
            throw new \InvalidArgumentException(
106 1
                'An attachment must have an Content-Disposition header'
107
            );
108
        }
109
110 8
        $clone = clone $this;
111 8
        $clone->attachments[] = $part;
112 8
        return $clone;
113
    }
114
115
    /**
116
     * @param EmbeddedImage $embeddedImage
117
     * @return MessageBodyCollection
118
     */
119 5
    public function withEmbeddedImage(EmbeddedImage $embeddedImage): self
120
    {
121 5
        $clone = clone $this;
122 5
        $clone->embedImages[] = $embeddedImage;
123 5
        return $clone;
124
    }
125
126
    /**
127
     * @param MessageInterface $message
128
     * @return MessageBodyCollection
129
     */
130 1
    public function withAttachedMessage(MessageInterface $message): self
131
    {
132
        return $this
133 1
            ->withAttachment(
134 1
                ResourceAttachment::fromString(
135 1
                    (string)$message,
136 1
                    \transliterator_transliterate(
137 1
                        'Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;',
138 1
                        $this->extractSubject($message)
139 1
                    ) . '.eml',
140 1
                    new ContentType('message/rfc822')
141
                )
142
            );
143
    }
144
145
    /**
146
     * @param MessageInterface $message
147
     * @param QuotationInterface $quotation
148
     * @return MessageBodyCollection
149
     */
150 7
    public function withQuotedMessage(MessageInterface $message, QuotationInterface $quotation): self
151
    {
152 7
        return $quotation->quote($this, $message);
153
    }
154
155
    /**
156
     * @return string
157
     */
158 17
    public function getHtml(): string
159
    {
160 17
        return $this->html;
161
    }
162
163
    /**
164
     * @return AlternativeText
165
     */
166 17
    public function getText(): AlternativeText
167
    {
168 17
        return $this->text;
169
    }
170
171
    /**
172
     * @return array|PartInterface[]
173
     */
174 4
    public function getAttachments(): iterable
175
    {
176 4
        return $this->attachments;
177
    }
178
179
    /**
180
     * @return array|PartInterface[]
181
     */
182 4
    public function getEmbeddedImages(): iterable
183
    {
184 4
        return $this->embedImages;
185
    }
186
187
    /**
188
     * @return MessageInterface
189
     */
190 25
    public function createMessage(): MessageInterface
191
    {
192 25
        return (new MimeMessageFactory())->createMessage($this->createMessageRoot());
193
    }
194
195
    /**
196
     * @param MessageInterface $message
197
     * @return MessageInterface
198
     */
199 1
    public function attachToMessage(MessageInterface $message): MessageInterface
200
    {
201 1
        $newMessage = $this->createMessage();
202
203
        /** @var HeaderInterface[] $headers */
204 1
        foreach ($newMessage->getHeaders() as $headers) {
205 1
            foreach ($headers as $header) {
206 1
                $message = $message->withHeader($header);
207
            }
208
        }
209
210 1
        return $message->withBody($newMessage->getBody());
211
    }
212
213
    /**
214
     * @param MessageInterface $originalMessage
215
     * @return MessageInterface
216
     */
217 6
    public function inReplyTo(MessageInterface $originalMessage): MessageInterface
218
    {
219 6
        return $this->newReply(
220 6
            $originalMessage,
221 6
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From']
222
        );
223
    }
224
225
    /**
226
     * @param MessageInterface $originalMessage
227
     * @return MessageInterface
228
     */
229 2
    public function inReplyToAll(MessageInterface $originalMessage): MessageInterface
230
    {
231 2
        return $this->newReply(
232 2
            $originalMessage,
233 2
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From', 'Cc']
234
        );
235
    }
236
237
    /**
238
     * @param MessageInterface $originalMessage
239
     * @param array<int, string> $replyRecipientHeaderNames
240
     * @return MessageInterface
241
     */
242 8
    private function newReply(MessageInterface $originalMessage, array $replyRecipientHeaderNames): MessageInterface
243
    {
244 8
        $extractedSubject = $this->extractSubject($originalMessage);
245
246
        $reply = $this
247 8
            ->createReferencedMessage($originalMessage)
248 8
            ->withHeader(
249 8
                new Subject(
250 8
                    \substr($extractedSubject, 0, 3) === 'Re:' ? $extractedSubject : 'Re: ' . $extractedSubject
251
                )
252
            );
253
254 8
        foreach ($replyRecipientHeaderNames as $replyRecipientHeaderName) {
255 8
            foreach ($originalMessage->getHeader($replyRecipientHeaderName) as $recipientHeader) {
256 5
                $reply = $reply->withHeader($this->determineReplyHeader($recipientHeader));
257
            }
258
        }
259
260 8
        return $reply;
261
    }
262
263
    /**
264
     * @param MessageInterface $originalMessage
265
     * @return MessageInterface
266
     */
267 3
    public function asForwardTo(MessageInterface $originalMessage): MessageInterface
268
    {
269 3
        $extractedSubject = $this->extractSubject($originalMessage);
270
271
        return $this
272 3
            ->createReferencedMessage($originalMessage)
273 3
            ->withHeader(
274 3
                new Subject(
275 3
                    \substr($extractedSubject, 0, 4) === 'Fwd:' ? $extractedSubject : 'Fwd: ' . $extractedSubject
276
                )
277
            );
278
    }
279
280
    /**
281
     * @return PartInterface
282
     */
283 25
    private function createMessageRoot(): PartInterface
284
    {
285 25
        if (!empty($this->attachments)) {
286 6
            return (new MultiPart(
287 6
                Boundary::newRandom(),
288 6
                new ContentType('multipart/mixed')
289
            ))
290 6
                ->withPart($this->createMessageHumanReadable())
291 6
                ->withParts($this->attachments);
292
        }
293
294 19
        return $this->createMessageHumanReadable();
295
    }
296
297
    /**
298
     * @return PartInterface
299
     */
300 25
    private function createMessageHumanReadable(): PartInterface
301
    {
302 25
        if (!empty($this->embedImages)) {
303 3
            return (new MultiPart(
304 3
                Boundary::newRandom(),
305 3
                new ContentType('multipart/related')
306
            ))
307 3
                ->withPart($this->createMessageText())
308 3
                ->withParts($this->embedImages);
309
        }
310
311 22
        return $this->createMessageText();
312
    }
313
314
    /**
315
     * @return PartInterface
316
     */
317 25
    private function createMessageText(): PartInterface
318
    {
319 25
        if ($this->text->isEmpty() && $this->html === '') {
320 8
            return new PlainTextPart('');
321
        }
322
323 17
        if ($this->text->isEmpty()) {
324 2
            return new HtmlPart($this->html);
325
        }
326
327 15
        if ($this->html === '') {
328 1
            return new PlainTextPart((string)$this->text);
329
        }
330
331 14
        return (new MultiPart(
332 14
            Boundary::newRandom(),
333 14
            new ContentType('multipart/alternative')
334
        ))
335 14
            ->withPart(new PlainTextPart((string)$this->text))
336 14
            ->withPart(new HtmlPart($this->html));
337
    }
338
339
    /**
340
     * @param MessageInterface $message
341
     * @return MessageBodyCollection
342
     */
343 18
    public static function extract(MessageInterface $message): MessageBodyCollection
344
    {
345 18
        $collection = new self();
346
347
        try {
348 18
            $collection->extractFromMimePart(MultiPart::fromMessage($message));
349 4
        } catch (\InvalidArgumentException $e) {
350 4
            foreach ($message->getHeader('Content-Type') as $header) {
351 4
                $contentType = $header->getValue()->getRaw();
352 4
                if ($contentType === 'text/html') {
353 1
                    $collection->html = \rtrim((string)self::decodeMessageBody($message));
354
                }
355
356 4
                if ($contentType === 'text/plain') {
357 3
                    $collection->text = new AlternativeText(
358 3
                        \rtrim((string)self::decodeMessageBody($message))
359
                    );
360
                }
361
            }
362
        }
363
364 18
        return $collection;
365
    }
366
367
    /**
368
     * @param MultiPartInterface $parts
369
     */
370 14
    private function extractFromMimePart(MultiPartInterface $parts): void
371
    {
372 14
        foreach ($parts->getParts() as $part) {
373 14
            $contentType = $part->getHeader('Content-Type')->getValue()->getRaw();
374 14
            $hasDisposition = $part->hasHeader('Content-Disposition');
375
376 14
            if (!$hasDisposition && $contentType === 'text/html') {
377 14
                $this->html = (string)new MimeBodyDecodedStream($part);
378 14
                continue;
379
            }
380
381 14
            if (!$hasDisposition && $contentType === 'text/plain') {
382 14
                $this->text = new AlternativeText((string)new MimeBodyDecodedStream($part));
383 14
                continue;
384
            }
385
386 2
            if ($hasDisposition) {
387 2
                $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
388
389 2
                if ($disposition === 'attachment') {
390 2
                    $this->attachments[] = $part;
391 2
                    continue;
392
                }
393
394 2
                if ($disposition === 'inline' && \substr($contentType, 0, 6) === 'image/' && $part->hasHeader('Content-ID')) {
395 2
                    $this->embedImages[] = $part;
396 2
                    continue;
397
                }
398
            }
399
400 2
            if ($part instanceof MultiPartInterface) {
401 2
                $this->extractFromMimePart($part);
402
            }
403
        }
404 14
    }
405
406
    /**
407
     * @param MessageInterface $message
408
     * @return string
409
     */
410 12
    private function extractSubject(MessageInterface $message): string
411
    {
412 12
        foreach ($message->getHeader('Subject') as $header) {
413 11
            return $header->getValue()->getRaw();
414
        }
415
416 1
        return '';
417
    }
418
419
    /**
420
     * @param HeaderInterface $header
421
     * @return HeaderInterface
422
     */
423 5
    private function determineReplyHeader(HeaderInterface $header): HeaderInterface
424
    {
425 5
        $headerName = $header->getName();
426 5
        if ($headerName->equals(new HeaderName('Reply-To')) || $headerName->equals(new HeaderName('From'))) {
0 ignored issues
show
Documentation introduced by
new \Genkgo\Mail\Header\HeaderName('Reply-To') is of type object<Genkgo\Mail\Header\HeaderName>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
new \Genkgo\Mail\Header\HeaderName('From') is of type object<Genkgo\Mail\Header\HeaderName>, but the function expects a object<self>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
427 5
            return new To(AddressList::fromString((string)$header->getValue()));
428
        }
429
430 1
        return new Cc(AddressList::fromString((string)$header->getValue()));
431
    }
432
433
    /**
434
     * @param MessageInterface $originalMessage
435
     * @return MessageInterface
436
     */
437 11
    private function createReferencedMessage(MessageInterface $originalMessage): MessageInterface
438
    {
439 11
        $reply = $this->createMessage();
440
441 11
        foreach ($originalMessage->getHeader('Message-ID') as $messageIdHeader) {
442 2
            $references = $messageIdHeader->getValue()->getRaw();
443 2
            foreach ($originalMessage->getHeader('References') as $referenceHeader) {
444 1
                $references = $referenceHeader->getValue()->getRaw() . ', ' . $references;
445
            }
446
447
            return $reply
448 2
                ->withHeader(
449 2
                    new ParsedHeader(
450 2
                        new HeaderName('In-Reply-To'),
451 2
                        $messageIdHeader->getValue()
452
                    )
453
                )
454 2
                ->withHeader(new GenericHeader('References', $references));
455
        }
456
457 9
        return $reply;
458
    }
459
460
    /**
461
     * @param MessageInterface $message
462
     * @return StreamInterface
463
     */
464 4
    private static function decodeMessageBody(MessageInterface $message): StreamInterface
465
    {
466 4
        foreach ($message->getHeader('Content-Transfer-Encoding') as $header) {
467 2
            $encoding = $header->getValue();
468 2
            switch ($encoding) {
469 2
                case 'quoted-printable':
470
                    return QuotedPrintableDecodedStream::fromString((string)$message->getBody());
471 2
                case 'base64':
472
                    return Base64DecodedStream::fromString((string)$message->getBody());
473 2
                case '7bit':
474
                case '8bit':
475 2
                    return $message->getBody();
476
                default:
477
                    throw new \UnexpectedValueException(
478
                        'Cannot decode message body, unknown transfer encoding ' . $encoding
479
                    );
480
            }
481
        }
482
483 2
        return $message->getBody();
484
    }
485
}
486