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

MessageBodyCollection::createMessageText()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 12
cts 12
cp 1
rs 8.7624
c 0
b 0
f 0
cc 5
eloc 12
nc 4
nop 0
crap 5
1
<?php
2
declare(strict_types=1);
3
4
namespace Genkgo\Mail;
5
6
use Genkgo\Mail\Header\ContentType;
7
use Genkgo\Mail\Mime\Boundary;
8
use Genkgo\Mail\Mime\EmbeddedImage;
9
use Genkgo\Mail\Mime\HtmlPart;
10
use Genkgo\Mail\Mime\MultiPart;
11
use Genkgo\Mail\Mime\MultiPartInterface;
12
use Genkgo\Mail\Mime\PartInterface;
13
use Genkgo\Mail\Mime\PlainTextPart;
14
15
final class MessageBodyCollection
16
{
17
    /**
18
     * @var string
19
     */
20
    private $html = '';
21
22
    /**
23
     * @var AlternativeText
24
     */
25
    private $text;
26
27
    /**
28
     * @var array|PartInterface[]
29
     */
30
    private $attachments = [];
31
32
    /**
33
     * @var array|PartInterface[]
34
     */
35
    private $embedImages = [];
36
37
    /**
38
     * @param string $html
39
     */
40 12
    public function __construct(string $html = '')
41
    {
42 12
        $this->html = $html;
43 12
        $this->text = AlternativeText::fromHtml($html);
44 12
    }
45
46
    /**
47
     * @param string $html
48
     * @return MessageBodyCollection
49
     */
50 6
    public function withHtml(string $html): self
51
    {
52 6
        $clone = clone $this;
53 6
        $clone->html = $html;
54 6
        $clone->text = AlternativeText::fromHtml($html);
55 6
        return $clone;
56
    }
57
58
    /**
59
     * @param string $html
60
     * @return MessageBodyCollection
61
     */
62 2
    public function withHtmlAndNoGeneratedAlternativeText(string $html): self
63
    {
64 2
        $clone = clone $this;
65 2
        $clone->html = $html;
66 2
        return $clone;
67
    }
68
69
    /**
70
     * @param AlternativeText $text
71
     * @return MessageBodyCollection
72
     */
73 4
    public function withAlternativeText(AlternativeText $text): self
74
    {
75 4
        $clone = clone $this;
76 4
        $clone->text = $text;
77 4
        return $clone;
78
    }
79
80
    /**
81
     * @param PartInterface $part
82
     * @return MessageBodyCollection
83
     */
84 7
    public function withAttachment(PartInterface $part): self
85
    {
86
        try {
87 7
            $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
88 6
            if ($disposition !== 'attachment') {
89 1
                throw new \InvalidArgumentException(
90 6
                    'An attachment must have Content-Disposition header with value `attachment`'
91
                );
92
            }
93 2
        } catch (\UnexpectedValueException $e) {
94 1
            throw new \InvalidArgumentException(
95 1
                'An attachment must have an Content-Disposition header'
96
            );
97
        }
98
99 5
        $clone = clone $this;
100 5
        $clone->attachments[] = $part;
101 5
        return $clone;
102
    }
103
104
    /**
105
     * @param EmbeddedImage $embeddedImage
106
     * @return MessageBodyCollection
107
     */
108 5
    public function withEmbeddedImage(EmbeddedImage $embeddedImage): self
109
    {
110 5
        $clone = clone $this;
111 5
        $clone->embedImages[] = $embeddedImage;
112 5
        return $clone;
113
    }
114
115
    /**
116
     * @return string
117
     */
118 1
    public function getHtml(): string
119
    {
120 1
        return $this->html;
121
    }
122
123
    /**
124
     * @return AlternativeText
125
     */
126 1
    public function getText(): AlternativeText
127
    {
128 1
        return $this->text;
129
    }
130
131
    /**
132
     * @return array|PartInterface[]
133
     */
134 1
    public function getAttachments(): iterable
135
    {
136 1
        return $this->attachments;
137
    }
138
139
    /**
140
     * @return array|PartInterface[]
141
     */
142 1
    public function getEmbeddedImages(): iterable
143
    {
144 1
        return $this->embedImages;
145
    }
146
147
    /**
148
     * @return MessageInterface
149
     */
150 7
    public function createMessage(): MessageInterface
151
    {
152 7
        return (new MimeMessageFactory())->createMessage($this->createMessageRoot());
153
    }
154
155
    /**
156
     * @param MessageInterface $message
157
     * @return MessageInterface
158
     */
159
    public function attachToMessage(MessageInterface $message): MessageInterface
160
    {
161
        $newMessage = $this->createMessage();
162
163
        foreach ($newMessage->getHeader('Content-Type') as $header) {
164
            $message = $message->withHeader($header);
165
        }
166
167
        return $message->withBody($newMessage->getBody());
168
    }
169
170
    /**
171
     * @return PartInterface
172
     */
173 7
    private function createMessageRoot(): PartInterface
174
    {
175 7
        if (!empty($this->attachments)) {
176 3
            return (new MultiPart(
177 3
                Boundary::newRandom(),
178 3
                new ContentType('multipart/mixed')
179
            ))
180 3
                ->withPart($this->createMessageHumanReadable())
181 3
                ->withParts($this->attachments);
182
        }
183
184 4
        return $this->createMessageHumanReadable();
185
    }
186
187
    /**
188
     * @return PartInterface
189
     */
190 7
    private function createMessageHumanReadable(): PartInterface
191
    {
192 7
        if (!empty($this->embedImages)) {
193 3
            return (new MultiPart(
194 3
                Boundary::newRandom(),
195 3
                new ContentType('multipart/related')
196
            ))
197 3
                ->withPart($this->createMessageText())
198 3
                ->withParts($this->embedImages);
199
        }
200
201 4
        return $this->createMessageText();
202
    }
203
204
    /**
205
     * @return PartInterface
206
     */
207 7
    private function createMessageText(): PartInterface
208
    {
209 7
        if ($this->text->isEmpty() && $this->html === '') {
210 1
            return new PlainTextPart('');
211
        }
212
213 6
        if ($this->text->isEmpty()) {
214 1
            return new HtmlPart($this->html);
215
        }
216
217 5
        if ($this->html === '') {
218 1
            return new PlainTextPart((string)$this->text);
219
        }
220
221 4
        return (new MultiPart(
222 4
            Boundary::newRandom(),
223 4
            new ContentType('multipart/alternative')
224
        ))
225 4
            ->withPart(new PlainTextPart((string)$this->text))
226 4
            ->withPart(new HtmlPart($this->html));
227
    }
228
229
    /**
230
     * @param MessageInterface $message
231
     * @return MessageBodyCollection
232
     */
233 2
    public static function fromMessage(MessageInterface $message): MessageBodyCollection
234
    {
235 2
        return (new self())->extract($message);
236
    }
237
238
    /**
239
     * @param MessageInterface $message
240
     * @return MessageBodyCollection
241
     */
242 2
    private function extract(MessageInterface $message): self
243
    {
244
        try {
245 2
            $this->extractFromMimePart(MultiPart::fromMessage($message));
246
        } catch (\InvalidArgumentException $e) {
247
            foreach ($message->getHeader('Content-Type') as $header) {
248
                $contentType = $header->getValue()->getRaw();
249
                if ($contentType === 'text/html') {
250
                    $this->html = (string)$message->getBody();
251
                }
252
253
                if ($contentType === 'text/plain') {
254
                    $this->text = new AlternativeText((string)$message->getBody());
255
                }
256
            }
257
        }
258
259 2
        return $this;
260
    }
261
262
    /**
263
     * @param MultiPartInterface $parts
264
     */
265 2
    private function extractFromMimePart(MultiPartInterface $parts): void
266
    {
267 2
        foreach ($parts->getParts() as $part) {
268 2
            $contentType = $part->getHeader('Content-Type')->getValue()->getRaw();
269 2
            $hasDisposition = $part->hasHeader('Content-Disposition');
270
271 2
            if (!$hasDisposition && $contentType === 'text/html') {
272 2
                $this->html = (string)$part->getBody();
273 2
                continue;
274
            }
275
276 2
            if (!$hasDisposition && $contentType === 'text/plain') {
277 2
                $this->text = new AlternativeText((string)$part->getBody());
278 2
                continue;
279
            }
280
281 2
            if ($hasDisposition) {
282 2
                $disposition = $part->getHeader('Content-Disposition')->getValue()->getRaw();
283
284 2
                if ($disposition === 'attachment') {
285 2
                    $this->attachments[] = $part;
286 2
                    continue;
287
                }
288
289 2
                if ($disposition === 'inline' && \substr($contentType, 0, 6) === 'image/' && $part->hasHeader('Content-ID')) {
290 2
                    $this->embedImages[] = $part;
291 2
                    continue;
292
                }
293
            }
294
295 2
            if ($part instanceof MultiPartInterface) {
296 2
                $this->extractFromMimePart($part);
297
            }
298
        }
299 2
    }
300
}
301