Passed
Push — master ( 37aad3...325143 )
by Zaahid
03:26
created

MultipartHelper::moveAllPartsAsAttachmentsExcept()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 3
dl 0
loc 16
ccs 9
cts 9
cp 1
crap 3
rs 9.9332
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 18
    public function __construct(
39
        MimePartFactory $mimePartFactory,
40
        UUEncodedPartFactory $uuEncodedPartFactory,
41
        PartBuilderFactory $partBuilderFactory,
42
        GenericHelper $genericHelper
43
    ) {
44 18
        parent::__construct($mimePartFactory, $uuEncodedPartFactory, $partBuilderFactory);
45 18
        $this->genericHelper = $genericHelper;
46 18
    }
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 12
    public function getUniqueBoundary($mimeType)
56
    {
57 12
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
58 12
        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 11
    public function setMimeHeaderBoundaryOnPart(ParentHeaderPart $part, $mimeType)
69
    {
70 11
        $part->setRawHeader(
71 11
            'Content-Type',
72
            "$mimeType;\r\n\tboundary=\""
73 11
                . $this->getUniqueBoundary($mimeType) . '"'
74
        );
75 11
    }
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 4
    public function setMessageAsMixed(Message $message)
87
    {
88 4
        if ($message->hasContent()) {
89 1
            $part = $this->genericHelper->createNewContentPartFrom($message);
90 1
            $message->addChild($part, 0);
91
        }
92 4
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed');
93 4
        $atts = $message->getAllAttachmentParts();
94 4
        if (!empty($atts)) {
95 2
            foreach ($atts as $att) {
96 2
                $att->markAsChanged();
97
            }
98
        }
99 4
    }
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 2
    public function setMessageAsAlternative(Message $message)
111
    {
112 2
        if ($message->hasContent()) {
113 1
            $part = $this->genericHelper->createNewContentPartFrom($message);
114 1
            $message->addChild($part, 0);
115
        }
116 2
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/alternative');
117 2
    }
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 1
    public function getContentPartContainerFromAlternative($mimeType, ParentHeaderPart $alternativePart)
133
    {
134 1
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
135 1
        $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 1
            if ($part === null) {
138 1
                return false;
139
            }
140 1
            $contPart = $part;
141 1
            $part = $part->getParent();
142 1
        } while ($part !== $alternativePart);
143 1
        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 2
    public function createAlternativeContentPart(Message $message, MessagePart $contentPart)
195
    {
196 2
        $altPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart();
197 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
198 2
        $message->removePart($contentPart);
199 2
        $message->addChild($altPart, 0);
200 2
        $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 2
        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 1
    public function moveAllPartsAsAttachmentsExcept(Message $message, ParentHeaderPart $from, $exceptMimeType)
214
    {
215 1
        $parts = $from->getAllParts(new PartFilter([
216 1
            'multipart' => PartFilter::FILTER_EXCLUDE,
217
            'headers' => [
218
                PartFilter::FILTER_EXCLUDE => [
219 1
                    'Content-Type' => $exceptMimeType
220
                ]
221
            ]
222
        ]));
223 1
        if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
224 1
            $this->setMessageAsMixed($message);
225
        }
226 1
        foreach ($parts as $part) {
227 1
            $from->removePart($part);
228 1
            $message->addChild($part);
229
        }
230 1
    }
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 6
    public function enforceMime(Message $message)
241
    {
242 6
        if (!$message->isMime()) {
243 3
            if ($message->getAttachmentCount()) {
244 1
                $this->setMessageAsMixed($message);
245
            } else {
246 2
                $message->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"iso-8859-1\"");
247
            }
248 3
            $message->setRawHeader('Mime-Version', '1.0');
249
        }
250 6
    }
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 2
    public function createMultipartRelatedPartForInlineChildrenOf(ParentHeaderPart $parent)
260
    {
261 2
        $relatedPart = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory)->createMessagePart();
262 2
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
263 2
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
264 2
            $parent->removePart($part);
265 2
            $relatedPart->addChild($part);
266
        }
267 2
        $parent->addChild($relatedPart, 0);
268 2
        return $relatedPart;
269
    }
270
271
    /**
272
     * Finds an alternative inline part in the message and returns it if one
273
     * exists.
274
     *
275
     * If the passed $mimeType is text/plain, searches for a text/html part.
276
     * Otherwise searches for a text/plain part to return.
277
     *
278
     * @param Message $message
279
     * @param string $mimeType
280
     * @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...
281
     */
282 5
    public function findOtherContentPartFor(Message $message, $mimeType)
283
    {
284 5
        $altPart = $message->getPart(
285 5
            0,
286 5
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
287
        );
288 5
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
289 1
            $altPartParent = $altPart->getParent();
290 1
            if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
291 1
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
292
            }
293
        }
294 5
        return $altPart;
295
    }
296
297
    /**
298
     * Creates a new content part for the passed mimeType and charset, making
299
     * space by creating a multipart/alternative if needed
300
     *
301
     * @param Message $message
302
     * @param string $mimeType
303
     * @param string $charset
304
     * @return MimePart
305
     */
306 4
    public function createContentPartForMimeType(Message $message, $mimeType, $charset)
307
    {
308 4
        $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
309 4
        $builder->addHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
310 4
        $builder->addHeader('Content-Transfer-Encoding', 'quoted-printable');
311 4
        $this->enforceMime($message);
312 4
        $mimePart = $builder->createMessagePart();
313
314 4
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
315
316 4
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
317 1
            $this->setMessageAsAlternative($message);
318 1
            $message->addChild($mimePart);
319 3
        } elseif ($altPart !== null) {
320 1
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
321 1
            $mimeAltPart->addChild($mimePart, 1);
322
        } else {
323 2
            $message->addChild($mimePart, 0);
324
        }
325
326 4
        return $mimePart;
327
    }
328
329
    /**
330
     * Creates and adds a MimePart for the passed content and options as an
331
     * attachment.
332
     *
333
     * @param Message $message
334
     * @param string|resource|StreamInterface $resource
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\...\Helper\StreamInterface 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...
335
     * @param string $mimeType
336
     * @param string $filename
337
     * @param string $disposition
338
     * @return MimePart
339
     */
340 2
    public function createAndAddPartForAttachment(Message $message, $resource, $mimeType, $filename, $disposition)
341
    {
342 2
        if ($filename === null) {
0 ignored issues
show
introduced by
The condition $filename === null is always false.
Loading history...
343 1
            $filename = 'file' . uniqid();
344
        }
345
346 2
        $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
347 2
        if ($message->isMime()) {
348 1
            $builder = $this->partBuilderFactory->newPartBuilder($this->mimePartFactory);
349 1
            $builder->addHeader('Content-Transfer-Encoding', 'base64');
350 1
            if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
351 1
                $this->setMessageAsMixed($message);
352
            }
353 1
            $builder->addHeader('Content-Type', "$mimeType;\r\n\tname=\"$safe\"");
354 1
            $builder->addHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$safe\"");
355
        } else {
356 1
            $builder = $this->partBuilderFactory->newPartBuilder(
357 1
                $this->uuEncodedPartFactory
358
            );
359 1
            $builder->setProperty('filename', $safe);
360
        }
361 2
        $part = $builder->createMessagePart();
362 2
        $part->setContent($resource);
363 2
        $message->addChild($part);
364 2
    }
365
366
    /**
367
     * Removes the content part of the message with the passed mime type.  If
368
     * there is a remaining content part and it is an alternative part of the
369
     * main message, the content part is moved to the message part.
370
     *
371
     * If the content part is part of an alternative part beneath the message,
372
     * the alternative part is replaced by the remaining content part,
373
     * optionally keeping other parts if $keepOtherContent is set to true.
374
     *
375
     * @param Message $message
376
     * @param string $mimeType
377
     * @param bool $keepOtherContent
378
     * @return boolean true on success
379
     */
380
    public function removeAllContentPartsByMimeType(Message $message, $mimeType, $keepOtherContent = false)
381
    {
382
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
383
        if ($alt !== null) {
384
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
385
        }
386
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
387
        return true;
388
    }
389
390
    /**
391
     * Removes the 'inline' part with the passed contentType, at the given index
392
     * defaulting to the first
393
     *
394
     * @param Message $message
395
     * @param string $mimeType
396
     * @param int $index
397
     * @return boolean true on success
398
     */
399
    public function removePartByMimeType(Message $message, $mimeType, $index = 0)
400
    {
401
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
402
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
403
        if ($parts === null || !isset($parts[$index])) {
404
            return false;
405
        } elseif (count($parts) === 1) {
406
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
407
        }
408
        $part = $parts[$index];
409
        $message->removePart($part);
410
        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

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

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