Passed
Branch php8-testing (0e47ea)
by Zaahid
03:09
created

MultipartHelper::removeAllContentPartsByMimeType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.1481

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 3
dl 0
loc 8
ccs 4
cts 6
cp 0.6667
crap 2.1481
rs 10
c 0
b 0
f 0
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
namespace ZBateson\MailMimeParser\Message\Helper;
8
9
use ZBateson\MailMimeParser\Message;
10
use ZBateson\MailMimeParser\Message\Part\Factory\MimePartFactory;
11
use ZBateson\MailMimeParser\Message\Part\Factory\PartBuilderFactory;
12
use ZBateson\MailMimeParser\Message\Part\Factory\UUEncodedPartFactory;
13
use ZBateson\MailMimeParser\Message\Part\MessagePart;
14
use ZBateson\MailMimeParser\Message\Part\MimePart;
15
use ZBateson\MailMimeParser\Message\Part\ParentHeaderPart;
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
    /**
33
     * Constructor
34
     * 
35
     * @param MimePartFactory $mimePartFactory
36
     * @param UUEncodedPartFactory $uuEncodedPartFactory
37
     * @param PartBuilderFactory $partBuilderFactory
38
     * @param GenericHelper $genericHelper
39
     */
40 20
    public function __construct(
41
        MimePartFactory $mimePartFactory,
42
        UUEncodedPartFactory $uuEncodedPartFactory,
43
        PartBuilderFactory $partBuilderFactory,
44
        GenericHelper $genericHelper
45
    ) {
46 20
        parent::__construct($mimePartFactory, $uuEncodedPartFactory, $partBuilderFactory);
47 20
        $this->genericHelper = $genericHelper;
48 20
    }
49
50
    /**
51
     * Creates and returns a unique boundary.
52
     *
53
     * @param string $mimeType first 3 characters of a multipart type are used,
54
     *      e.g. REL for relative or ALT for alternative
55
     * @return string
56
     */
57 32
    public function getUniqueBoundary($mimeType)
58
    {
59 32
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
60 32
        return uniqid('----=MMP-' . $type . '.', true);
61
    }
62
63
    /**
64
     * Creates a unique mime boundary and assigns it to the passed part's
65
     * Content-Type header with the passed mime type.
66
     *
67
     * @param ParentHeaderPart $part
68
     * @param string $mimeType
69
     */
70 24
    public function setMimeHeaderBoundaryOnPart(ParentHeaderPart $part, $mimeType)
71
    {
72 24
        $part->setRawHeader(
73 24
            'Content-Type',
74
            "$mimeType;\r\n\tboundary=\""
75 24
                . $this->getUniqueBoundary($mimeType) . '"'
76
        );
77 24
    }
78
79
    /**
80
     * Sets the passed message as multipart/mixed.
81
     * 
82
     * If the message has content, a new part is created and added as a child of
83
     * the message.  The message's content and content headers are moved to the
84
     * new part.
85
     *
86
     * @param Message $message
87
     */
88 15
    public function setMessageAsMixed(Message $message)
89
    {
90 15
        if ($message->hasContent()) {
91 10
            $part = $this->genericHelper->createNewContentPartFrom($message);
92 10
            $message->addChild($part, 0);
93
        }
94 15
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed');
95 15
        $atts = $message->getAllAttachmentParts();
96 15
        if (!empty($atts)) {
97 7
            foreach ($atts as $att) {
98 7
                $att->markAsChanged();
99
            }
100
        }
101 15
    }
102
103
    /**
104
     * Sets the passed message as multipart/alternative.
105
     *
106
     * If the message has content, a new part is created and added as a child of
107
     * the message.  The message's content and content headers are moved to the
108
     * new part.
109
     *
110
     * @param Message $message
111
     */
112 4
    public function setMessageAsAlternative(Message $message)
113
    {
114 4
        if ($message->hasContent()) {
115 3
            $part = $this->genericHelper->createNewContentPartFrom($message);
116 3
            $message->addChild($part, 0);
117
        }
118 4
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/alternative');
119 4
    }
120
121
    /**
122
     * Searches the passed $alternativePart for a part with the passed mime type
123
     * and returns its parent.
124
     *
125
     * Used for alternative mime types that have a multipart/mixed or
126
     * multipart/related child containing a content part of $mimeType, where
127
     * the whole mixed/related part should be removed.
128
     *
129
     * @param string $mimeType the content-type to find below $alternativePart
130
     * @param ParentHeaderPart $alternativePart The multipart/alternative part to look
131
     *        under
132
     * @return boolean|MimePart false if a part is not found
133
     */
134 11
    public function getContentPartContainerFromAlternative($mimeType, ParentHeaderPart $alternativePart)
135
    {
136 11
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
137 11
        $contPart = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $contPart is dead and can be removed.
Loading history...
138
        do {
139 11
            if ($part === null) {
140 1
                return false;
141
            }
142 11
            $contPart = $part;
143 11
            $part = $part->getParent();
144 11
        } while ($part !== $alternativePart);
145 11
        return $contPart;
146
    }
147
148
    /**
149
     * Removes all parts of $mimeType from $alternativePart.
150
     *
151
     * If $alternativePart contains a multipart/mixed or multipart/relative part
152
     * with other parts of different content-types, the multipart part is
153
     * removed, and parts of different content-types can optionally be moved to
154
     * the main message part.
155
     *
156
     * @param Message $message
157
     * @param string $mimeType
158
     * @param ParentHeaderPart $alternativePart
159
     * @param bool $keepOtherContent
160
     * @return bool
161
     */
162 6
    public function removeAllContentPartsFromAlternative(Message $message, $mimeType, ParentHeaderPart $alternativePart, $keepOtherContent)
163
    {
164 6
        $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart);
165 6
        if ($rmPart === false) {
0 ignored issues
show
introduced by
The condition $rmPart === false is always false.
Loading history...
166
            return false;
167
        }
168 6
        if ($keepOtherContent) {
169 6
            $this->moveAllPartsAsAttachmentsExcept($message, $rmPart, $mimeType);
170 6
            $alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
171
        } else {
172
            $rmPart->removeAllParts();
173
        }
174 6
        $message->removePart($rmPart);
175 6
        if ($alternativePart !== null) {
176 5
            if ($alternativePart->getChildCount() === 1) {
0 ignored issues
show
Bug introduced by
The method getChildCount() does not exist on ZBateson\MailMimeParser\Message\Part\MessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\Part\MessagePart such as ZBateson\MailMimeParser\Message\Part\ParentPart. ( Ignorable by Annotation )

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

176
            if ($alternativePart->/** @scrutinizer ignore-call */ getChildCount() === 1) {
Loading history...
177 1
                $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\Part\MessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\Part\MessagePart such as ZBateson\MailMimeParser\Message\Part\ParentPart. ( Ignorable by Annotation )

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

177
                $this->genericHelper->replacePart($message, $alternativePart, $alternativePart->/** @scrutinizer ignore-call */ getChild(0));
Loading history...
178 4
            } elseif ($alternativePart->getChildCount() === 0) {
179 4
                $message->removePart($alternativePart);
180
            }
181
        }
182 6
        while ($message->getChildCount() === 1) {
183 3
            $this->genericHelper->replacePart($message, $message, $message->getChild(0));
184
        }
185 6
        return true;
186
    }
187
188
    /**
189
     * Creates a new mime part as a multipart/alternative and assigns the passed
190
     * $contentPart as a part below it before returning it.
191
     *
192
     * @param Message $message
193
     * @param MessagePart $contentPart
194
     * @return MimePart the alternative part
195
     */
196 4
    public function createAlternativeContentPart(Message $message, MessagePart $contentPart)
197
    {
198 4
        $altPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart();
199 4
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
200 4
        $message->removePart($contentPart);
201 4
        $message->addChild($altPart, 0);
202 4
        $altPart->addChild($contentPart, 0);
0 ignored issues
show
Bug introduced by
The method addChild() does not exist on ZBateson\MailMimeParser\Message\Part\MessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\Part\MessagePart such as ZBateson\MailMimeParser\Message\Part\ParentPart. ( Ignorable by Annotation )

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

202
        $altPart->/** @scrutinizer ignore-call */ 
203
                  addChild($contentPart, 0);
Loading history...
203 4
        return $altPart;
204
    }
205
206
    /**
207
     * Moves all parts under $from into this message except those with a
208
     * content-type equal to $exceptMimeType.  If the message is not a
209
     * multipart/mixed message, it is set to multipart/mixed first.
210
     *
211
     * @param Message $message
212
     * @param ParentHeaderPart $from
213
     * @param string $exceptMimeType
214
     */
215 7
    public function moveAllPartsAsAttachmentsExcept(Message $message, ParentHeaderPart $from, $exceptMimeType)
216
    {
217 7
        $parts = $from->getAllParts(new PartFilter([
218 7
            'multipart' => PartFilter::FILTER_EXCLUDE,
219
            'headers' => [
220 7
                PartFilter::FILTER_EXCLUDE => [
221 7
                    'Content-Type' => $exceptMimeType
222
                ]
223
            ]
224
        ]));
225 7
        if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
226 6
            $this->setMessageAsMixed($message);
227
        }
228 7
        foreach ($parts as $part) {
229 3
            $from->removePart($part);
230 3
            $message->addChild($part);
231
        }
232 7
    }
233
234
    /**
235
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
236
     * or unspecified) message.  If the message has uuencoded attachments, sets
237
     * up the message as a multipart/mixed message and creates a separate
238
     * content part.
239
     *
240
     * @param Message $message
241
     */
242 18
    public function enforceMime(Message $message)
243
    {
244 18
        if (!$message->isMime()) {
245 5
            if ($message->getAttachmentCount()) {
246 3
                $this->setMessageAsMixed($message);
247
            } else {
248 2
                $message->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"iso-8859-1\"");
249
            }
250 5
            $message->setRawHeader('Mime-Version', '1.0');
251
        }
252 18
    }
253
254
    /**
255
     * Creates a multipart/related part out of 'inline' children of $parent and
256
     * returns it.
257
     *
258
     * @param ParentHeaderPart $parent
259
     * @return MimePart
260
     */
261 2
    public function createMultipartRelatedPartForInlineChildrenOf(ParentHeaderPart $parent)
262
    {
263 2
        $relatedPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart();
264 2
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
265 2
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
266 2
            $parent->removePart($part);
267 2
            $relatedPart->addChild($part);
268
        }
269 2
        $parent->addChild($relatedPart, 0);
270 2
        return $relatedPart;
271
    }
272
273
    /**
274
     * Finds an alternative inline part in the message and returns it if one
275
     * exists.
276
     *
277
     * If the passed $mimeType is text/plain, searches for a text/html part.
278
     * Otherwise searches for a text/plain part to return.
279
     *
280
     * @param Message $message
281
     * @param string $mimeType
282
     * @return \ZBateson\MailMimeParser\Message\Part\MimeType or null if not
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Message\Part\MimeType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
283
     *         found
284
     */
285 9
    public function findOtherContentPartFor(Message $message, $mimeType)
286
    {
287 9
        $altPart = $message->getPart(
288 9
            0,
289 9
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
290
        );
291 9
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
292 3
            $altPartParent = $altPart->getParent();
293 3
            if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
294 1
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
295
            }
296
        }
297 9
        return $altPart;
298
    }
299
300
    /**
301
     * Creates a new content part for the passed mimeType and charset, making
302
     * space by creating a multipart/alternative if needed
303
     *
304
     * @param Message $message
305
     * @param string $mimeType
306
     * @param string $charset
307
     * @return \ZBateson\MailMimeParser\Message\Part\MimePart
308
     */
309 8
    public function createContentPartForMimeType(Message $message, $mimeType, $charset)
310
    {
311 8
        $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
312 8
        $builder->addHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
313 8
        $builder->addHeader('Content-Transfer-Encoding', 'quoted-printable');
314 8
        $this->enforceMime($message);
315 8
        $mimePart = $builder->createMessagePart();
316
317 8
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
318
319 8
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
320 3
            $this->setMessageAsAlternative($message);
321 3
            $message->addChild($mimePart);
322 5
        } elseif ($altPart !== null) {
323 3
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
324 3
            $mimeAltPart->addChild($mimePart, 1);
325
        } else {
326 3
            $message->addChild($mimePart, 0);
327
        }
328
329 8
        return $mimePart;
330
    }
331
332
    /**
333
     * Creates and adds a MimePart for the passed content and options as an
334
     * attachment.
335
     *
336
     * @param Message $message
337
     * @param string|resource|Psr\Http\Message\StreamInterface\StreamInterface
338
     *        $resource
339
     * @param string $mimeType
340
     * @param string $disposition
341
     * @param string $filename
342
     * @param string $encoding
343
     * @return \ZBateson\MailMimeParser\Message\Part\MimePart
344
     */
345 6
    public function createAndAddPartForAttachment(Message $message, $resource, $mimeType, $disposition, $filename = null, $encoding = 'base64')
346
    {
347 6
        if ($filename === null) {
348 2
            $filename = 'file' . uniqid();
349
        }
350
351 6
        $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
352 6
        if ($message->isMime()) {
353 5
            $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
354 5
            $builder->addHeader('Content-Transfer-Encoding', $encoding);
355 5
            if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
356 5
                $this->setMessageAsMixed($message);
357
            }
358 5
            $builder->addHeader('Content-Type', "$mimeType;\r\n\tname=\"$safe\"");
359 5
            $builder->addHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$safe\"");
360
        } else {
361 1
            $builder = $this->partBuilderFactory->newPartBuilder(
362 1
                $this->uuEncodedPartFactory
363
            );
364 1
            $builder->setProperty('filename', $safe);
365
        }
366 6
        $part = $builder->createMessagePart();
367 6
        $part->setContent($resource);
368 6
        $message->addChild($part);
369 6
    }
370
371
    /**
372
     * Removes the content part of the message with the passed mime type.  If
373
     * there is a remaining content part and it is an alternative part of the
374
     * main message, the content part is moved to the message part.
375
     *
376
     * If the content part is part of an alternative part beneath the message,
377
     * the alternative part is replaced by the remaining content part,
378
     * optionally keeping other parts if $keepOtherContent is set to true.
379
     *
380
     * @param Message $message
381
     * @param string $mimeType
382
     * @param bool $keepOtherContent
383
     * @return boolean true on success
384
     */
385 6
    public function removeAllContentPartsByMimeType(Message $message, $mimeType, $keepOtherContent = false)
386
    {
387 6
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
388 6
        if ($alt !== null) {
389 6
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
390
        }
391
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
392
        return true;
393
    }
394
395
    /**
396
     * Removes the 'inline' part with the passed contentType, at the given index
397
     * defaulting to the first
398
     *
399
     * @param Message $message
400
     * @param string $mimeType
401
     * @param int $index
402
     * @return boolean true on success
403
     */
404 5
    public function removePartByMimeType(Message $message, $mimeType, $index = 0)
405
    {
406 5
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
407 5
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
408 5
        if ($parts === null || !isset($parts[$index])) {
409
            return false;
410 5
        } elseif (count($parts) === 1) {
411 5
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
412
        }
413 1
        $part = $parts[$index];
414 1
        $message->removePart($part);
415 1
        if ($alt !== null && $alt->getChildCount() === 1) {
416
            $this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
417
        }
418 1
        return true;
419
    }
420
421
    /**
422
     * Either creates a mime part or sets the existing mime part with the passed
423
     * mimeType to $strongOrHandle.
424
     *
425
     * @param Message $message
426
     * @param string $mimeType
427
     * @param string|resource $stringOrHandle
428
     * @param string $charset
429
     */
430 6
    public function setContentPartForMimeType(Message $message, $mimeType, $stringOrHandle, $charset)
431
    {
432 6
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
433 6
        if ($part === null) {
434 5
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
435
        } else {
436 1
            $contentType = $part->getContentType();
437 1
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
0 ignored issues
show
Bug introduced by
The method setRawHeader() does not exist on ZBateson\MailMimeParser\Message\Part\MessagePart. It seems like you code against a sub-type of ZBateson\MailMimeParser\Message\Part\MessagePart such as ZBateson\MailMimeParser\...e\Part\ParentHeaderPart. ( Ignorable by Annotation )

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

437
            $part->/** @scrutinizer ignore-call */ 
438
                   setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
Loading history...
438
        }
439 6
        $part->setContent($stringOrHandle);
440 6
    }
441
}
442