Completed
Push — master ( ebecd2...4b8afe )
by Zaahid
03:22
created

Message::createPartForAttachment()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 8
cts 9
cp 0.8889
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 0
crap 3.0123
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
        $contentPart = $this->contentPart->getPart(0);
228 3
        if ($contentPart !== null) {
229 3
            if ($this->contentPart === $this) {
230 2
                $this->overrideAlternativeMessageContentFromContentPart($contentPart);
231 3
            } elseif ($this->contentPart->getPartCount() === 1) {
232 1
                $this->removePart($this->contentPart);
233 1
                $contentPart->setParent($this);
234 1
                $this->contentPart = null;
235 1
                $this->addPart($contentPart, 0);
236 1
            }
237 3
        }
238 3
    }
239
    
240
    /**
241
     * Loops over children of the content part looking for a part with the
242
     * passed mime type, then proceeds to remove it by calling
243
     * removePartFromAlternativeContentPart.
244
     * 
245
     * @param string $contentType
246
     * @return boolean true on success
247
     */
248 3
    private function removeContentPartFromAlternative($contentType)
249
    {
250 3
        $parts = $this->contentPart->getAllParts();
251 3
        foreach ($parts as $part) {
252 3
            $type = $part->getHeaderValue('Content-Type', 'text/plain');
253 3
            if (strcasecmp($type, $contentType) === 0) {
254 3
                $this->removePartFromAlternativeContentPart($part);
255 3
                return true;
256
            }
257 2
        }
258
        return false;
259
    }
260
    
261
    /**
262
     * Removes the content part of the message with the passed mime type.  If
263
     * there is a remaining content part and it is an alternative part of the
264
     * main message, the content part is moved to the message part.
265
     * 
266
     * If the content part is part of an alternative part beneath the message,
267
     * the alternative part is replaced by the remaining content part.
268
     * 
269
     * @param string $contentType
270
     * @return boolean true on success
271
     */
272 3
    protected function removeContentPart($contentType)
273
    {
274 3
        if (!isset($this->contentPart)) {
275
            return false;
276
        }
277 3
        $type = $this->contentPart->getHeaderValue('Content-Type', 'text/plain');
278 3
        if (strcasecmp($type, $contentType) === 0) {
279
            if ($this->contentPart === $this) {
280
                return false;
281
            }
282
            $this->removePart($this->contentPart);
283
            $this->contentPart = null;
284
            return true;
285
        }
286 3
        return $this->removeContentPartFromAlternative($contentType);
287
    }
288
    
289
    /**
290
     * Returns the text part (or null if none is set.)
291
     * 
292
     * @return \ZBateson\MailMimeParser\Message\MimePart
293
     */
294 64
    public function getTextPart()
295
    {
296 64
        return $this->getContentPartByMimeType('text/plain');
297
    }
298
    
299
    /**
300
     * Returns the HTML part (or null if none is set.)
301
     * 
302
     * @return \ZBateson\MailMimeParser\Message\MimePart
303
     */
304 36
    public function getHtmlPart()
305
    {
306 36
        return $this->getContentPartByMimeType('text/html');
307
    }
308
    
309
    /**
310
     * Returns the content MimePart, which could be a text/plain, text/html or
311
     * multipart/alternative part or null if none is set.
312
     * 
313
     * @return \ZBateson\MailMimeParser\Message\MimePart
314
     */
315 1
    public function getContentPart()
316
    {
317 1
        return $this->contentPart;
318
    }
319
    
320
    /**
321
     * Returns an open resource handle for the passed string or resource handle.
322
     * 
323
     * For a string, creates a php://temp stream and returns it.
324
     * 
325
     * @param resource|string $stringOrHandle
326
     * @return resource
327
     */
328 5
    private function getHandleForStringOrHandle($stringOrHandle)
329
    {
330 5
        $tempHandle = fopen('php://temp', 'r+');
331 5
        if (is_string($stringOrHandle)) {
332 5
            fwrite($tempHandle, $stringOrHandle);
333 5
        } else {
334
            stream_copy_to_stream($stringOrHandle, $tempHandle);
335
        }
336 5
        rewind($tempHandle);
337 5
        return $tempHandle;
338
    }
339
    
340
    /**
341
     * Creates and returns a unique boundary.
342
     * 
343
     * @param string $mimeType first 3 characters of a multipart type are used,
344
     *      e.g. REL for relative or ALT for alternative
345
     * @return string
346
     */
347 14
    private function getUniqueBoundary($mimeType)
348
    {
349 14
        $type = ltrim(strtoupper(preg_replace('/^(multipart\/(.{3}).*|.*)$/i', '$2-', $mimeType)), '-');
350 14
        return uniqid('----=MMP-' . $type . $this->objectId . '.', true);
351
    }
352
    
353
    /**
354
     * Creates a unique mime boundary and assigns it to the passed part's
355
     * Content-Type header with the passed mime type.
356
     * 
357
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
358
     * @param string $mimeType
359
     */
360 7
    private function setMimeHeaderBoundaryOnPart(MimePart $part, $mimeType)
361
    {
362 7
        $part->setRawHeader(
363 7
            'Content-Type',
364 7
            "$mimeType;\r\n\tboundary=\"" 
365 7
                . $this->getUniqueBoundary($mimeType) . '"'
366 7
        );
367 7
    }
368
    
369
    /**
370
     * Sets this message to be a multipart/alternative message, making space for
371
     * another alternative content part.
372
     * 
373
     * Creates a content part and assigns the content stream from the message to
374
     * that newly created part.
375
     */
376 2
    private function setMessageAsAlternative()
377
    {
378 2
        $contentPart = $this->mimePartFactory->newMimePart();
379 2
        $contentPart->attachContentResourceHandle($this->handle);
380 2
        $this->detachContentResourceHandle();
381 2
        $this->removePart($this);
382 2
        $contentType = 'text/plain; charset="us-ascii"';
383 2
        $contentHeader = $this->getHeader('Content-Type');
384 2
        if ($contentHeader !== null) {
385 2
            $contentType = $contentHeader->getRawValue();
386 2
        }
387 2
        $contentPart->setRawHeader('Content-Type', $contentType);
388 2
        $contentPart->setParent($this);
389 2
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/alternative');
390 2
        $this->contentPart = null;
391 2
        $this->addPart($this);
392 2
        $this->addPart($contentPart, 0);
393 2
    }
394
    
395
    /**
396
     * Creates a new mime part as a multipart/alternative, assigning it to
397
     * $this->contentPart.  Adds the current contentPart below the newly created
398
     * alternative part.
399
     */
400 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...
401
    {
402 2
        $altPart = $this->mimePartFactory->newMimePart();
403 2
        $contentPart = $this->contentPart;
404 2
        $this->setMimeHeaderBoundaryOnPart($altPart, 'multipart/alternative');
405 2
        $this->removePart($contentPart);
406 2
        $contentPart->setParent($altPart);
407 2
        $this->contentPart = null;
408 2
        $altPart->setParent($this);
409 2
        $this->addPart($altPart, 0);
410 2
        $this->addPart($contentPart, 0);
411 2
    }
412
    
413
    /**
414
     * Copies Content-Type, Content-Disposition and Content-Transfer-Encoding
415
     * headers from the $from header into the $to header. If the Content-Type
416
     * header isn't defined in $from, defaults to text/plain and
417
     * quoted-printable.
418
     * 
419
     * @param \ZBateson\MailMimeParser\Message\MimePart $from
420
     * @param \ZBateson\MailMimeParser\Message\MimePart $to
421
     */
422 11
    private function copyTypeHeadersFromPartToPart(MimePart $from, MimePart $to)
423
    {
424 11
        $typeHeader = $from->getHeader('Content-Type');
425 11
        if ($typeHeader !== null) {
426 11
            $to->setRawHeader('Content-Type', $typeHeader->getRawValue());
427 11
            $encodingHeader = $from->getHeader('Content-Transfer-Encoding');
428 11
            if ($encodingHeader !== null) {
429 4
                $to->setRawHeader('Content-Transfer-Encoding', $encodingHeader->getRawValue());
430 4
            }
431 11
            $dispositionHeader = $from->getHeader('Content-Disposition');
432 11
            if ($dispositionHeader !== null) {
433 1
                $to->setRawHeader('Content-Disposition', $dispositionHeader->getRawValue());
434 1
            }
435 11
        } else {
436
            $to->setRawHeader('Content-Type', 'text/plain;charset=us-ascii');
437
            $to->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
438
        }
439 11
    }
440
    
441
    /**
442
     * Creates a new content part from the passed part, allowing the part to be
443
     * used for something else (e.g. changing a non-mime message to a multipart
444
     * mime message).
445
     */
446 4
    private function createNewContentPartFromPart(MimePart $part)
447
    {
448 4
        $contPart = $this->mimePartFactory->newMimePart();
449 4
        $this->copyTypeHeadersFromPartToPart($part, $contPart);
450 4
        $contPart->attachContentResourceHandle($part->handle);
451 4
        $part->detachContentResourceHandle();
452 4
        return $contPart;
453
    }
454
    
455
    /**
456
     * Creates a new part out of the current contentPart and sets the message's
457
     * type to be multipart/mixed.
458
     */
459 4
    private function setMessageAsMixed()
460
    {
461 4
        $part = $this->createNewContentPartFromPart($this->contentPart);
462 4
        $this->removePart($this->contentPart);
463 4
        $this->contentPart = null;
464 4
        $this->addPart($part, 0);
465 4
        $this->setMimeHeaderBoundaryOnPart($this, 'multipart/mixed');
466 4
    }
467
    
468
    /**
469
     * This function makes space by moving the main message part down one level.
470
     * 
471
     * The content-type, content-disposition and content-transfer-encoding
472
     * headers are copied from this message to the newly created part, the 
473
     * resource handle is moved and detached, any attachments and content parts
474
     * with parents set to this message get their parents set to the newly
475
     * created part.
476
     */
477 8
    private function makeSpaceForMultipartSignedMessage()
478
    {
479 8
        $this->enforceMime();
480 8
        $messagePart = $this->mimePartFactory->newMimePart();
481 8
        $messagePart->setParent($this);
482
        
483 8
        $this->copyTypeHeadersFromPartToPart($this, $messagePart);
484 8
        $messagePart->attachContentResourceHandle($this->handle);
485 8
        $this->detachContentResourceHandle();
486
        
487 8
        $this->contentPart = null;
488 8
        $this->addPart($messagePart, 0);
489 8
        foreach ($this->getChildParts() as $part) {
490 8
            if ($part === $messagePart) {
491 8
                continue;
492
            }
493 5
            $this->removePart($part);
494 5
            $part->setParent($messagePart);
495 5
            $this->addPart($part);
496 8
        }
497 8
    }
498
    
499
    /**
500
     * Creates and returns a new MimePart for the signature part of a
501
     * multipart/signed message and assigns it to $this->signedSignaturePart.
502
     * 
503
     * @param string $body
504
     */
505 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...
506
    {
507 8
        $signedPart = $this->signedSignaturePart;
508 8
        if ($signedPart === null) {
509 8
            $signedPart = $this->mimePartFactory->newMimePart();
510 8
            $signedPart->setParent($this);
511 8
            $this->addPart($signedPart);
512 8
            $this->signedSignaturePart = $signedPart;
513 8
        }
514 8
        $signedPart->setRawHeader(
515 8
            'Content-Type',
516 8
            $this->getHeaderParameter('Content-Type', 'protocol')
517 8
        );
518 8
        $signedPart->setContent($body);
519 8
    }
520
521
    /**
522
     * Loops over parts of this message and sets the content-transfer-encoding
523
     * header to quoted-printable for text/* mime parts, and to base64
524
     * otherwise for parts that are '8bit' encoded.
525
     * 
526
     * Used for multipart/signed messages which doesn't support 8bit transfer
527
     * encodings.
528
     */
529 8
    private function overwrite8bitContentEncoding()
530
    {
531 8
        $parts = array_merge([ $this ], $this->getAllParts());
532 8
        foreach ($parts as $part) {
533 8
            if ($part->getHeaderValue('Content-Transfer-Encoding') === '8bit') {
534 1
                if (preg_match('/text\/.*/', $part->getHeaderValue('Content-Type'))) {
535 1
                    $part->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
536 1
                } else {
537
                    $part->setRawHeader('Content-Transfer-Encoding', 'base64');
538
                }
539 1
            }
540 8
        }
541 8
    }
542
    
543
    /**
544
     * Ensures a non-text part comes first in a signed multipart/alternative
545
     * message as some clients seem to prefer the first content part if the
546
     * client doesn't understand multipart/signed.
547
     */
548 8
    private function ensureHtmlPartFirstForSignedMessage()
549
    {
550 8
        if ($this->contentPart === null) {
551 2
            return;
552
        }
553 6
        $type = strtolower($this->contentPart->getHeaderValue('Content-Type', 'text/plain'));
554 6
        if ($type === 'multipart/alternative' && count($this->contentPart->parts) > 1) {
555 4
            if (strtolower($this->contentPart->parts[0]->getHeaderValue('Content-Type', 'text/plain')) === 'text/plain') {
556 4
                $tmp = $this->contentPart->parts[0];
557 4
                $this->contentPart->parts[0] = $this->contentPart->parts[1];
558 4
                $this->contentPart->parts[1] = $tmp;
559 4
            }
560 4
        }
561 6
    }
562
    
563
    /**
564
     * Turns the message into a multipart/signed message, moving the actual
565
     * message into a child part, sets the content-type of the main message to
566
     * multipart/signed and adds a signature part as well.
567
     * 
568
     * @param string $micalg The Message Integrity Check algorithm being used
569
     * @param string $protocol The mime-type of the signature body
570
     */
571 8
    public function setAsMultipartSigned($micalg, $protocol)
572
    {
573 8
        $contentType = $this->getHeaderValue('Content-Type', 'text/plain');
574 8
        if (strcasecmp($contentType, 'multipart/signed') !== 0) {
575 8
            $this->makeSpaceForMultipartSignedMessage();
576 8
        }
577 8
        $boundary = $this->getUniqueBoundary('multipart/signed');
578 8
        $this->setRawHeader(
579 8
            'Content-Type',
580 8
            "multipart/signed;\r\n\tboundary=\"$boundary\";\r\n\tmicalg=\"$micalg\"; protocol=\"$protocol\""
581 8
        );
582 8
        $this->removeHeader('Content-Transfer-Encoding');
583 8
        $this->overwrite8bitContentEncoding();
584 8
        $this->ensureHtmlPartFirstForSignedMessage();
585 8
        $this->createSignaturePart('Not set');
586 8
    }
587
    
588
    /**
589
     * Returns the signed part or null if not set.
590
     * 
591
     * @return \ZBateson\MailMimeParser\Message\MimePart
592
     */
593 12
    public function getSignaturePart()
594
    {
595 12
        return $this->signedSignaturePart;
596
    }
597
    
598
    /**
599
     * Enforces the message to be a mime message for a non-mime (e.g. uuencoded
600
     * or unspecified) message.  If the message has uuencoded attachments, sets
601
     * up the message as a multipart/mixed message and creates a content part.
602
     */
603 12
    private function enforceMime()
604
    {
605 12
        if (!$this->isMime()) {
606 2
            if ($this->getAttachmentCount()) {
607 2
                $this->setMessageAsMixed();
608 2
            } else {
609
                $this->setRawHeader('Content-Type', "text/plain;\r\n\tcharset=\"us-ascii\"");
610
            }
611 2
            $this->setRawHeader('Mime-Version', '1.0');
612 2
        }
613 12
    }
614
    
615
    /**
616
     * Creates a new content part for the passed mimeType and charset, making
617
     * space by creating a multipart/alternative if needed
618
     * 
619
     * @param string $mimeType
620
     * @param string $charset
621
     * @return \ZBateson\MailMimeParser\Message\MimePart
622
     */
623 4
    private function createContentPartForMimeType($mimeType, $charset)
624
    {
625
        // wouldn't come here unless there's only one 'content part' anyway
626
        // if this->contentPart === $this, then $this is not a multipart/alternative
627
        // message
628 4
        $mimePart = $this->mimePartFactory->newMimePart();
629 4
        $cset = ($charset === null) ? 'UTF-8' : $charset;
630 4
        $mimePart->setRawHeader('Content-Type', "$mimeType;\r\n\tcharset=\"$cset\"");
631 4
        $mimePart->setRawHeader('Content-Transfer-Encoding', 'quoted-printable');
632 4
        $this->enforceMime();
633 4
        if ($this->contentPart === $this) {
634 2
            $this->setMessageAsAlternative();
635 2
            $mimePart->setParent($this->contentPart);
636 2
            $this->addPart($mimePart, 0);
637 4
        } elseif ($this->contentPart !== null) {
638 2
            $this->createAlternativeContentPart();
639 2
            $mimePart->setParent($this->contentPart);
640 2
            $this->addPart($mimePart, 0);
641 2
        } else {
642 1
            $mimePart->setParent($this);
643 1
            $this->addPart($mimePart, 0);
644
        }
645 4
        return $mimePart;
646
    }
647
    
648
    /**
649
     * Either creates a mime part or sets the existing mime part with the passed
650
     * mimeType to $strongOrHandle.
651
     * 
652
     * @param string $mimeType
653
     * @param string|resource $stringOrHandle
654
     * @param string $charset
655
     */
656 4
    protected function setContentPartForMimeType($mimeType, $stringOrHandle, $charset)
657
    {
658 4
        $part = ($mimeType === 'text/html') ? $this->getHtmlPart() : $this->getTextPart();
659 4
        $handle = $this->getHandleForStringOrHandle($stringOrHandle);
660 4
        if ($part === null) {
661 4
            $part = $this->createContentPartForMimeType($mimeType, $charset);
662 4
        } elseif ($charset !== null) {
663
            $cset = ($charset === null) ? 'UTF-8' : $charset;
664
            $contentType = $part->getHeaderValue('Content-Type', 'text/plain');
665
            $part->setRawHeader('Content-Type', "$contentType;\r\n\tcharset=\"$cset\"");
666
        }
667 4
        $part->attachContentResourceHandle($handle);
668 4
    }
669
    
670
    /**
671
     * Sets the text/plain part of the message to the passed $stringOrHandle,
672
     * either creating a new part if one doesn't exist for text/plain, or
673
     * assigning the value of $stringOrHandle to an existing text/plain part.
674
     * 
675
     * The optional $charset parameter is the charset for saving to.
676
     * $stringOrHandle is expected to be in UTF-8.
677
     * 
678
     * @param string|resource $stringOrHandle
679
     * @param string $charset
680
     */
681 1
    public function setTextPart($stringOrHandle, $charset = null)
682
    {
683 1
        $this->setContentPartForMimeType('text/plain', $stringOrHandle, $charset);
684 1
    }
685
    
686
    /**
687
     * Sets the text/html part of the message to the passed $stringOrHandle,
688
     * either creating a new part if one doesn't exist for text/html, or
689
     * assigning the value of $stringOrHandle to an existing text/html part.
690
     * 
691
     * The optional $charset parameter is the charset for saving to.
692
     * $stringOrHandle is expected to be in UTF-8.
693
     * 
694
     * @param string|resource $stringOrHandle
695
     * @param string $charset
696
     */
697 4
    public function setHtmlPart($stringOrHandle, $charset = null)
698
    {
699 4
        $this->setContentPartForMimeType('text/html', $stringOrHandle, $charset);
700 4
    }
701
    
702
    /**
703
     * Removes the text part of the message if one exists.  Returns true on
704
     * success.
705
     * 
706
     * @return bool true on success
707
     */
708 2
    public function removeTextPart()
709
    {
710 2
        return $this->removeContentPart('text/plain');
711
    }
712
    
713
    /**
714
     * Removes the html part of the message if one exists.  Returns true on
715
     * success.
716
     * 
717
     * @return bool true on success
718
     */
719 1
    public function removeHtmlPart()
720
    {
721 1
        return $this->removeContentPart('text/html');
722
    }
723
    
724
    /**
725
     * Returns the non-content part at the given 0-based index, or null if none
726
     * is set.
727
     * 
728
     * @param int $index
729
     * @return \ZBateson\MailMimeParser\Message\MimePart
730
     */
731 7
    public function getAttachmentPart($index)
732
    {
733 7
        if (!isset($this->attachmentParts[$index])) {
734 2
            return null;
735
        }
736 5
        return $this->attachmentParts[$index];
737
    }
738
    
739
    /**
740
     * Returns all attachment parts.
741
     * 
742
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
743
     */
744 47
    public function getAllAttachmentParts()
745
    {
746 47
        return $this->attachmentParts;
747
    }
748
    
749
    /**
750
     * Returns the number of attachments available.
751
     * 
752
     * @return int
753
     */
754 48
    public function getAttachmentCount()
755
    {
756 48
        return count($this->attachmentParts);
757
    }
758
    
759
    /**
760
     * Removes the attachment with the given index
761
     * 
762
     * @param int $index
763
     */
764 2
    public function removeAttachmentPart($index)
765
    {
766 2
        $part = $this->attachmentParts[$index];
767 2
        $this->removePart($part);
768 2
        array_splice($this->attachmentParts, $index, 1);
769 2
    }
770
    
771
    /**
772
     * Creates and returns a MimePart for use with a new attachment part being
773
     * created.
774
     * 
775
     * @return \ZBateson\MailMimeParser\Message\MimePart
776
     */
777 2
    protected function createPartForAttachment()
778
    {
779 2
        if ($this->isMime()) {
780 2
            $part = $this->mimePartFactory->newMimePart();
781 2
            $part->setRawHeader('Content-Transfer-Encoding', 'base64');
782 2
            if ($this->getHeaderValue('Content-Type') !== 'multipart/mixed') {
783 2
                $this->setMessageAsMixed();
784 2
            }
785 2
            return $part;
786
        }
787
        return $this->mimePartFactory->newUUEncodedPart();
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\"");
807 1
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
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\"");
832 2
        $part->setRawHeader('Content-Disposition', "$disposition;\r\n\tfilename=\"$filename\"");
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