Passed
Push — master ( 181d77...7fe46a )
by Zaahid
06:13 queued 12s
created

MultipartHelper   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 356
Duplicated Lines 0 %

Test Coverage

Coverage 96.08%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 55
eloc 139
c 1
b 0
f 0
dl 0
loc 356
ccs 147
cts 153
cp 0.9608
rs 6

17 Methods

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

152
            if ($alternativePart->/** @scrutinizer ignore-call */ getChildCount() === 1) {
Loading history...
153 4
                $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->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

153
                $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->/** @scrutinizer ignore-call */ getChild(0));
Loading history...
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

153
                $this->genericHelper->replacePart($message, $alternativePart, /** @scrutinizer ignore-type */ $alternativePart->getChild(0));
Loading history...
154 2
            } elseif ($alternativePart->getChildCount() === 0) {
155 2
                $message->removePart($alternativePart);
156
            }
157
        }
158 6
        while ($message->getChildCount() === 1) {
159
            $this->genericHelper->replacePart($message, $message, $message->getChild(0));
160
        }
161 6
        return true;
162
    }
163
164
    /**
165
     * Creates a new mime part as a multipart/alternative and assigns the passed
166
     * $contentPart as a part below it before returning it.
167
     *
168
     * @return IMimePart the alternative part
169
     */
170 4
    public function createAlternativeContentPart(IMessage $message, IMessagePart $contentPart)
171
    {
172 4
        $altPart = $this->mimePartFactory->newInstance();
173 4
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
174 4
        $message->removePart($contentPart);
175 4
        $message->addChild($altPart, 0);
176 4
        $altPart->addChild($contentPart, 0);
177 4
        return $altPart;
178
    }
179
180
    /**
181
     * Moves all parts under $from into this message except those with a
182
     * content-type equal to $exceptMimeType.  If the message is not a
183
     * multipart/mixed message, it is set to multipart/mixed first.
184
     */
185 3
    public function moveAllNonMultiPartsToMessageExcept(IMessage $message, IMimePart $from, string $exceptMimeType) : self
186
    {
187 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...
188 2
            if ($part instanceof IMimePart && $part->isMultiPart()) {
189 2
                return false;
190
            }
191 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

191
            return \strcasecmp(/** @scrutinizer ignore-type */ $part->getContentType(), $exceptMimeType) !== 0;
Loading history...
192 3
        });
193 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

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

307
            if (\strcasecmp(/** @scrutinizer ignore-type */ $message->getContentType(), 'multipart/mixed') !== 0) {
Loading history...
308 5
                $this->setMessageAsMixed($message);
309
            }
310 5
            $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tname=\"$safe\"");
311 5
            $part->setRawHeader(HeaderConsts::CONTENT_DISPOSITION, "$disposition;\r\n\tfilename=\"$safe\"");
312
        } else {
313 1
            $part = $this->uuEncodedPartFactory->newInstance();
314 1
            $part->setFilename($safe);
315
        }
316 6
        $part->setContent($resource);
317 6
        $message->addChild($part);
318
    }
319
320
    /**
321
     * Removes the content part of the message with the passed mime type.  If
322
     * there is a remaining content part and it is an alternative part of the
323
     * main message, the content part is moved to the message part.
324
     *
325
     * If the content part is part of an alternative part beneath the message,
326
     * the alternative part is replaced by the remaining content part,
327
     * optionally keeping other parts if $keepOtherContent is set to true.
328
     *
329
     * @return bool true on success
330
     */
331 6
    public function removeAllContentPartsByMimeType(IMessage $message, string $mimeType, bool $keepOtherContent = false) : bool
332
    {
333 6
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
334 6
        if ($alt !== null) {
335 6
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
336
        }
337
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
338
        return true;
339
    }
340
341
    /**
342
     * Removes the 'inline' part with the passed contentType, at the given index
343
     * defaulting to the first
344
     *
345
     * @return bool true on success
346
     */
347 5
    public function removePartByMimeType(IMessage $message, string $mimeType, int $index = 0) : bool
348
    {
349 5
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
350 5
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
351 5
        if ($parts === null || !isset($parts[$index])) {
352
            return false;
353 5
        } elseif (\count($parts) === 1) {
354 5
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
355
        }
356 1
        $part = $parts[$index];
357 1
        $message->removePart($part);
358 1
        if ($alt !== null && $alt->getChildCount() === 1) {
359
            $this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
360
        }
361 1
        return true;
362
    }
363
364
    /**
365
     * Either creates a mime part or sets the existing mime part with the passed
366
     * mimeType to $strongOrHandle.
367
     *
368
     * @param string|resource $stringOrHandle
369
     */
370 6
    public function setContentPartForMimeType(IMessage $message, string $mimeType, $stringOrHandle, string $charset) : self
371
    {
372 6
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
373 6
        if ($part === null) {
374 5
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
375
        } else {
376 1
            $contentType = $part->getContentType();
377 1
            $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$contentType;\r\n\tcharset=\"$charset\"");
0 ignored issues
show
Bug introduced by
The method setRawHeader() 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\MimePart or ZBateson\MailMimeParser\Message\IMimePart or ZBateson\MailMimeParser\Message\MimePart. ( Ignorable by Annotation )

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

377
            $part->/** @scrutinizer ignore-call */ 
378
                   setRawHeader(HeaderConsts::CONTENT_TYPE, "$contentType;\r\n\tcharset=\"$charset\"");
Loading history...
378
        }
379 6
        $part->setContent($stringOrHandle);
380 6
        return $this;
381
    }
382
}
383