Test Failed
Branch 1.0.0 (84f469)
by Zaahid
05:36
created

MessageParser::attachStreamHandles()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 19
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 15
cts 15
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 14
nc 2
nop 5
crap 4
1
<?php
2
/**
3
 * This file is part of the ZBateson\MailMimeParser project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
namespace ZBateson\MailMimeParser\Message;
8
9
use ZBateson\MailMimeParser\Stream\PartStreamRegistry;
10
use ZBateson\MailMimeParser\Message\Part\PartBuilder;
11
use ZBateson\MailMimeParser\Message\Part\PartBuilderFactory;
12
use ZBateson\MailMimeParser\Message\Part\PartFactoryService;
13
14
/**
15
 * Parses a mail mime message into its component parts.  To invoke, call
16
 * MailMimeParser::parse.
17
 *
18
 * @author Zaahid Bateson
19
 */
20
class MessageParser
21
{
22
    /**
23
     * @var \ZBateson\MailMimeParser\Message\Part\PartFactoryService service
24
     * instance used to create MimePartFactory objects.
25
     */
26
    protected $partFactoryService;
27
    
28
    /**
29
     * @var \ZBateson\MailMimeParser\Message\Part\PartBuilderFactory used to
30
     *      create PartBuilders
31
     */
32
    protected $partBuilderFactory;
33
    
34
    /**
35
     * @var \ZBateson\MailMimeParser\Stream\PartStreamRegistry used for
36
     *      registering message part streams.
37
     */
38
    protected $partStreamRegistry;
39
    
40
    /**
41
     * Sets up the parser with its dependencies.
42
     * 
43
     * @param \ZBateson\MailMimeParser\Message\Part\PartFactoryService $pfs
44
     * @param \ZBateson\MailMimeParser\Message\Part\PartBuilderFactory $pbf
45
     * @param PartStreamRegistry $psr
46
     */
47
    public function __construct(
48
        PartFactoryService $pfs,
49
        PartBuilderFactory $pbf,
50
        PartStreamRegistry $psr
51
    ) {
52
        $this->partFactoryService = $pfs;
53
        $this->partBuilderFactory = $pbf;
54
        $this->partStreamRegistry = $psr;
55
    }
56
    
57
    /**
58
     * Parses the passed stream handle into a ZBateson\MailMimeParser\Message
59
     * object and returns it.
60
     * 
61
     * @param resource $fhandle the resource handle to the input stream of the
62
     *        mime message
63
     * @return \ZBateson\MailMimeParser\Message
64
     */
65
    public function parse($fhandle)
66
    {
67
        $messageObjectId = uniqid();
68
        $this->partStreamRegistry->register($messageObjectId, $fhandle);
69
        $partBuilder = $this->read($fhandle);
70
        return $partBuilder->createMessagePart($messageObjectId);
71
    }
72
    
73
    /**
74
     * Ensures the header isn't empty and contains a colon separator character,
75
     * then splits it and calls $partBuilder->addHeader.
76
     * 
77
     * @param string $header
78
     * @param PartBuilder $partBuilder
79
     */
80
    private function addRawHeaderToPart($header, PartBuilder $partBuilder)
81
    {
82
        if ($header !== '' && strpos($header, ':') !== false) {
83
            $a = explode(':', $header, 2);
84
            $partBuilder->addHeader($a[0], trim($a[1]));
85
        }
86
    }
87
    
88
    /**
89
     * Reads header lines up to an empty line, adding them to the passed
90
     * $partBuilder.
91
     * 
92
     * @param resource $handle the resource handle to read from
93
     * @param PartBuilder $partBuilder the current part to add headers to
94
     */
95
    protected function readHeaders($handle, PartBuilder $partBuilder)
96
    {
97
        $header = '';
98
        do {
99
            $line = fgets($handle, 1000);
100
            if (empty($line) || $line[0] !== "\t" && $line[0] !== ' ') {
101
                $this->addRawHeaderToPart($header, $partBuilder);
102
                $header = '';
103
            } else {
104
                $line = "\r\n" . $line;
105
            }
106
            $header .= rtrim($line, "\r\n");
107
        } while ($header !== '');
108
    }
109
    
110
    /**
111
     * Reads lines from the passed $handle, calling $partBuilder->setEndBoundary
112
     * with the passed line until either setEndBoundary returns true or there
113
     * are no more lines to be read.
114
     * 
115
     * setEndBoundary returns true if the passed line matches a boundary for the
116
     * $partBuilder itself or any of its parents.
117
     * 
118
     * As lines are read, setStreamPartAndContentEndPos is called with the
119
     * passed $handle's read pos (ftell($handle)) to update the position of
120
     * content in the part.
121
     * 
122
     * If the entire stream is read and an end boundary was found, i.e.
123
     * $partBuilder->setEndBoundary returns true, true is returned to indicate
124
     * that a content boundary was found.  Otherwise false is returned.
125
     * 
126
     * @param resource $handle
127
     * @param PartBuilder $partBuilder
128
     * @return boolean true if a mime content boundary was found for
129
     *         $partBuilder
130
     */
131
    private function findContentBoundary($handle, PartBuilder $partBuilder)
132
    {
133
        while (!feof($handle)) {
134
            $partBuilder->setStreamPartAndContentEndPos(ftell($handle));
135
            $line = fgets($handle);
136
            $test = rtrim($line);
137
            if ($partBuilder->setEndBoundary($test)) {
138
                return true;
139
            }
140
        }
141
        $partBuilder->setStreamPartAndContentEndPos(ftell($handle));
142
        return false;
143
    }
144
    
145
    /**
146
     * Reads content for a non-mime message.  If there are uuencoded attachment
147
     * parts in the message (denoted by 'begin' lines), those parts are read and
148
     * added to the passed $partBuilder as children.
149
     * 
150
     * @param resource $handle
151
     * @param PartBuilder $partBuilder
152
     * @return string
153
     */
154
    protected function readUUEncodedOrPlainTextMessage($handle, PartBuilder $partBuilder)
155
    {
156
        $partBuilder->setStreamContentStartPos(ftell($handle));
157
        $part = $partBuilder;
158
        while (!feof($handle)) {
159
            $start = ftell($handle);
160
            $line = trim(fgets($handle));
161
            if (preg_match('/^begin ([0-7]{3}) (.*)$/', $line, $matches)) {
162
                $part = $this->partBuilderFactory->newPartBuilder(
163
                    $this->partFactoryService->getUUEncodedPartFactory()
164
                );
165
                $part->setStreamPartStartPos($start);
166
                // 'begin' line is part of the content
167
                $part->setStreamContentStartPos($start);
168
                $part->setProperty('mode', $matches[1]);
169
                $part->setProperty('filename', $matches[2]);
170
                $partBuilder->addChild($part);
171
            }
172
            $part->setStreamPartAndContentEndPos(ftell($handle));
173
        }
174
        $partBuilder->setStreamPartEndPos(ftell($handle));
175
    }
176
    
177
    /**
178
     * Reads content for a single part of a MIME message.
179
     * 
180
     * If the part being read is in turn a multipart part, readPart is called on
181
     * it recursively to read its headers and content.
182
     * 
183
     * The method tries to read content until a mime boundary (for this part for
184
     * a multipart, or for the parent boundary) is found or EOF is reached.
185
     * 
186
     * The start/end positions of the part's content are set on the passed
187
     * $partBuilder as lines are read, as is the part's end position.
188
     * 
189
     * @param resource $handle
190
     * @param PartBuilder $partBuilder
191
     */
192
    private function readPartContent($handle, PartBuilder $partBuilder)
193
    {
194
        $partBuilder->setStreamContentStartPos(ftell($handle));
195
        if ($this->findContentBoundary($handle, $partBuilder) && $partBuilder->isMultiPart()) {
196
            while (!feof($handle) && !$partBuilder->isEndBoundaryFound()) {
197
                $child = $this->partBuilderFactory->newPartBuilder(
198
                    $this->partFactoryService->getMimePartFactory()
199
                );
200
                $partBuilder->addChild($child);
201
                $this->readPart($handle, $child);
202
                if ($child->isEndBoundaryFound()) {
203
                    $discard = $this->partBuilderFactory->newPartBuilder(
204
                        $this->partFactoryService->getMimePartFactory()
205
                    );
206
                    $discard->setParent($partBuilder);
207
                    $this->findContentBoundary($handle, $discard);
208
                }
209
            }
210
            // for non-multipart parts, setStreamContentAndPartEndPos is called
211
            // in findContentBoundary
212
            $partBuilder->setStreamPartEndPos(ftell($handle));
213
        }
214
    }
215
    
216
    /**
217
     * Reads a part and any of its children, into the passed $partBuilder.
218
     * 
219
     * @param resource $handle
220
     * @param PartBuilder $partBuilder
221
     * @param boolean $isMessage
222
     */
223
    protected function readPart($handle, PartBuilder $partBuilder, $isMessage = false)
224
    {
225
        $partBuilder->setStreamPartStartPos(ftell($handle));
226
        $this->readHeaders($handle, $partBuilder);
227
        if ($isMessage && !$partBuilder->isMime()) {
228
            $this->readUUEncodedOrPlainTextMessage($handle, $partBuilder);
229
        } else {
230
            $this->readPartContent($handle, $partBuilder);
231
        }
232
    }
233
    
234
    /**
235
     * Reads the message from the input stream $handle and returns a PartBuilder
236
     * representing it.
237
     * 
238
     * @param resource $handle
239
     * @return PartBuilder
240
     */
241
    protected function read($handle)
242
    {
243
        $partBuilder = $this->partBuilderFactory->newPartBuilder(
244
            $this->partFactoryService->getMessageFactory()
245
        );
246
        $this->readPart($handle, $partBuilder, true);
247
        return $partBuilder;
248
    }
249
}
250