Completed
Push — master ( 6512b0...b69ec3 )
by Zaahid
03:33
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 92
    public function __construct(
70
        HeaderFactory $headerFactory,   
71
        MessageWriter $messageWriter,
72
        MimePartFactory $mimePartFactory
73
    ) {
74 92
        parent::__construct($headerFactory, $messageWriter);
75 92
        $this->messageWriter = $messageWriter;
76 92
        $this->mimePartFactory = $mimePartFactory;
77 92
        $this->objectId = uniqid();
78 92
    }
79
    
80
    /**
81
     * Returns the unique object ID registered with the PartStreamRegistry
82
     * service object.
83
     * 
84
     * @return string
85
     */
86 87
    public function getObjectId()
87
    {
88 87
        return $this->objectId;
89
    }
90
91
    /**
92
     * Returns the text/plain part at the given index (or null if not found.)
93
     * 
94
     * @param int $index
95
     * @return \ZBateson\MailMimeParser\Message\MimePart
96
     */
97 67
    public function getTextPart($index = 0)
98
    {
99 67
        return $this->getPart(
100 67
            $index,
101 67
            PartFilter::fromInlineContentType('text/plain')
102 67
        );
103
    }
104
    
105
    /**
106
     * Returns the number of text/plain parts in this message.
107
     * 
108
     * @return int
109
     */
110
    public function getTextPartCount()
111
    {
112
        return $this->getPartCount(PartFilter::fromInlineContentType('text/plain'));
113
    }
114
    
115
    /**
116
     * Returns the text/html part at the given index (or null if not found.)
117
     * 
118
     * @param $index
119
     * @return \ZBateson\MailMimeParser\Message\MimePart
120
     */
121 39
    public function getHtmlPart($index = 0)
122
    {
123 39
        return $this->getPart(
124 39
            $index,
125 39
            PartFilter::fromInlineContentType('text/html')
126 39
        );
127
    }
128
    
129
    /**
130
     * Returns the number of text/html parts in this message.
131
     * 
132
     * @return int
133
     */
134
    public function getHtmlPartCount()
135
    {
136
        return $this->getPartCount(PartFilter::fromInlineContentType('text/html'));
137
    }
138
    
139
    /**
140
     * Returns the content MimePart, which could be a text/plain part,
141
     * text/html part, multipart/alternative part, or null if none is set.
142
     * 
143
     * This function is deprecated in favour of getTextPart/getHtmlPart and 
144
     * getPartByMimeType.
145
     * 
146
     * @deprecated since version 0.4.2
147
     * @return \ZBateson\MailMimeParser\Message\MimePart
148
     */
149
    public function getContentPart()
150
    {
151
        $alternative = $this->getPartByMimeType('multipart/alternative');
152
        if ($alternative !== null) {
153
            return $alternative;
154
        }
155
        $text = $this->getTextPart();
156
        return ($text !== null) ? $text : $this->getHtmlPart();
157
    }
158
    
159
    /**
160
     * Returns an open resource handle for the passed string or resource handle.
161
     * 
162
     * For a string, creates a php://temp stream and returns it.
163
     * 
164
     * @param resource|string $stringOrHandle
165
     * @return resource
166
     */
167 5
    private function getHandleForStringOrHandle($stringOrHandle)
168
    {
169 5
        $tempHandle = fopen('php://temp', 'r+');
170 5
        if (is_string($stringOrHandle)) {
171 5
            fwrite($tempHandle, $stringOrHandle);
172 5
        } else {
173
            stream_copy_to_stream($stringOrHandle, $tempHandle);
174
        }
175 5
        rewind($tempHandle);
176 5
        return $tempHandle;
177
    }
178
    
179
    /**
180
     * Creates and returns a unique boundary.
181
     * 
182
     * @param string $mimeType first 3 characters of a multipart type are used,
183
     *      e.g. REL for relative or ALT for alternative
184
     * @return string
185
     */
186 15
    private function getUniqueBoundary($mimeType)
187
    {
188 15
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
189 15
        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 8
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
200
    {
201 8
        $part->setRawHeader(
202 8
            'Content-Type',
203 8
            "$mimeType;\r\n\tboundary=\"" 
204 8
                . $this->getUniqueBoundary($mimeType) . '"'
205 8
        );
206 8
    }
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 5
    private function getContentPartContainerFromAlternative($mimeType, MimePart $alternativePart)
244
    {
245 5
        $part = $alternativePart->getPart(0, PartFilter::fromInlineContentType($mimeType));
246 5
        $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 5
            if ($part === null) {
249
                return false;
250
            }
251 5
            $contPart = $part;
252 5
            $part = $part->getParent();
253 5
        } while ($part !== $alternativePart);
254 5
        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 1
    private function moveAllPartsAsAttachmentsExcept(MimePart $from, $exceptMimeType)
266
    {
267 1
        $parts = $from->getAllParts(new PartFilter([
268 1
            'multipart' => PartFilter::FILTER_EXCLUDE,
269
            'headers' => [
270 1
                PartFilter::FILTER_EXCLUDE => [
271
                    'Content-Type' => $exceptMimeType
272 1
                ]
273 1
            ]
274 1
        ]));
275 1
        if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
276 1
            $this->setMessageAsMixed();
277 1
        }
278 1
        foreach ($parts as $part) {
279 1
            $this->addPart($part);
280 1
        }
281 1
    }
282
283
    /**
284
     * Removes all parts of $mimeType from $alternativePart.
285
     * 
286
     * If $alternativePart contains a multipart/mixed or multipart/relative part
287
     * with other parts of different content-types, the multipart part is
288
     * removed, and parts of different content-types can optionally be moved to
289
     * the main message part.
290
     * 
291
     * @param string $mimeType
292
     * @param MimePart $alternativePart
293
     * @param bool $keepOtherContent
294
     * @return bool
295
     */
296 1
    private function removeAllContentPartsFromAlternative($mimeType, $alternativePart, $keepOtherContent)
297
    {
298 1
        $rmPart = $this->getContentPartContainerFromAlternative($mimeType, $alternativePart);
299 1
        if ($rmPart === false) {
300
            return false;
301
        }
302 1
        if ($keepOtherContent) {
303 1
            $this->moveAllPartsAsAttachmentsExcept($alternativePart, $mimeType);
304 1
        }
305 1
        $rmPart->removeAllParts();
306 1
        $this->removePart($rmPart);
307 1
        if ($alternativePart->getChildCount() === 1) {
308
            $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...
309
        }
310 1
        return true;
311
    }
312
    
313
    /**
314
     * Removes the content part of the message with the passed mime type.  If
315
     * there is a remaining content part and it is an alternative part of the
316
     * main message, the content part is moved to the message part.
317
     * 
318
     * If the content part is part of an alternative part beneath the message,
319
     * the alternative part is replaced by the remaining content part,
320
     * optionally keeping other parts if $keepOtherContent is set to true.
321
     * 
322
     * @param string $mimeType
323
     * @param bool $keepOtherContent
324
     * @return boolean true on success
325
     */
326 1
    protected function removeAllContentPartsByMimeType($mimeType, $keepOtherContent = false)
327
    {
328 1
        $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
329 1
        if ($alt !== null) {
330 1
            return $this->removeAllContentPartsFromAlternative($mimeType, $alt, $keepOtherContent);
331
        }
332
        $this->removeAllParts(PartFilter::fromInlineContentType($mimeType));
333
        return true;
334
    }
335
    
336
    /**
337
     * Removes the 'inline' part with the passed contentType, at the given index
338
     * defaulting to the first 
339
     * 
340
     * @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...
341
     * @param int $index
342
     * @return boolean true on success
343
     */
344 5
    protected function removePartByMimeType($mimeType, $index = 0)
345
    {
346 5
        $parts = $this->getAllParts(PartFilter::fromInlineContentType($mimeType));
347 5
        $alt = $this->getPart(0, PartFilter::fromInlineContentType('multipart/alternative'));
348 5
        if ($parts === null || !isset($parts[$index])) {
349
            return false;
350
        }
351 5
        $part = $parts[$index];
352 5
        $this->removePart($part);
353 5
        if ($alt !== null && $alt->getChildCount() === 1) {
354 4
            $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...
355 4
        }
356 5
        return true;
357
    }
358
    
359
    /**
360
     * Creates a new mime part as a multipart/alternative and assigns the passed
361
     * $contentPart as a part below it before returning it.
362
     * 
363
     * @param MimePart $contentPart
364
     * @return MimePart the alternative part
365
     */
366 2
    private function createAlternativeContentPart(MimePart $contentPart)
367
    {
368 2
        $altPart = $this->mimePartFactory->newMimePart();
369 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
370 2
        $this->removePart($contentPart);
371 2
        $this->addPart($altPart, 0);
372 2
        $altPart->addPart($contentPart, 0);
373 2
        return $altPart;
374
    }
375
376
    /**
377
     * Copies type headers (Content-Type, Content-Disposition,
378
     * Content-Transfer-Encoding) from the $from MimePart to $to.  Attaches the
379
     * content resource handle of $from to $to, and loops over child parts,
380
     * removing them from $from and adding them to $to.
381
     * 
382
     * @param MimePart $from
383
     * @param MimePart $to
384
     */
385 3
    private function movePartContentAndChildrenToPart(MimePart $from, MimePart $to)
386
    {
387 3
        $this->copyTypeHeadersFromPartToPart($from, $to);
388 3
        $to->attachContentResourceHandle($from->getContentResourceHandle());
389 3
        $from->detachContentResourceHandle();
390 3
        foreach ($from->getChildParts() as $child) {
391 1
            $from->removePart($child);
392 1
            $to->addPart($child);
393 3
        }
394 3
    }
395
396
    /**
397
     * Replaces the $part MimePart with $replacement.
398
     * 
399
     * Essentially removes $part from its parent, and adds $replacement in its
400
     * same position.  If $part is this Message, its type headers are moved from
401
     * this message to $replacement, the content resource is moved, and children
402
     * are assigned to $replacement.
403
     * 
404
     * @param MimePart $part
405
     * @param MimePart $replacement
406
     */
407 4
    private function replacePart(MimePart $part, MimePart $replacement)
408
    {
409 4
        $this->removePart($replacement);
410 4
        if ($part === $this) {
411 3
            $this->movePartContentAndChildrenToPart($replacement, $part);
412 3
            return;
413
        }
414 1
        $parent = $part->getParent();
415 1
        $position = $parent->removePart($part);
416 1
        $parent->addPart($replacement, $position);
417 1
    }
418
419
    /**
420
     * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding
421
     * headers from the $from header into the $to header. If the Content-Type
422
     * header isn't defined in $from, defaults to text/plain and
423
     * quoted-printable.
424
     * 
425
     * @param \ZBateson\MailMimeParser\Message\MimePart $from
426
     * @param \ZBateson\MailMimeParser\Message\MimePart $to
427
     */
428 14
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
429
    {
430 14
        $typeHeader = $from->getHeader('Content-Type');
431 14
        if ($typeHeader !== null) {
432 14
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
433 14
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
434 14
            if ($encodingHeader !== null) {
435 6
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
436 6
            }
437 14
            $dispositionHeader = $from->getHeader('Content-Disposition');
438 14
            if ($dispositionHeader !== null) {
439 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
440 1
            }
441 14
        } else {
442
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
443
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
444
        }
445 14
    }
446
    
447
    /**
448
     * Creates a new content part from the passed part, allowing the part to be
449
     * used for something else (e.g. changing a non-mime message to a multipart
450
     * mime message).
451
     * 
452
     * @param MimePart $part
453
     * @return MimePart the newly-created MimePart   
454
    */
455 3
    private function createNewContentPartFromPart(MimePart $part)
456
    {
457 3
        $contPart = $this->mimePartFactory->newMimePart();
458 3
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
459 3
        $contPart->attachContentResourceHandle($part->handle);
460 3
        $part->detachContentResourceHandle();
461 3
        return $contPart;
462
    }
463
    
464
    /**
465
     * Creates a new part out of the current contentPart and sets the message's
466
     * type to be multipart/mixed.
467
     */
468 5
    private function setMessageAsMixed()
469
    {
470 5
        if ($this->handle !== null) {
471 3
            $part = $this->createNewContentPartFromPart($this);
472 3
            $this->addPart($part, 0);
473 3
        }
474 5
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
475 5
        $this->removeHeader('Content-Transfer-Encoding');
476 5
        $this->removeHeader('Content-Disposition');
477 5
    }
478
    
479
    /**
480
     * This function makes space by moving the main message part down one level.
481
     * 
482
     * The content-type, content-disposition and content-transfer-encoding
483
     * headers are copied from this message to the newly created part, the 
484
     * resource handle is moved and detached, any attachments and content parts
485
     * with parents set to this message get their parents set to the newly
486
     * created part.
487
     */
488 8
    private function makeSpaceForMultipartSignedMessage()
489
    {
490 8
        $this->enforceMime();
491 8
        $messagePart = $this->mimePartFactory->newMimePart();
492
493 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
494 8
        $messagePart->attachContentResourceHandle($this->handle);
495 8
        $this->detachContentResourceHandle();
496
        
497 8
        foreach ($this->getChildParts() as $part) {
498 5
            $this->removePart($part);
499 5
            $messagePart->addPart($part);
500 8
        }
501 8
        $this->addPart($messagePart, 0);
502 8
    }
503
    
504
    /**
505
     * Creates and returns a new MimePart for the signature part of a
506
     * multipart/signed message
507
     * 
508
     * @param string $body
509
     */
510 8
    public function createSignaturePart($body)
511
    {
512 8
        $signedPart = $this->getSignaturePart();
513 8
        if ($signedPart === null) {
514 8
            $signedPart = $this->mimePartFactory->newMimePart();
515 8
            $this->addPart($signedPart);
516 8
        }
517 8
        $signedPart->setRawHeader(
518 8
            'Content-Type',
519 8
            $this->getHeaderParameter('Content-Type', 'protocol')
520 8
        );
521 8
        $signedPart->setContent($body);
522 8
    }
523
524
    /**
525
     * Loops over parts of this message and sets the content-transfer-encoding
526
     * header to quoted-printable for text/* mime parts, and to base64
527
     * otherwise for parts that are '8bit' encoded.
528
     * 
529
     * Used for multipart/signed messages which doesn't support 8bit transfer
530
     * encodings.
531
     */
532 8
    private function overwrite8bitContentEncoding()
533
    {
534 8
        $parts = $this->getAllParts(new PartFilter([
535 8
            'headers' => [ PartFilter::FILTER_INCLUDE => [
536
                'Content-Transfer-Encoding' => '8bit'
537 8
            ] ]
538 8
        ]));
539 8
        foreach ($parts as $part) {
540 1
            $contentType = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
541 1
            if ($contentType === 'text/plain' || $contentType === 'text/html') {
542 1
                $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
543 1
            } else {
544
                $part->setRawHeader('Content-Transfer-Encoding', 'base64');
545
            }
546 8
        }
547 8
    }
548
    
549
    /**
550
     * Ensures a non-text part comes first in a signed multipart/alternative
551
     * message as some clients seem to prefer the first content part if the
552
     * client doesn't understand multipart/signed.
553
     */
554 8
    private function ensureHtmlPartFirstForSignedMessage()
555
    {
556 8
        $alt = $this->getPartByMimeType('multipart/alternative');
557 8
        if ($alt !== null) {
558 4
            $cont = $this->getContentPartContainerFromAlternative('text/html', $alt);
559 4
            $pos = array_search($cont, $alt->parts, true);
560 4
            if ($pos !== false && $pos !== 0) {
561 4
                $tmp = $alt->parts[0];
562 4
                $alt->parts[0] = $alt->parts[$pos];
563 4
                $alt->parts[$pos] = $tmp;
564 4
            }
565 4
        }
566 8
    }
567
    
568
    /**
569
     * Turns the message into a multipart/signed message, moving the actual
570
     * message into a child part, sets the content-type of the main message to
571
     * multipart/signed and adds a signature part as well.
572
     * 
573
     * @param string $micalg The Message Integrity Check algorithm being used
574
     * @param string $protocol The mime-type of the signature body
575
     */
576 8
    public function setAsMultipartSigned($micalg, $protocol)
577
    {
578 8
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
579 8
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
580 8
            $this->makeSpaceForMultipartSignedMessage();
581 8
            $boundary = $this->getUniqueBoundary('multipart/signed');
582 8
            $this->setRawHeader(
583 8
                'Content-Type',
584 8
                "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
585 8
            );
586 8
            $this->removeHeader('Content-Disposition');
587 8
            $this->removeHeader('Content-Transfer-Encoding');
588 8
        }
589 8
        $this->overwrite8bitContentEncoding();
590 8
        $this->ensureHtmlPartFirstForSignedMessage();
591 8
        $this->createSignaturePart('Not set');
592 8
    }
593
    
594
    /**
595
     * Returns the signed part or null if not set.
596
     * 
597
     * @return \ZBateson\MailMimeParser\Message\MimePart
598
     */
599 12
    public function getSignaturePart()
600
    {
601 12
        return $this->getChild(0, new PartFilter([ 'signedpart' => PartFilter::FILTER_INCLUDE ]));
602
    }
603
    
604
    /**
605
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
606
     * or unspecified) message.  If the message has uuencoded attachments, sets
607
     * up the message as a multipart/mixed message and creates a content part.
608
     */
609 12
    private function enforceMime()
610
    {
611 12
        if (!$this->isMime()) {
612 2
            if ($this->getAttachmentCount()) {
613 2
                $this->setMessageAsMixed();
614 2
            } else {
615
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
616
            }
617 2
            $this->setRawHeader('Mime-Version', '1.0');
618 2
        }
619 12
    }
620
    
621
    /**
622
     * Creates a multipart/related part out of 'inline' children of $parent and
623
     * returns it.
624
     * 
625
     * @param MimePart $parent
626
     * @return MimePart
627
     */
628 2
    private function createMultipartRelatedPartForInlineChildrenOf(MimePart $parent)
629
    {
630 2
        $relatedPart = $this->mimePartFactory->newMimePart();
631 2
        $relatedPart->setRawHeader('Content-Type', 'multipart/related');
632 2
        $parent->addPart($relatedPart, 0);
633 2
        foreach ($parent->getAllParts(PartFilter::fromDisposition('inline')) as $part) {
634
            $this->removePart($part);
635
            $relatedPart->addPart($part);
636 2
        }
637 2
        return $relatedPart;
638
    }
639
640
    /**
641
     * Finds an alternative inline part in the message and returns it if one
642
     * exists.
643
     * 
644
     * If the passed $mimeType is text/plain, searches for a text/html part.
645
     * Otherwise searches for a text/plain part to return.
646
     * 
647
     * @param string $mimeType
648
     * @return MimeType or null if not found
649
     */
650 4
    private function findOtherContentPartFor($mimeType)
651
    {
652 4
        $altPart = $this->getPart(
653 4
            0,
654 4
            PartFilter::fromInlineContentType(($mimeType === 'text/plain') ? 'text/html' : 'text/plain')
655 4
        );
656 4
        if ($altPart !== null && $altPart->getParent() !== null && $altPart->getParent()->isMultiPart()) {
657 2
            $altPart = $altPart->getParent();
658 2
            if ($altPart->getPartCount(PartFilter::fromDisposition('inline', PartFilter::FILTER_EXCLUDE)) !== $altPart->getChildCount()) {
659 2
                $altPart = $this->createMultipartRelatedPartForInlineChildrenOf($altPart);
660 2
            }
661 2
        }
662 4
        return $altPart;
663
    }
664
    
665
    /**
666
     * Creates a new content part for the passed mimeType and charset, making
667
     * space by creating a multipart/alternative if needed
668
     * 
669
     * @param string $mimeType
670
     * @param string $charset
671
     * @return \ZBateson\MailMimeParser\Message\MimePart
672
     */
673 4
    private function createContentPartForMimeType($mimeType, $charset)
674
    {
675 4
        $mimePart = $this->mimePartFactory->newMimePart();
676 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$charset\"");
677 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
678 4
        $this->enforceMime();
679
        
680 4
        $altPart = $this->findOtherContentPartFor($mimeType);
681
        
682 4
        if ($altPart === $this) {
683 2
            $this->setMessageAsAlternative();
684 2
            $this->addPart($mimePart);
685 4
        } elseif ($altPart !== null) {
686 2
            $mimeAltPart = $this->createAlternativeContentPart($altPart);
687 2
            $mimeAltPart->addPart($mimePart, 1);
688 2
        } else {
689 1
            $this->addPart($mimePart, 0);
690
        }
691
        
692 4
        return $mimePart;
693
    }
694
    
695
    /**
696
     * Either creates a mime part or sets the existing mime part with the passed
697
     * mimeType to $strongOrHandle.
698
     * 
699
     * @param string $mimeType
700
     * @param string|resource $stringOrHandle
701
     * @param string $charset
702
     */
703 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
704
    {
705 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
706 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
707 4
        if ($part === null) {
708 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
709 4
        } else {
710
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
711
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$charset\"");
712
        }
713 4
        $part->attachContentResourceHandle($handle);
714 4
    }
715
    
716
    /**
717
     * Sets the text/plain part of the message to the passed $stringOrHandle,
718
     * either creating a new part if one doesn't exist for text/plain, or
719
     * assigning the value of $stringOrHandle to an existing text/plain part.
720
     * 
721
     * The optional $charset parameter is the charset for saving to.
722
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
723
     * charset.
724
     * 
725
     * @param string|resource $stringOrHandle
726
     * @param string $charset
727
     */
728 1
    public function setTextPart($stringOrHandle, $charset = 'UTF-8')
729
    {
730 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
731 1
    }
732
    
733
    /**
734
     * Sets the text/html part of the message to the passed $stringOrHandle,
735
     * either creating a new part if one doesn't exist for text/html, or
736
     * assigning the value of $stringOrHandle to an existing text/html part.
737
     * 
738
     * The optional $charset parameter is the charset for saving to.
739
     * $stringOrHandle is expected to be in UTF-8 regardless of the target
740
     * charset.
741
     * 
742
     * @param string|resource $stringOrHandle
743
     * @param string $charset
744
     */
745 4
    public function setHtmlPart($stringOrHandle, $charset = 'UTF-8')
746
    {
747 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
748 4
    }
749
    
750
    /**
751
     * Removes the text/plain part of the message at the passed index if one
752
     * exists.  Returns true on success.
753
     * 
754
     * @return bool true on success
755
     */
756 3
    public function removeTextPart($index = 0)
757
    {
758 3
        return $this->removePartByMimeType('text/plain', $index);
759
    }
760
761
    /**
762
     * Removes all text/plain inline parts in this message, optionally keeping
763
     * other inline parts as attachments on the main message (defaults to
764
     * keeping them).
765
     * 
766
     * @param bool $keepOtherPartsAsAttachments
767
     * @return bool true on success
768
     */
769
    public function removeAllTextParts($keepOtherPartsAsAttachments = true)
770
    {
771
        return $this->removeAllContentPartsByMimeType('text/plain', $keepOtherPartsAsAttachments);
772
    }
773
    
774
    /**
775
     * Removes the html part of the message if one exists.  Returns true on
776
     * success.
777
     * 
778
     * @return bool true on success
779
     */
780 2
    public function removeHtmlPart($index = 0)
781
    {
782 2
        return $this->removePartByMimeType('text/html', $index);
783
    }
784
    
785
    /**
786
     * Removes all text/html inline parts in this message, optionally keeping
787
     * other inline parts as attachments on the main message (defaults to
788
     * keeping them).
789
     * 
790
     * @param bool $keepOtherPartsAsAttachments
791
     * @return bool true on success
792
     */
793 1
    public function removeAllHtmlParts($keepOtherPartsAsAttachments = true)
794
    {
795 1
        return $this->removeAllContentPartsByMimeType('text/html', $keepOtherPartsAsAttachments);
796
    }
797
    
798
    /**
799
     * Returns the attachment part at the given 0-based index, or null if none
800
     * is set.
801
     * 
802
     * @param int $index
803
     * @return \ZBateson\MailMimeParser\Message\MimePart
804
     */
805 7
    public function getAttachmentPart($index)
806
    {
807 7
        $attachments = $this->getAllAttachmentParts();
808 7
        if (!isset($attachments[$index])) {
809 2
            return null;
810
        }
811 5
        return $attachments[$index];
812
    }
813
    
814
    /**
815
     * Returns all attachment parts.
816
     * 
817
     * Attachments are any non-multipart, non-signature and non inline text or
818
     * html part (a text or html part with a Content-Disposition set to 
819
     * 'attachment' is considered an attachment).
820
     * 
821
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
822
     */
823 50
    public function getAllAttachmentParts()
824
    {
825 50
        $parts = $this->getAllParts(
826 50
            new PartFilter([
827
                'multipart' => PartFilter::FILTER_EXCLUDE
828 50
            ])
829 50
        );
830 50
        return array_values(array_filter(
831 50
            $parts,
832 50
            function ($part) {
833
                return !(
834 50
                    $part->isTextPart()
835 50
                    && $part->getHeaderValue('Content-Disposition', 'inline') === 'inline'
836 50
                );
837
            }
838 50
        ));
839
    }
840
    
841
    /**
842
     * Returns the number of attachments available.
843
     * 
844
     * @return int
845
     */
846 47
    public function getAttachmentCount()
847
    {
848 47
        return count($this->getAllAttachmentParts());
849
    }
850
    
851
    /**
852
     * Removes the attachment with the given index
853
     * 
854
     * @param int $index
855
     */
856 2
    public function removeAttachmentPart($index)
857
    {
858 2
        $part = $this->getAttachmentPart($index);
859 2
        $this->removePart($part);
0 ignored issues
show
Bug introduced by
It seems like $part defined by $this->getAttachmentPart($index) on line 858 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...
860 2
    }
861
    
862
    /**
863
     * Creates and returns a MimePart for use with a new attachment part being
864
     * created.
865
     * 
866
     * @return \ZBateson\MailMimeParser\Message\MimePart
867
     */
868 2
    protected function createPartForAttachment()
869
    {
870 2
        if ($this->isMime()) {
871 2
            $part = $this->mimePartFactory->newMimePart();
872 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
873 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
874 2
                $this->setMessageAsMixed();
875 2
            }
876 2
            return $part;
877
        }
878
        return $this->mimePartFactory->newUUEncodedPart();
879
    }
880
    
881
    /**
882
     * Adds an attachment part for the passed raw data string or handle and
883
     * given parameters.
884
     * 
885
     * @param string|handle $stringOrHandle
886
     * @param strubg $mimeType
887
     * @param string $filename
888
     * @param string $disposition
889
     */
890 1
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
891
    {
892 1
        if ($filename === null) {
893
            $filename = 'file' . uniqid();
894
        }
895 1
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
896 1
        $part = $this->createPartForAttachment();
897 1
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
898 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
899 1
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
900 1
        $this->addPart($part);
901 1
    }
902
    
903
    /**
904
     * Adds an attachment part using the passed file.
905
     * 
906
     * Essentially creates a file stream and uses it.
907
     * 
908
     * @param string $file
909
     * @param string $mimeType
910
     * @param string $filename
911
     * @param string $disposition
912
     */
913 2
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
914
    {
915 2
        $handle = fopen($file, 'r');
916 2
        if ($filename === null) {
917 2
            $filename = basename($file);
918 2
        }
919 2
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
920 2
        $part = $this->createPartForAttachment();
921 2
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
922 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
923 2
        $part->attachContentResourceHandle($handle);
924 2
        $this->addPart($part);
925 2
    }
926
    
927
    /**
928
     * Returns a resource handle where the 'inline' text/plain content at the
929
     * passed $index can be read or null if unavailable.
930
     * 
931
     * @param int $index
932
     * @return resource
933
     */
934 61
    public function getTextStream($index = 0)
935
    {
936 61
        $textPart = $this->getTextPart($index);
937 61
        if ($textPart !== null) {
938 60
            return $textPart->getContentResourceHandle();
939
        }
940 1
        return null;
941
    }
942
    
943
    /**
944
     * Returns the content of the inline text/plain part at the given index.
945
     * 
946
     * Reads the entire stream content into a string and returns it.  Returns
947
     * null if the message doesn't have an inline text part.
948
     * 
949
     * @param int $index
950
     * @return string
951
     */
952 1
    public function getTextContent($index = 0)
953
    {
954 1
        $part = $this->getTextPart($index);
955 1
        if ($part !== null) {
956 1
            return $part->getContent();
957
        }
958
        return null;
959
    }
960
    
961
    /**
962
     * Returns a resource handle where the 'inline' text/html content at the
963
     * passed $index can be read or null if unavailable.
964
     * 
965
     * @return resource
966
     */
967 31
    public function getHtmlStream($index = 0)
968
    {
969 31
        $htmlPart = $this->getHtmlPart($index);
970 31
        if ($htmlPart !== null) {
971 30
            return $htmlPart->getContentResourceHandle();
972
        }
973 1
        return null;
974
    }
975
    
976
    /**
977
     * Returns the content of the inline text/html part at the given index.
978
     * 
979
     * Reads the entire stream content into a string and returns it.  Returns
980
     * null if the message doesn't have an inline html part.
981
     * 
982
     * @param int $index
983
     * @return string
984
     */
985
    public function getHtmlContent($index = 0)
986
    {
987
        $part = $this->getHtmlPart($index);
988
        if ($part !== null) {
989
            return $part->getContent();
990
        }
991
        return null;
992
    }
993
    
994
    /**
995
     * Returns true if either a Content-Type or Mime-Version header are defined
996
     * in this Message.
997
     * 
998
     * @return bool
999
     */
1000 87
    public function isMime()
1001
    {
1002 87
        $contentType = $this->getHeaderValue('Content-Type');
1003 87
        $mimeVersion = $this->getHeaderValue('Mime-Version');
1004 87
        return ($contentType !== null || $mimeVersion !== null);
1005
    }
1006
    
1007
    /**
1008
     * Saves the message as a MIME message to the passed resource handle.
1009
     * 
1010
     * @param resource $handle
1011
     */
1012 83
    public function save($handle)
1013
    {
1014 83
        $this->messageWriter->writeMessageTo($this, $handle);
1015 83
    }
1016
    
1017
    /**
1018
     * Returns the content part of a signed message for a signature to be
1019
     * calculated on the message.
1020
     * 
1021
     * @return string
1022
     */
1023 8
    public function getSignableBody()
1024
    {
1025 8
        return $this->messageWriter->getSignableBody($this);
1026
    }
1027
    
1028
    /**
1029
     * Shortcut to call Message::save with a php://temp stream and return the
1030
     * written email message as a string.
1031
     * 
1032
     * @return string
1033
     */
1034
    public function __toString()
1035
    {
1036
        $handle = fopen('php://temp', 'r+');
1037
        $this->save($handle);
1038
        rewind($handle);
1039
        $str = stream_get_contents($handle);
1040
        fclose($handle);
1041
        return $str;
1042
    }
1043
}
1044