Completed
Push — master ( 74fe50...0f19ea )
by Zaahid
09:04
created

overrideAlternativeMessageContentFromContentPart()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2.0094

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 18
ccs 13
cts 15
cp 0.8667
rs 9.4285
cc 2
eloc 13
nc 2
nop 1
crap 2.0094
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
14
/**
15
 * A parsed mime message with optional mime parts depending on its type.
16
 * 
17
 * A mime message may have any number of mime parts, and each part may have any
18
 * number of sub-parts, etc...
19
 * 
20
 * A message is a specialized "mime part". Namely the message keeps hold of text
21
 * versus HTML parts (and associated streams for easy access), holds a stream
22
 * for the entire message and all its parts, and maintains parts and their
23
 * relationships.
24
 *
25
 * @author Zaahid Bateson
26
 */
27
class Message extends MimePart
28
{
29
    /**
30
     * @var string unique ID used to identify the object to
31
     *      $this->partStreamRegistry when registering the stream.  The ID is
32
     *      used for opening stream parts with the mmp-mime-message "protocol".
33
     * 
34
     * @see \ZBateson\MailMimeParser\SimpleDi::registerStreamExtensions
35
     * @see \ZBateson\MailMimeParser\Stream\PartStream::stream_open
36
     */
37
    protected $objectId;
38
    
39
    /**
40
     * @var \ZBateson\MailMimeParser\Message\MimePart represents the content portion of
41
     *      the email message.  It is assigned either a text or HTML part, or a
42
     *      MultipartAlternativePart
43
     */
44
    protected $contentPart;
45
    
46
    /**
47
     * @var \ZBateson\MailMimeParser\Message\MimePart contains the body of the signature
48
     *      for a multipart/signed message.
49
     */
50
    protected $signedSignaturePart;
51
    
52
    /**
53
     * @var \ZBateson\MailMimeParser\Message\MimePart[] array of non-content parts in
54
     *      this message 
55
     */
56
    protected $attachmentParts = [];
57
    
58
    /**
59
     * @var \ZBateson\MailMimeParser\Message\MimePartFactory a MimePartFactory to create
60
     *      parts for attachments/content
61
     */
62
    protected $mimePartFactory;
63
    
64
    /**
65
     * @var \ZBateson\MailMimeParser\Message\Writer\MessageWriter the part
66
     *      writer for this Message.  The same object is assigned to $partWriter
67
     *      but as an AbstractWriter -- not really needed in PHP but helps with
68
     *      auto-complete and code analyzers.
69
     */
70
    protected $messageWriter = null;
71
    
72
    /**
73
     * @var bool set to true if a newline should be inserted before the next
74
     *      boundary (signed messages are finicky)
75
     */
76
    private $insertNewLineBeforeBoundary = false;
0 ignored issues
show
Unused Code introduced by
The property $insertNewLineBeforeBoundary is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
77
    
78
    /**
79
     * Convenience method to parse a handle or string into a Message without
80
     * requiring including MailMimeParser, instantiating it, and calling parse.
81
     * 
82
     * @param resource|string $handleOrString the resource handle to the input
83
     *        stream of the mime message, or a string containing a mime message
84
     */
85 1
    public static function from($handleOrString)
86
    {
87 1
        $mmp = new MailMimeParser();
88 1
        return $mmp->parse($handleOrString);
89
    }
90
    
91
    /**
92
     * Constructs a Message.
93
     * 
94
     * @param HeaderFactory $headerFactory
95
     * @param MessageWriter $messageWriter
96
     * @param MimePartFactory $mimePartFactory
97
     */
98 89
    public function __construct(
99
        HeaderFactory $headerFactory,   
100
        MessageWriter $messageWriter,
101
        MimePartFactory $mimePartFactory
102
    ) {
103 89
        parent::__construct($headerFactory, $messageWriter);
104 89
        $this->messageWriter = $messageWriter;
105 89
        $this->mimePartFactory = $mimePartFactory;
106 89
        $this->objectId = uniqid();
107 89
    }
108
    
109
    /**
110
     * Returns the unique object ID registered with the PartStreamRegistry
111
     * service object.
112
     * 
113
     * @return string
114
     */
115 84
    public function getObjectId()
116
    {
117 84
        return $this->objectId;
118
    }
119
120
    /**
121
     * Returns true if the $part should be assigned as this message's main
122
     * content part.
123
     * 
124
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
125
     * @return bool
126
     */
127 84
    private function addContentPartFromParsed(MimePart $part)
128
    {
129 84
        $type = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
130
        // separate if statements for clarity
131
        if ($type === 'multipart/alternative'
132 84
            || $type === 'text/plain'
133 84
            || $type === 'text/html') {
134 81
            if ($this->contentPart === null) {
135 81
                $this->contentPart = $part;
136 81
            }
137 81
            return true;
138
        }
139 51
        return false;
140
    }
141
    
142
    /**
143
     * Adds the passed part to the message with the passed position, or at the
144
     * end if not passed.
145
     * 
146
     * This should not be used by a user directly and will be set 'protected' in
147
     * the future.  Instead setTextPart, setHtmlPart and addAttachment should be
148
     * used.
149
     * 
150
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
151
     * @param int $position
152
     */
153 87
    public function addPart(MimePart $part, $position = null)
154
    {
155 87
        parent::addPart($part, $position);
156 87
        $disposition = $part->getHeaderValue('Content-Disposition');
157 87
        $mtype = $this->getHeaderValue('Content-Type');
158 87
        $protocol = $this->getHeaderParameter('Content-Type', 'protocol');
159 87
        $type = $part->getHeaderValue('Content-Type');
160 87
        if (strcasecmp($mtype, 'multipart/signed') === 0 && $protocol !== null && $part->getParent() === $this && strcasecmp($protocol, $type) === 0) {
161 12
            $this->signedSignaturePart = $part;
162 87
        } else if (($disposition !== null || !$this->addContentPartFromParsed($part)) && !$part->isMultiPart()) {
163 53
            $this->attachmentParts[] = $part;
164 53
        }
165 87
    }
166
    
167
    /**
168
     * Returns the content part (or null) for the passed mime type looking at
169
     * the assigned content part, and if it's a multipart/alternative part,
170
     * looking to find an alternative part of the passed mime type.
171
     * 
172
     * @param string $mimeType
173
     * @return \ZBateson\MailMimeParser\Message\MimePart or null if not
174
     *         available
175
     */
176 74
    protected function getContentPartByMimeType($mimeType)
177
    {
178 74
        if (!isset($this->contentPart)) {
179 2
            return null;
180
        }
181 73
        $type = strtolower($this->contentPart->getHeaderValue('Content-Type', 'text/plain'));
182 73
        if ($type === 'multipart/alternative') {
183 22
            return $this->getPartByMimeType($mimeType);
184 55
        } elseif ($type === $mimeType) {
185 53
            return $this->contentPart;
186
        }
187 11
        return null;
188
    }
189
    
190
    /**
191
     * Sets the content of the message to the content of the passed part, for a
192
     * message with a multipart/alternative content type where the other part
193
     * has been removed, and this is the only remaining part.
194
     * 
195
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
196
     */
197 2
    private function overrideAlternativeMessageContentFromContentPart(MimePart $part)
198
    {
199 2
        $contentType = $part->getHeaderValue('Content-Type');
200 2
        if ($contentType === null) {
201
            $contentType = 'text/plain; charset="us-ascii"';
202
        }
203 2
        $this->setRawHeader(
204 2
            'Content-Type',
205
            $contentType
206 2
        );
207 2
        $this->setRawHeader(
208 2
            'Content-Transfer-Encoding',
209
            'quoted-printable'
210 2
        );
211 2
        $this->attachContentResourceHandle($part->getContentResourceHandle());
212 2
        $part->detachContentResourceHandle();
213 2
        $this->removePart($part);
214 2
    }
215
    
216
    /**
217
     * Removes the passed MimePart as a content part.  If there's a remaining
218
     * part, either sets the content on this message if the message itself is a
219
     * multipart/alternative message, or overrides the contentPart with the
220
     * remaining part.
221
     * 
222
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
223
     */
224 3
    private function removePartFromAlternativeContentPart(MimePart $part)
225
    {
226 3
        $this->removePart($part);
227 3
        if ($this->contentPart === $this) {
228 2
            $this->overrideAlternativeMessageContentFromContentPart($this->getPart(0));
0 ignored issues
show
Bug introduced by
It seems like $this->getPart(0) can be null; however, overrideAlternativeMessageContentFromContentPart() 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...
229 3
        } elseif ($this->contentPart->getPartCount() === 1) {
230 1
            $this->removePart($this->contentPart);
231 1
            $contentPart = $this->contentPart->getChild(0);
232 1
            $contentPart->setParent($this);
233 1
            $this->contentPart = null;
234 1
            $this->addPart($contentPart, 0);
0 ignored issues
show
Bug introduced by
It seems like $contentPart defined by $this->contentPart->getChild(0) on line 231 can be null; however, ZBateson\MailMimeParser\Message::addPart() 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...
235 1
        }
236 3
    }
237
    
238
    /**
239
     * Loops over children of the content part looking for a part with the
240
     * passed mime type, then proceeds to remove it by calling
241
     * removePartFromAlternativeContentPart.
242
     * 
243
     * @param string $contentType
244
     * @return boolean true on success
245
     */
246 3
    private function removeContentPartFromAlternative($contentType)
247
    {
248 3
        $parts = $this->contentPart->getAllParts();
249 3
        foreach ($parts as $part) {
250 3
            $type = $part->getHeaderValue('Content-Type', 'text/plain');
251 3
            if (strcasecmp($type, $contentType) === 0) {
252 3
                $this->removePartFromAlternativeContentPart($part);
253 3
                return true;
254
            }
255 2
        }
256
        return false;
257
    }
258
    
259
    /**
260
     * Removes the content part of the message with the passed mime type.  If
261
     * there is a remaining content part and it is an alternative part of the
262
     * main message, the content part is moved to the message part.
263
     * 
264
     * If the content part is part of an alternative part beneath the message,
265
     * the alternative part is replaced by the remaining content part.
266
     * 
267
     * @param string $contentType
268
     * @return boolean true on success
269
     */
270 3
    protected function removeContentPart($contentType)
271
    {
272 3
        if (!isset($this->contentPart)) {
273
            return false;
274
        }
275 3
        $type = $this->contentPart->getHeaderValue('Content-Type', 'text/plain');
276 3
        if (strcasecmp($type, $contentType) === 0) {
277
            if ($this->contentPart === $this) {
278
                return false;
279
            }
280
            $this->removePart($this->contentPart);
281
            $this->contentPart = null;
282
            return true;
283
        }
284 3
        return $this->removeContentPartFromAlternative($contentType);
285
    }
286
    
287
    /**
288
     * Returns the text part (or null if none is set.)
289
     * 
290
     * @return \ZBateson\MailMimeParser\Message\MimePart
291
     */
292 64
    public function getTextPart()
293
    {
294 64
        return $this->getContentPartByMimeType('text/plain');
295
    }
296
    
297
    /**
298
     * Returns the HTML part (or null if none is set.)
299
     * 
300
     * @return \ZBateson\MailMimeParser\Message\MimePart
301
     */
302 36
    public function getHtmlPart()
303
    {
304 36
        return $this->getContentPartByMimeType('text/html');
305
    }
306
    
307
    /**
308
     * Returns the content MimePart, which could be a text/plain, text/html or
309
     * multipart/alternative part or null if none is set.
310
     * 
311
     * @return \ZBateson\MailMimeParser\Message\MimePart
312
     */
313 1
    public function getContentPart()
314
    {
315 1
        return $this->contentPart;
316
    }
317
    
318
    /**
319
     * Returns an open resource handle for the passed string or resource handle.
320
     * 
321
     * For a string, creates a php://temp stream and returns it.
322
     * 
323
     * @param resource|string $stringOrHandle
324
     * @return resource
325
     */
326 5
    private function getHandleForStringOrHandle($stringOrHandle)
327
    {
328 5
        $tempHandle = fopen('php://temp', 'r+');
329 5
        if (is_string($stringOrHandle)) {
330 5
            fwrite($tempHandle, $stringOrHandle);
331 5
        } else {
332
            stream_copy_to_stream($stringOrHandle, $tempHandle);
333
        }
334 5
        rewind($tempHandle);
335 5
        return $tempHandle;
336
    }
337
    
338
    /**
339
     * Creates and returns a unique boundary.
340
     * 
341
     * @param string $mimeType first 3 characters of a multipart type are used,
342
     *      e.g. REL for relative or ALT for alternative
343
     * @return string
344
     */
345 14
    private function getUniqueBoundary($mimeType)
346
    {
347 14
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
348 14
        return uniqid('----=MMP-' . $type . $this->objectId . '.', true);
349
    }
350
    
351
    /**
352
     * Creates a unique mime boundary and assigns it to the passed part's
353
     * Content-Type header with the passed mime type.
354
     * 
355
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
356
     * @param string $mimeType
357
     */
358 7
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
359
    {
360 7
        $part->setRawHeader(
361 7
            'Content-Type',
362 7
            "$mimeType;\r\n\tboundary=\"" 
1 ignored issue
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $mimeType instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
363 7
                . $this->getUniqueBoundary($mimeType) . "\""
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal \" does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
364 7
        );
365 7
    }
366
    
367
    /**
368
     * Sets this message to be a multipart/alternative message, making space for
369
     * another alternative content part.
370
     * 
371
     * Creates a content part and assigns the content stream from the message to
372
     * that newly created part.
373
     */
374 2
    private function setMessageAsAlternative()
375
    {
376 2
        $contentPart = $this->mimePartFactory->newMimePart();
377 2
        $contentPart->attachContentResourceHandle($this->handle);
378 2
        $this->detachContentResourceHandle();
379 2
        $this->removePart($this);
380 2
        $contentType = 'text/plain; charset="us-ascii"';
381 2
        $contentHeader = $this->getHeader('Content-Type');
382 2
        if ($contentHeader !== null) {
383 2
            $contentType = $contentHeader->getRawValue();
384 2
        }
385 2
        $contentPart->setRawHeader('Content-Type', $contentType);
386 2
        $contentPart->setParent($this);
387 2
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/alternative');
388 2
        $this->contentPart = null;
389 2
        $this->addPart($this);
390 2
        $this->addPart($contentPart, 0);
391 2
    }
392
    
393
    /**
394
     * Creates a new mime part as a multipart/alternative, assigning it to
395
     * $this->contentPart.  Adds the current contentPart below the newly created
396
     * alternative part.
397
     */
398 2 View Code Duplication
    private function createAlternativeContentPart()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399
    {
400 2
        $altPart = $this->mimePartFactory->newMimePart();
401 2
        $contentPart = $this->contentPart;
402 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
403 2
        $this->removePart($contentPart);
404 2
        $contentPart->setParent($altPart);
405 2
        $this->contentPart = null;
406 2
        $altPart->setParent($this);
407 2
        $this->addPart($altPart, 0);
408 2
        $this->addPart($contentPart, 0);
409 2
    }
410
    
411
    /**
412
     * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding
413
     * headers from the $from header into the $to header. If the Content-Type
414
     * header isn't defined in $from, defaults to text/plain and
415
     * quoted-printable.
416
     * 
417
     * @param \ZBateson\MailMimeParser\Message\MimePart $from
418
     * @param \ZBateson\MailMimeParser\Message\MimePart $to
419
     */
420 11
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
421
    {
422 11
        $typeHeader = $from->getHeader('Content-Type');
423 11
        if ($typeHeader !== null) {
424 11
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
425 11
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
426 11
            if ($encodingHeader !== null) {
427 4
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
428 4
            }
429 11
            $dispositionHeader = $from->getHeader('Content-Disposition');
430 11
            if ($dispositionHeader !== null) {
431 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
432 1
            }
433 11
        } else {
434
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
435
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
436
        }
437 11
    }
438
    
439
    /**
440
     * Creates a new content part from the passed part, allowing the part to be
441
     * used for something else (e.g. changing a non-mime message to a multipart
442
     * mime message).
443
     */
444 4
    private function createNewContentPartFromPart(MimePart $part)
445
    {
446 4
        $contPart = $this->mimePartFactory->newMimePart();
447 4
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
448 4
        $contPart->attachContentResourceHandle($part->handle);
449 4
        $part->detachContentResourceHandle();
450 4
        return $contPart;
451
    }
452
    
453
    /**
454
     * Creates a new part out of the current contentPart and sets the message's
455
     * type to be multipart/mixed.
456
     */
457 4
    private function setMessageAsMixed()
458
    {
459 4
        $part = $this->createNewContentPartFromPart($this->contentPart);
460 4
        $this->removePart($this->contentPart);
461 4
        $this->contentPart = null;
462 4
        $this->addPart($part, 0);
463 4
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
464 4
    }
465
    
466
    /**
467
     * This function makes space by moving the main message part down one level.
468
     * 
469
     * The content-type, content-disposition and content-transfer-encoding
470
     * headers are copied from this message to the newly created part, the 
471
     * resource handle is moved and detached, any attachments and content parts
472
     * with parents set to this message get their parents set to the newly
473
     * created part.
474
     */
475 8
    private function makeSpaceForMultipartSignedMessage()
476
    {
477 8
        $this->enforceMime();
478 8
        $messagePart = $this->mimePartFactory->newMimePart();
479 8
        $messagePart->setParent($this);
480
        
481 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
482 8
        $messagePart->attachContentResourceHandle($this->handle);
483 8
        $this->detachContentResourceHandle();
484
        
485 8
        $this->contentPart = null;
486 8
        $this->addPart($messagePart, 0);
487 8
        foreach ($this->getChildParts() as $part) {
488 8
            if ($part === $messagePart) {
489 8
                continue;
490
            }
491 5
            $this->removePart($part);
492 5
            $part->setParent($messagePart);
493 5
            $this->addPart($part);
494 8
        }
495 8
    }
496
    
497
    /**
498
     * Creates and returns a new MimePart for the signature part of a
499
     * multipart/signed message and assigns it to $this->signedSignaturePart.
500
     * 
501
     * @param string $body
502
     */
503 8 View Code Duplication
    public function createSignaturePart($body)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
504
    {
505 8
        $signedPart = $this->signedSignaturePart;
506 8
        if ($signedPart === null) {
507 8
            $signedPart = $this->mimePartFactory->newMimePart();
508 8
            $signedPart->setParent($this);
509 8
            $this->addPart($signedPart);
510 8
            $this->signedSignaturePart = $signedPart;
511 8
        }
512 8
        $signedPart->setRawHeader(
513 8
            'Content-Type',
514 8
            $this->getHeaderParameter('Content-Type', 'protocol')
515 8
        );
516 8
        $signedPart->setContent($body);
517 8
    }
518
519
    /**
520
     * Loops over parts of this message and sets the content-transfer-encoding
521
     * header to quoted-printable for text/* mime parts, and to base64
522
     * otherwise for parts that are '8bit' encoded.
523
     * 
524
     * Used for multipart/signed messages which doesn't support 8bit transfer
525
     * encodings.
526
     */
527 8
    private function overwrite8bitContentEncoding()
528
    {
529 8
        $parts = array_merge([ $this ], $this->getAllParts());
530 8
        foreach ($parts as $part) {
531 8
            if ($part->getHeaderValue('Content-Transfer-Encoding') === '8bit') {
532 1
                if (preg_match('/text\/.*/', $part->getHeaderValue('Content-Type'))) {
533 1
                    $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
534 1
                } else {
535
                    $part->setRawHeader('Content-Transfer-Encoding', 'base64');
536
                }
537 1
            }
538 8
        }
539 8
    }
540
    
541
    /**
542
     * Ensures a non-text part comes first in a signed multipart/alternative
543
     * message as some clients seem to prefer the first content part if the
544
     * client doesn't understand multipart/signed.
545
     */
546 8
    private function ensureHtmlPartFirstForSignedMessage()
547
    {
548 8
        if ($this->contentPart === null) {
549 2
            return;
550
        }
551 6
        $type = strtolower($this->contentPart->getHeaderValue('Content-Type', 'text/plain'));
552 6
        if ($type === 'multipart/alternative' && count($this->contentPart->parts) > 1) {
553 4
            if (strtolower($this->contentPart->parts[0]->getHeaderValue('Content-Type', 'text/plain')) === 'text/plain') {
554 4
                $tmp = $this->contentPart->parts[0];
555 4
                $this->contentPart->parts[0] = $this->contentPart->parts[1];
556 4
                $this->contentPart->parts[1] = $tmp;
557 4
            }
558 4
        }
559 6
    }
560
    
561
    /**
562
     * Turns the message into a multipart/signed message, moving the actual
563
     * message into a child part, sets the content-type of the main message to
564
     * multipart/signed and adds a signature part as well.
565
     * 
566
     * @param string $micalg The Message Integrity Check algorithm being used
567
     * @param string $protocol The mime-type of the signature body
568
     */
569 8
    public function setAsMultipartSigned($micalg, $protocol)
570
    {
571 8
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
572 8
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
573 8
            $this->makeSpaceForMultipartSignedMessage();
574 8
        }
575 8
        $boundary = $this->getUniqueBoundary('multipart/signed');
576 8
        $this->setRawHeader(
577 8
            'Content-Type',
578 8
            "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
3 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $boundary instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $micalg instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $protocol instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
579 8
        );
580 8
        $this->removeHeader('Content-Transfer-Encoding');
581 8
        $this->overwrite8bitContentEncoding();
582 8
        $this->ensureHtmlPartFirstForSignedMessage();
583 8
        $this->createSignaturePart('Not set');
584 8
    }
585
    
586
    /**
587
     * Returns the signed part or null if not set.
588
     * 
589
     * @return \ZBateson\MailMimeParser\Message\MimePart
590
     */
591 12
    public function getSignaturePart()
592
    {
593 12
        return $this->signedSignaturePart;
594
    }
595
    
596
    /**
597
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
598
     * or unspecified) message.  If the message has uuencoded attachments, sets
599
     * up the message as a multipart/mixed message and creates a content part.
600
     */
601 12
    private function enforceMime()
602
    {
603 12
        if (!$this->isMime()) {
604 2
            if ($this->getAttachmentCount()) {
605 2
                $this->setMessageAsMixed();
606 2
            } else {
607
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
608
            }
609 2
            $this->setRawHeader('Mime-Version', '1.0');
610 2
        }
611 12
    }
612
    
613
    /**
614
     * Creates a new content part for the passed mimeType and charset, making
615
     * space by creating a multipart/alternative if needed
616
     * 
617
     * @param string $mimeType
618
     * @param string $charset
619
     * @return \ZBateson\MailMimeParser\Message\MimePart
620
     */
621 4
    private function createContentPartForMimeType($mimeType, $charset)
622
    {
623
        // wouldn't come here unless there's only one 'content part' anyway
624
        // if this->contentPart === $this, then $this is not a multipart/alternative
625
        // message
626 4
        $mimePart = $this->mimePartFactory->newMimePart();
627 4
        $cset = ($charset === null) ? 'UTF-8' : $charset;
628 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$cset\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $mimeType instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $cset instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
629 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
630 4
        $this->enforceMime();
631 4
        if ($this->contentPart === $this) {
632 2
            $this->setMessageAsAlternative();
633 2
            $mimePart->setParent($this->contentPart);
634 2
            $this->addPart($mimePart, 0);
635 4
        } elseif ($this->contentPart !== null) {
636 2
            $this->createAlternativeContentPart();
637 2
            $mimePart->setParent($this->contentPart);
638 2
            $this->addPart($mimePart, 0);
639 2
        } else {
640 1
            $mimePart->setParent($this);
641 1
            $this->addPart($mimePart, 0);
642
        }
643 4
        return $mimePart;
644
    }
645
    
646
    /**
647
     * Either creates a mime part or sets the existing mime part with the passed
648
     * mimeType to $strongOrHandle.
649
     * 
650
     * @param string $mimeType
651
     * @param string|resource $stringOrHandle
652
     * @param string $charset
653
     */
654 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
655
    {
656 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
657 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
658 4
        if ($part === null) {
659 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
660 4
        } elseif ($charset !== null) {
661
            $cset = ($charset === null) ? 'UTF-8' : $charset;
662
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
663
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$cset\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $contentType instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $cset instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
664
        }
665 4
        $part->attachContentResourceHandle($handle);
666 4
    }
667
    
668
    /**
669
     * Sets the text/plain part of the message to the passed $stringOrHandle,
670
     * either creating a new part if one doesn't exist for text/plain, or
671
     * assigning the value of $stringOrHandle to an existing text/plain part.
672
     * 
673
     * The optional $charset parameter is the charset for saving to.
674
     * $stringOrHandle is expected to be in UTF-8.
675
     * 
676
     * @param string|resource $stringOrHandle
677
     * @param string $charset
678
     */
679 1
    public function setTextPart($stringOrHandle, $charset = null)
680
    {
681 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
682 1
    }
683
    
684
    /**
685
     * Sets the text/html part of the message to the passed $stringOrHandle,
686
     * either creating a new part if one doesn't exist for text/html, or
687
     * assigning the value of $stringOrHandle to an existing text/html part.
688
     * 
689
     * The optional $charset parameter is the charset for saving to.
690
     * $stringOrHandle is expected to be in UTF-8.
691
     * 
692
     * @param string|resource $stringOrHandle
693
     * @param string $charset
694
     */
695 4
    public function setHtmlPart($stringOrHandle, $charset = null)
696
    {
697 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
698 4
    }
699
    
700
    /**
701
     * Removes the text part of the message if one exists.  Returns true on
702
     * success.
703
     * 
704
     * @return bool true on success
705
     */
706 2
    public function removeTextPart()
707
    {
708 2
        return $this->removeContentPart('text/plain');
709
    }
710
    
711
    /**
712
     * Removes the html part of the message if one exists.  Returns true on
713
     * success.
714
     * 
715
     * @return bool true on success
716
     */
717 1
    public function removeHtmlPart()
718
    {
719 1
        return $this->removeContentPart('text/html');
720
    }
721
    
722
    /**
723
     * Returns the non-content part at the given 0-based index, or null if none
724
     * is set.
725
     * 
726
     * @param int $index
727
     * @return \ZBateson\MailMimeParser\Message\MimePart
728
     */
729 7
    public function getAttachmentPart($index)
730
    {
731 7
        if (!isset($this->attachmentParts[$index])) {
732 2
            return null;
733
        }
734 5
        return $this->attachmentParts[$index];
735
    }
736
    
737
    /**
738
     * Returns all attachment parts.
739
     * 
740
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
741
     */
742 47
    public function getAllAttachmentParts()
743
    {
744 47
        return $this->attachmentParts;
745
    }
746
    
747
    /**
748
     * Returns the number of attachments available.
749
     * 
750
     * @return int
751
     */
752 48
    public function getAttachmentCount()
753
    {
754 48
        return count($this->attachmentParts);
755
    }
756
    
757
    /**
758
     * Removes the attachment with the given index
759
     * 
760
     * @param int $index
761
     */
762 2
    public function removeAttachmentPart($index)
763
    {
764 2
        $part = $this->attachmentParts[$index];
765 2
        $this->removePart($part);
766 2
        array_splice($this->attachmentParts, $index, 1);
767 2
    }
768
    
769
    /**
770
     * Creates and returns a MimePart for use with a new attachment part being
771
     * created.
772
     * 
773
     * @return \ZBateson\MailMimeParser\Message\MimePart
774
     */
775 2
    protected function createPartForAttachment()
776
    {
777 2
        $part = null;
0 ignored issues
show
Unused Code introduced by
$part 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...
778 2
        if ($this->isMime()) {
779 2
            $part = $this->mimePartFactory->newMimePart();
780 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
781 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
782 2
                $this->setMessageAsMixed();
783 2
            }
784 2
        } else {
785
            $part = $this->mimePartFactory->newUUEncodedPart();
0 ignored issues
show
Bug introduced by
The call to newUUEncodedPart() misses some required arguments starting with $mode.
Loading history...
786
        }
787 2
        return $part;
788
    }
789
    
790
    /**
791
     * Adds an attachment part for the passed raw data string or handle and
792
     * given parameters.
793
     * 
794
     * @param string|handle $stringOrHandle
795
     * @param strubg $mimeType
796
     * @param string $filename
797
     * @param string $disposition
798
     */
799 1
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
800
    {
801 1
        if ($filename === null) {
802
            $filename = 'file' . uniqid();
803
        }
804 1
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
805 1
        $part = $this->createPartForAttachment();
806 1
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $mimeType instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $filename instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
807 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $disposition instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $filename instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
808 1
        $part->setParent($this);
809 1
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
810 1
        $this->addPart($part);
811 1
    }
812
    
813
    /**
814
     * Adds an attachment part using the passed file.
815
     * 
816
     * Essentially creates a file stream and uses it.
817
     * 
818
     * @param string $file
819
     * @param string $mimeType
820
     * @param string $filename
821
     * @param string $disposition
822
     */
823 2
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
824
    {
825 2
        $handle = fopen($file, 'r');
826 2
        if ($filename === null) {
827 2
            $filename = basename($file);
828 2
        }
829 2
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
830 2
        $part = $this->createPartForAttachment();
831 2
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $mimeType instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $filename instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
832 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
2 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $disposition instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $filename instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
833 2
        $part->setParent($this);
834 2
        $part->attachContentResourceHandle($handle);
835 2
        $this->addPart($part);
836 2
    }
837
    
838
    /**
839
     * Returns a resource handle where the text content can be read or null if
840
     * unavailable.
841
     * 
842
     * @return resource
843
     */
844 60
    public function getTextStream()
845
    {
846 60
        $textPart = $this->getTextPart();
847 60
        if ($textPart !== null) {
848 59
            return $textPart->getContentResourceHandle();
849
        }
850 1
        return null;
851
    }
852
    
853
    /**
854
     * Returns the text content as a string.
855
     * 
856
     * Reads the entire stream content into a string and returns it.  Returns
857
     * null if the message doesn't have a text part.
858
     * 
859
     * @return string
860
     */
861 1
    public function getTextContent()
862
    {
863 1
        $stream = $this->getTextStream();
864 1
        if ($stream === null) {
865
            return null;
866
        }
867 1
        return stream_get_contents($stream);
868
    }
869
    
870
    /**
871
     * Returns a resource handle where the HTML content can be read or null if
872
     * unavailable.
873
     * 
874
     * @return resource
875
     */
876 30
    public function getHtmlStream()
877
    {
878 30
        $htmlPart = $this->getHtmlPart();
879 30
        if ($htmlPart !== null) {
880 29
            return $htmlPart->getContentResourceHandle();
881
        }
882 1
        return null;
883
    }
884
    
885
    /**
886
     * Returns the HTML content as a string.
887
     * 
888
     * Reads the entire stream content into a string and returns it.  Returns
889
     * null if the message doesn't have an HTML part.
890
     * 
891
     * @return string
892
     */
893
    public function getHtmlContent()
894
    {
895
        $stream = $this->getHtmlStream();
896
        if ($stream === null) {
897
            return null;
898
        }
899
        return stream_get_contents($stream);
900
    }
901
    
902
    /**
903
     * Returns true if either a Content-Type or Mime-Version header are defined
904
     * in this Message.
905
     * 
906
     * @return bool
907
     */
908 84
    public function isMime()
909
    {
910 84
        $contentType = $this->getHeaderValue('Content-Type');
911 84
        $mimeVersion = $this->getHeaderValue('Mime-Version');
912 84
        return ($contentType !== null || $mimeVersion !== null);
913
    }
914
    
915
    /**
916
     * Saves the message as a MIME message to the passed resource handle.
917
     * 
918
     * @param resource $handle
919
     */
920 80
    public function save($handle)
921
    {
922 80
        $this->messageWriter->writeMessageTo($this, $handle);
923 80
    }
924
    
925
    /**
926
     * Returns the content part of a signed message for a signature to be
927
     * calculated on the message.
928
     * 
929
     * @return string
930
     */
931 8
    public function getSignableBody()
932
    {
933 8
        return $this->messageWriter->getSignableBody($this);
934
    }
935
    
936
    /**
937
     * Shortcut to call Message::save with a php://temp stream and return the
938
     * written email message as a string.
939
     * 
940
     * @return string
941
     */
942
    public function __toString()
943
    {
944
        $handle = fopen('php://temp', 'r+');
945
        $this->save($handle);
946
        rewind($handle);
947
        $str = stream_get_contents($handle);
948
        fclose($handle);
949
        return $str;
950
    }
951
}
952