Completed
Push — master ( 8b1198...1ba554 )
by Zaahid
10:14
created

Message::copyTypeHeadersFromPartToPart()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4

Importance

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