Passed
Push — master ( 2ea88e...404a7f )
by Zaahid
03:05
created

Message::getContentPart()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 9.6666
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 0
crap 12
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 97
    public function __construct(
70
        HeaderFactory $headerFactory,   
71
        MessageWriter $messageWriter,
72
        MimePartFactory $mimePartFactory
73
    ) {
74 97
        parent::__construct($headerFactory, $messageWriter);
75 97
        $this->messageWriter = $messageWriter;
76 97
        $this->mimePartFactory = $mimePartFactory;
77 97
        $this->objectId = uniqid();
78 97
    }
79
    
80
    /**
81
     * Returns the unique object ID registered with the PartStreamRegistry
82
     * service object.
83
     * 
84
     * @return string
85
     */
86 91
    public function getObjectId()
87
    {
88 91
        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 16
    public function getSignaturePart()
612
    {
613 16
        return $this->getChild(0, new PartFilter([ 'signedpart' => PartFilter::FILTER_INCLUDE ]));
614
    }
615
    
616
    /**
617
     * Returns a string containing the original message's signed part, useful
618
     * for verifying the email.
619
     * 
620
     * If the signed part of the message ends in a final empty line, the line is
621
     * removed as it's considered part of the signature's mime boundary.  From
622
     * RFC-3156:
623
     * 
624
     * Note: The accepted OpenPGP convention is for signed data to end
625
     * with a <CR><LF> sequence.  Note that the <CR><LF> sequence
626
     * immediately preceding a MIME boundary delimiter line is considered
627
     * to be part of the delimiter in [3], 5.1.  Thus, it is not part of
628
     * the signed data preceding the delimiter line.  An implementation
629
     * which elects to adhere to the OpenPGP convention has to make sure
630
     * it inserts a <CR><LF> pair on the last line of the data to be
631
     * signed and transmitted (signed message and transmitted message
632
     * MUST be identical).
633
     * 
634
     * The additional line should be inserted by the signer -- for verification
635
     * purposes if it's missing, it would seem the content part would've been
636
     * signed without a last <CR><LF>.
637
     * 
638
     * @return string or null if the message doesn't have any children, or the
639
     *      child returns null for getOriginalStreamHandle
640
     */
641 12
    public function getOriginalMessageStringForSignatureVerification()
642
    {
643 12
        $child = $this->getChild(0);
644 12
        if ($child !== null && $child->getOriginalStreamHandle() !== null) {
645 12
            $normalized = preg_replace(
646 12
                '/\r\n|\r|\n/',
647 12
                "\r\n",
648 12
                stream_get_contents($child->getOriginalStreamHandle())
649 12
            );
650 12
            $len = strlen($normalized);
651 12
            if ($len > 0 && strrpos($normalized, "\r\n") == $len - 2) {
652 12
                return substr($normalized, 0, -2);
653
            }
654
            return $normalized;
655
        }
656
        return null;
657
    }
658
    
659
    /**
660
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
661
     * or unspecified) message.  If the message has uuencoded attachments, sets
662
     * up the message as a multipart/mixed message and creates a content part.
663
     */
664 12
    private function enforceMime()
665
    {
666 12
        if (!$this->isMime()) {
667 2
            if ($this->getAttachmentCount()) {
668 2
                $this->setMessageAsMixed();
669 2
            } else {
670
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
671
            }
672 2
            $this->setRawHeader('Mime-Version', '1.0');
673 2
        }
674 12
    }
675
    
676
    /**
677
     * Creates a multipart/related part out of 'inline' children of $parent and
678
     * returns it.
679
     * 
680
     * @param MimePart $parent
681
     * @return MimePart
682
     */
683
    private function createMultipartRelatedPartForInlineChildrenOf(MimePart $parent)
684
    {
685
        $relatedPart = $this->mimePartFactory->newMimePart();
686
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
687
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
688
            $this->removePart($part);
689
            $relatedPart->addPart($part);
690
        }
691
        $parent->addPart($relatedPart, 0);
692
        return $relatedPart;
693
    }
694
695
    /**
696
     * Finds an alternative inline part in the message and returns it if one
697
     * exists.
698
     * 
699
     * If the passed $mimeType is text/plain, searches for a text/html part.
700
     * Otherwise searches for a text/plain part to return.
701
     * 
702
     * @param string $mimeType
703
     * @return MimeType or null if not found
704
     */
705 4
    private function findOtherContentPartFor($mimeType)
706
    {
707 4
        $altPart = $this->getPart(
708 4
            0,
709 4
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
710 4
        );
711 4
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
712 2
            $altPartParent = $altPart->getParent();
713 2
            if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
714
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
715
            }
716 2
        }
717 4
        return $altPart;
718
    }
719
    
720
    /**
721
     * Creates a new content part for the passed mimeType and charset, making
722
     * space by creating a multipart/alternative if needed
723
     * 
724
     * @param string $mimeType
725
     * @param string $charset
726
     * @return \ZBateson\MailMimeParser\Message\MimePart
727
     */
728 4
    private function createContentPartForMimeType($mimeType, $charset)
729
    {
730 4
        $mimePart = $this->mimePartFactory->newMimePart();
731 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
732 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
733 4
        $this->enforceMime();
734
        
735 4
        $altPart = $this->findOtherContentPartFor($mimeType);
736
        
737 4
        if ($altPart === $this) {
738 2
            $this->setMessageAsAlternative();
739 2
            $this->addPart($mimePart);
740 4
        } elseif ($altPart !== null) {
741 2
            $mimeAltPart = $this->createAlternativeContentPart($altPart);
742 2
            $mimeAltPart->addPart($mimePart, 1);
743 2
        } else {
744 1
            $this->addPart($mimePart, 0);
745
        }
746
        
747 4
        return $mimePart;
748
    }
749
    
750
    /**
751
     * Either creates a mime part or sets the existing mime part with the passed
752
     * mimeType to $strongOrHandle.
753
     * 
754
     * @param string $mimeType
755
     * @param string|resource $stringOrHandle
756
     * @param string $charset
757
     */
758 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
759
    {
760 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
761 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
762 4
        if ($part === null) {
763 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
764 4
        } else {
765
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
766
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
767
        }
768 4
        $part->attachContentResourceHandle($handle);
769 4
    }
770
    
771
    /**
772
     * Sets the text/plain part of the message to the passed $stringOrHandle,
773
     * either creating a new part if one doesn't exist for text/plain, or
774
     * assigning the value of $stringOrHandle to an existing text/plain part.
775
     * 
776
     * The optional $charset parameter is the charset for saving to.
777
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
778
     * charset.
779
     * 
780
     * @param string|resource $stringOrHandle
781
     * @param string $charset
782
     */
783 1
    public function setTextPart($stringOrHandle, $charset = 'UTF-8')
784
    {
785 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
786 1
    }
787
    
788
    /**
789
     * Sets the text/html part of the message to the passed $stringOrHandle,
790
     * either creating a new part if one doesn't exist for text/html, or
791
     * assigning the value of $stringOrHandle to an existing text/html part.
792
     * 
793
     * The optional $charset parameter is the charset for saving to.
794
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
795
     * charset.
796
     * 
797
     * @param string|resource $stringOrHandle
798
     * @param string $charset
799
     */
800 4
    public function setHtmlPart($stringOrHandle, $charset = 'UTF-8')
801
    {
802 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
803 4
    }
804
    
805
    /**
806
     * Removes the text/plain part of the message at the passed index if one
807
     * exists.  Returns true on success.
808
     * 
809
     * @return bool true on success
810
     */
811 3
    public function removeTextPart($index = 0)
812
    {
813 3
        return $this->removePartByMimeType('text/plain', $index);
814
    }
815
816
    /**
817
     * Removes all text/plain inline parts in this message, optionally keeping
818
     * other inline parts as attachments on the main message (defaults to
819
     * keeping them).
820
     * 
821
     * @param bool $keepOtherPartsAsAttachments
822
     * @return bool true on success
823
     */
824
    public function removeAllTextParts($keepOtherPartsAsAttachments = true)
825
    {
826
        return $this->removeAllContentPartsByMimeType('text/plain', $keepOtherPartsAsAttachments);
827
    }
828
    
829
    /**
830
     * Removes the html part of the message if one exists.  Returns true on
831
     * success.
832
     * 
833
     * @return bool true on success
834
     */
835 2
    public function removeHtmlPart($index = 0)
836
    {
837 2
        return $this->removePartByMimeType('text/html', $index);
838
    }
839
    
840
    /**
841
     * Removes all text/html inline parts in this message, optionally keeping
842
     * other inline parts as attachments on the main message (defaults to
843
     * keeping them).
844
     * 
845
     * @param bool $keepOtherPartsAsAttachments
846
     * @return bool true on success
847
     */
848 1
    public function removeAllHtmlParts($keepOtherPartsAsAttachments = true)
849
    {
850 1
        return $this->removeAllContentPartsByMimeType('text/html', $keepOtherPartsAsAttachments);
851
    }
852
    
853
    /**
854
     * Returns the attachment part at the given 0-based index, or null if none
855
     * is set.
856
     * 
857
     * @param int $index
858
     * @return \ZBateson\MailMimeParser\Message\MimePart
859
     */
860 7
    public function getAttachmentPart($index)
861
    {
862 7
        $attachments = $this->getAllAttachmentParts();
863 7
        if (!isset($attachments[$index])) {
864 2
            return null;
865
        }
866 5
        return $attachments[$index];
867
    }
868
    
869
    /**
870
     * Returns all attachment parts.
871
     * 
872
     * Attachments are any non-multipart, non-signature and non inline text or
873
     * html part (a text or html part with a Content-Disposition set to 
874
     * 'attachment' is considered an attachment).
875
     * 
876
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
877
     */
878 52
    public function getAllAttachmentParts()
879
    {
880 52
        $parts = $this->getAllParts(
881 52
            new PartFilter([
882
                'multipart' => PartFilter::FILTER_EXCLUDE
883 52
            ])
884 52
        );
885 52
        return array_values(array_filter(
886 52
            $parts,
887 52
            function ($part) {
888
                return !(
889 52
                    $part->isTextPart()
890 52
                    && $part->getHeaderValue('Content-Disposition', 'inline') === 'inline'
891 52
                );
892
            }
893 52
        ));
894
    }
895
    
896
    /**
897
     * Returns the number of attachments available.
898
     * 
899
     * @return int
900
     */
901 49
    public function getAttachmentCount()
902
    {
903 49
        return count($this->getAllAttachmentParts());
904
    }
905
    
906
    /**
907
     * Removes the attachment with the given index
908
     * 
909
     * @param int $index
910
     */
911 2
    public function removeAttachmentPart($index)
912
    {
913 2
        $part = $this->getAttachmentPart($index);
914 2
        $this->removePart($part);
0 ignored issues
show
Bug introduced by
It seems like $part defined by $this->getAttachmentPart($index) on line 913 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...
915 2
    }
916
    
917
    /**
918
     * Creates and returns a MimePart for use with a new attachment part being
919
     * created.
920
     * 
921
     * @return \ZBateson\MailMimeParser\Message\MimePart
922
     */
923 2
    protected function createPartForAttachment()
924
    {
925 2
        if ($this->isMime()) {
926 2
            $part = $this->mimePartFactory->newMimePart();
927 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
928 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
929 2
                $this->setMessageAsMixed();
930 2
            }
931 2
            return $part;
932
        }
933
        return $this->mimePartFactory->newUUEncodedPart();
934
    }
935
    
936
    /**
937
     * Adds an attachment part for the passed raw data string or handle and
938
     * given parameters.
939
     * 
940
     * @param string|handle $stringOrHandle
941
     * @param strubg $mimeType
942
     * @param string $filename
943
     * @param string $disposition
944
     */
945 1
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
946
    {
947 1
        if ($filename === null) {
948
            $filename = 'file' . uniqid();
949
        }
950 1
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
951 1
        $part = $this->createPartForAttachment();
952 1
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
953 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
954 1
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
955 1
        $this->addPart($part);
956 1
    }
957
    
958
    /**
959
     * Adds an attachment part using the passed file.
960
     * 
961
     * Essentially creates a file stream and uses it.
962
     * 
963
     * @param string $file
964
     * @param string $mimeType
965
     * @param string $filename
966
     * @param string $disposition
967
     */
968 2
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
969
    {
970 2
        $handle = fopen($file, 'r');
971 2
        if ($filename === null) {
972 2
            $filename = basename($file);
973 2
        }
974 2
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
975 2
        $part = $this->createPartForAttachment();
976 2
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
977 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
978 2
        $part->attachContentResourceHandle($handle);
979 2
        $this->addPart($part);
980 2
    }
981
    
982
    /**
983
     * Returns a resource handle where the 'inline' text/plain content at the
984
     * passed $index can be read or null if unavailable.
985
     * 
986
     * @param int $index
987
     * @return resource
988
     */
989 61
    public function getTextStream($index = 0)
990
    {
991 61
        $textPart = $this->getTextPart($index);
992 61
        if ($textPart !== null) {
993 60
            return $textPart->getContentResourceHandle();
994
        }
995 1
        return null;
996
    }
997
    
998
    /**
999
     * Returns the content of the inline text/plain part at the given index.
1000
     * 
1001
     * Reads the entire stream content into a string and returns it.  Returns
1002
     * null if the message doesn't have an inline text part.
1003
     * 
1004
     * @param int $index
1005
     * @return string
1006
     */
1007 1
    public function getTextContent($index = 0)
1008
    {
1009 1
        $part = $this->getTextPart($index);
1010 1
        if ($part !== null) {
1011 1
            return $part->getContent();
1012
        }
1013
        return null;
1014
    }
1015
    
1016
    /**
1017
     * Returns a resource handle where the 'inline' text/html content at the
1018
     * passed $index can be read or null if unavailable.
1019
     * 
1020
     * @return resource
1021
     */
1022 31
    public function getHtmlStream($index = 0)
1023
    {
1024 31
        $htmlPart = $this->getHtmlPart($index);
1025 31
        if ($htmlPart !== null) {
1026 30
            return $htmlPart->getContentResourceHandle();
1027
        }
1028 1
        return null;
1029
    }
1030
    
1031
    /**
1032
     * Returns the content of the inline text/html part at the given index.
1033
     * 
1034
     * Reads the entire stream content into a string and returns it.  Returns
1035
     * null if the message doesn't have an inline html part.
1036
     * 
1037
     * @param int $index
1038
     * @return string
1039
     */
1040 1
    public function getHtmlContent($index = 0)
1041
    {
1042 1
        $part = $this->getHtmlPart($index);
1043 1
        if ($part !== null) {
1044 1
            return $part->getContent();
1045
        }
1046
        return null;
1047
    }
1048
    
1049
    /**
1050
     * Returns true if either a Content-Type or Mime-Version header are defined
1051
     * in this Message.
1052
     * 
1053
     * @return bool
1054
     */
1055 91
    public function isMime()
1056
    {
1057 91
        $contentType = $this->getHeaderValue('Content-Type');
1058 91
        $mimeVersion = $this->getHeaderValue('Mime-Version');
1059 91
        return ($contentType !== null || $mimeVersion !== null);
1060
    }
1061
    
1062
    /**
1063
     * Saves the message as a MIME message to the passed resource handle.
1064
     * 
1065
     * @param resource $handle
1066
     */
1067 83
    public function save($handle)
1068
    {
1069 83
        $this->messageWriter->writeMessageTo($this, $handle);
1070 83
    }
1071
    
1072
    /**
1073
     * Returns the content part of a signed message for a signature to be
1074
     * calculated on the message.
1075
     * 
1076
     * @return string
1077
     */
1078 8
    public function getSignableBody()
1079
    {
1080 8
        return $this->messageWriter->getSignableBody($this);
1081
    }
1082
    
1083
    /**
1084
     * Shortcut to call Message::save with a php://temp stream and return the
1085
     * written email message as a string.
1086
     * 
1087
     * @return string
1088
     */
1089
    public function __toString()
1090
    {
1091
        $handle = fopen('php://temp', 'r+');
1092
        $this->save($handle);
1093
        rewind($handle);
1094
        $str = stream_get_contents($handle);
1095
        fclose($handle);
1096
        return $str;
1097
    }
1098
}
1099