Completed
Pull Request — master (#50)
by Frederik
02:46
created

MessageBodyCollection::getEmbeddedImages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

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