Completed
Push — master ( 3968e7...cc370f )
by Frederik
01:57
created

MessageBodyCollection::getText()   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\QuotedPrintableDecodedStream;
24
25
final class MessageBodyCollection
26
{
27
    /**
28
     * @var string
29
     */
30
    private $html = '';
31
32
    /**
33
     * @var AlternativeText
34
     */
35
    private $text;
36
37
    /**
38
     * @var array|PartInterface[]
39
     */
40
    private $attachments = [];
41
42
    /**
43
     * @var array|PartInterface[]
44
     */
45
    private $embedImages = [];
46
47
    /**
48
     * @param string $html
49
     */
50 37
    public function __construct(string $html = '')
51
    {
52 37
        $this->html = $html;
53 37
        $this->text = AlternativeText::fromHtml($html);
54 37
    }
55
56
    /**
57
     * @param string $html
58
     * @return MessageBodyCollection
59
     */
60 6
    public function withHtml(string $html): self
61
    {
62 6
        $clone = clone $this;
63 6
        $clone->html = $html;
64 6
        $clone->text = AlternativeText::fromHtml($html);
65 6
        return $clone;
66
    }
67
68
    /**
69
     * @param string $html
70
     * @return MessageBodyCollection
71
     */
72 16
    public function withHtmlAndNoGeneratedAlternativeText(string $html): self
73
    {
74 16
        $clone = clone $this;
75 16
        $clone->html = $html;
76 16
        return $clone;
77
    }
78
79
    /**
80
     * @param AlternativeText $text
81
     * @return MessageBodyCollection
82
     */
83 17
    public function withAlternativeText(AlternativeText $text): self
84
    {
85 17
        $clone = clone $this;
86 17
        $clone->text = $text;
87 17
        return $clone;
88
    }
89
90
    /**
91
     * @param PartInterface $part
92
     * @return MessageBodyCollection
93
     */
94 10
    public function withAttachment(PartInterface $part): self
95
    {
96
        try {
97 10
            $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
98 9
            if ($disposition !== 'attachment') {
99 1
                throw new \InvalidArgumentException(
100 9
                    'An attachment must have Content-Disposition header with value `attachment`'
101
                );
102
            }
103 2
        } catch (\UnexpectedValueException $e) {
104 1
            throw new \InvalidArgumentException(
105 1
                'An attachment must have an Content-Disposition header'
106
            );
107
        }
108
109 8
        $clone = clone $this;
110 8
        $clone->attachments[] = $part;
111 8
        return $clone;
112
    }
113
114
    /**
115
     * @param EmbeddedImage $embeddedImage
116
     * @return MessageBodyCollection
117
     */
118 5
    public function withEmbeddedImage(EmbeddedImage $embeddedImage): self
119
    {
120 5
        $clone = clone $this;
121 5
        $clone->embedImages[] = $embeddedImage;
122 5
        return $clone;
123
    }
124
125
    /**
126
     * @param MessageInterface $message
127
     * @return MessageBodyCollection
128
     */
129 1
    public function withAttachedMessage(MessageInterface $message): self
130
    {
131
        return $this
132 1
            ->withAttachment(
133 1
                ResourceAttachment::fromString(
134 1
                    (string)$message,
135 1
                    \transliterator_transliterate(
136 1
                        'Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;',
137 1
                        $this->extractSubject($message)
138 1
                    ) . '.eml',
139 1
                    new ContentType('message/rfc822')
140
                )
141
            );
142
    }
143
144
    /**
145
     * @param MessageInterface $message
146
     * @param QuotationInterface $quotation
147
     * @return MessageBodyCollection
148
     */
149 7
    public function withQuotedMessage(MessageInterface $message, QuotationInterface $quotation): self
150
    {
151 7
        return $quotation->quote($this, $message);
152
    }
153
154
    /**
155
     * @return string
156
     */
157 17
    public function getHtml(): string
158
    {
159 17
        return $this->html;
160
    }
161
162
    /**
163
     * @return AlternativeText
164
     */
165 17
    public function getText(): AlternativeText
166
    {
167 17
        return $this->text;
168
    }
169
170
    /**
171
     * @return array|PartInterface[]
172
     */
173 4
    public function getAttachments(): iterable
174
    {
175 4
        return $this->attachments;
176
    }
177
178
    /**
179
     * @return array|PartInterface[]
180
     */
181 4
    public function getEmbeddedImages(): iterable
182
    {
183 4
        return $this->embedImages;
184
    }
185
186
    /**
187
     * @return MessageInterface
188
     */
189 25
    public function createMessage(): MessageInterface
190
    {
191 25
        return (new MimeMessageFactory())->createMessage($this->createMessageRoot());
192
    }
193
194
    /**
195
     * @param MessageInterface $message
196
     * @return MessageInterface
197
     */
198 1
    public function attachToMessage(MessageInterface $message): MessageInterface
199
    {
200 1
        $newMessage = $this->createMessage();
201
202
        /** @var HeaderInterface[] $headers */
203 1
        foreach ($newMessage->getHeaders() as $headers) {
204 1
            foreach ($headers as $header) {
205 1
                $message = $message->withHeader($header);
206
            }
207
        }
208
209 1
        return $message->withBody($newMessage->getBody());
210
    }
211
212
    /**
213
     * @param MessageInterface $originalMessage
214
     * @return MessageInterface
215
     */
216 6
    public function inReplyTo(MessageInterface $originalMessage): MessageInterface
217
    {
218 6
        return $this->newReply(
219 6
            $originalMessage,
220 6
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From']
221
        );
222
    }
223
224
    /**
225
     * @param MessageInterface $originalMessage
226
     * @return MessageInterface
227
     */
228 2
    public function inReplyToAll(MessageInterface $originalMessage): MessageInterface
229
    {
230 2
        return $this->newReply(
231 2
            $originalMessage,
232 2
            $originalMessage->hasHeader('Reply-To') ? ['Reply-To'] : ['From', 'Cc']
233
        );
234
    }
235
236
    /**
237
     * @param MessageInterface $originalMessage
238
     * @param array<int, string> $replyRecipientHeaderNames
239
     * @return MessageInterface
240
     */
241 8
    private function newReply(MessageInterface $originalMessage, array $replyRecipientHeaderNames): MessageInterface
242
    {
243 8
        $extractedSubject = $this->extractSubject($originalMessage);
244
245
        $reply = $this
246 8
            ->createReferencedMessage($originalMessage)
247 8
            ->withHeader(
248 8
                new Subject(
249 8
                    \substr($extractedSubject, 0, 3) === 'Re:' ? $extractedSubject : 'Re: ' . $extractedSubject
250
                )
251
            );
252
253 8
        foreach ($replyRecipientHeaderNames as $replyRecipientHeaderName) {
254 8
            foreach ($originalMessage->getHeader($replyRecipientHeaderName) as $recipientHeader) {
255 5
                $reply = $reply->withHeader($this->determineReplyHeader($recipientHeader));
256
            }
257
        }
258
259 8
        return $reply;
260
    }
261
262
    /**
263
     * @param MessageInterface $originalMessage
264
     * @return MessageInterface
265
     */
266 3
    public function asForwardTo(MessageInterface $originalMessage): MessageInterface
267
    {
268 3
        $extractedSubject = $this->extractSubject($originalMessage);
269
270
        return $this
271 3
            ->createReferencedMessage($originalMessage)
272 3
            ->withHeader(
273 3
                new Subject(
274 3
                    \substr($extractedSubject, 0, 4) === 'Fwd:' ? $extractedSubject : 'Fwd: ' . $extractedSubject
275
                )
276
            );
277
    }
278
279
    /**
280
     * @return PartInterface
281
     */
282 25
    private function createMessageRoot(): PartInterface
283
    {
284 25
        if (!empty($this->attachments)) {
285 6
            return (new MultiPart(
286 6
                Boundary::newRandom(),
287 6
                new ContentType('multipart/mixed')
288
            ))
289 6
                ->withPart($this->createMessageHumanReadable())
290 6
                ->withParts($this->attachments);
291
        }
292
293 19
        return $this->createMessageHumanReadable();
294
    }
295
296
    /**
297
     * @return PartInterface
298
     */
299 25
    private function createMessageHumanReadable(): PartInterface
300
    {
301 25
        if (!empty($this->embedImages)) {
302 3
            return (new MultiPart(
303 3
                Boundary::newRandom(),
304 3
                new ContentType('multipart/related')
305
            ))
306 3
                ->withPart($this->createMessageText())
307 3
                ->withParts($this->embedImages);
308
        }
309
310 22
        return $this->createMessageText();
311
    }
312
313
    /**
314
     * @return PartInterface
315
     */
316 25
    private function createMessageText(): PartInterface
317
    {
318 25
        if ($this->text->isEmpty() && $this->html === '') {
319 8
            return new PlainTextPart('');
320
        }
321
322 17
        if ($this->text->isEmpty()) {
323 2
            return new HtmlPart($this->html);
324
        }
325
326 15
        if ($this->html === '') {
327 1
            return new PlainTextPart((string)$this->text);
328
        }
329
330 14
        return (new MultiPart(
331 14
            Boundary::newRandom(),
332 14
            new ContentType('multipart/alternative')
333
        ))
334 14
            ->withPart(new PlainTextPart((string)$this->text))
335 14
            ->withPart(new HtmlPart($this->html));
336
    }
337
338
    /**
339
     * @param MessageInterface $message
340
     * @return MessageBodyCollection
341
     */
342 18
    public static function extract(MessageInterface $message): MessageBodyCollection
343
    {
344 18
        $collection = new self();
345
346
        try {
347 18
            $collection->extractFromMimePart(MultiPart::fromMessage($message));
348 4
        } catch (\InvalidArgumentException $e) {
349 4
            foreach ($message->getHeader('Content-Type') as $header) {
350 4
                $contentType = $header->getValue()->getRaw();
351 4
                if ($contentType === 'text/html') {
352 1
                    $collection->html = \rtrim((string)self::decodeMessageBody($message));
353
                }
354
355 4
                if ($contentType === 'text/plain') {
356 3
                    $collection->text = new AlternativeText(
357 3
                        \rtrim((string)self::decodeMessageBody($message))
358
                    );
359
                }
360
            }
361
        }
362
363 18
        return $collection;
364
    }
365
366
    /**
367
     * @param MultiPartInterface $parts
368
     */
369 14
    private function extractFromMimePart(MultiPartInterface $parts): void
370
    {
371 14
        foreach ($parts->getParts() as $part) {
372 14
            $contentType = $part->getHeader('Content-Type')->getValue()->getRaw();
373 14
            $hasDisposition = $part->hasHeader('Content-Disposition');
374
375 14
            if (!$hasDisposition && $contentType === 'text/html') {
376 14
                $this->html = (string)$this->decodeBodyPart($part);
377 14
                continue;
378
            }
379
380 14
            if (!$hasDisposition && $contentType === 'text/plain') {
381 14
                $this->text = new AlternativeText((string)$this->decodeBodyPart($part));
382 14
                continue;
383
            }
384
385 2
            if ($hasDisposition) {
386 2
                $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
387
388 2
                if ($disposition === 'attachment') {
389 2
                    $this->attachments[] = $part;
390 2
                    continue;
391
                }
392
393 2
                if ($disposition === 'inline' && \substr($contentType, 0, 6) === 'image/' && $part->hasHeader('Content-ID')) {
394 2
                    $this->embedImages[] = $part;
395 2
                    continue;
396
                }
397
            }
398
399 2
            if ($part instanceof MultiPartInterface) {
400 2
                $this->extractFromMimePart($part);
401
            }
402
        }
403 14
    }
404
405
    /**
406
     * @param MessageInterface $message
407
     * @return string
408
     */
409 12
    private function extractSubject(MessageInterface $message): string
410
    {
411 12
        foreach ($message->getHeader('Subject') as $header) {
412 11
            return $header->getValue()->getRaw();
413
        }
414
415 1
        return '';
416
    }
417
418
    /**
419
     * @param HeaderInterface $header
420
     * @return HeaderInterface
421
     */
422 5
    private function determineReplyHeader(HeaderInterface $header): HeaderInterface
423
    {
424 5
        $headerName = $header->getName();
425 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...
426 5
            return new To(AddressList::fromString((string)$header->getValue()));
427
        }
428
429 1
        return new Cc(AddressList::fromString((string)$header->getValue()));
430
    }
431
432
    /**
433
     * @param MessageInterface $originalMessage
434
     * @return MessageInterface
435
     */
436 11
    private function createReferencedMessage(MessageInterface $originalMessage): MessageInterface
437
    {
438 11
        $reply = $this->createMessage();
439
440 11
        foreach ($originalMessage->getHeader('Message-ID') as $messageIdHeader) {
441 2
            $references = $messageIdHeader->getValue()->getRaw();
442 2
            foreach ($originalMessage->getHeader('References') as $referenceHeader) {
443 1
                $references = $referenceHeader->getValue()->getRaw() . ', ' . $references;
444
            }
445
446
            return $reply
447 2
                ->withHeader(
448 2
                    new ParsedHeader(
449 2
                        new HeaderName('In-Reply-To'),
450 2
                        $messageIdHeader->getValue()
451
                    )
452
                )
453 2
                ->withHeader(new GenericHeader('References', $references));
454
        }
455
456 9
        return $reply;
457
    }
458
459
    /**
460
     * @param PartInterface $part
461
     * @return StreamInterface
462
     */
463 14
    private static function decodeBodyPart(PartInterface $part): StreamInterface
464
    {
465 14
        if (!$part->hasHeader('Content-Transfer-Encoding')) {
466
            return $part->getBody();
467
        }
468
469 14
        $encoding = $part->getHeader('Content-Transfer-Encoding')->getValue();
470 14
        switch ($encoding) {
471 14
            case 'quoted-printable':
472 2
                return QuotedPrintableDecodedStream::fromString((string)$part->getBody());
473 13
            case 'base64':
474 1
                return Base64DecodedStream::fromString((string)$part->getBody());
475 12
            case '7bit':
476
            case '8bit':
477 12
                return $part->getBody();
478
            default:
479
                throw new \UnexpectedValueException(
480
                    'Cannot decode body of mime part, unknown transfer encoding ' . $encoding
481
                );
482
        }
483
    }
484
485
    /**
486
     * @param MessageInterface $message
487
     * @return StreamInterface
488
     */
489 4
    private static function decodeMessageBody(MessageInterface $message): StreamInterface
490
    {
491 4
        foreach ($message->getHeader('Content-Transfer-Encoding') as $header) {
492 2
            $encoding = $header->getValue();
493 2
            switch ($encoding) {
494 2
                case 'quoted-printable':
495
                    return QuotedPrintableDecodedStream::fromString((string)$message->getBody());
496 2
                case 'base64':
497
                    return Base64DecodedStream::fromString((string)$message->getBody());
498 2
                case '7bit':
499
                case '8bit':
500 2
                    return $message->getBody();
501
                default:
502
                    throw new \UnexpectedValueException(
503
                        'Cannot decode message body, unknown transfer encoding ' . $encoding
504
                    );
505
            }
506
        }
507
508 2
        return $message->getBody();
509
    }
510
}
511