Passed
Push — master ( 9e6d2f...c6b62f )
by Zaahid
06:54
created

removeAllContentPartsFromAlternative()   B

Complexity

Conditions 7
Paths 17

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

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

200
        $altPart->/** @scrutinizer ignore-call */ 
201
                  addChild($contentPart, 0);
Loading history...
201
        return $altPart;
202
    }
203
204
    /**
205
     * Moves all parts under $from into this message except those with a
206
     * content-type equal to $exceptMimeType.  If the message is not a
207
     * multipart/mixed message, it is set to multipart/mixed first.
208
     *
209
     * @param Message $message
210
     * @param ParentHeaderPart $from
211
     * @param string $exceptMimeType
212
     */
213
    public function moveAllPartsAsAttachmentsExcept(Message $message, ParentHeaderPart $from, $exceptMimeType)
214
    {
215
        $parts = $from->getAllParts(new PartFilter([
216
            'multipart' => PartFilter::FILTER_EXCLUDE,
217
            'headers' => [
218
                PartFilter::FILTER_EXCLUDE => [
219
                    'Content-Type' => $exceptMimeType
220
                ]
221
            ]
222
        ]));
223
        if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
224
            $this->setMessageAsMixed($message);
225
        }
226
        foreach ($parts as $part) {
227
            $from->removePart($part);
228
            $message->addChild($part);
229
        }
230
    }
231
232
    /**
233
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
234
     * or unspecified) message.  If the message has uuencoded attachments, sets
235
     * up the message as a multipart/mixed message and creates a separate
236
     * content part.
237
     *
238
     * @param Message $message
239
     */
240
    public function enforceMime(Message $message)
241
    {
242
        if (!$message->isMime()) {
243
            if ($message->getAttachmentCount()) {
244
                $this->setMessageAsMixed($message);
245
            } else {
246
                $message->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"iso-8859-1\"");
247
            }
248
            $message->setRawHeader('Mime-Version', '1.0');
249
        }
250
    }
251
252
    /**
253
     * Creates a multipart/related part out of 'inline' children of $parent and
254
     * returns it.
255
     *
256
     * @param ParentHeaderPart $parent
257
     * @return MimePart
258
     */
259
    public function createMultipartRelatedPartForInlineChildrenOf(ParentHeaderPart $parent)
260
    {
261
        $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
262
        $relatedPart = $builder->createMessagePart();
263
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
264
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
265
            $parent->removePart($part);
266
            $relatedPart->addChild($part);
267
        }
268
        $parent->addChild($relatedPart, 0);
269
        return $relatedPart;
270
    }
271
272
    /**
273
     * Finds an alternative inline part in the message and returns it if one
274
     * exists.
275
     *
276
     * If the passed $mimeType is text/plain, searches for a text/html part.
277
     * Otherwise searches for a text/plain part to return.
278
     *
279
     * @param Message $message
280
     * @param string $mimeType
281
     * @return MimeType or null if not found
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Message\Helper\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...
282
     */
283
    public function findOtherContentPartFor(Message $message, $mimeType)
284
    {
285
        $altPart = $message->getPart(
286
            0,
287
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
288
        );
289
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
290
            $altPartParent = $altPart->getParent();
291
            if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
292
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
293
            }
294
        }
295
        return $altPart;
296
    }
297
298
    /**
299
     * Creates a new content part for the passed mimeType and charset, making
300
     * space by creating a multipart/alternative if needed
301
     *
302
     * @param Message $message
303
     * @param string $mimeType
304
     * @param string $charset
305
     * @return MimePart
306
     */
307
    public function createContentPartForMimeType(Message $message, $mimeType, $charset)
308
    {
309
        $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
310
        $builder->addHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
311
        $builder->addHeader('Content-Transfer-Encoding', 'quoted-printable');
312
        $this->enforceMime($message);
313
        $mimePart = $builder->createMessagePart();
314
315
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
316
317
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
318
            $this->setMessageAsAlternative($message);
319
            $message->addChild($mimePart);
320
        } elseif ($altPart !== null) {
321
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
322
            $mimeAltPart->addChild($mimePart, 1);
323
        } else {
324
            $message->addChild($mimePart, 0);
325
        }
326
327
        return $mimePart;
328
    }
329
330
    /**
331
     * Creates and returns a MimePart for use with a new attachment part being
332
     * created.
333
     *
334
     * @param Message $message
335
     * @param string $mimeType
336
     * @param string $filename
337
     * @param string $disposition
338
     * @return MimePart
339
     */
340
    public function createPartForAttachment(Message $message, $mimeType, $filename, $disposition)
341
    {
342
        $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
343
        if ($message->isMime()) {
344
            $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
345
            $builder->addHeader('Content-Transfer-Encoding', 'base64');
346
            if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
347
                $this->setMessageAsMixed($message);
348
            }
349
            $builder->addHeader('Content-Type', "$mimeType;\r\n\tname=\"$safe\"");
350
            $builder->addHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$safe\"");
351
        } else {
352
            $builder = $this->partBuilderFactory->newPartBuilder(
353
                $this->uuEncodedPartFactory
354
            );
355
            $builder->setProperty('filename', $safe);
356
        }
357
        return $builder->createMessagePart();
358
    }
359
360
    /**
361
     * Removes the content part of the message with the passed mime type.  If
362
     * there is a remaining content part and it is an alternative part of the
363
     * main message, the content part is moved to the message part.
364
     *
365
     * If the content part is part of an alternative part beneath the message,
366
     * the alternative part is replaced by the remaining content part,
367
     * optionally keeping other parts if $keepOtherContent is set to true.
368
     *
369
     * @param Message $message
370
     * @param string $mimeType
371
     * @param bool $keepOtherContent
372
     * @return boolean true on success
373
     */
374
    public function removeAllContentPartsByMimeType(Message $message, $mimeType, $keepOtherContent = false)
375
    {
376
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
377
        if ($alt !== null) {
378
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
379
        }
380
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
381
        return true;
382
    }
383
384
    /**
385
     * Removes the 'inline' part with the passed contentType, at the given index
386
     * defaulting to the first
387
     *
388
     * @param Message $message
389
     * @param string $mimeType
390
     * @param int $index
391
     * @return boolean true on success
392
     */
393
    public function removePartByMimeType(Message $message, $mimeType, $index = 0)
394
    {
395
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
396
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
397
        if ($parts === null || !isset($parts[$index])) {
398
            return false;
399
        } elseif (count($parts) === 1) {
400
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
401
        }
402
        $part = $parts[$index];
403
        $message->removePart($part);
404
        if ($alt !== null && $alt->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

404
        if ($alt !== null && $alt->/** @scrutinizer ignore-call */ getChildCount() === 1) {
Loading history...
405
            $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\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

405
            $this->genericHelper->replacePart($message, $alt, $alt->/** @scrutinizer ignore-call */ getChild(0));
Loading history...
406
        }
407
        return true;
408
    }
409
410
    /**
411
     * Either creates a mime part or sets the existing mime part with the passed
412
     * mimeType to $strongOrHandle.
413
     *
414
     * @param Message $message
415
     * @param string $mimeType
416
     * @param string|resource $stringOrHandle
417
     * @param string $charset
418
     */
419
    public function setContentPartForMimeType(Message $message, $mimeType, $stringOrHandle, $charset)
420
    {
421
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
422
        if ($part === null) {
423
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
424
        } else {
425
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
426
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
427
        }
428
        $part->setContent($stringOrHandle);
429
    }
430
}
431