MultipartHelper   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Test Coverage

Coverage 96.15%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 57
eloc 142
c 1
b 0
f 0
dl 0
loc 369
ccs 150
cts 156
cp 0.9615
rs 5.04

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getContentPartContainerFromAlternative() 0 12 3
A setMessageAsAlternative() 0 8 2
A removePartByMimeType() 0 15 6
A createAndAddPartForAttachment() 0 28 4
A createMultipartRelatedPartForInlineChildrenOf() 0 10 2
A __construct() 0 7 1
A findOtherContentPartFor() 0 13 6
B removeAllContentPartsFromAlternative() 0 26 9
A setContentPartForMimeType() 0 13 4
A enforceMime() 0 11 3
A createAlternativeContentPart() 0 8 1
A createContentPartForMimeType() 0 20 3
A moveAllNonMultiPartsToMessageExcept() 0 16 5
A getUniqueBoundary() 0 4 1
A setMimeHeaderBoundaryOnPart() 0 9 1
A removeAllContentPartsByMimeType() 0 8 2
A setMessageAsMixed() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like MultipartHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MultipartHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the ZBateson\MailMimeParser project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
8
namespace ZBateson\MailMimeParser\Message\Helper;
9
10
use ZBateson\MailMimeParser\Header\HeaderConsts;
11
use ZBateson\MailMimeParser\IMessage;
12
use ZBateson\MailMimeParser\Message\Factory\IMimePartFactory;
13
use ZBateson\MailMimeParser\Message\Factory\IUUEncodedPartFactory;
14
use ZBateson\MailMimeParser\Message\IMessagePart;
15
use ZBateson\MailMimeParser\Message\IMimePart;
16
use ZBateson\MailMimeParser\Message\IMultiPart;
17
use ZBateson\MailMimeParser\Message\PartFilter;
18
19
/**
20
 * Provides various routines to manipulate and create multipart messages from an
21
 * existing message (e.g. to make space for attachments in a message, or to
22
 * change a simple message to a multipart/alternative one, etc...)
23
 *
24
 * @author Zaahid Bateson
25
 */
26
class MultipartHelper extends AbstractHelper
27
{
28
    /**
29
     * @var GenericHelper a GenericHelper instance
30
     */
31
    private GenericHelper $genericHelper;
32
33 21
    public function __construct(
34
        IMimePartFactory $mimePartFactory,
35
        IUUEncodedPartFactory $uuEncodedPartFactory,
36
        GenericHelper $genericHelper
37
    ) {
38 21
        parent::__construct($mimePartFactory, $uuEncodedPartFactory);
39 21
        $this->genericHelper = $genericHelper;
40
    }
41
42
    /**
43
     * Creates and returns a unique boundary.
44
     *
45
     * @param string $mimeType first 3 characters of a multipart type are used,
46
     *      e.g. REL for relative or ALT for alternative
47
     */
48 31
    public function getUniqueBoundary(string $mimeType) : string
49
    {
50 31
        $type = \ltrim(\strtoupper(\preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
51 31
        return \uniqid('----=MMP-' . $type . '-', true);
52
    }
53
54
    /**
55
     * Creates a unique mime boundary and assigns it to the passed part's
56
     * Content-Type header with the passed mime type.
57
     */
58 22
    public function setMimeHeaderBoundaryOnPart(IMimePart $part, string $mimeType) : static
59
    {
60 22
        $part->setRawHeader(
61 22
            HeaderConsts::CONTENT_TYPE,
62 22
            "$mimeType;\r\n\tboundary=\""
63 22
                . $this->getUniqueBoundary($mimeType) . '"'
64 22
        );
65 22
        $part->notify();
66 22
        return $this;
67
    }
68
69
    /**
70
     * Sets the passed message as multipart/mixed.
71
     *
72
     * If the message has content, a new part is created and added as a child of
73
     * the message.  The message's content and content headers are moved to the
74
     * new part.
75
     */
76 12
    public function setMessageAsMixed(IMessage $message) : static
77
    {
78 12
        if ($message->hasContent()) {
79 8
            $part = $this->genericHelper->createNewContentPartFrom($message);
80 8
            $message->addChild($part, 0);
81
        }
82 12
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed');
83 12
        $atts = $message->getAllAttachmentParts();
84 12
        if (!empty($atts)) {
85 6
            foreach ($atts as $att) {
86 6
                $att->notify();
87
            }
88
        }
89 12
        return $this;
90
    }
91
92
    /**
93
     * Sets the passed message as multipart/alternative.
94
     *
95
     * If the message has content, a new part is created and added as a child of
96
     * the message.  The message's content and content headers are moved to the
97
     * new part.
98
     */
99 4
    public function setMessageAsAlternative(IMessage $message) : static
100
    {
101 4
        if ($message->hasContent()) {
102 3
            $part = $this->genericHelper->createNewContentPartFrom($message);
103 3
            $message->addChild($part, 0);
104
        }
105 4
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/alternative');
106 4
        return $this;
107
    }
108
109
    /**
110
     * Searches the passed $alternativePart for a part with the passed mime type
111
     * and returns its parent.
112
     *
113
     * Used for alternative mime types that have a multipart/mixed or
114
     * multipart/related child containing a content part of $mimeType, where
115
     * the whole mixed/related part should be removed.
116
     *
117
     * @param string $mimeType the content-type to find below $alternativePart
118
     * @param IMimePart $alternativePart The multipart/alternative part to look
119
     *        under
120
     * @return bool|IMimePart false if a part is not found
121
     */
122 7
    public function getContentPartContainerFromAlternative($mimeType, IMimePart $alternativePart) : bool|IMimePart
123
    {
124 7
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
125 7
        $contPart = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $contPart is dead and can be removed.
Loading history...
126
        do {
127 7
            if ($part === null) {
128 1
                return false;
129
            }
130 7
            $contPart = $part;
131 7
            $part = $part->getParent();
132 7
        } while ($part !== $alternativePart);
133 7
        return $contPart;
134
    }
135
136
    /**
137
     * Removes all parts of $mimeType from $alternativePart.
138
     *
139
     * If $alternativePart contains a multipart/mixed or multipart/relative part
140
     * with other parts of different content-types, the multipart part is
141
     * removed, and parts of different content-types can optionally be moved to
142
     * the main message part.
143
     */
144 6
    public function removeAllContentPartsFromAlternative(
145
        IMessage $message,
146
        string $mimeType,
147
        IMimePart $alternativePart,
148
        bool $keepOtherContent
149
    ) : bool {
150 6
        $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart);
151 6
        if ($rmPart === false) {
0 ignored issues
show
introduced by
The condition $rmPart === false is always false.
Loading history...
152
            return false;
153
        }
154 6
        if ($keepOtherContent && $rmPart->getChildCount() > 0) {
155 2
            $this->moveAllNonMultiPartsToMessageExcept($message, $rmPart, $mimeType);
156 2
            $alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
157
        }
158 6
        $message->removePart($rmPart);
159 6
        if ($alternativePart !== null && $alternativePart instanceof IMultiPart) {
160 6
            if ($alternativePart->getChildCount() === 1) {
161 4
                $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->getChild(0));
0 ignored issues
show
Bug introduced by
It seems like $alternativePart->getChild(0) can also be of type null; however, parameter $replacement of ZBateson\MailMimeParser\...icHelper::replacePart() does only seem to accept ZBateson\MailMimeParser\Message\IMimePart, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
                $this->genericHelper->replacePart($message, $alternativePart, /** @scrutinizer ignore-type */ $alternativePart->getChild(0));
Loading history...
162 2
            } elseif ($alternativePart->getChildCount() === 0) {
163 2
                $message->removePart($alternativePart);
164
            }
165
        }
166 6
        while ($message->getChildCount() === 1) {
167
            $this->genericHelper->replacePart($message, $message, $message->getChild(0));
168
        }
169 6
        return true;
170
    }
171
172
    /**
173
     * Creates a new mime part as a multipart/alternative and assigns the passed
174
     * $contentPart as a part below it before returning it.
175
     *
176
     * @return IMimePart the alternative part
177
     */
178 4
    public function createAlternativeContentPart(IMessage $message, IMessagePart $contentPart) : IMimePart
179
    {
180 4
        $altPart = $this->mimePartFactory->newInstance();
181 4
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
182 4
        $message->removePart($contentPart);
183 4
        $message->addChild($altPart, 0);
184 4
        $altPart->addChild($contentPart, 0);
185 4
        return $altPart;
186
    }
187
188
    /**
189
     * Moves all parts under $from into this message except those with a
190
     * content-type equal to $exceptMimeType.  If the message is not a
191
     * multipart/mixed message, it is set to multipart/mixed first.
192
     */
193 3
    public function moveAllNonMultiPartsToMessageExcept(IMessage $message, IMimePart $from, string $exceptMimeType) : static
194
    {
195 3
        $parts = $from->getAllParts(function(IMessagePart $part) use ($exceptMimeType) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
196 2
            if ($part instanceof IMimePart && $part->isMultiPart()) {
197 2
                return false;
198
            }
199 2
            return \strcasecmp($part->getContentType(), $exceptMimeType) !== 0;
0 ignored issues
show
Bug introduced by
It seems like $part->getContentType() can also be of type null; however, parameter $string1 of strcasecmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

199
            return \strcasecmp(/** @scrutinizer ignore-type */ $part->getContentType(), $exceptMimeType) !== 0;
Loading history...
200 3
        });
201 3
        if (\strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $message->getContentType() can also be of type null; however, parameter $string1 of strcasecmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
        if (\strcasecmp(/** @scrutinizer ignore-type */ $message->getContentType(), 'multipart/mixed') !== 0) {
Loading history...
202 3
            $this->setMessageAsMixed($message);
203
        }
204 3
        foreach ($parts as $key => $part) {
205 3
            $from->removePart($part);
206 3
            $message->addChild($part);
207
        }
208 3
        return $this;
209
    }
210
211
    /**
212
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
213
     * or unspecified) message.  If the message has uuencoded attachments, sets
214
     * up the message as a multipart/mixed message and creates a separate
215
     * content part.
216
     */
217 19
    public function enforceMime(IMessage $message) : static
218
    {
219 19
        if (!$message->isMime()) {
220 5
            if ($message->getAttachmentCount()) {
221 3
                $this->setMessageAsMixed($message);
222
            } else {
223 2
                $message->setRawHeader(HeaderConsts::CONTENT_TYPE, "text/plain;\r\n\tcharset=\"iso-8859-1\"");
224
            }
225 5
            $message->setRawHeader(HeaderConsts::MIME_VERSION, '1.0');
226
        }
227 19
        return $this;
228
    }
229
230
    /**
231
     * Creates a multipart/related part out of 'inline' children of $parent and
232
     * returns it.
233
     */
234 2
    public function createMultipartRelatedPartForInlineChildrenOf(IMimePart $parent) : IMimePart
235
    {
236 2
        $relatedPart = $this->mimePartFactory->newInstance();
237 2
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
238 2
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline')) as $part) {
239 2
            $parent->removePart($part);
240 2
            $relatedPart->addChild($part);
241
        }
242 2
        $parent->addChild($relatedPart, 0);
243 2
        return $relatedPart;
244
    }
245
246
    /**
247
     * Finds an alternative inline part in the message and returns it if one
248
     * exists.
249
     *
250
     * If the passed $mimeType is text/plain, searches for a text/html part.
251
     * Otherwise searches for a text/plain part to return.
252
     *
253
     * @return IMimePart or null if not found
254
     */
255 9
    public function findOtherContentPartFor(IMessage $message, string $mimeType) : ?IMimePart
256
    {
257 9
        $altPart = $message->getPart(
258 9
            0,
259 9
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
260 9
        );
261 9
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
262 3
            $altPartParent = $altPart->getParent();
263 3
            if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline')) !== 1) {
264 1
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
265
            }
266
        }
267 9
        return $altPart;
268
    }
269
270
    /**
271
     * Creates a new content part for the passed mimeType and charset, making
272
     * space by creating a multipart/alternative if needed
273
     */
274 8
    public function createContentPartForMimeType(IMessage $message, string $mimeType, string $charset) : IMimePart
275
    {
276 8
        $mimePart = $this->mimePartFactory->newInstance();
277 8
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tcharset=\"$charset\"");
278 8
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, 'quoted-printable');
279
280 8
        $this->enforceMime($message);
281 8
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
282
283 8
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
284 3
            $this->setMessageAsAlternative($message);
285 3
            $message->addChild($mimePart);
286 5
        } elseif ($altPart !== null) {
287 3
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
288 3
            $mimeAltPart->addChild($mimePart, 1);
289
        } else {
290 3
            $message->addChild($mimePart, 0);
291
        }
292
293 8
        return $mimePart;
294
    }
295
296
    /**
297
     * Creates and adds a IMimePart for the passed content and options as an
298
     * attachment.
299
     *
300
     * @param string|resource|\Psr\Http\Message\StreamInterface $resource
301
     */
302 6
    public function createAndAddPartForAttachment(
303
        IMessage $message,
304
        $resource,
305
        string $mimeType,
306
        string $disposition,
307
        ?string $filename = null,
308
        string $encoding = 'base64'
309
    ) : IMessagePart {
310 6
        if ($filename === null) {
311 2
            $filename = 'file' . \uniqid();
312
        }
313
314 6
        $safe = \iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
315 6
        if ($message->isMime()) {
316 5
            $part = $this->mimePartFactory->newInstance();
317 5
            $part->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, $encoding);
318 5
            if (\strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
0 ignored issues
show
Bug introduced by
It seems like $message->getContentType() can also be of type null; however, parameter $string1 of strcasecmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

318
            if (\strcasecmp(/** @scrutinizer ignore-type */ $message->getContentType(), 'multipart/mixed') !== 0) {
Loading history...
319 5
                $this->setMessageAsMixed($message);
320
            }
321 5
            $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tname=\"$safe\"");
322 5
            $part->setRawHeader(HeaderConsts::CONTENT_DISPOSITION, "$disposition;\r\n\tfilename=\"$safe\"");
323
        } else {
324 1
            $part = $this->uuEncodedPartFactory->newInstance();
325 1
            $part->setFilename($safe);
326
        }
327 6
        $part->setContent($resource);
328 6
        $message->addChild($part);
329 6
        return $part;
330
    }
331
332
    /**
333
     * Removes the content part of the message with the passed mime type.  If
334
     * there is a remaining content part and it is an alternative part of the
335
     * main message, the content part is moved to the message part.
336
     *
337
     * If the content part is part of an alternative part beneath the message,
338
     * the alternative part is replaced by the remaining content part,
339
     * optionally keeping other parts if $keepOtherContent is set to true.
340
     *
341
     * @return bool true on success
342
     */
343 6
    public function removeAllContentPartsByMimeType(IMessage $message, string $mimeType, bool $keepOtherContent = false) : bool
344
    {
345 6
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
346 6
        if ($alt !== null) {
347 6
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
348
        }
349
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
350
        return true;
351
    }
352
353
    /**
354
     * Removes the 'inline' part with the passed contentType, at the given index
355
     * defaulting to the first
356
     *
357
     * @return bool true on success
358
     */
359 5
    public function removePartByMimeType(IMessage $message, string $mimeType, int $index = 0) : bool
360
    {
361 5
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
362 5
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
363 5
        if ($parts === null || !isset($parts[$index])) {
364
            return false;
365 5
        } elseif (\count($parts) === 1) {
366 5
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
367
        }
368 1
        $part = $parts[$index];
369 1
        $message->removePart($part);
370 1
        if ($alt !== null && $alt->getChildCount() === 1) {
0 ignored issues
show
Bug introduced by
The method getChildCount() does not exist on ZBateson\MailMimeParser\Message\IMessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\IMessagePart such as ZBateson\MailMimeParser\Message\IMultiPart or ZBateson\MailMimeParser\Message\MultiPart. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

370
        if ($alt !== null && $alt->/** @scrutinizer ignore-call */ getChildCount() === 1) {
Loading history...
371
            $this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
0 ignored issues
show
Bug introduced by
The method getChild() does not exist on ZBateson\MailMimeParser\Message\IMessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\IMessagePart such as ZBateson\MailMimeParser\Message\IMultiPart or ZBateson\MailMimeParser\Message\MultiPart. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

371
            $this->genericHelper->replacePart($message, $alt, $alt->/** @scrutinizer ignore-call */ getChild(0));
Loading history...
372
        }
373 1
        return true;
374
    }
375
376
    /**
377
     * Either creates a mime part or sets the existing mime part with the passed
378
     * mimeType to $strongOrHandle.
379
     *
380
     * @param string|resource $stringOrHandle
381
     */
382 6
    public function setContentPartForMimeType(IMessage $message, string $mimeType, mixed $stringOrHandle, string $charset) : static
383
    {
384 6
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
385 6
        if ($part === null) {
386 5
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
387
        } else {
388 1
            $contentType = $part->getContentType();
389 1
            if ($part instanceof IMimePart) {
390 1
                $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$contentType;\r\n\tcharset=\"$charset\"");
391
            }
392
        }
393 6
        $part->setContent($stringOrHandle);
394 6
        return $this;
395
    }
396
}
397