Completed
Push — master ( 1a3789...b27911 )
by Zaahid
08:22
created

Message::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 8
nc 1
nop 3
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
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
     * Convenience method to parse a handle or string into a Message without
74
     * requiring including MailMimeParser, instantiating it, and calling parse.
75
     * 
76
     * @param resource|string $handleOrString the resource handle to the input
77
     *        stream of the mime message, or a string containing a mime message
78
     */
79 1
    public static function from($handleOrString)
80
    {
81 1
        $mmp = new MailMimeParser();
82 1
        return $mmp->parse($handleOrString);
83
    }
84
    
85
    /**
86
     * Constructs a Message.
87
     * 
88
     * @param HeaderFactory $headerFactory
89
     * @param MessageWriter $messageWriter
90
     * @param MimePartFactory $mimePartFactory
91
     */
92 91
    public function __construct(
93
        HeaderFactory $headerFactory,   
94
        MessageWriter $messageWriter,
95
        MimePartFactory $mimePartFactory
96
    ) {
97 91
        parent::__construct($headerFactory, $messageWriter);
98 91
        $this->messageWriter = $messageWriter;
99 91
        $this->mimePartFactory = $mimePartFactory;
100 91
        $this->objectId = uniqid();
101 91
    }
102
    
103
    /**
104
     * Returns the unique object ID registered with the PartStreamRegistry
105
     * service object.
106
     * 
107
     * @return string
108
     */
109 86
    public function getObjectId()
110
    {
111 86
        return $this->objectId;
112
    }
113
114
    /**
115
     * Returns true if the $part should be assigned as this message's main
116
     * content part.
117
     * 
118
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
119
     * @return bool
120
     */
121 86
    private function addContentPartFromParsed(MimePart $part)
122
    {
123 86
        $type = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
124
        // separate if statements for clarity
125
        if ($type === 'multipart/alternative'
126 86
            || $type === 'text/plain'
127 86
            || $type === 'text/html') {
128 83
            if ($this->contentPart === null) {
129 83
                $this->contentPart = $part;
130 83
            }
131 83
            return true;
132
        }
133 53
        return false;
134
    }
135
    
136
    /**
137
     * Adds the passed part to the message with the passed position, or at the
138
     * end if not passed.
139
     * 
140
     * This should not be used by a user directly and will be set 'protected' in
141
     * the future.  Instead setTextPart, setHtmlPart and addAttachment should be
142
     * used.
143
     * 
144
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
145
     * @param int $position
146
     */
147 89
    public function addPart(MimePart $part, $position = null)
148
    {
149 89
        parent::addPart($part, $position);
150 89
        $disposition = $part->getHeaderValue('Content-Disposition');
151 89
        $mtype = $this->getHeaderValue('Content-Type');
152 89
        $protocol = $this->getHeaderParameter('Content-Type', 'protocol');
153 89
        $type = $part->getHeaderValue('Content-Type');
154 89
        if (strcasecmp($mtype, 'multipart/signed') === 0 && $protocol !== null && $part->getParent() === $this && strcasecmp($protocol, $type) === 0) {
155 12
            $this->signedSignaturePart = $part;
156 89
        } else if (($disposition !== null || !$this->addContentPartFromParsed($part)) && !$part->isMultiPart()) {
157 55
            $this->attachmentParts[] = $part;
158 55
        }
159 89
    }
160
    
161
    /**
162
     * Returns the content part (or null) for the passed mime type looking at
163
     * the assigned content part, and if it's a multipart/alternative part,
164
     * looking to find an alternative part of the passed mime type.
165
     * 
166
     * @param string $mimeType
167
     * @return \ZBateson\MailMimeParser\Message\MimePart or null if not
168
     *         available
169
     */
170 76
    protected function getContentPartByMimeType($mimeType)
171
    {
172 76
        if (!isset($this->contentPart)) {
173 2
            return null;
174
        }
175 75
        $type = strtolower($this->contentPart->getHeaderValue('Content-Type', 'text/plain'));
176 75
        if ($type === $mimeType) {
177 55
            return $this->contentPart;
178
        }
179 31
        return $this->getPartByMimeType($mimeType);
180
    }
181
    
182
    /**
183
     * Sets the content of the message to the content of the passed part, for a
184
     * message with a multipart/alternative content type where the other part
185
     * has been removed, and this is the only remaining part.
186
     * 
187
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
188
     */
189 4
    private function overrideAlternativeMessageContentFromContentPart(MimePart $part)
190
    {
191 4
        $contentType = $part->getHeader('Content-Type')->getRawValue();
192 4
        if ($contentType === null) {
193
            $contentType = 'text/plain; charset="us-ascii"';
194
        }
195 4
        $this->setRawHeader(
196 4
            'Content-Type',
197
            $contentType
198 4
        );
199 4
        $this->setRawHeader(
200 4
            'Content-Transfer-Encoding',
201
            'quoted-printable'
202 4
        );
203 4
        $this->attachContentResourceHandle($part->getContentResourceHandle());
204 4
        $part->detachContentResourceHandle();
205 4
        foreach ($part->getChildParts() as $child) {
206 1
            $child->setParent($this);
207 1
            parent::addPart($child);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (addPart() instead of overrideAlternativeMessageContentFromContentPart()). Are you sure this is correct? If so, you might want to change this to $this->addPart().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
208 4
        }
209 4
        $this->removePart($part);
210 4
    }
211
    
212
    /**
213
     * Removes the passed MimePart as a content part.  If there's a remaining
214
     * part, either sets the content on this message if the message itself is a
215
     * multipart/alternative message, or overrides the contentPart with the
216
     * remaining part.
217
     * 
218
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
219
     */
220 5
    private function removePartFromAlternativeContentPart(MimePart $part)
221
    {
222 5
        foreach ($part->getAllParts() as $cpart) {
223 1
            $this->removePart($cpart);
224 5
        }
225 5
        $this->removePart($part);
226 5
        $contentPart = $this->contentPart->getPart(0);
227 5
        if ($contentPart !== null) {
228 5
            if ($this->contentPart === $this) {
229 4
                $this->overrideAlternativeMessageContentFromContentPart($contentPart);
230 5
            } elseif ($this->contentPart->getChildCount() === 1) {
231 1
                $this->removePart($this->contentPart);
232 1
                $contentPart->setParent($this);
233 1
                $this->contentPart = null;
234 1
                $this->addPart($contentPart, 0);
235 1
            }
236 5
        }
237 5
    }
238
    
239
    /**
240
     * Loops over children of the content part looking for a part with the
241
     * passed mime type, then proceeds to remove it by calling
242
     * removePartFromAlternativeContentPart.
243
     * 
244
     * @param string $contentType
245
     * @return boolean true on success
246
     */
247 5
    private function removeContentPartFromAlternative($contentType)
248
    {
249 5
        $parts = $this->contentPart->getChildParts();
250 5
        foreach ($parts as $child) {
251 5
            $type = $child->getHeaderValue('Content-Type', 'text/plain');
252 5
            if (strcasecmp($type, $contentType) === 0) {
253 4
                $this->removePartFromAlternativeContentPart($child);
254 4
                return true;
255 3
            } elseif ($child->isMultiPart() && $child->getPartByMimeType($contentType)) {
256 1
                $this->removePartFromAlternativeContentPart($child);
257 1
                return true;
258
            }
259 3
        }
260
        return false;
261
    }
262
    
263
    /**
264
     * Removes the content part of the message with the passed mime type.  If
265
     * there is a remaining content part and it is an alternative part of the
266
     * main message, the content part is moved to the message part.
267
     * 
268
     * If the content part is part of an alternative part beneath the message,
269
     * the alternative part is replaced by the remaining content part.
270
     * 
271
     * @param string $contentType
272
     * @return boolean true on success
273
     */
274 5
    protected function removeContentPart($contentType)
275
    {
276 5
        if (!isset($this->contentPart)) {
277
            return false;
278
        }
279 5
        $type = $this->contentPart->getHeaderValue('Content-Type', 'text/plain');
280 5
        if (strcasecmp($type, $contentType) === 0) {
281
            if ($this->contentPart === $this) {
282
                return false;
283
            }
284
            $this->removePart($this->contentPart);
285
            $this->contentPart = null;
286
            return true;
287
        }
288 5
        return $this->removeContentPartFromAlternative($contentType);
289
    }
290
    
291
    /**
292
     * Returns the text part (or null if none is set.)
293
     * 
294
     * @return \ZBateson\MailMimeParser\Message\MimePart
295
     */
296 66
    public function getTextPart()
297
    {
298 66
        return $this->getContentPartByMimeType('text/plain');
299
    }
300
    
301
    /**
302
     * Returns the HTML part (or null if none is set.)
303
     * 
304
     * @return \ZBateson\MailMimeParser\Message\MimePart
305
     */
306 38
    public function getHtmlPart()
307
    {
308 38
        return $this->getContentPartByMimeType('text/html');
309
    }
310
    
311
    /**
312
     * Returns the content MimePart, which could be a text/plain, text/html or
313
     * multipart/alternative part or null if none is set.
314
     * 
315
     * @return \ZBateson\MailMimeParser\Message\MimePart
316
     */
317 1
    public function getContentPart()
318
    {
319 1
        return $this->contentPart;
320
    }
321
    
322
    /**
323
     * Returns an open resource handle for the passed string or resource handle.
324
     * 
325
     * For a string, creates a php://temp stream and returns it.
326
     * 
327
     * @param resource|string $stringOrHandle
328
     * @return resource
329
     */
330 5
    private function getHandleForStringOrHandle($stringOrHandle)
331
    {
332 5
        $tempHandle = fopen('php://temp', 'r+');
333 5
        if (is_string($stringOrHandle)) {
334 5
            fwrite($tempHandle, $stringOrHandle);
335 5
        } else {
336
            stream_copy_to_stream($stringOrHandle, $tempHandle);
337
        }
338 5
        rewind($tempHandle);
339 5
        return $tempHandle;
340
    }
341
    
342
    /**
343
     * Creates and returns a unique boundary.
344
     * 
345
     * @param string $mimeType first 3 characters of a multipart type are used,
346
     *      e.g. REL for relative or ALT for alternative
347
     * @return string
348
     */
349 14
    private function getUniqueBoundary($mimeType)
350
    {
351 14
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
352 14
        return uniqid('----=MMP-' . $type . $this->objectId . '.', true);
353
    }
354
    
355
    /**
356
     * Creates a unique mime boundary and assigns it to the passed part's
357
     * Content-Type header with the passed mime type.
358
     * 
359
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
360
     * @param string $mimeType
361
     */
362 7
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
363
    {
364 7
        $part->setRawHeader(
365 7
            'Content-Type',
366 7
            "$mimeType;\r\n\tboundary=\"" 
367 7
                . $this->getUniqueBoundary($mimeType) . '"'
368 7
        );
369 7
    }
370
    
371
    /**
372
     * Sets this message to be a multipart/alternative message, making space for
373
     * another alternative content part.
374
     * 
375
     * Creates a content part and assigns the content stream from the message to
376
     * that newly created part.
377
     */
378 2
    private function setMessageAsAlternative()
379
    {
380 2
        $contentPart = $this->mimePartFactory->newMimePart();
381 2
        $contentPart->attachContentResourceHandle($this->handle);
382 2
        $this->detachContentResourceHandle();
383 2
        $this->removePart($this);
384 2
        $contentType = 'text/plain; charset="us-ascii"';
385 2
        $contentHeader = $this->getHeader('Content-Type');
386 2
        if ($contentHeader !== null) {
387 2
            $contentType = $contentHeader->getRawValue();
388 2
        }
389 2
        $contentPart->setRawHeader('Content-Type', $contentType);
390 2
        $contentPart->setParent($this);
391 2
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/alternative');
392 2
        $this->contentPart = null;
393 2
        $this->addPart($this);
394 2
        $this->addPart($contentPart, 0);
395 2
    }
396
    
397
    /**
398
     * Creates a new mime part as a multipart/alternative, assigning it to
399
     * $this->contentPart.  Adds the current contentPart below the newly created
400
     * alternative part.
401
     */
402 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...
403
    {
404 2
        $altPart = $this->mimePartFactory->newMimePart();
405 2
        $contentPart = $this->contentPart;
406 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
407 2
        $this->removePart($contentPart);
408 2
        $contentPart->setParent($altPart);
409 2
        $this->contentPart = null;
410 2
        $altPart->setParent($this);
411 2
        $this->addPart($altPart, 0);
412 2
        $this->addPart($contentPart, 0);
413 2
    }
414
    
415
    /**
416
     * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding
417
     * headers from the $from header into the $to header. If the Content-Type
418
     * header isn't defined in $from, defaults to text/plain and
419
     * quoted-printable.
420
     * 
421
     * @param \ZBateson\MailMimeParser\Message\MimePart $from
422
     * @param \ZBateson\MailMimeParser\Message\MimePart $to
423
     */
424 11
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
425
    {
426 11
        $typeHeader = $from->getHeader('Content-Type');
427 11
        if ($typeHeader !== null) {
428 11
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
429 11
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
430 11
            if ($encodingHeader !== null) {
431 4
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
432 4
            }
433 11
            $dispositionHeader = $from->getHeader('Content-Disposition');
434 11
            if ($dispositionHeader !== null) {
435 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
436 1
            }
437 11
        } else {
438
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
439
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
440
        }
441 11
    }
442
    
443
    /**
444
     * Creates a new content part from the passed part, allowing the part to be
445
     * used for something else (e.g. changing a non-mime message to a multipart
446
     * mime message).
447
     */
448 4
    private function createNewContentPartFromPart(MimePart $part)
449
    {
450 4
        $contPart = $this->mimePartFactory->newMimePart();
451 4
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
452 4
        $contPart->attachContentResourceHandle($part->handle);
453 4
        $part->detachContentResourceHandle();
454 4
        return $contPart;
455
    }
456
    
457
    /**
458
     * Creates a new part out of the current contentPart and sets the message's
459
     * type to be multipart/mixed.
460
     */
461 4
    private function setMessageAsMixed()
462
    {
463 4
        $part = $this->createNewContentPartFromPart($this->contentPart);
464 4
        $this->removePart($this->contentPart);
465 4
        $this->contentPart = null;
466 4
        $this->addPart($part, 0);
467 4
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
468 4
    }
469
    
470
    /**
471
     * This function makes space by moving the main message part down one level.
472
     * 
473
     * The content-type, content-disposition and content-transfer-encoding
474
     * headers are copied from this message to the newly created part, the 
475
     * resource handle is moved and detached, any attachments and content parts
476
     * with parents set to this message get their parents set to the newly
477
     * created part.
478
     */
479 8
    private function makeSpaceForMultipartSignedMessage()
480
    {
481 8
        $this->enforceMime();
482 8
        $messagePart = $this->mimePartFactory->newMimePart();
483 8
        $messagePart->setParent($this);
484
        
485 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
486 8
        $messagePart->attachContentResourceHandle($this->handle);
487 8
        $this->detachContentResourceHandle();
488
        
489 8
        $this->contentPart = null;
490 8
        $this->addPart($messagePart, 0);
491 8
        foreach ($this->getChildParts() as $part) {
492 8
            if ($part === $messagePart) {
493 8
                continue;
494
            }
495 5
            $this->removePart($part);
496 5
            $part->setParent($messagePart);
497 5
            $this->addPart($part);
498 8
        }
499 8
    }
500
    
501
    /**
502
     * Creates and returns a new MimePart for the signature part of a
503
     * multipart/signed message and assigns it to $this->signedSignaturePart.
504
     * 
505
     * @param string $body
506
     */
507 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...
508
    {
509 8
        $signedPart = $this->signedSignaturePart;
510 8
        if ($signedPart === null) {
511 8
            $signedPart = $this->mimePartFactory->newMimePart();
512 8
            $signedPart->setParent($this);
513 8
            $this->addPart($signedPart);
514 8
            $this->signedSignaturePart = $signedPart;
515 8
        }
516 8
        $signedPart->setRawHeader(
517 8
            'Content-Type',
518 8
            $this->getHeaderParameter('Content-Type', 'protocol')
519 8
        );
520 8
        $signedPart->setContent($body);
521 8
    }
522
523
    /**
524
     * Loops over parts of this message and sets the content-transfer-encoding
525
     * header to quoted-printable for text/* mime parts, and to base64
526
     * otherwise for parts that are '8bit' encoded.
527
     * 
528
     * Used for multipart/signed messages which doesn't support 8bit transfer
529
     * encodings.
530
     */
531 8
    private function overwrite8bitContentEncoding()
532
    {
533 8
        $parts = array_merge([ $this ], $this->getAllParts());
534 8
        foreach ($parts as $part) {
535 8
            if ($part->getHeaderValue('Content-Transfer-Encoding') === '8bit') {
536 1
                if (preg_match('/text\/.*/', $part->getHeaderValue('Content-Type'))) {
537 1
                    $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
538 1
                } else {
539
                    $part->setRawHeader('Content-Transfer-Encoding', 'base64');
540
                }
541 1
            }
542 8
        }
543 8
    }
544
    
545
    /**
546
     * Ensures a non-text part comes first in a signed multipart/alternative
547
     * message as some clients seem to prefer the first content part if the
548
     * client doesn't understand multipart/signed.
549
     */
550 8
    private function ensureHtmlPartFirstForSignedMessage()
551
    {
552 8
        if ($this->contentPart === null) {
553 2
            return;
554
        }
555 6
        $type = strtolower($this->contentPart->getHeaderValue('Content-Type', 'text/plain'));
556 6
        if ($type === 'multipart/alternative' && count($this->contentPart->parts) > 1) {
557 4
            if (strtolower($this->contentPart->parts[0]->getHeaderValue('Content-Type', 'text/plain')) === 'text/plain') {
558 4
                $tmp = $this->contentPart->parts[0];
559 4
                $this->contentPart->parts[0] = $this->contentPart->parts[1];
560 4
                $this->contentPart->parts[1] = $tmp;
561 4
            }
562 4
        }
563 6
    }
564
    
565
    /**
566
     * Turns the message into a multipart/signed message, moving the actual
567
     * message into a child part, sets the content-type of the main message to
568
     * multipart/signed and adds a signature part as well.
569
     * 
570
     * @param string $micalg The Message Integrity Check algorithm being used
571
     * @param string $protocol The mime-type of the signature body
572
     */
573 8
    public function setAsMultipartSigned($micalg, $protocol)
574
    {
575 8
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
576 8
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
577 8
            $this->makeSpaceForMultipartSignedMessage();
578 8
        }
579 8
        $boundary = $this->getUniqueBoundary('multipart/signed');
580 8
        $this->setRawHeader(
581 8
            'Content-Type',
582 8
            "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
583 8
        );
584 8
        $this->removeHeader('Content-Transfer-Encoding');
585 8
        $this->overwrite8bitContentEncoding();
586 8
        $this->ensureHtmlPartFirstForSignedMessage();
587 8
        $this->createSignaturePart('Not set');
588 8
    }
589
    
590
    /**
591
     * Returns the signed part or null if not set.
592
     * 
593
     * @return \ZBateson\MailMimeParser\Message\MimePart
594
     */
595 12
    public function getSignaturePart()
596
    {
597 12
        return $this->signedSignaturePart;
598
    }
599
    
600
    /**
601
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
602
     * or unspecified) message.  If the message has uuencoded attachments, sets
603
     * up the message as a multipart/mixed message and creates a content part.
604
     */
605 12
    private function enforceMime()
606
    {
607 12
        if (!$this->isMime()) {
608 2
            if ($this->getAttachmentCount()) {
609 2
                $this->setMessageAsMixed();
610 2
            } else {
611
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
612
            }
613 2
            $this->setRawHeader('Mime-Version', '1.0');
614 2
        }
615 12
    }
616
    
617
    /**
618
     * Creates a new content part for the passed mimeType and charset, making
619
     * space by creating a multipart/alternative if needed
620
     * 
621
     * @param string $mimeType
622
     * @param string $charset
623
     * @return \ZBateson\MailMimeParser\Message\MimePart
624
     */
625 4
    private function createContentPartForMimeType($mimeType, $charset)
626
    {
627
        // wouldn't come here unless there's only one 'content part' anyway
628
        // if this->contentPart === $this, then $this is not a multipart/alternative
629
        // message
630 4
        $mimePart = $this->mimePartFactory->newMimePart();
631 4
        $cset = ($charset === null) ? 'UTF-8' : $charset;
632 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$cset\"");
633 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
634 4
        $this->enforceMime();
635 4
        if ($this->contentPart === $this) {
636 2
            $this->setMessageAsAlternative();
637 2
            $mimePart->setParent($this->contentPart);
638 2
            $this->addPart($mimePart, 0);
639 4
        } elseif ($this->contentPart !== null) {
640 2
            $this->createAlternativeContentPart();
641 2
            $mimePart->setParent($this->contentPart);
642 2
            $this->addPart($mimePart, 0);
643 2
        } else {
644 1
            $mimePart->setParent($this);
645 1
            $this->addPart($mimePart, 0);
646
        }
647 4
        return $mimePart;
648
    }
649
    
650
    /**
651
     * Either creates a mime part or sets the existing mime part with the passed
652
     * mimeType to $strongOrHandle.
653
     * 
654
     * @param string $mimeType
655
     * @param string|resource $stringOrHandle
656
     * @param string $charset
657
     */
658 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
659
    {
660 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
661 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
662 4
        if ($part === null) {
663 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
664 4
        } elseif ($charset !== null) {
665
            $cset = ($charset === null) ? 'UTF-8' : $charset;
666
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
667
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$cset\"");
668
        }
669 4
        $part->attachContentResourceHandle($handle);
670 4
    }
671
    
672
    /**
673
     * Sets the text/plain part of the message to the passed $stringOrHandle,
674
     * either creating a new part if one doesn't exist for text/plain, or
675
     * assigning the value of $stringOrHandle to an existing text/plain part.
676
     * 
677
     * The optional $charset parameter is the charset for saving to.
678
     * $stringOrHandle is expected to be in UTF-8.
679
     * 
680
     * @param string|resource $stringOrHandle
681
     * @param string $charset
682
     */
683 1
    public function setTextPart($stringOrHandle, $charset = null)
684
    {
685 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
686 1
    }
687
    
688
    /**
689
     * Sets the text/html part of the message to the passed $stringOrHandle,
690
     * either creating a new part if one doesn't exist for text/html, or
691
     * assigning the value of $stringOrHandle to an existing text/html part.
692
     * 
693
     * The optional $charset parameter is the charset for saving to.
694
     * $stringOrHandle is expected to be in UTF-8.
695
     * 
696
     * @param string|resource $stringOrHandle
697
     * @param string $charset
698
     */
699 4
    public function setHtmlPart($stringOrHandle, $charset = null)
700
    {
701 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
702 4
    }
703
    
704
    /**
705
     * Removes the text part of the message if one exists.  Returns true on
706
     * success.
707
     * 
708
     * @return bool true on success
709
     */
710 3
    public function removeTextPart()
711
    {
712 3
        return $this->removeContentPart('text/plain');
713
    }
714
    
715
    /**
716
     * Removes the html part of the message if one exists.  Returns true on
717
     * success.
718
     * 
719
     * @return bool true on success
720
     */
721 2
    public function removeHtmlPart()
722
    {
723 2
        return $this->removeContentPart('text/html');
724
    }
725
    
726
    /**
727
     * Returns the non-content part at the given 0-based index, or null if none
728
     * is set.
729
     * 
730
     * @param int $index
731
     * @return \ZBateson\MailMimeParser\Message\MimePart
732
     */
733 7
    public function getAttachmentPart($index)
734
    {
735 7
        if (!isset($this->attachmentParts[$index])) {
736 2
            return null;
737
        }
738 5
        return $this->attachmentParts[$index];
739
    }
740
    
741
    /**
742
     * Returns all attachment parts.
743
     * 
744
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
745
     */
746 48
    public function getAllAttachmentParts()
747
    {
748 48
        return $this->attachmentParts;
749
    }
750
    
751
    /**
752
     * Returns the number of attachments available.
753
     * 
754
     * @return int
755
     */
756 49
    public function getAttachmentCount()
757
    {
758 49
        return count($this->attachmentParts);
759
    }
760
    
761
    /**
762
     * Removes the attachment with the given index
763
     * 
764
     * @param int $index
765
     */
766 2
    public function removeAttachmentPart($index)
767
    {
768 2
        $part = $this->attachmentParts[$index];
769 2
        $this->removePart($part);
770 2
        array_splice($this->attachmentParts, $index, 1);
771 2
    }
772
    
773
    /**
774
     * Creates and returns a MimePart for use with a new attachment part being
775
     * created.
776
     * 
777
     * @return \ZBateson\MailMimeParser\Message\MimePart
778
     */
779 2
    protected function createPartForAttachment()
780
    {
781 2
        if ($this->isMime()) {
782 2
            $part = $this->mimePartFactory->newMimePart();
783 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
784 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
785 2
                $this->setMessageAsMixed();
786 2
            }
787 2
            return $part;
788
        }
789
        return $this->mimePartFactory->newUUEncodedPart();
790
    }
791
    
792
    /**
793
     * Adds an attachment part for the passed raw data string or handle and
794
     * given parameters.
795
     * 
796
     * @param string|handle $stringOrHandle
797
     * @param strubg $mimeType
798
     * @param string $filename
799
     * @param string $disposition
800
     */
801 1
    public function addAttachmentPart($stringOrHandle, $mimeType, $filename = null, $disposition = 'attachment')
802
    {
803 1
        if ($filename === null) {
804
            $filename = 'file' . uniqid();
805
        }
806 1
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
807 1
        $part = $this->createPartForAttachment();
808 1
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
809 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
810 1
        $part->setParent($this);
811 1
        $part->attachContentResourceHandle($this->getHandleForStringOrHandle($stringOrHandle));
812 1
        $this->addPart($part);
813 1
    }
814
    
815
    /**
816
     * Adds an attachment part using the passed file.
817
     * 
818
     * Essentially creates a file stream and uses it.
819
     * 
820
     * @param string $file
821
     * @param string $mimeType
822
     * @param string $filename
823
     * @param string $disposition
824
     */
825 2
    public function addAttachmentPartFromFile($file, $mimeType, $filename = null, $disposition = 'attachment')
826
    {
827 2
        $handle = fopen($file, 'r');
828 2
        if ($filename === null) {
829 2
            $filename = basename($file);
830 2
        }
831 2
        $filename = iconv('UTF-8', 'US-ASCII//translit//ignore', $filename);
832 2
        $part = $this->createPartForAttachment();
833 2
        $part->setRawHeader('Content-Type', "$mimeType;\r\n\tname=\"$filename\"");
834 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
835 2
        $part->setParent($this);
836 2
        $part->attachContentResourceHandle($handle);
837 2
        $this->addPart($part);
838 2
    }
839
    
840
    /**
841
     * Returns a resource handle where the text content can be read or null if
842
     * unavailable.
843
     * 
844
     * @return resource
845
     */
846 61
    public function getTextStream()
847
    {
848 61
        $textPart = $this->getTextPart();
849 61
        if ($textPart !== null) {
850 60
            return $textPart->getContentResourceHandle();
851
        }
852 1
        return null;
853
    }
854
    
855
    /**
856
     * Returns the text content as a string.
857
     * 
858
     * Reads the entire stream content into a string and returns it.  Returns
859
     * null if the message doesn't have a text part.
860
     * 
861
     * @return string
862
     */
863 1
    public function getTextContent()
864
    {
865 1
        $stream = $this->getTextStream();
866 1
        if ($stream === null) {
867
            return null;
868
        }
869 1
        return stream_get_contents($stream);
870
    }
871
    
872
    /**
873
     * Returns a resource handle where the HTML content can be read or null if
874
     * unavailable.
875
     * 
876
     * @return resource
877
     */
878 31
    public function getHtmlStream()
879
    {
880 31
        $htmlPart = $this->getHtmlPart();
881 31
        if ($htmlPart !== null) {
882 30
            return $htmlPart->getContentResourceHandle();
883
        }
884 1
        return null;
885
    }
886
    
887
    /**
888
     * Returns the HTML content as a string.
889
     * 
890
     * Reads the entire stream content into a string and returns it.  Returns
891
     * null if the message doesn't have an HTML part.
892
     * 
893
     * @return string
894
     */
895
    public function getHtmlContent()
896
    {
897
        $stream = $this->getHtmlStream();
898
        if ($stream === null) {
899
            return null;
900
        }
901
        return stream_get_contents($stream);
902
    }
903
    
904
    /**
905
     * Returns true if either a Content-Type or Mime-Version header are defined
906
     * in this Message.
907
     * 
908
     * @return bool
909
     */
910 86
    public function isMime()
911
    {
912 86
        $contentType = $this->getHeaderValue('Content-Type');
913 86
        $mimeVersion = $this->getHeaderValue('Mime-Version');
914 86
        return ($contentType !== null || $mimeVersion !== null);
915
    }
916
    
917
    /**
918
     * Saves the message as a MIME message to the passed resource handle.
919
     * 
920
     * @param resource $handle
921
     */
922 82
    public function save($handle)
923
    {
924 82
        $this->messageWriter->writeMessageTo($this, $handle);
925 82
    }
926
    
927
    /**
928
     * Returns the content part of a signed message for a signature to be
929
     * calculated on the message.
930
     * 
931
     * @return string
932
     */
933 8
    public function getSignableBody()
934
    {
935 8
        return $this->messageWriter->getSignableBody($this);
936
    }
937
    
938
    /**
939
     * Shortcut to call Message::save with a php://temp stream and return the
940
     * written email message as a string.
941
     * 
942
     * @return string
943
     */
944
    public function __toString()
945
    {
946
        $handle = fopen('php://temp', 'r+');
947
        $this->save($handle);
948
        rewind($handle);
949
        $str = stream_get_contents($handle);
950
        fclose($handle);
951
        return $str;
952
    }
953
}
954