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

MessageBodyCollection::withAttachedMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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