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