Completed
Pull Request — master (#78)
by Frederik
01:53
created

withHtmlAndNoGeneratedAlternativeText()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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 34
    public function __construct(string $html = '')
51
    {
52 34
        $this->html = $html;
53 34
        $this->text = AlternativeText::fromHtml($html);
54 34
    }
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 22
    public function createMessage(): MessageInterface
190
    {
191 22
        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 5
    public function inReplyTo(MessageInterface $originalMessage): MessageInterface
217
    {
218 5
        return $this->newReply(
219 5
            $originalMessage,
220 5
            $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 7
    private function newReply(MessageInterface $originalMessage, array $replyRecipientHeaderNames): MessageInterface
242
    {
243
        $reply = $this
244 7
            ->createReferencedMessage($originalMessage)
245 7
            ->withHeader(new Subject('Re: ' . $this->extractSubject($originalMessage)));
246
247 7
        foreach ($replyRecipientHeaderNames as $replyRecipientHeaderName) {
248 7
            foreach ($originalMessage->getHeader($replyRecipientHeaderName) as $recipientHeader) {
249 5
                $reply = $reply->withHeader($this->determineReplyHeader($recipientHeader));
250
            }
251
        }
252
253 7
        return $reply;
254
    }
255
256
    /**
257
     * @param MessageInterface $originalMessage
258
     * @return MessageInterface
259
     */
260 1
    public function asForwardTo(MessageInterface $originalMessage): MessageInterface
261
    {
262
        return $this
263 1
            ->createReferencedMessage($originalMessage)
264 1
            ->withHeader(new Subject('Fwd: ' . $this->extractSubject($originalMessage)));
265
    }
266
267
    /**
268
     * @return PartInterface
269
     */
270 22
    private function createMessageRoot(): PartInterface
271
    {
272 22
        if (!empty($this->attachments)) {
273 6
            return (new MultiPart(
274 6
                Boundary::newRandom(),
275 6
                new ContentType('multipart/mixed')
276
            ))
277 6
                ->withPart($this->createMessageHumanReadable())
278 6
                ->withParts($this->attachments);
279
        }
280
281 16
        return $this->createMessageHumanReadable();
282
    }
283
284
    /**
285
     * @return PartInterface
286
     */
287 22
    private function createMessageHumanReadable(): PartInterface
288
    {
289 22
        if (!empty($this->embedImages)) {
290 3
            return (new MultiPart(
291 3
                Boundary::newRandom(),
292 3
                new ContentType('multipart/related')
293
            ))
294 3
                ->withPart($this->createMessageText())
295 3
                ->withParts($this->embedImages);
296
        }
297
298 19
        return $this->createMessageText();
299
    }
300
301
    /**
302
     * @return PartInterface
303
     */
304 22
    private function createMessageText(): PartInterface
305
    {
306 22
        if ($this->text->isEmpty() && $this->html === '') {
307 5
            return new PlainTextPart('');
308
        }
309
310 17
        if ($this->text->isEmpty()) {
311 2
            return new HtmlPart($this->html);
312
        }
313
314 15
        if ($this->html === '') {
315 1
            return new PlainTextPart((string)$this->text);
316
        }
317
318 14
        return (new MultiPart(
319 14
            Boundary::newRandom(),
320 14
            new ContentType('multipart/alternative')
321
        ))
322 14
            ->withPart(new PlainTextPart((string)$this->text))
323 14
            ->withPart(new HtmlPart($this->html));
324
    }
325
326
    /**
327
     * @param MessageInterface $message
328
     * @return MessageBodyCollection
329
     */
330 18
    public static function extract(MessageInterface $message): MessageBodyCollection
331
    {
332 18
        $collection = new self();
333
334
        try {
335 18
            $collection->extractFromMimePart(MultiPart::fromMessage($message));
336 4
        } catch (\InvalidArgumentException $e) {
337 4
            foreach ($message->getHeader('Content-Type') as $header) {
338 4
                $contentType = $header->getValue()->getRaw();
339 4
                if ($contentType === 'text/html') {
340 1
                    $collection->html = \rtrim((string)self::decodeMessageBody($message));
341
                }
342
343 4
                if ($contentType === 'text/plain') {
344 3
                    $collection->text = new AlternativeText(
345 3
                        \rtrim((string)self::decodeMessageBody($message))
346
                    );
347
                }
348
            }
349
        }
350
351 18
        return $collection;
352
    }
353
354
    /**
355
     * @param MultiPartInterface $parts
356
     */
357 14
    private function extractFromMimePart(MultiPartInterface $parts): void
358
    {
359 14
        foreach ($parts->getParts() as $part) {
360 14
            $contentType = $part->getHeader('Content-Type')->getValue()->getRaw();
361 14
            $hasDisposition = $part->hasHeader('Content-Disposition');
362
363 14
            if (!$hasDisposition && $contentType === 'text/html') {
364 14
                $this->html = (string)$this->decodeBodyPart($part);
365 14
                continue;
366
            }
367
368 14
            if (!$hasDisposition && $contentType === 'text/plain') {
369 14
                $this->text = new AlternativeText((string)$this->decodeBodyPart($part));
370 14
                continue;
371
            }
372
373 2
            if ($hasDisposition) {
374 2
                $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
375
376 2
                if ($disposition === 'attachment') {
377 2
                    $this->attachments[] = $part;
378 2
                    continue;
379
                }
380
381 2
                if ($disposition === 'inline' && \substr($contentType, 0, 6) === 'image/' && $part->hasHeader('Content-ID')) {
382 2
                    $this->embedImages[] = $part;
383 2
                    continue;
384
                }
385
            }
386
387 2
            if ($part instanceof MultiPartInterface) {
388 2
                $this->extractFromMimePart($part);
389
            }
390
        }
391 14
    }
392
393
    /**
394
     * @param MessageInterface $message
395
     * @return string
396
     */
397 9
    private function extractSubject(MessageInterface $message): string
398
    {
399 9
        foreach ($message->getHeader('Subject') as $header) {
400 7
            return $header->getValue()->getRaw();
401
        }
402
403 2
        return '';
404
    }
405
406
    /**
407
     * @param HeaderInterface $header
408
     * @return HeaderInterface
409
     */
410 5
    private function determineReplyHeader(HeaderInterface $header): HeaderInterface
411
    {
412 5
        $headerName = $header->getName();
413 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...
414 5
            return new To(AddressList::fromString((string)$header->getValue()));
415
        }
416
417 1
        return new Cc(AddressList::fromString((string)$header->getValue()));
418
    }
419
420
    /**
421
     * @param MessageInterface $originalMessage
422
     * @return MessageInterface
423
     */
424 8
    private function createReferencedMessage(MessageInterface $originalMessage): MessageInterface
425
    {
426 8
        $reply = $this->createMessage();
427
428 8
        foreach ($originalMessage->getHeader('Message-ID') as $messageIdHeader) {
429 2
            $references = $messageIdHeader->getValue()->getRaw();
430 2
            foreach ($originalMessage->getHeader('References') as $referenceHeader) {
431 1
                $references = $referenceHeader->getValue()->getRaw() . ', ' . $references;
432
            }
433
434
            return $reply
435 2
                ->withHeader(
436 2
                    new ParsedHeader(
437 2
                        new HeaderName('In-Reply-To'),
438 2
                        $messageIdHeader->getValue()
439
                    )
440
                )
441 2
                ->withHeader(new GenericHeader('References', $references));
442
        }
443
444 6
        return $reply;
445
    }
446
447
    /**
448
     * @param PartInterface $part
449
     * @return StreamInterface
450
     */
451 14
    private static function decodeBodyPart(PartInterface $part): StreamInterface
452
    {
453 14
        if (!$part->hasHeader('Content-Transfer-Encoding')) {
454
            return $part->getBody();
455
        }
456
457 14
        $encoding = $part->getHeader('Content-Transfer-Encoding')->getValue();
458 14
        switch ($encoding) {
459 14
            case 'quoted-printable':
460 2
                return QuotedPrintableDecodedStream::fromString((string)$part->getBody());
461 13
            case 'base64':
462 1
                return Base64DecodedStream::fromString((string)$part->getBody());
463 12
            case '7bit':
464
            case '8bit':
465 12
                return $part->getBody();
466
            default:
467
                throw new \UnexpectedValueException(
468
                    'Cannot decode body of mime part, unknown transfer encoding ' . $encoding
469
                );
470
        }
471
    }
472
473
    /**
474
     * @param MessageInterface $message
475
     * @return StreamInterface
476
     */
477 4
    private static function decodeMessageBody(MessageInterface $message): StreamInterface
478
    {
479 4
        foreach ($message->getHeader('Content-Transfer-Encoding') as $header) {
480 2
            $encoding = $header->getValue();
481 2
            switch ($encoding) {
482 2
                case 'quoted-printable':
483
                    return QuotedPrintableDecodedStream::fromString((string)$message->getBody());
484 2
                case 'base64':
485
                    return Base64DecodedStream::fromString((string)$message->getBody());
486 2
                case '7bit':
487
                case '8bit':
488 2
                    return $message->getBody();
489
                default:
490
                    throw new \UnexpectedValueException(
491
                        'Cannot decode message body, unknown transfer encoding ' . $encoding
492
                    );
493
            }
494
        }
495
496 2
        return $message->getBody();
497
    }
498
}
499