Passed
Branch master (ba441e)
by Zaahid
03:56
created

MimePartWriter::writePartContentTo()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 0
cts 16
cp 0
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 15
nc 5
nop 2
crap 20
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\Writer;
8
9
use ZBateson\MailMimeParser\Message\MimePart;
10
use ZBateson\MailMimeParser\Stream\StreamLeftover;
11
12
/**
13
 * Writes a MimePart to a resource handle.
14
 * 
15
 * The class is responsible for writing out the headers and content of a
16
 * MimePart to an output stream buffer, taking care of encoding and filtering.
17
 * 
18
 * @author Zaahid Bateson
19
 */
20
class MimePartWriter
21
{
22
    /**
23
     * @var array default params for stream filters in
24
     *      setTransferEncodingFilterOnStream
25
     */
26
    private static $defaultStreamFilterParams = [
27
        'line-length' => 76,
28
        'line-break-chars' => "\r\n",
29
    ];
30
    
31
    /**
32
     * @var array map of transfer-encoding types to registered stream filter
33
     *      names used in setTransferEncodingFilterOnStream
34
     */
35
    private static $typeToEncodingMap = [
36
        'quoted-printable' => 'mmp-convert.quoted-printable-encode',
37
        'base64' => 'mmp-convert.base64-encode',
38
        'x-uuencode' => 'mailmimeparser-uuencode',
39
        'x-uue' => 'mailmimeparser-uuencode',
40
        'uuencode' => 'mailmimeparser-uuencode',
41
        'uue' => 'mailmimeparser-uuencode',
42
    ];
43
    
44
    /**
45
     * Returns the singleton instance for the class, instantiating it if not
46
     * already created.
47
     */
48
    public static function getInstance()
49
    {
50
        static $instances = [];
51
        $class = get_called_class();
52
        if (!isset($instances[$class])) {
53
            $instances[$class] = new static();
54
        }
55
        return $instances[$class];
56
    }
57
    
58
    /**
59
     * Writes out the headers of the passed MimePart and follows them with an
60
     * empty line.
61
     *
62
     * @param MimePart $part
63
     * @param resource $handle
64
     */
65
    public function writePartHeadersTo(MimePart $part, $handle)
66
    {
67
        $headers = $part->getHeaders();
68
        foreach ($headers as $header) {
69
            fwrite($handle, "$header\r\n");
70
        }
71
        fwrite($handle, "\r\n");
72
    }
73
    
74
    /**
75
     * Sets up a mailmimeparser-encode stream filter on the content resource 
76
     * handle of the passed MimePart if applicable and returns a reference to
77
     * the filter.
78
     *
79
     * @param MimePart $part
80
     * @return resource a reference to the appended stream filter or null
81
     */
82
    private function setCharsetStreamFilterOnPartStream(MimePart $part)
83
    {
84
        $handle = $part->getContentResourceHandle();
85
        if ($part->isTextPart()) {
86
            return stream_filter_append(
87
                $handle,
88
                'mailmimeparser-encode',
89
                STREAM_FILTER_READ,
90
                [
91
                    'charset' => 'UTF-8',
92
                    'to' => $part->getHeaderParameter(
93
                        'Content-Type',
94
                        'charset',
95
                        'ISO-8859-1'
96
                    )
97
                ]
98
            );
99
        }
100
        return null;
101
    }
102
    
103
    /**
104
     * Appends a stream filter on the passed MimePart's content resource handle
105
     * based on the type of encoding for the passed part.
106
     *
107
     * @param MimePart $part
108
     * @param resource $handle
109
     * @param StreamLeftover $leftovers
110
     * @return resource the stream filter
111
     */
112
    private function setTransferEncodingFilterOnStream(MimePart $part, $handle, StreamLeftover $leftovers)
113
    {
114
        $contentHandle = $part->getContentResourceHandle();
115
        $encoding = strtolower($part->getHeaderValue('Content-Transfer-Encoding'));
116
        $params = array_merge(self::$defaultStreamFilterParams, [
117
            'leftovers' => $leftovers,
118
            'filename' => $part->getHeaderParameter(
119
                'Content-Type',
120
                'name',
121
                'null'
122
            )
123
        ]);
124
        if (isset(self::$typeToEncodingMap[$encoding])) {
125
            return stream_filter_append(
126
                $contentHandle,
127
                self::$typeToEncodingMap[$encoding],
128
                STREAM_FILTER_READ,
129
                $params
130
            );
131
        }
132
        return null;
133
    }
134
135
    /**
136
     * Trims out any starting and ending CRLF characters in the stream.
137
     *
138
     * @param string $read the read string, and where the result will be written
139
     *        to
140
     * @param bool $first set to true if this is the first set of read
141
     *        characters from the stream (ltrims CRLF)
142
     * @param string $lastChars contains any CRLF characters from the last $read
143
     *        line if it ended with a CRLF (because they're trimmed from the
144
     *        end, and get prepended to $read).
145
     */
146
    private function trimTextBeforeCopying(&$read, &$first, &$lastChars)
147
    {
148
        if ($first) {
149
            $first = false;
150
            $read = ltrim($read, "\r\n");
151
        }
152
        $read = $lastChars . $read;
153
        $lastChars = '';
154
        $matches = null;
155
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
156
            $lastChars = $matches[0];
157
            $read = rtrim($read, "\r\n");
158
        }
159
    }
160
161
    /**
162
     * Copies the content of the $fromHandle stream into the $toHandle stream,
163
     * maintaining the current read position in $fromHandle.  The passed
164
     * MimePart is where $fromHandle originated after setting up filters on
165
     * $fromHandle.
166
     *
167
     * @param MimePart $part
168
     * @param resource $fromHandle
169
     * @param resource $toHandle
170
     */
171
    private function copyContentStream(MimePart $part, $fromHandle, $toHandle)
172
    {
173
        $pos = ftell($fromHandle);
174
        rewind($fromHandle);
175
        // changed from stream_copy_to_stream because hhvm seems to stop before
176
        // end of file for some reason
177
        $lastChars = '';
178
        $first = true;
179
        while (!feof($fromHandle)) {
180
            $read = fread($fromHandle, 1024);
181
            if (strcasecmp($part->getHeaderValue('Content-Encoding'), '8bit') !== 0) {
182
                $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
183
            }
184
            if ($part->isTextPart()) {
185
                $this->trimTextBeforeCopying($read, $first, $lastChars);
186
            }
187
            fwrite($toHandle, $read);
188
        }
189
        fseek($fromHandle, $pos);
190
    }
191
192
    /**
193
     * Writes out the content portion of the mime part based on the headers that
194
     * are set on the part, taking care of character/content-transfer encoding.
195
     *
196
     * @param MimePart $part
197
     * @param resource $handle
198
     */
199
    public function writePartContentTo(MimePart $part, $handle)
200
    {
201
        $contentHandle = $part->getContentResourceHandle();
202
        if ($contentHandle !== null) {
0 ignored issues
show
introduced by
The condition $contentHandle !== null can never be false.
Loading history...
203
            
204
            $filter = $this->setCharsetStreamFilterOnPartStream($part);
205
            $leftovers = new StreamLeftover();
206
            $encodingFilter = $this->setTransferEncodingFilterOnStream(
207
                $part,
208
                $handle,
209
                $leftovers
210
            );
211
            $this->copyContentStream($part, $contentHandle, $handle);
212
            
213
            if ($encodingFilter !== null) {
0 ignored issues
show
introduced by
The condition $encodingFilter !== null can never be false.
Loading history...
214
                fflush($handle);
215
                stream_filter_remove($encodingFilter);
216
                fwrite($handle, $leftovers->encodedValue);
217
            }
218
            if ($filter !== null) {
0 ignored issues
show
introduced by
The condition $filter !== null can never be false.
Loading history...
219
                stream_filter_remove($filter);
220
            }
221
        }
222
    }
223
224
    /**
225
     * Writes out the MimePart to the passed resource.
226
     *
227
     * Takes care of character and content transfer encoding on the output based
228
     * on what headers are set.
229
     *
230
     * @param MimePart $part
231
     * @param resource $handle
232
     */
233
    public function writePartTo(MimePart $part, $handle)
234
    {
235
        $this->writePartHeadersTo($part, $handle);
236
        $this->writePartContentTo($part, $handle);
237
    }
238
}
239