Test Failed
Push — 2.0 ( 3431b8...9e8731 )
by Zaahid
03:11
created

MultipartHelper::moveAllPartsAsAttachmentsExcept()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 4
nop 3
dl 0
loc 14
rs 9.6111
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
    public function __construct(
32
        IMimePartFactory $mimePartFactory,
33
        IUUEncodedPartFactory $uuEncodedPartFactory,
34
        GenericHelper $genericHelper
35
    ) {
36
        parent::__construct($mimePartFactory, $uuEncodedPartFactory);
37
        $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
    public function getUniqueBoundary($mimeType)
48
    {
49
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
50
        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
    public function setMimeHeaderBoundaryOnPart(IMimePart $part, $mimeType)
61
    {
62
        $part->setRawHeader(
63
            HeaderConsts::CONTENT_TYPE,
64
            "$mimeType;\r\n\tboundary=\""
65
                . $this->getUniqueBoundary($mimeType) . '"'
66
        );
67
        $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
    public function setMessageAsMixed(IMessage $message)
80
    {
81
        if ($message->hasContent()) {
82
            $part = $this->genericHelper->createNewContentPartFrom($message);
83
            $message->addChild($part, 0);
84
        }
85
        $this->setMimeHeaderBoundaryOnPart($message, 'multipart/mixed');
86
        $atts = $message->getAllAttachmentParts();
87
        if (!empty($atts)) {
88
            foreach ($atts as $att) {
89
                $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
    public function setMessageAsAlternative(IMessage $message)
104
    {
105
        if ($message->hasContent()) {
106
            $part = $this->genericHelper->createNewContentPartFrom($message);
107
            $message->addChild($part, 0);
108
        }
109
        $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
    public function getContentPartContainerFromAlternative($mimeType, IMimePart $alternativePart)
126
    {
127
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
128
        $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
            if ($part === null) {
131
                return false;
132
            }
133
            $contPart = $part;
134
            $part = $part->getParent();
135
        } while ($part !== $alternativePart);
136
        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
    public function removeAllContentPartsFromAlternative(IMessage $message, $mimeType, IMimePart $alternativePart, $keepOtherContent)
154
    {
155
        $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart);
156
        if ($rmPart === false) {
0 ignored issues
show
introduced by
The condition $rmPart === false is always false.
Loading history...
157
            return false;
158
        }
159
        if ($keepOtherContent && $rmPart->getChildCount() > 0) {
160
            $this->moveAllNonMultiPartsToMessageExcept($message, $rmPart, $mimeType);
161
            $alternativePart = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
162
        }
163
        $message->removePart($rmPart);
164
        if ($alternativePart !== null) {
165
            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
                $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
            } elseif ($alternativePart->getChildCount() === 0) {
168
                $message->removePart($alternativePart);
169
            }
170
        }
171
        while ($message->getChildCount() === 1) {
172
            $this->genericHelper->replacePart($message, $message, $message->getChild(0));
173
        }
174
        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
    public function createAlternativeContentPart(IMessage $message, IMessagePart $contentPart)
186
    {
187
        $altPart = $this->mimePartFactory->newInstance();
188
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
189
        $message->removePart($contentPart);
190
        $message->addChild($altPart, 0);
191
        $altPart->addChild($contentPart, 0);
192
        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
    public function moveAllNonMultiPartsToMessageExcept(IMessage $message, IMimePart $from, $exceptMimeType)
205
    {
206
        $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...
207
            if ($part instanceof IMimePart && $part->isMultiPart()) {
208
                return false;
209
            }
210
            return strcasecmp($part->getContentType(), $exceptMimeType) !== 0;
211
        });
212
        if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
213
            $this->setMessageAsMixed($message);
214
        }
215
        foreach ($parts as $key => $part) {
216
            $from->removePart($part);
217
            $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
    public function enforceMime(IMessage $message)
230
    {
231
        if (!$message->isMime()) {
232
            if ($message->getAttachmentCount()) {
233
                $this->setMessageAsMixed($message);
234
            } else {
235
                $message->setRawHeader(HeaderConsts::CONTENT_TYPE, "text/plain;\r\n\tcharset=\"iso-8859-1\"");
236
            }
237
            $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
    public function createMultipartRelatedPartForInlineChildrenOf(IMimePart $parent)
249
    {
250
        $relatedPart = $this->mimePartFactory->newInstance();
251
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
252
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline')) as $part) {
253
            $parent->removePart($part);
254
            $relatedPart->addChild($part);
255
        }
256
        $parent->addChild($relatedPart, 0);
257
        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 \ZBateson\MailMimeParser\Message\MimeType or null if not
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Message\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...
270
     *         found
271
     */
272
    public function findOtherContentPartFor(IMessage $message, $mimeType)
273
    {
274
        $altPart = $message->getPart(
275
            0,
276
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
277
        );
278
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
279
            $altPartParent = $altPart->getParent();
280
            if ($altPartParent->getChildCount(PartFilter::fromDisposition('inline')) !== 1) {
281
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
282
            }
283
        }
284
        return $altPart;
285
    }
286
287
    /**
288
     * Creates a new content part for the passed mimeType and charset, making
289
     * space by creating a multipart/alternative if needed
290
     *
291
     * @param IMessage $message
292
     * @param string $mimeType
293
     * @param string $charset
294
     * @return \ZBateson\MailMimeParser\Message\IMimePart
295
     */
296
    public function createContentPartForMimeType(IMessage $message, $mimeType, $charset)
297
    {
298
        $mimePart = $this->mimePartFactory->newInstance();
299
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tcharset=\"$charset\"");
300
        $mimePart->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, 'quoted-printable');
301
302
        $this->enforceMime($message);
303
        $altPart = $this->findOtherContentPartFor($message, $mimeType);
304
305
        if ($altPart === $message) {
0 ignored issues
show
introduced by
The condition $altPart === $message is always false.
Loading history...
306
            $this->setMessageAsAlternative($message);
307
            $message->addChild($mimePart);
308
        } elseif ($altPart !== null) {
309
            $mimeAltPart = $this->createAlternativeContentPart($message, $altPart);
310
            $mimeAltPart->addChild($mimePart, 1);
311
        } else {
312
            $message->addChild($mimePart, 0);
313
        }
314
315
        return $mimePart;
316
    }
317
318
    /**
319
     * Creates and adds a IMimePart for the passed content and options as an
320
     * attachment.
321
     *
322
     * @param IMessage $message
323
     * @param string|resource|Psr\Http\Message\StreamInterface\StreamInterface
324
     *        $resource
325
     * @param string $mimeType
326
     * @param string $disposition
327
     * @param string $filename
328
     * @param string $encoding
329
     * @return \ZBateson\MailMimeParser\Message\IMimePart
330
     */
331
    public function createAndAddPartForAttachment(IMessage $message, $resource, $mimeType, $disposition, $filename = null, $encoding = 'base64')
332
    {
333
        if ($filename === null) {
334
            $filename = 'file' . uniqid();
335
        }
336
337
        $safe = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
338
        if ($message->isMime()) {
339
            $part = $this->mimePartFactory->newInstance();
340
            $part->setRawHeader(HeaderConsts::CONTENT_TRANSFER_ENCODING, $encoding);
341
            if (strcasecmp($message->getContentType(), 'multipart/mixed') !== 0) {
342
                $this->setMessageAsMixed($message);
343
            }
344
            $part->setRawHeader(HeaderConsts::CONTENT_TYPE, "$mimeType;\r\n\tname=\"$safe\"");
345
            $part->setRawHeader(HeaderConsts::CONTENT_DISPOSITION, "$disposition;\r\n\tfilename=\"$safe\"");
346
        } else {
347
            $part = $this->uuEncodedPartFactory->newInstance();
348
            $part->setFilename($safe);
349
        }
350
        $part->setContent($resource);
351
        $message->addChild($part);
352
    }
353
354
    /**
355
     * Removes the content part of the message with the passed mime type.  If
356
     * there is a remaining content part and it is an alternative part of the
357
     * main message, the content part is moved to the message part.
358
     *
359
     * If the content part is part of an alternative part beneath the message,
360
     * the alternative part is replaced by the remaining content part,
361
     * optionally keeping other parts if $keepOtherContent is set to true.
362
     *
363
     * @param IMessage $message
364
     * @param string $mimeType
365
     * @param bool $keepOtherContent
366
     * @return boolean true on success
367
     */
368
    public function removeAllContentPartsByMimeType(IMessage $message, $mimeType, $keepOtherContent = false)
369
    {
370
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
371
        if ($alt !== null) {
372
            return $this->removeAllContentPartsFromAlternative($message, $mimeType, $alt, $keepOtherContent);
373
        }
374
        $message->removeAllParts(PartFilter::fromInlineContentType($mimeType));
375
        return true;
376
    }
377
378
    /**
379
     * Removes the 'inline' part with the passed contentType, at the given index
380
     * defaulting to the first
381
     *
382
     * @param IMessage $message
383
     * @param string $mimeType
384
     * @param int $index
385
     * @return boolean true on success
386
     */
387
    public function removePartByMimeType(IMessage $message, $mimeType, $index = 0)
388
    {
389
        $parts = $message->getAllParts(PartFilter::fromInlineContentType($mimeType));
390
        $alt = $message->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
391
        if ($parts === null || !isset($parts[$index])) {
392
            return false;
393
        } elseif (count($parts) === 1) {
394
            return $this->removeAllContentPartsByMimeType($message, $mimeType, true);
395
        }
396
        $part = $parts[$index];
397
        $message->removePart($part);
398
        if ($alt !== null && $alt->getChildCount() === 1) {
399
            $this->genericHelper->replacePart($message, $alt, $alt->getChild(0));
400
        }
401
        return true;
402
    }
403
404
    /**
405
     * Either creates a mime part or sets the existing mime part with the passed
406
     * mimeType to $strongOrHandle.
407
     *
408
     * @param IMessage $message
409
     * @param string $mimeType
410
     * @param string|resource $stringOrHandle
411
     * @param string $charset
412
     */
413
    public function setContentPartForMimeType(IMessage $message, $mimeType, $stringOrHandle, $charset)
414
    {
415
        $part = ($mimeType === 'text/html') ? $message->getHtmlPart() : $message->getTextPart();
416
        if ($part === null) {
417
            $part = $this->createContentPartForMimeType($message, $mimeType, $charset);
418
        } else {
419
            $contentType = $part->getContentType();
420
            $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

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