Passed
Push — master ( 24955d...c95513 )
by Zaahid
06:27 queued 13s
created

MultipartHelper::removePartByMimeType()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6.1666

Importance

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

165
            if ($alternativePart->/** @scrutinizer ignore-call */ getChildCount() === 1) {
Loading history...
166 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

166
                $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

166
                $this->genericHelper->replacePart($message, $alternativePart, /** @scrutinizer ignore-type */ $alternativePart->getChild(0));
Loading history...
167 2
            } elseif ($alternativePart->getChildCount() === 0) {
168 2
                $message->removePart($alternativePart);
169
            }
170
        }
171 6
        while ($message->getChildCount() === 1) {
172
            $this->genericHelper->replacePart($message, $message, $message->getChild(0));
173
        }
174 6
        return true;
175
    }
176
177
    /**
178
     * Creates a new mime part as a multipart/alternative and assigns the passed
179
     * $contentPart as a part below it before returning it.
180
     *
181
     * @param IMessage $message
182
     * @param IMessagePart $contentPart
183
     * @return IMimePart the alternative part
184
     */
185 4
    public function createAlternativeContentPart(IMessage $message, IMessagePart $contentPart)
186
    {
187 4
        $altPart = $this->mimePartFactory->newInstance();
188 4
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
189 4
        $message->removePart($contentPart);
190 4
        $message->addChild($altPart, 0);
191 4
        $altPart->addChild($contentPart, 0);
192 4
        return $altPart;
193
    }
194
195
    /**
196
     * Moves all parts under $from into this message except those with a
197
     * content-type equal to $exceptMimeType.  If the message is not a
198
     * multipart/mixed message, it is set to multipart/mixed first.
199
     *
200
     * @param IMessage $message
201
     * @param IMimePart $from
202
     * @param string $exceptMimeType
203
     */
204 3
    public function moveAllNonMultiPartsToMessageExcept(IMessage $message, IMimePart $from, $exceptMimeType)
205
    {
206 3
        $parts = $from->getAllParts(function (IMessagePart $part) use ($exceptMimeType) {
207 2
            if ($part instanceof IMimePart && $part->isMultiPart()) {
208 2
                return false;
209
            }
210 2
            return strcasecmp($part->getContentType(), $exceptMimeType) !== 0;
211
        });
212 3
        if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
213 3
            $this->setMessageAsMixed($message);
214
        }
215 3
        foreach ($parts as $key => $part) {
216 3
            $from->removePart($part);
217 3
            $message->addChild($part);
218
        }
219
    }
220
221
    /**
222
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
223
     * or unspecified) message.  If the message has uuencoded attachments, sets
224
     * up the message as a multipart/mixed message and creates a separate
225
     * content part.
226
     *
227
     * @param IMessage $message
228
     */
229 18
    public function enforceMime(IMessage $message)
230
    {
231 18
        if (!$message->isMime()) {
232 5
            if ($message->getAttachmentCount()) {
233 3
                $this->setMessageAsMixed($message);
234
            } else {
235 2
                $message->setRawHeader(HeaderConsts::CONTENT_TYPE, "text/plain;\r\n\tcharset=\"iso-8859-1\"");
236
            }
237 5
            $message->setRawHeader(HeaderConsts::MIME_VERSION, '1.0');
238
        }
239
    }
240
241
    /**
242
     * Creates a multipart/related part out of 'inline' children of $parent and
243
     * returns it.
244
     *
245
     * @param IMimePart $parent
246
     * @return IMimePart
247
     */
248 2
    public function createMultipartRelatedPartForInlineChildrenOf(IMimePart $parent)
249
    {
250 2
        $relatedPart = $this->mimePartFactory->newInstance();
251 2
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
252 2
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline')) as $part) {
253 2
            $parent->removePart($part);
254 2
            $relatedPart->addChild($part);
255
        }
256 2
        $parent->addChild($relatedPart, 0);
257 2
        return $relatedPart;
258
    }
259
260
    /**
261
     * Finds an alternative inline part in the message and returns it if one
262
     * exists.
263
     *
264
     * If the passed $mimeType is text/plain, searches for a text/html part.
265
     * Otherwise searches for a text/plain part to return.
266
     *
267
     * @param IMessage $message
268
     * @param string $mimeType
269
     * @return IMimePart or null if not found
270
     */
271 9
    public function findOtherContentPartFor(IMessage $message, $mimeType)
272
    {
273 9
        $altPart = $message->getPart(
274
            0,
275 9
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
276
        );
277 9
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
278 3
            $altPartParent = $altPart->getParent();
279 3
            if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline')) !== 1) {
280 1
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
281
            }
282
        }
283 9
        return $altPart;
284
    }
285
286
    /**
287
     * Creates a new content part for the passed mimeType and charset, making
288
     * space by creating a multipart/alternative if needed
289
     *
290
     * @param IMessage $message
291
     * @param string $mimeType
292
     * @param string $charset
293
     * @return \ZBateson\MailMimeParser\Message\IMimePart
294
     */
295 8
    public function createContentPartForMimeType(IMessage $message, $mimeType, $charset)
296
    {
297 8
        $mimePart = $this->mimePartFactory->newInstance();
298 8
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tcharset=\"$charset\"");
299 8
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, 'quoted-printable');
300
301 8
        $this->enforceMime($message);
302 8
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
303
304 8
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
305 3
            $this->setMessageAsAlternative($message);
306 3
            $message->addChild($mimePart);
307 5
        } elseif ($altPart !== null) {
308 3
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
309 3
            $mimeAltPart->addChild($mimePart, 1);
310
        } else {
311 3
            $message->addChild($mimePart, 0);
312
        }
313
314 8
        return $mimePart;
315
    }
316
317
    /**
318
     * Creates and adds a IMimePart for the passed content and options as an
319
     * attachment.
320
     *
321
     * @param IMessage $message
322
     * @param string|resource|Psr\Http\Message\StreamInterface\StreamInterface
323
     *        $resource
324
     * @param string $mimeType
325
     * @param string $disposition
326
     * @param string $filename
327
     * @param string $encoding
328
     * @return \ZBateson\MailMimeParser\Message\IMimePart
329
     */
330 6
    public function createAndAddPartForAttachment(IMessage $message, $resource, $mimeType, $disposition, $filename = null, $encoding = 'base64')
331
    {
332 6
        if ($filename === null) {
333 2
            $filename = 'file' . uniqid();
334
        }
335
336 6
        $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
337 6
        if ($message->isMime()) {
338 5
            $part = $this->mimePartFactory->newInstance();
339 5
            $part->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, $encoding);
340 5
            if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
341 5
                $this->setMessageAsMixed($message);
342
            }
343 5
            $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tname=\"$safe\"");
344 5
            $part->setRawHeader(HeaderConsts::CONTENT_DISPOSITION, "$disposition;\r\n\tfilename=\"$safe\"");
345
        } else {
346 1
            $part = $this->uuEncodedPartFactory->newInstance();
347 1
            $part->setFilename($safe);
348
        }
349 6
        $part->setContent($resource);
350 6
        $message->addChild($part);
351
    }
352
353
    /**
354
     * Removes the content part of the message with the passed mime type.  If
355
     * there is a remaining content part and it is an alternative part of the
356
     * main message, the content part is moved to the message part.
357
     *
358
     * If the content part is part of an alternative part beneath the message,
359
     * the alternative part is replaced by the remaining content part,
360
     * optionally keeping other parts if $keepOtherContent is set to true.
361
     *
362
     * @param IMessage $message
363
     * @param string $mimeType
364
     * @param bool $keepOtherContent
365
     * @return boolean true on success
366
     */
367 6
    public function removeAllContentPartsByMimeType(IMessage $message, $mimeType, $keepOtherContent = false)
368
    {
369 6
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
370 6
        if ($alt !== null) {
371 6
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
372
        }
373
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
374
        return true;
375
    }
376
377
    /**
378
     * Removes the 'inline' part with the passed contentType, at the given index
379
     * defaulting to the first
380
     *
381
     * @param IMessage $message
382
     * @param string $mimeType
383
     * @param int $index
384
     * @return boolean true on success
385
     */
386 5
    public function removePartByMimeType(IMessage $message, $mimeType, $index = 0)
387
    {
388 5
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
389 5
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
390 5
        if ($parts === null || !isset($parts[$index])) {
391
            return false;
392 5
        } elseif (count($parts) === 1) {
393 5
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
394
        }
395 1
        $part = $parts[$index];
396 1
        $message->removePart($part);
397 1
        if ($alt !== null && $alt->getChildCount() === 1) {
398
            $this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
399
        }
400 1
        return true;
401
    }
402
403
    /**
404
     * Either creates a mime part or sets the existing mime part with the passed
405
     * mimeType to $strongOrHandle.
406
     *
407
     * @param IMessage $message
408
     * @param string $mimeType
409
     * @param string|resource $stringOrHandle
410
     * @param string $charset
411
     */
412 6
    public function setContentPartForMimeType(IMessage $message, $mimeType, $stringOrHandle, $charset)
413
    {
414 6
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
415 6
        if ($part === null) {
416 5
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
417
        } else {
418 1
            $contentType = $part->getContentType();
419 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

419
            $part->/** @scrutinizer ignore-call */ 
420
                   setRawHeader(HeaderConsts::CONTENT_TYPE, "$contentType;\r\n\tcharset=\"$charset\"");
Loading history...
420
        }
421 6
        $part->setContent($stringOrHandle);
422
    }
423
}
424