Completed
Push — master ( b69ec3...c60636 )
by Zaahid
09:03
created

Message::removeAllContentPartsFromAlternative()   C

Complexity

Conditions 7
Paths 17

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7.0422

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 25
ccs 19
cts 21
cp 0.9048
rs 6.7272
cc 7
eloc 18
nc 17
nop 3
crap 7.0422
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;
8
9
use ZBateson\MailMimeParser\Header\HeaderFactory;
10
use ZBateson\MailMimeParser\Message\MimePart;
11
use ZBateson\MailMimeParser\Message\MimePartFactory;
12
use ZBateson\MailMimeParser\Message\Writer\MessageWriter;
13
use ZBateson\MailMimeParser\Message\PartFilter;
14
15
/**
16
 * A parsed mime message with optional mime parts depending on its type.
17
 * 
18
 * A mime message may have any number of mime parts, and each part may have any
19
 * number of sub-parts, etc...
20
 *
21
 * @author Zaahid Bateson
22
 */
23
class Message extends MimePart
24
{
25
    /**
26
     * @var string unique ID used to identify the object to
27
     *      $this->partStreamRegistry when registering the stream.  The ID is
28
     *      used for opening stream parts with the mmp-mime-message "protocol".
29
     * 
30
     * @see \ZBateson\MailMimeParser\SimpleDi::registerStreamExtensions
31
     * @see \ZBateson\MailMimeParser\Stream\PartStream::stream_open
32
     */
33
    protected $objectId;
34
35
    /**
36
     * @var \ZBateson\MailMimeParser\Message\MimePartFactory a MimePartFactory to create
37
     *      parts for attachments/content
38
     */
39
    protected $mimePartFactory;
40
    
41
    /**
42
     * @var \ZBateson\MailMimeParser\Message\Writer\MessageWriter the part
43
     *      writer for this Message.  The same object is assigned to $partWriter
44
     *      but as an AbstractWriter -- not really needed in PHP but helps with
45
     *      auto-complete and code analyzers.
46
     */
47
    protected $messageWriter = null;
48
    
49
    /**
50
     * Convenience method to parse a handle or string into a Message without
51
     * requiring including MailMimeParser, instantiating it, and calling parse.
52
     * 
53
     * @param resource|string $handleOrString the resource handle to the input
54
     *        stream of the mime message, or a string containing a mime message
55
     */
56 1
    public static function from($handleOrString)
57
    {
58 1
        $mmp = new MailMimeParser();
59 1
        return $mmp->parse($handleOrString);
60
    }
61
    
62
    /**
63
     * Constructs a Message.
64
     * 
65
     * @param HeaderFactory $headerFactory
66
     * @param MessageWriter $messageWriter
67
     * @param MimePartFactory $mimePartFactory
68
     */
69 93
    public function __construct(
70
        HeaderFactory $headerFactory,   
71
        MessageWriter $messageWriter,
72
        MimePartFactory $mimePartFactory
73
    ) {
74 93
        parent::__construct($headerFactory, $messageWriter);
75 93
        $this->messageWriter = $messageWriter;
76 93
        $this->mimePartFactory = $mimePartFactory;
77 93
        $this->objectId = uniqid();
78 93
    }
79
    
80
    /**
81
     * Returns the unique object ID registered with the PartStreamRegistry
82
     * service object.
83
     * 
84
     * @return string
85
     */
86 87
    public function getObjectId()
87
    {
88 87
        return $this->objectId;
89
    }
90
91
    /**
92
     * Returns the text/plain part at the given index (or null if not found.)
93
     * 
94
     * @param int $index
95
     * @return \ZBateson\MailMimeParser\Message\MimePart
96
     */
97 68
    public function getTextPart($index = 0)
98
    {
99 68
        return $this->getPart(
100 68
            $index,
101 68
            PartFilter::fromInlineContentType('text/plain')
102 68
        );
103
    }
104
    
105
    /**
106
     * Returns the number of text/plain parts in this message.
107
     * 
108
     * @return int
109
     */
110
    public function getTextPartCount()
111
    {
112
        return $this->getPartCount(PartFilter::fromInlineContentType('text/plain'));
113
    }
114
    
115
    /**
116
     * Returns the text/html part at the given index (or null if not found.)
117
     * 
118
     * @param $index
119
     * @return \ZBateson\MailMimeParser\Message\MimePart
120
     */
121 39
    public function getHtmlPart($index = 0)
122
    {
123 39
        return $this->getPart(
124 39
            $index,
125 39
            PartFilter::fromInlineContentType('text/html')
126 39
        );
127
    }
128
    
129
    /**
130
     * Returns the number of text/html parts in this message.
131
     * 
132
     * @return int
133
     */
134
    public function getHtmlPartCount()
135
    {
136
        return $this->getPartCount(PartFilter::fromInlineContentType('text/html'));
137
    }
138
    
139
    /**
140
     * Returns the content MimePart, which could be a text/plain part,
141
     * text/html part, multipart/alternative part, or null if none is set.
142
     * 
143
     * This function is deprecated in favour of getTextPart/getHtmlPart and 
144
     * getPartByMimeType.
145
     * 
146
     * @deprecated since version 0.4.2
147
     * @return \ZBateson\MailMimeParser\Message\MimePart
148
     */
149
    public function getContentPart()
150
    {
151
        $alternative = $this->getPartByMimeType('multipart/alternative');
152
        if ($alternative !== null) {
153
            return $alternative;
154
        }
155
        $text = $this->getTextPart();
156
        return ($text !== null) ? $text : $this->getHtmlPart();
157
    }
158
    
159
    /**
160
     * Returns an open resource handle for the passed string or resource handle.
161
     * 
162
     * For a string, creates a php://temp stream and returns it.
163
     * 
164
     * @param resource|string $stringOrHandle
165
     * @return resource
166
     */
167 5
    private function getHandleForStringOrHandle($stringOrHandle)
168
    {
169 5
        $tempHandle = fopen('php://temp', 'r+');
170 5
        if (is_string($stringOrHandle)) {
171 5
            fwrite($tempHandle, $stringOrHandle);
172 5
        } else {
173
            stream_copy_to_stream($stringOrHandle, $tempHandle);
174
        }
175 5
        rewind($tempHandle);
176 5
        return $tempHandle;
177
    }
178
    
179
    /**
180
     * Creates and returns a unique boundary.
181
     * 
182
     * @param string $mimeType first 3 characters of a multipart type are used,
183
     *      e.g. REL for relative or ALT for alternative
184
     * @return string
185
     */
186 18
    private function getUniqueBoundary($mimeType)
187
    {
188 18
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
189 18
        return uniqid('----=MMP-' . $type . $this->objectId . '.', true);
190
    }
191
    
192
    /**
193
     * Creates a unique mime boundary and assigns it to the passed part's
194
     * Content-Type header with the passed mime type.
195
     * 
196
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
197
     * @param string $mimeType
198
     */
199 11
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
200
    {
201 11
        $part->setRawHeader(
202 11
            'Content-Type',
203 11
            "$mimeType;\r\n\tboundary=\"" 
204 11
                . $this->getUniqueBoundary($mimeType) . '"'
205 11
        );
206 11
    }
207
    
208
    /**
209
     * Sets this message to be a multipart/alternative message, making space for
210
     * a second content part.
211
     * 
212
     * Creates a content part and assigns the content stream from the message to
213
     * that newly created part.
214
     */
215 2
    private function setMessageAsAlternative()
216
    {
217 2
        $contentPart = $this->mimePartFactory->newMimePart();
218 2
        $contentPart->attachContentResourceHandle($this->handle);
219 2
        $this->detachContentResourceHandle();
220 2
        $contentType = 'text/plain; charset="us-ascii"';
221 2
        $contentHeader = $this->getHeader('Content-Type');
222 2
        if ($contentHeader !== null) {
223 2
            $contentType = $contentHeader->getRawValue();
224 2
        }
225 2
        $contentPart->setRawHeader('Content-Type', $contentType);
226 2
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/alternative');
227 2
        $this->addPart($contentPart, 0);
228 2
    }
229
230
    /**
231
     * Returns the direct child of $alternativePart containing a part of
232
     * $mimeType.
233
     * 
234
     * Used for alternative mime types that have a multipart/mixed or
235
     * multipart/related child containing a content part of $mimeType, where
236
     * the whole mixed/related part should be removed.
237
     * 
238
     * @param string $mimeType the content-type to find below $alternativePart
239
     * @param MimePart $alternativePart The multipart/alternative part to look
240
     *        under
241
     * @return boolean|MimePart false if a part is not found
242
     */
243 10
    private function getContentPartContainerFromAlternative($mimeType, MimePart $alternativePart)
244
    {
245 10
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
246 10
        $contPart = null;
0 ignored issues
show
Unused Code introduced by
$contPart is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
247
        do {
248 10
            if ($part === null) {
249
                return false;
250
            }
251 10
            $contPart = $part;
252 10
            $part = $part->getParent();
253 10
        } while ($part !== $alternativePart);
254 10
        return $contPart;
255
    }
256
    
257
    /**
258
     * Moves all parts under $from into this message except those with a
259
     * content-type equal to $exceptMimeType.  If the message is not a
260
     * multipart/mixed message, it is set to multipart/mixed first.
261
     * 
262
     * @param MimePart $from
263
     * @param string $exceptMimeType
264
     */
265 6
    private function moveAllPartsAsAttachmentsExcept(MimePart $from, $exceptMimeType)
266
    {
267 6
        $parts = $from->getAllParts(new PartFilter([
268 6
            'multipart' => PartFilter::FILTER_EXCLUDE,
269
            'headers' => [
270 6
                PartFilter::FILTER_EXCLUDE => [
271
                    'Content-Type' => $exceptMimeType
272 6
                ]
273 6
            ]
274 6
        ]));
275 6
        if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
276 5
            $this->setMessageAsMixed();
277 5
        }
278 6
        foreach ($parts as $part) {
279 2
            $from->removePart($part);
280 2
            $this->addPart($part);
281 6
        }
282 6
    }
283
284
    /**
285
     * Removes all parts of $mimeType from $alternativePart.
286
     * 
287
     * If $alternativePart contains a multipart/mixed or multipart/relative part
288
     * with other parts of different content-types, the multipart part is
289
     * removed, and parts of different content-types can optionally be moved to
290
     * the main message part.
291
     * 
292
     * @param string $mimeType
293
     * @param MimePart $alternativePart
294
     * @param bool $keepOtherContent
295
     * @return bool
296
     */
297 6
    private function removeAllContentPartsFromAlternative($mimeType, $alternativePart, $keepOtherContent)
298
    {
299 6
        $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart);
300 6
        if ($rmPart === false) {
301
            return false;
302
        }
303 6
        if ($keepOtherContent) {
304 6
            $this->moveAllPartsAsAttachmentsExcept($rmPart, $mimeType);
305 6
            $alternativePart = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
306 6
        } else {
307
            $rmPart->removeAllParts();
308
        }
309 6
        $this->removePart($rmPart);
310 6
        if ($alternativePart !== null) {
311 5
            if ($alternativePart->getChildCount() === 1) {
312 1
                $this->replacePart($alternativePart, $alternativePart->getChild(0));
0 ignored issues
show
Bug introduced by
It seems like $alternativePart->getChild(0) can be null; however, replacePart() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
313 5
            } elseif ($alternativePart->getChildCount() === 0) {
314 4
                $this->removePart($alternativePart);
315 4
            }
316 5
        }
317 6
        while ($this->getChildCount() === 1) {
318 3
            $this->replacePart($this, $this->getChild(0));
0 ignored issues
show
Bug introduced by
It seems like $this->getChild(0) can be null; however, replacePart() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
319 3
        }
320 6
        return true;
321
    }
322
    
323
    /**
324
     * Removes the content part of the message with the passed mime type.  If
325
     * there is a remaining content part and it is an alternative part of the
326
     * main message, the content part is moved to the message part.
327
     * 
328
     * If the content part is part of an alternative part beneath the message,
329
     * the alternative part is replaced by the remaining content part,
330
     * optionally keeping other parts if $keepOtherContent is set to true.
331
     * 
332
     * @param string $mimeType
333
     * @param bool $keepOtherContent
334
     * @return boolean true on success
335
     */
336 6
    protected function removeAllContentPartsByMimeType($mimeType, $keepOtherContent = false)
337
    {
338 6
        $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
339 6
        if ($alt !== null) {
340 6
            return $this->removeAllContentPartsFromAlternative($mimeType, $alt, $keepOtherContent);
341
        }
342
        $this->removeAllParts(PartFilter::fromInlineContentType($mimeType));
343
        return true;
344
    }
345
    
346
    /**
347
     * Removes the 'inline' part with the passed contentType, at the given index
348
     * defaulting to the first 
349
     * 
350
     * @param string $contentType
0 ignored issues
show
Bug introduced by
There is no parameter named $contentType. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
351
     * @param int $index
352
     * @return boolean true on success
353
     */
354 5
    protected function removePartByMimeType($mimeType, $index = 0)
355
    {
356 5
        $parts = $this->getAllParts(PartFilter::fromInlineContentType($mimeType));
357 5
        $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
358 5
        if ($parts === null || !isset($parts[$index])) {
359
            return false;
360 5
        } elseif (count($parts) === 1) {
361 5
            return $this->removeAllContentPartsByMimeType($mimeType, true);
362
        }
363 1
        $part = $parts[$index];
364 1
        $this->removePart($part);
365 1
        if ($alt !== null && $alt->getChildCount() === 1) {
366
            $this->replacePart($alt, $alt->getChild(0));
0 ignored issues
show
Bug introduced by
It seems like $alt->getChild(0) can be null; however, replacePart() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
367
        }
368 1
        return true;
369
    }
370
    
371
    /**
372
     * Creates a new mime part as a multipart/alternative and assigns the passed
373
     * $contentPart as a part below it before returning it.
374
     * 
375
     * @param MimePart $contentPart
376
     * @return MimePart the alternative part
377
     */
378 2
    private function createAlternativeContentPart(MimePart $contentPart)
379
    {
380 2
        $altPart = $this->mimePartFactory->newMimePart();
381 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
382 2
        $this->removePart($contentPart);
383 2
        $this->addPart($altPart, 0);
384 2
        $altPart->addPart($contentPart, 0);
385 2
        return $altPart;
386
    }
387
388
    /**
389
     * Copies type headers (Content-Type, Content-Disposition,
390
     * Content-Transfer-Encoding) from the $from MimePart to $to.  Attaches the
391
     * content resource handle of $from to $to, and loops over child parts,
392
     * removing them from $from and adding them to $to.
393
     * 
394
     * @param MimePart $from
395
     * @param MimePart $to
396
     */
397 3
    private function movePartContentAndChildrenToPart(MimePart $from, MimePart $to)
398
    {
399 3
        $this->copyTypeHeadersFromPartToPart($from, $to);
400 3
        $to->attachContentResourceHandle($from->getContentResourceHandle());
401 3
        $from->detachContentResourceHandle();
402 3
        foreach ($from->getChildParts() as $child) {
403 1
            $from->removePart($child);
404 1
            $to->addPart($child);
405 3
        }
406 3
    }
407
408
    /**
409
     * Replaces the $part MimePart with $replacement.
410
     * 
411
     * Essentially removes $part from its parent, and adds $replacement in its
412
     * same position.  If $part is this Message, its type headers are moved from
413
     * this message to $replacement, the content resource is moved, and children
414
     * are assigned to $replacement.
415
     * 
416
     * @param MimePart $part
417
     * @param MimePart $replacement
418
     */
419 4
    private function replacePart(MimePart $part, MimePart $replacement)
420
    {
421 4
        $this->removePart($replacement);
422 4
        if ($part === $this) {
423 3
            $this->movePartContentAndChildrenToPart($replacement, $part);
424 3
            return;
425
        }
426 1
        $parent = $part->getParent();
427 1
        $position = $parent->removePart($part);
428 1
        $parent->addPart($replacement, $position);
429 1
    }
430
431
    /**
432
     * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding
433
     * headers from the $from header into the $to header. If the Content-Type
434
     * header isn't defined in $from, defaults to text/plain and
435
     * quoted-printable.
436
     * 
437
     * @param \ZBateson\MailMimeParser\Message\MimePart $from
438
     * @param \ZBateson\MailMimeParser\Message\MimePart $to
439
     */
440 16
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
441
    {
442 16
        $typeHeader = $from->getHeader('Content-Type');
443 16
        if ($typeHeader !== null) {
444 15
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
445 15
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
446 15
            if ($encodingHeader !== null) {
447 6
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
448 6
            }
449 15
            $dispositionHeader = $from->getHeader('Content-Disposition');
450 15
            if ($dispositionHeader !== null) {
451 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
452 1
            }
453 15
        } else {
454 2
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
455 2
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
456
        }
457 16
    }
458
    
459
    /**
460
     * Creates a new content part from the passed part, allowing the part to be
461
     * used for something else (e.g. changing a non-mime message to a multipart
462
     * mime message).
463
     * 
464
     * @param MimePart $part
465
     * @return MimePart the newly-created MimePart   
466
    */
467 8
    private function createNewContentPartFromPart(MimePart $part)
468
    {
469 8
        $contPart = $this->mimePartFactory->newMimePart();
470 8
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
471 8
        $contPart->attachContentResourceHandle($part->handle);
472 8
        $part->detachContentResourceHandle();
473 8
        return $contPart;
474
    }
475
    
476
    /**
477
     * Creates a new part out of the current contentPart and sets the message's
478
     * type to be multipart/mixed.
479
     */
480 9
    private function setMessageAsMixed()
481
    {
482 9
        if ($this->handle !== null) {
483 8
            $part = $this->createNewContentPartFromPart($this);
484 8
            $this->addPart($part, 0);
485 8
        }
486 9
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
487 9
        $this->removeHeader('Content-Transfer-Encoding');
488 9
        $this->removeHeader('Content-Disposition');
489 9
    }
490
    
491
    /**
492
     * This function makes space by moving the main message part down one level.
493
     * 
494
     * The content-type, content-disposition and content-transfer-encoding
495
     * headers are copied from this message to the newly created part, the 
496
     * resource handle is moved and detached, any attachments and content parts
497
     * with parents set to this message get their parents set to the newly
498
     * created part.
499
     */
500 8
    private function makeSpaceForMultipartSignedMessage()
501
    {
502 8
        $this->enforceMime();
503 8
        $messagePart = $this->mimePartFactory->newMimePart();
504
505 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
506 8
        $messagePart->attachContentResourceHandle($this->handle);
507 8
        $this->detachContentResourceHandle();
508
        
509 8
        foreach ($this->getChildParts() as $part) {
510 5
            $this->removePart($part);
511 5
            $messagePart->addPart($part);
512 8
        }
513 8
        $this->addPart($messagePart, 0);
514 8
    }
515
    
516
    /**
517
     * Creates and returns a new MimePart for the signature part of a
518
     * multipart/signed message
519
     * 
520
     * @param string $body
521
     */
522 8
    public function createSignaturePart($body)
523
    {
524 8
        $signedPart = $this->getSignaturePart();
525 8
        if ($signedPart === null) {
526 8
            $signedPart = $this->mimePartFactory->newMimePart();
527 8
            $this->addPart($signedPart);
528 8
        }
529 8
        $signedPart->setRawHeader(
530 8
            'Content-Type',
531 8
            $this->getHeaderParameter('Content-Type', 'protocol')
532 8
        );
533 8
        $signedPart->setContent($body);
534 8
    }
535
536
    /**
537
     * Loops over parts of this message and sets the content-transfer-encoding
538
     * header to quoted-printable for text/* mime parts, and to base64
539
     * otherwise for parts that are '8bit' encoded.
540
     * 
541
     * Used for multipart/signed messages which doesn't support 8bit transfer
542
     * encodings.
543
     */
544 8
    private function overwrite8bitContentEncoding()
545
    {
546 8
        $parts = $this->getAllParts(new PartFilter([
547 8
            'headers' => [ PartFilter::FILTER_INCLUDE => [
548
                'Content-Transfer-Encoding' => '8bit'
549 8
            ] ]
550 8
        ]));
551 8
        foreach ($parts as $part) {
552 1
            $contentType = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
553 1
            if ($contentType === 'text/plain' || $contentType === 'text/html') {
554 1
                $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
555 1
            } else {
556
                $part->setRawHeader('Content-Transfer-Encoding', 'base64');
557
            }
558 8
        }
559 8
    }
560
    
561
    /**
562
     * Ensures a non-text part comes first in a signed multipart/alternative
563
     * message as some clients seem to prefer the first content part if the
564
     * client doesn't understand multipart/signed.
565
     */
566 8
    private function ensureHtmlPartFirstForSignedMessage()
567
    {
568 8
        $alt = $this->getPartByMimeType('multipart/alternative');
569 8
        if ($alt !== null) {
570 4
            $cont = $this->getContentPartContainerFromAlternative('text/html', $alt);
571 4
            $pos = array_search($cont, $alt->parts, true);
572 4
            if ($pos !== false && $pos !== 0) {
573 4
                $tmp = $alt->parts[0];
574 4
                $alt->parts[0] = $alt->parts[$pos];
575 4
                $alt->parts[$pos] = $tmp;
576 4
            }
577 4
        }
578 8
    }
579
    
580
    /**
581
     * Turns the message into a multipart/signed message, moving the actual
582
     * message into a child part, sets the content-type of the main message to
583
     * multipart/signed and adds a signature part as well.
584
     * 
585
     * @param string $micalg The Message Integrity Check algorithm being used
586
     * @param string $protocol The mime-type of the signature body
587
     */
588 8
    public function setAsMultipartSigned($micalg, $protocol)
589
    {
590 8
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
591 8
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
592 8
            $this->makeSpaceForMultipartSignedMessage();
593 8
            $boundary = $this->getUniqueBoundary('multipart/signed');
594 8
            $this->setRawHeader(
595 8
                'Content-Type',
596 8
                "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
597 8
            );
598 8
            $this->removeHeader('Content-Disposition');
599 8
            $this->removeHeader('Content-Transfer-Encoding');
600 8
        }
601 8
        $this->overwrite8bitContentEncoding();
602 8
        $this->ensureHtmlPartFirstForSignedMessage();
603 8
        $this->createSignaturePart('Not set');
604 8
    }
605
    
606
    /**
607
     * Returns the signed part or null if not set.
608
     * 
609
     * @return \ZBateson\MailMimeParser\Message\MimePart
610
     */
611 12
    public function getSignaturePart()
612
    {
613 12
        return $this->getChild(0, new PartFilter([ 'signedpart' => PartFilter::FILTER_INCLUDE ]));
614
    }
615
    
616
    /**
617
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
618
     * or unspecified) message.  If the message has uuencoded attachments, sets
619
     * up the message as a multipart/mixed message and creates a content part.
620
     */
621 12
    private function enforceMime()
622
    {
623 12
        if (!$this->isMime()) {
624 2
            if ($this->getAttachmentCount()) {
625 2
                $this->setMessageAsMixed();
626 2
            } else {
627
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
628
            }
629 2
            $this->setRawHeader('Mime-Version', '1.0');
630 2
        }
631 12
    }
632
    
633
    /**
634
     * Creates a multipart/related part out of 'inline' children of $parent and
635
     * returns it.
636
     * 
637
     * @param MimePart $parent
638
     * @return MimePart
639
     */
640
    private function createMultipartRelatedPartForInlineChildrenOf(MimePart $parent)
641
    {
642
        $relatedPart = $this->mimePartFactory->newMimePart();
643
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
644
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
645
            $this->removePart($part);
646
            $relatedPart->addPart($part);
647
        }
648
        $parent->addPart($relatedPart, 0);
649
        return $relatedPart;
650
    }
651
652
    /**
653
     * Finds an alternative inline part in the message and returns it if one
654
     * exists.
655
     * 
656
     * If the passed $mimeType is text/plain, searches for a text/html part.
657
     * Otherwise searches for a text/plain part to return.
658
     * 
659
     * @param string $mimeType
660
     * @return MimeType or null if not found
661
     */
662 4
    private function findOtherContentPartFor($mimeType)
663
    {
664 4
        $altPart = $this->getPart(
665 4
            0,
666 4
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
667 4
        );
668 4
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
669 2
            $altPartParent = $altPart->getParent();
670 2
            if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
671
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
672
            }
673 2
        }
674 4
        return $altPart;
675
    }
676
    
677
    /**
678
     * Creates a new content part for the passed mimeType and charset, making
679
     * space by creating a multipart/alternative if needed
680
     * 
681
     * @param string $mimeType
682
     * @param string $charset
683
     * @return \ZBateson\MailMimeParser\Message\MimePart
684
     */
685 4
    private function createContentPartForMimeType($mimeType, $charset)
686
    {
687 4
        $mimePart = $this->mimePartFactory->newMimePart();
688 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
689 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
690 4
        $this->enforceMime();
691
        
692 4
        $altPart = $this->findOtherContentPartFor($mimeType);
693
        
694 4
        if ($altPart === $this) {
695 2
            $this->setMessageAsAlternative();
696 2
            $this->addPart($mimePart);
697 4
        } elseif ($altPart !== null) {
698 2
            $mimeAltPart = $this->createAlternativeContentPart($altPart);
699 2
            $mimeAltPart->addPart($mimePart, 1);
700 2
        } else {
701 1
            $this->addPart($mimePart, 0);
702
        }
703
        
704 4
        return $mimePart;
705
    }
706
    
707
    /**
708
     * Either creates a mime part or sets the existing mime part with the passed
709
     * mimeType to $strongOrHandle.
710
     * 
711
     * @param string $mimeType
712
     * @param string|resource $stringOrHandle
713
     * @param string $charset
714
     */
715 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
716
    {
717 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
718 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
719 4
        if ($part === null) {
720 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
721 4
        } else {
722
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
723
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
724
        }
725 4
        $part->attachContentResourceHandle($handle);
726 4
    }
727
    
728
    /**
729
     * Sets the text/plain part of the message to the passed $stringOrHandle,
730
     * either creating a new part if one doesn't exist for text/plain, or
731
     * assigning the value of $stringOrHandle to an existing text/plain part.
732
     * 
733
     * The optional $charset parameter is the charset for saving to.
734
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
735
     * charset.
736
     * 
737
     * @param string|resource $stringOrHandle
738
     * @param string $charset
739
     */
740 1
    public function setTextPart($stringOrHandle, $charset = 'UTF-8')
741
    {
742 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
743 1
    }
744
    
745
    /**
746
     * Sets the text/html part of the message to the passed $stringOrHandle,
747
     * either creating a new part if one doesn't exist for text/html, or
748
     * assigning the value of $stringOrHandle to an existing text/html part.
749
     * 
750
     * The optional $charset parameter is the charset for saving to.
751
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
752
     * charset.
753
     * 
754
     * @param string|resource $stringOrHandle
755
     * @param string $charset
756
     */
757 4
    public function setHtmlPart($stringOrHandle, $charset = 'UTF-8')
758
    {
759 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
760 4
    }
761
    
762
    /**
763
     * Removes the text/plain part of the message at the passed index if one
764
     * exists.  Returns true on success.
765
     * 
766
     * @return bool true on success
767
     */
768 3
    public function removeTextPart($index = 0)
769
    {
770 3
        return $this->removePartByMimeType('text/plain', $index);
771
    }
772
773
    /**
774
     * Removes all text/plain inline parts in this message, optionally keeping
775
     * other inline parts as attachments on the main message (defaults to
776
     * keeping them).
777
     * 
778
     * @param bool $keepOtherPartsAsAttachments
779
     * @return bool true on success
780
     */
781
    public function removeAllTextParts($keepOtherPartsAsAttachments = true)
782
    {
783
        return $this->removeAllContentPartsByMimeType('text/plain', $keepOtherPartsAsAttachments);
784
    }
785
    
786
    /**
787
     * Removes the html part of the message if one exists.  Returns true on
788
     * success.
789
     * 
790
     * @return bool true on success
791
     */
792 2
    public function removeHtmlPart($index = 0)
793
    {
794 2
        return $this->removePartByMimeType('text/html', $index);
795
    }
796
    
797
    /**
798
     * Removes all text/html inline parts in this message, optionally keeping
799
     * other inline parts as attachments on the main message (defaults to
800
     * keeping them).
801
     * 
802
     * @param bool $keepOtherPartsAsAttachments
803
     * @return bool true on success
804
     */
805 1
    public function removeAllHtmlParts($keepOtherPartsAsAttachments = true)
806
    {
807 1
        return $this->removeAllContentPartsByMimeType('text/html', $keepOtherPartsAsAttachments);
808
    }
809
    
810
    /**
811
     * Returns the attachment part at the given 0-based index, or null if none
812
     * is set.
813
     * 
814
     * @param int $index
815
     * @return \ZBateson\MailMimeParser\Message\MimePart
816
     */
817 7
    public function getAttachmentPart($index)
818
    {
819 7
        $attachments = $this->getAllAttachmentParts();
820 7
        if (!isset($attachments[$index])) {
821 2
            return null;
822
        }
823 5
        return $attachments[$index];
824
    }
825
    
826
    /**
827
     * Returns all attachment parts.
828
     * 
829
     * Attachments are any non-multipart, non-signature and non inline text or
830
     * html part (a text or html part with a Content-Disposition set to 
831
     * 'attachment' is considered an attachment).
832
     * 
833
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
834
     */
835 52
    public function getAllAttachmentParts()
836
    {
837 52
        $parts = $this->getAllParts(
838 52
            new PartFilter([
839
                'multipart' => PartFilter::FILTER_EXCLUDE
840 52
            ])
841 52
        );
842 52
        return array_values(array_filter(
843 52
            $parts,
844 52
            function ($part) {
845
                return !(
846 52
                    $part->isTextPart()
847 52
                    && $part->getHeaderValue('Content-Disposition', 'inline') === 'inline'
848 52
                );
849
            }
850 52
        ));
851
    }
852
    
853
    /**
854
     * Returns the number of attachments available.
855
     * 
856
     * @return int
857
     */
858 49
    public function getAttachmentCount()
859
    {
860 49
        return count($this->getAllAttachmentParts());
861
    }
862
    
863
    /**
864
     * Removes the attachment with the given index
865
     * 
866
     * @param int $index
867
     */
868 2
    public function removeAttachmentPart($index)
869
    {
870 2
        $part = $this->getAttachmentPart($index);
871 2
        $this->removePart($part);
0 ignored issues
show
Bug introduced by
It seems like $part defined by $this->getAttachmentPart($index) on line 870 can be null; however, ZBateson\MailMimeParser\...\MimePart::removePart() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
872 2
    }
873
    
874
    /**
875
     * Creates and returns a MimePart for use with a new attachment part being
876
     * created.
877
     * 
878
     * @return \ZBateson\MailMimeParser\Message\MimePart
879
     */
880 2
    protected function createPartForAttachment()
881
    {
882 2
        if ($this->isMime()) {
883 2
            $part = $this->mimePartFactory->newMimePart();
884 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
885 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
886 2
                $this->setMessageAsMixed();
887 2
            }
888 2
            return $part;
889
        }
890
        return $this->mimePartFactory->newUUEncodedPart();
891
    }
892
    
893
    /**
894
     * Adds an attachment part for the passed raw data string or handle and
895
     * given parameters.
896
     * 
897
     * @param string|handle $stringOrHandle
898
     * @param strubg $mimeType
899
     * @param string $filename
900
     * @param string $disposition
901
     */
902 1
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
903
    {
904 1
        if ($filename === null) {
905
            $filename = 'file' . uniqid();
906
        }
907 1
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
908 1
        $part = $this->createPartForAttachment();
909 1
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
910 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
911 1
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
912 1
        $this->addPart($part);
913 1
    }
914
    
915
    /**
916
     * Adds an attachment part using the passed file.
917
     * 
918
     * Essentially creates a file stream and uses it.
919
     * 
920
     * @param string $file
921
     * @param string $mimeType
922
     * @param string $filename
923
     * @param string $disposition
924
     */
925 2
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
926
    {
927 2
        $handle = fopen($file, 'r');
928 2
        if ($filename === null) {
929 2
            $filename = basename($file);
930 2
        }
931 2
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
932 2
        $part = $this->createPartForAttachment();
933 2
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
934 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
935 2
        $part->attachContentResourceHandle($handle);
936 2
        $this->addPart($part);
937 2
    }
938
    
939
    /**
940
     * Returns a resource handle where the 'inline' text/plain content at the
941
     * passed $index can be read or null if unavailable.
942
     * 
943
     * @param int $index
944
     * @return resource
945
     */
946 61
    public function getTextStream($index = 0)
947
    {
948 61
        $textPart = $this->getTextPart($index);
949 61
        if ($textPart !== null) {
950 60
            return $textPart->getContentResourceHandle();
951
        }
952 1
        return null;
953
    }
954
    
955
    /**
956
     * Returns the content of the inline text/plain part at the given index.
957
     * 
958
     * Reads the entire stream content into a string and returns it.  Returns
959
     * null if the message doesn't have an inline text part.
960
     * 
961
     * @param int $index
962
     * @return string
963
     */
964 1
    public function getTextContent($index = 0)
965
    {
966 1
        $part = $this->getTextPart($index);
967 1
        if ($part !== null) {
968 1
            return $part->getContent();
969
        }
970
        return null;
971
    }
972
    
973
    /**
974
     * Returns a resource handle where the 'inline' text/html content at the
975
     * passed $index can be read or null if unavailable.
976
     * 
977
     * @return resource
978
     */
979 31
    public function getHtmlStream($index = 0)
980
    {
981 31
        $htmlPart = $this->getHtmlPart($index);
982 31
        if ($htmlPart !== null) {
983 30
            return $htmlPart->getContentResourceHandle();
984
        }
985 1
        return null;
986
    }
987
    
988
    /**
989
     * Returns the content of the inline text/html part at the given index.
990
     * 
991
     * Reads the entire stream content into a string and returns it.  Returns
992
     * null if the message doesn't have an inline html part.
993
     * 
994
     * @param int $index
995
     * @return string
996
     */
997 1
    public function getHtmlContent($index = 0)
998
    {
999 1
        $part = $this->getHtmlPart($index);
1000 1
        if ($part !== null) {
1001 1
            return $part->getContent();
1002
        }
1003
        return null;
1004
    }
1005
    
1006
    /**
1007
     * Returns true if either a Content-Type or Mime-Version header are defined
1008
     * in this Message.
1009
     * 
1010
     * @return bool
1011
     */
1012 87
    public function isMime()
1013
    {
1014 87
        $contentType = $this->getHeaderValue('Content-Type');
1015 87
        $mimeVersion = $this->getHeaderValue('Mime-Version');
1016 87
        return ($contentType !== null || $mimeVersion !== null);
1017
    }
1018
    
1019
    /**
1020
     * Saves the message as a MIME message to the passed resource handle.
1021
     * 
1022
     * @param resource $handle
1023
     */
1024 83
    public function save($handle)
1025
    {
1026 83
        $this->messageWriter->writeMessageTo($this, $handle);
1027 83
    }
1028
    
1029
    /**
1030
     * Returns the content part of a signed message for a signature to be
1031
     * calculated on the message.
1032
     * 
1033
     * @return string
1034
     */
1035 8
    public function getSignableBody()
1036
    {
1037 8
        return $this->messageWriter->getSignableBody($this);
1038
    }
1039
    
1040
    /**
1041
     * Shortcut to call Message::save with a php://temp stream and return the
1042
     * written email message as a string.
1043
     * 
1044
     * @return string
1045
     */
1046
    public function __toString()
1047
    {
1048
        $handle = fopen('php://temp', 'r+');
1049
        $this->save($handle);
1050
        rewind($handle);
1051
        $str = stream_get_contents($handle);
1052
        fclose($handle);
1053
        return $str;
1054
    }
1055
}
1056