Passed
Push — master ( 1ba554...6d939f )
by Zaahid
03:46
created

Message::createAlternativeContentPart()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 7
cts 7
cp 1
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 1
crap 1
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 104
    public function __construct(
70
        HeaderFactory $headerFactory,   
71
        MessageWriter $messageWriter,
72
        MimePartFactory $mimePartFactory
73
    ) {
74 104
        parent::__construct($headerFactory, $messageWriter);
75 104
        $this->messageWriter = $messageWriter;
76 104
        $this->mimePartFactory = $mimePartFactory;
77 104
        $this->objectId = uniqid();
78 104
    }
79
    
80
    /**
81
     * Returns the unique object ID registered with the PartStreamRegistry
82
     * service object.
83
     * 
84
     * @return string
85
     */
86 98
    public function getObjectId()
87
    {
88 98
        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 74
    public function getTextPart($index = 0)
98
    {
99 74
        return $this->getPart(
100 74
            $index,
101 74
            PartFilter::fromInlineContentType('text/plain')
102 74
        );
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 40
    public function getHtmlPart($index = 0)
122
    {
123 40
        return $this->getPart(
124 40
            $index,
125 40
            PartFilter::fromInlineContentType('text/html')
126 40
        );
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 8
    private function getHandleForStringOrHandle($stringOrHandle)
168
    {
169 8
        $tempHandle = fopen('php://temp', 'r+');
170 8
        if (is_string($stringOrHandle)) {
171 8
            fwrite($tempHandle, $stringOrHandle);
172 8
        } else {
173
            stream_copy_to_stream($stringOrHandle, $tempHandle);
174
        }
175 8
        rewind($tempHandle);
176 8
        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 20
    private function getUniqueBoundary($mimeType)
187
    {
188 20
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
189 20
        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 12
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
200
    {
201 12
        $part->setRawHeader(
202 12
            'Content-Type',
203 12
            "$mimeType;\r\n\tboundary=\"" 
204 12
                . $this->getUniqueBoundary($mimeType) . '"'
205 12
        );
206 12
    }
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 6
            if ($alternativePart->getChildCount() === 1) {
312 6
                $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 6
            } elseif ($alternativePart->getChildCount() === 0) {
314
                $this->removePart($alternativePart);
315
            }
316 6
        }
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 6
    private function movePartContentAndChildrenToPart(MimePart $from, MimePart $to)
398
    {
399 6
        $this->copyTypeHeadersFromPartToPart($from, $to);
400 6
        $to->attachContentResourceHandle($from->getContentResourceHandle());
401 6
        $from->detachContentResourceHandle();
402 6
        foreach ($from->getChildParts() as $child) {
403 6
            $from->removePart($child);
404 6
            $to->addPart($child);
405 6
        }
406 6
    }
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 6
    private function replacePart(MimePart $part, MimePart $replacement)
420
    {
421 6
        $this->removePart($replacement);
422 6
        if ($part === $this) {
423 3
            $this->movePartContentAndChildrenToPart($replacement, $part);
424 3
            return;
425
        }
426 6
        $parent = $part->getParent();
427 6
        $position = $parent->removePart($part);
428 6
        $parent->addPart($replacement, $position);
429 6
    }
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 17
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
441
    {
442 17
        $typeHeader = $from->getHeader('Content-Type');
443 17
        if ($typeHeader !== null) {
444 16
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
445 16
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
446 16
            if ($encodingHeader !== null) {
447 6
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
448 6
            }
449 16
            $dispositionHeader = $from->getHeader('Content-Disposition');
450 16
            if ($dispositionHeader !== null) {
451 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
452 1
            }
453 16
        } else {
454 2
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
455 2
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
456
        }
457 17
    }
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 4
    private function createNewContentPartFromPart(MimePart $part)
468
    {
469 4
        $contPart = $this->mimePartFactory->newMimePart();
470 4
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
471 4
        $contPart->attachContentResourceHandle($part->handle);
472 4
        $part->detachContentResourceHandle();
473 4
        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 10
    private function setMessageAsMixed()
481
    {
482 10
        if ($this->isMultiPart()) {
483 6
            $part = $this->mimePartFactory->newMimePart();
484 6
            $this->movePartContentAndChildrenToPart($this, $part);
485 6
            $this->addPart($part, 0);
486 10
        } elseif ($this->handle !== null) {
487 4
            $part = $this->createNewContentPartFromPart($this);
488 4
            $this->addPart($part, 0);
489 4
        }
490 10
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
491 10
        $this->removeHeader('Content-Transfer-Encoding');
492 10
        $this->removeHeader('Content-Disposition');
493 10
    }
494
    
495
    /**
496
     * This function makes space by moving the main message part down one level.
497
     * 
498
     * The content-type, content-disposition and content-transfer-encoding
499
     * headers are copied from this message to the newly created part, the 
500
     * resource handle is moved and detached, any attachments and content parts
501
     * with parents set to this message get their parents set to the newly
502
     * created part.
503
     */
504 8
    private function makeSpaceForMultipartSignedMessage()
505
    {
506 8
        $this->enforceMime();
507 8
        $messagePart = $this->mimePartFactory->newMimePart();
508
509 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
510 8
        $messagePart->attachContentResourceHandle($this->handle);
511 8
        $this->detachContentResourceHandle();
512
        
513 8
        foreach ($this->getChildParts() as $part) {
514 5
            $this->removePart($part);
515 5
            $messagePart->addPart($part);
516 8
        }
517 8
        $this->addPart($messagePart, 0);
518 8
    }
519
    
520
    /**
521
     * Creates and returns a new MimePart for the signature part of a
522
     * multipart/signed message
523
     * 
524
     * @param string $body
525
     */
526 9
    public function createSignaturePart($body)
527
    {
528 9
        $signedPart = $this->getSignaturePart();
529 9
        if ($signedPart === null) {
530 8
            $signedPart = $this->mimePartFactory->newMimePart();
531 8
            $this->addPart($signedPart);
532 8
        }
533 9
        $signedPart->setRawHeader(
534 9
            'Content-Type',
535 9
            $this->getHeaderParameter('Content-Type', 'protocol')
536 9
        );
537 9
        $signedPart->setContent($body);
538 9
    }
539
540
    /**
541
     * Loops over parts of this message and sets the content-transfer-encoding
542
     * header to quoted-printable for text/* mime parts, and to base64
543
     * otherwise for parts that are '8bit' encoded.
544
     * 
545
     * Used for multipart/signed messages which doesn't support 8bit transfer
546
     * encodings.
547
     */
548 9
    private function overwrite8bitContentEncoding()
549
    {
550 9
        $parts = $this->getAllParts(new PartFilter([
551 9
            'headers' => [ PartFilter::FILTER_INCLUDE => [
552
                'Content-Transfer-Encoding' => '8bit'
553 9
            ] ]
554 9
        ]));
555 9
        foreach ($parts as $part) {
556 2
            $contentType = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
557 2
            if ($contentType === 'text/plain' || $contentType === 'text/html') {
558 2
                $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
559 2
            } else {
560
                $part->setRawHeader('Content-Transfer-Encoding', 'base64');
561
            }
562 9
        }
563 9
    }
564
    
565
    /**
566
     * Ensures a non-text part comes first in a signed multipart/alternative
567
     * message as some clients seem to prefer the first content part if the
568
     * client doesn't understand multipart/signed.
569
     */
570 9
    private function ensureHtmlPartFirstForSignedMessage()
571
    {
572 9
        $alt = $this->getPartByMimeType('multipart/alternative');
573 9
        if ($alt !== null) {
574 4
            $cont = $this->getContentPartContainerFromAlternative('text/html', $alt);
575 4
            $pos = array_search($cont, $alt->parts, true);
576 4
            if ($pos !== false && $pos !== 0) {
577 4
                $tmp = $alt->parts[0];
578 4
                $alt->parts[0] = $alt->parts[$pos];
579 4
                $alt->parts[$pos] = $tmp;
580 4
            }
581 4
        }
582 9
    }
583
    
584
    /**
585
     * Turns the message into a multipart/signed message, moving the actual
586
     * message into a child part, sets the content-type of the main message to
587
     * multipart/signed and adds a signature part as well.
588
     * 
589
     * @param string $micalg The Message Integrity Check algorithm being used
590
     * @param string $protocol The mime-type of the signature body
591
     */
592 9
    public function setAsMultipartSigned($micalg, $protocol)
593
    {
594 9
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
595 9
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
596 8
            $this->makeSpaceForMultipartSignedMessage();
597 8
            $this->removeHeader('Content-Disposition');
598 8
            $this->removeHeader('Content-Transfer-Encoding');
599 8
        }
600 9
        $boundary = $this->getUniqueBoundary('multipart/signed');
601 9
        $this->setRawHeader(
602 9
            'Content-Type',
603 9
            "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
604 9
        );
605 9
        $this->overwrite8bitContentEncoding();
606 9
        $this->ensureHtmlPartFirstForSignedMessage();
607 9
        $this->createSignaturePart('Not set');
608 9
    }
609
    
610
    /**
611
     * Returns the signed part or null if not set.
612
     * 
613
     * @return \ZBateson\MailMimeParser\Message\MimePart
614
     */
615 19
    public function getSignaturePart()
616
    {
617 19
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
618 19
        if (strcasecmp($contentType, 'multipart/signed') === 0) {
619 19
            return $this->getChild(1);
620
        } else {
621
            return null;
622
        }
623
    }
624
    
625
    /**
626
     * Returns a string containing the original message's signed part, useful
627
     * for verifying the email.
628
     * 
629
     * If the signed part of the message ends in a final empty line, the line is
630
     * removed as it's considered part of the signature's mime boundary.  From
631
     * RFC-3156:
632
     * 
633
     * Note: The accepted OpenPGP convention is for signed data to end
634
     * with a <CR><LF> sequence.  Note that the <CR><LF> sequence
635
     * immediately preceding a MIME boundary delimiter line is considered
636
     * to be part of the delimiter in [3], 5.1.  Thus, it is not part of
637
     * the signed data preceding the delimiter line.  An implementation
638
     * which elects to adhere to the OpenPGP convention has to make sure
639
     * it inserts a <CR><LF> pair on the last line of the data to be
640
     * signed and transmitted (signed message and transmitted message
641
     * MUST be identical).
642
     * 
643
     * The additional line should be inserted by the signer -- for verification
644
     * purposes if it's missing, it would seem the content part would've been
645
     * signed without a last <CR><LF>.
646
     * 
647
     * @return string or null if the message doesn't have any children, or the
648
     *      child returns null for getOriginalStreamHandle
649
     */
650 14
    public function getOriginalMessageStringForSignatureVerification()
651
    {
652 14
        $child = $this->getChild(0);
653 14
        if ($child !== null && $child->getOriginalStreamHandle() !== null) {
654 14
            $normalized = preg_replace(
655 14
                '/\r\n|\r|\n/',
656 14
                "\r\n",
657 14
                stream_get_contents($child->getOriginalStreamHandle())
658 14
            );
659 14
            $len = strlen($normalized);
660 14
            if ($len > 0 && strrpos($normalized, "\r\n") == $len - 2) {
661 14
                return substr($normalized, 0, -2);
662
            }
663
            return $normalized;
664
        }
665
        return null;
666
    }
667
    
668
    /**
669
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
670
     * or unspecified) message.  If the message has uuencoded attachments, sets
671
     * up the message as a multipart/mixed message and creates a content part.
672
     */
673 12
    private function enforceMime()
674
    {
675 12
        if (!$this->isMime()) {
676 2
            if ($this->getAttachmentCount()) {
677 2
                $this->setMessageAsMixed();
678 2
            } else {
679
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
680
            }
681 2
            $this->setRawHeader('Mime-Version', '1.0');
682 2
        }
683 12
    }
684
    
685
    /**
686
     * Creates a multipart/related part out of 'inline' children of $parent and
687
     * returns it.
688
     * 
689
     * @param MimePart $parent
690
     * @return MimePart
691
     */
692
    private function createMultipartRelatedPartForInlineChildrenOf(MimePart $parent)
693
    {
694
        $relatedPart = $this->mimePartFactory->newMimePart();
695
        $this->setMimeHeaderBoundaryOnPart($relatedPart, 'multipart/related');
696
        foreach ($parent->getChildParts(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) as $part) {
697
            $this->removePart($part);
698
            $relatedPart->addPart($part);
699
        }
700
        $parent->addPart($relatedPart, 0);
701
        return $relatedPart;
702
    }
703
704
    /**
705
     * Finds an alternative inline part in the message and returns it if one
706
     * exists.
707
     * 
708
     * If the passed $mimeType is text/plain, searches for a text/html part.
709
     * Otherwise searches for a text/plain part to return.
710
     * 
711
     * @param string $mimeType
712
     * @return MimeType or null if not found
713
     */
714 4
    private function findOtherContentPartFor($mimeType)
715
    {
716 4
        $altPart = $this->getPart(
717 4
            0,
718 4
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
719 4
        );
720 4
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
721 2
            $altPartParent = $altPart->getParent();
722 2
            if ($altPartParent->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== 1) {
723
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPartParent);
724
            }
725 2
        }
726 4
        return $altPart;
727
    }
728
    
729
    /**
730
     * Creates a new content part for the passed mimeType and charset, making
731
     * space by creating a multipart/alternative if needed
732
     * 
733
     * @param string $mimeType
734
     * @param string $charset
735
     * @return \ZBateson\MailMimeParser\Message\MimePart
736
     */
737 4
    private function createContentPartForMimeType($mimeType, $charset)
738
    {
739 4
        $mimePart = $this->mimePartFactory->newMimePart();
740 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
741 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
742 4
        $this->enforceMime();
743
        
744 4
        $altPart = $this->findOtherContentPartFor($mimeType);
745
        
746 4
        if ($altPart === $this) {
747 2
            $this->setMessageAsAlternative();
748 2
            $this->addPart($mimePart);
749 4
        } elseif ($altPart !== null) {
750 2
            $mimeAltPart = $this->createAlternativeContentPart($altPart);
751 2
            $mimeAltPart->addPart($mimePart, 1);
752 2
        } else {
753 1
            $this->addPart($mimePart, 0);
754
        }
755
        
756 4
        return $mimePart;
757
    }
758
    
759
    /**
760
     * Either creates a mime part or sets the existing mime part with the passed
761
     * mimeType to $strongOrHandle.
762
     * 
763
     * @param string $mimeType
764
     * @param string|resource $stringOrHandle
765
     * @param string $charset
766
     */
767 5
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
768
    {
769 5
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
770 5
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
771 5
        if ($part === null) {
772 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
773 4
        } else {
774 1
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
775 1
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
776
        }
777 5
        $part->attachContentResourceHandle($handle);
778 5
    }
779
    
780
    /**
781
     * Sets the text/plain part of the message to the passed $stringOrHandle,
782
     * either creating a new part if one doesn't exist for text/plain, or
783
     * assigning the value of $stringOrHandle to an existing text/plain part.
784
     * 
785
     * The optional $charset parameter is the charset for saving to.
786
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
787
     * charset.
788
     * 
789
     * @param string|resource $stringOrHandle
790
     * @param string $charset
791
     */
792 2
    public function setTextPart($stringOrHandle, $charset = 'UTF-8')
793
    {
794 2
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
795 2
    }
796
    
797
    /**
798
     * Sets the text/html part of the message to the passed $stringOrHandle,
799
     * either creating a new part if one doesn't exist for text/html, or
800
     * assigning the value of $stringOrHandle to an existing text/html part.
801
     * 
802
     * The optional $charset parameter is the charset for saving to.
803
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
804
     * charset.
805
     * 
806
     * @param string|resource $stringOrHandle
807
     * @param string $charset
808
     */
809 4
    public function setHtmlPart($stringOrHandle, $charset = 'UTF-8')
810
    {
811 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
812 4
    }
813
    
814
    /**
815
     * Removes the text/plain part of the message at the passed index if one
816
     * exists.  Returns true on success.
817
     * 
818
     * @return bool true on success
819
     */
820 3
    public function removeTextPart($index = 0)
821
    {
822 3
        return $this->removePartByMimeType('text/plain', $index);
823
    }
824
825
    /**
826
     * Removes all text/plain inline parts in this message, optionally keeping
827
     * other inline parts as attachments on the main message (defaults to
828
     * keeping them).
829
     * 
830
     * @param bool $keepOtherPartsAsAttachments
831
     * @return bool true on success
832
     */
833
    public function removeAllTextParts($keepOtherPartsAsAttachments = true)
834
    {
835
        return $this->removeAllContentPartsByMimeType('text/plain', $keepOtherPartsAsAttachments);
836
    }
837
    
838
    /**
839
     * Removes the html part of the message if one exists.  Returns true on
840
     * success.
841
     * 
842
     * @return bool true on success
843
     */
844 2
    public function removeHtmlPart($index = 0)
845
    {
846 2
        return $this->removePartByMimeType('text/html', $index);
847
    }
848
    
849
    /**
850
     * Removes all text/html inline parts in this message, optionally keeping
851
     * other inline parts as attachments on the main message (defaults to
852
     * keeping them).
853
     * 
854
     * @param bool $keepOtherPartsAsAttachments
855
     * @return bool true on success
856
     */
857 1
    public function removeAllHtmlParts($keepOtherPartsAsAttachments = true)
858
    {
859 1
        return $this->removeAllContentPartsByMimeType('text/html', $keepOtherPartsAsAttachments);
860
    }
861
    
862
    /**
863
     * Returns the attachment part at the given 0-based index, or null if none
864
     * is set.
865
     * 
866
     * @param int $index
867
     * @return \ZBateson\MailMimeParser\Message\MimePart
868
     */
869 7
    public function getAttachmentPart($index)
870
    {
871 7
        $attachments = $this->getAllAttachmentParts();
872 7
        if (!isset($attachments[$index])) {
873 2
            return null;
874
        }
875 5
        return $attachments[$index];
876
    }
877
    
878
    /**
879
     * Returns all attachment parts.
880
     * 
881
     * Attachments are any non-multipart, non-signature and non inline text or
882
     * html part (a text or html part with a Content-Disposition set to 
883
     * 'attachment' is considered an attachment).
884
     * 
885
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
886
     */
887 55
    public function getAllAttachmentParts()
888
    {
889 55
        $parts = $this->getAllParts(
890 55
            new PartFilter([
891
                'multipart' => PartFilter::FILTER_EXCLUDE
892 55
            ])
893 55
        );
894 55
        return array_values(array_filter(
895 55
            $parts,
896 55
            function ($part) {
897
                return !(
898 55
                    $part->isTextPart()
899 55
                    && $part->getHeaderValue('Content-Disposition', 'inline') === 'inline'
900 55
                );
901
            }
902 55
        ));
903
    }
904
    
905
    /**
906
     * Returns the number of attachments available.
907
     * 
908
     * @return int
909
     */
910 52
    public function getAttachmentCount()
911
    {
912 52
        return count($this->getAllAttachmentParts());
913
    }
914
    
915
    /**
916
     * Removes the attachment with the given index
917
     * 
918
     * @param int $index
919
     */
920 2
    public function removeAttachmentPart($index)
921
    {
922 2
        $part = $this->getAttachmentPart($index);
923 2
        $this->removePart($part);
0 ignored issues
show
Bug introduced by
It seems like $part defined by $this->getAttachmentPart($index) on line 922 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...
924 2
    }
925
    
926
    /**
927
     * Creates and returns a MimePart for use with a new attachment part being
928
     * created.
929
     * 
930
     * @return \ZBateson\MailMimeParser\Message\MimePart
931
     */
932 4
    protected function createPartForAttachment()
933
    {
934 4
        if ($this->isMime()) {
935 4
            $part = $this->mimePartFactory->newMimePart();
936 4
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
937 4
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
938 3
                $this->setMessageAsMixed();
939 3
            }
940 4
            return $part;
941
        }
942
        return $this->mimePartFactory->newUUEncodedPart();
943
    }
944
    
945
    /**
946
     * Adds an attachment part for the passed raw data string or handle and
947
     * given parameters.
948
     * 
949
     * @param string|handle $stringOrHandle
950
     * @param strubg $mimeType
951
     * @param string $filename
952
     * @param string $disposition
953
     */
954 3
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
955
    {
956 3
        if ($filename === null) {
957
            $filename = 'file' . uniqid();
958
        }
959 3
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
960 3
        $part = $this->createPartForAttachment();
961 3
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
962 3
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
963 3
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
964 3
        $this->addPart($part);
965 3
    }
966
    
967
    /**
968
     * Adds an attachment part using the passed file.
969
     * 
970
     * Essentially creates a file stream and uses it.
971
     * 
972
     * @param string $file
973
     * @param string $mimeType
974
     * @param string $filename
975
     * @param string $disposition
976
     */
977 4
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
978
    {
979 4
        $handle = fopen($file, 'r');
980 4
        if ($filename === null) {
981 2
            $filename = basename($file);
982 2
        }
983 4
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
984 4
        $part = $this->createPartForAttachment();
985 4
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
986 4
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
987 4
        $part->attachContentResourceHandle($handle);
988 4
        $this->addPart($part);
989 4
    }
990
    
991
    /**
992
     * Returns a resource handle where the 'inline' text/plain content at the
993
     * passed $index can be read or null if unavailable.
994
     * 
995
     * @param int $index
996
     * @return resource
997
     */
998 66
    public function getTextStream($index = 0)
999
    {
1000 66
        $textPart = $this->getTextPart($index);
1001 66
        if ($textPart !== null) {
1002 65
            return $textPart->getContentResourceHandle();
1003
        }
1004 1
        return null;
1005
    }
1006
    
1007
    /**
1008
     * Returns the content of the inline text/plain part at the given index.
1009
     * 
1010
     * Reads the entire stream content into a string and returns it.  Returns
1011
     * null if the message doesn't have an inline text part.
1012
     * 
1013
     * @param int $index
1014
     * @return string
1015
     */
1016 2
    public function getTextContent($index = 0)
1017
    {
1018 2
        $part = $this->getTextPart($index);
1019 2
        if ($part !== null) {
1020 2
            return $part->getContent();
1021
        }
1022
        return null;
1023
    }
1024
    
1025
    /**
1026
     * Returns a resource handle where the 'inline' text/html content at the
1027
     * passed $index can be read or null if unavailable.
1028
     * 
1029
     * @return resource
1030
     */
1031 32
    public function getHtmlStream($index = 0)
1032
    {
1033 32
        $htmlPart = $this->getHtmlPart($index);
1034 32
        if ($htmlPart !== null) {
1035 31
            return $htmlPart->getContentResourceHandle();
1036
        }
1037 1
        return null;
1038
    }
1039
    
1040
    /**
1041
     * Returns the content of the inline text/html part at the given index.
1042
     * 
1043
     * Reads the entire stream content into a string and returns it.  Returns
1044
     * null if the message doesn't have an inline html part.
1045
     * 
1046
     * @param int $index
1047
     * @return string
1048
     */
1049 1
    public function getHtmlContent($index = 0)
1050
    {
1051 1
        $part = $this->getHtmlPart($index);
1052 1
        if ($part !== null) {
1053 1
            return $part->getContent();
1054
        }
1055
        return null;
1056
    }
1057
    
1058
    /**
1059
     * Returns true if either a Content-Type or Mime-Version header are defined
1060
     * in this Message.
1061
     * 
1062
     * @return bool
1063
     */
1064 98
    public function isMime()
1065
    {
1066 98
        $contentType = $this->getHeaderValue('Content-Type');
1067 98
        $mimeVersion = $this->getHeaderValue('Mime-Version');
1068 98
        return ($contentType !== null || $mimeVersion !== null);
1069
    }
1070
    
1071
    /**
1072
     * Saves the message as a MIME message to the passed resource handle.
1073
     * 
1074
     * @param resource $handle
1075
     */
1076 89
    public function save($handle)
1077
    {
1078 89
        $this->messageWriter->writeMessageTo($this, $handle);
1079 89
    }
1080
    
1081
    /**
1082
     * Returns the content part of a signed message for a signature to be
1083
     * calculated on the message.
1084
     * 
1085
     * @return string
1086
     */
1087 9
    public function getSignableBody()
1088
    {
1089 9
        return $this->messageWriter->getSignableBody($this);
1090
    }
1091
    
1092
    /**
1093
     * Shortcut to call Message::save with a php://temp stream and return the
1094
     * written email message as a string.
1095
     * 
1096
     * @return string
1097
     */
1098
    public function __toString()
1099
    {
1100
        $handle = fopen('php://temp', 'r+');
1101
        $this->save($handle);
1102
        rewind($handle);
1103
        $str = stream_get_contents($handle);
1104
        fclose($handle);
1105
        return $str;
1106
    }
1107
}
1108