|
1 <?php |
|
2 |
|
3 /* |
|
4 * This file is part of SwiftMailer. |
|
5 * (c) 2004-2009 Chris Corbyn |
|
6 * |
|
7 * For the full copyright and license information, please view the LICENSE |
|
8 * file that was distributed with this source code. |
|
9 */ |
|
10 |
|
11 /** |
|
12 * An abstract base MIME Header. |
|
13 * @package Swift |
|
14 * @subpackage Mime |
|
15 * @author Chris Corbyn |
|
16 */ |
|
17 abstract class Swift_Mime_Headers_AbstractHeader implements Swift_Mime_Header |
|
18 { |
|
19 |
|
20 /** |
|
21 * The name of this Header. |
|
22 * @var string |
|
23 * @access private |
|
24 */ |
|
25 private $_name; |
|
26 |
|
27 /** |
|
28 * The Grammar used for this Header. |
|
29 * @var Swift_Mime_Grammar |
|
30 * @access private |
|
31 */ |
|
32 private $_grammar; |
|
33 |
|
34 /** |
|
35 * The Encoder used to encode this Header. |
|
36 * @var Swift_Encoder |
|
37 * @access private |
|
38 */ |
|
39 private $_encoder; |
|
40 |
|
41 /** |
|
42 * The maximum length of a line in the header. |
|
43 * @var int |
|
44 * @access private |
|
45 */ |
|
46 private $_lineLength = 78; |
|
47 |
|
48 /** |
|
49 * The language used in this Header. |
|
50 * @var string |
|
51 */ |
|
52 private $_lang; |
|
53 |
|
54 /** |
|
55 * The character set of the text in this Header. |
|
56 * @var string |
|
57 * @access private |
|
58 */ |
|
59 private $_charset = 'utf-8'; |
|
60 |
|
61 /** |
|
62 * The value of this Header, cached. |
|
63 * @var string |
|
64 * @access private |
|
65 */ |
|
66 private $_cachedValue = null; |
|
67 |
|
68 /** |
|
69 * Creates a new Header. |
|
70 * @param Swift_Mime_Grammar $grammar |
|
71 */ |
|
72 public function __construct(Swift_Mime_Grammar $grammar) |
|
73 { |
|
74 $this->setGrammar($grammar); |
|
75 } |
|
76 |
|
77 /** |
|
78 * Set the character set used in this Header. |
|
79 * @param string $charset |
|
80 */ |
|
81 public function setCharset($charset) |
|
82 { |
|
83 $this->clearCachedValueIf($charset != $this->_charset); |
|
84 $this->_charset = $charset; |
|
85 if (isset($this->_encoder)) |
|
86 { |
|
87 $this->_encoder->charsetChanged($charset); |
|
88 } |
|
89 } |
|
90 |
|
91 /** |
|
92 * Get the character set used in this Header. |
|
93 * @return string |
|
94 */ |
|
95 public function getCharset() |
|
96 { |
|
97 return $this->_charset; |
|
98 } |
|
99 |
|
100 /** |
|
101 * Set the language used in this Header. |
|
102 * For example, for US English, 'en-us'. |
|
103 * This can be unspecified. |
|
104 * @param string $lang |
|
105 */ |
|
106 public function setLanguage($lang) |
|
107 { |
|
108 $this->clearCachedValueIf($this->_lang != $lang); |
|
109 $this->_lang = $lang; |
|
110 } |
|
111 |
|
112 /** |
|
113 * Get the language used in this Header. |
|
114 * @return string |
|
115 */ |
|
116 public function getLanguage() |
|
117 { |
|
118 return $this->_lang; |
|
119 } |
|
120 |
|
121 /** |
|
122 * Set the encoder used for encoding the header. |
|
123 * @param Swift_Mime_HeaderEncoder $encoder |
|
124 */ |
|
125 public function setEncoder(Swift_Mime_HeaderEncoder $encoder) |
|
126 { |
|
127 $this->_encoder = $encoder; |
|
128 $this->setCachedValue(null); |
|
129 } |
|
130 |
|
131 /** |
|
132 * Get the encoder used for encoding this Header. |
|
133 * @return Swift_Mime_HeaderEncoder |
|
134 */ |
|
135 public function getEncoder() |
|
136 { |
|
137 return $this->_encoder; |
|
138 } |
|
139 |
|
140 /** |
|
141 * Set the grammar used for the header. |
|
142 * @param Swift_Mime_Grammar $grammar |
|
143 */ |
|
144 public function setGrammar(Swift_Mime_Grammar $grammar) |
|
145 { |
|
146 $this->_grammar = $grammar; |
|
147 $this->setCachedValue(null); |
|
148 } |
|
149 |
|
150 /** |
|
151 * Get the grammar used for this Header. |
|
152 * @return Swift_Mime_Grammar |
|
153 */ |
|
154 public function getGrammar() |
|
155 { |
|
156 return $this->_grammar; |
|
157 } |
|
158 |
|
159 /** |
|
160 * Get the name of this header (e.g. charset). |
|
161 * @return string |
|
162 */ |
|
163 public function getFieldName() |
|
164 { |
|
165 return $this->_name; |
|
166 } |
|
167 |
|
168 /** |
|
169 * Set the maximum length of lines in the header (excluding EOL). |
|
170 * @param int $lineLength |
|
171 */ |
|
172 public function setMaxLineLength($lineLength) |
|
173 { |
|
174 $this->clearCachedValueIf($this->_lineLength != $lineLength); |
|
175 $this->_lineLength = $lineLength; |
|
176 } |
|
177 |
|
178 /** |
|
179 * Get the maximum permitted length of lines in this Header. |
|
180 * @return int |
|
181 */ |
|
182 public function getMaxLineLength() |
|
183 { |
|
184 return $this->_lineLength; |
|
185 } |
|
186 |
|
187 /** |
|
188 * Get this Header rendered as a RFC 2822 compliant string. |
|
189 * @return string |
|
190 * @throws Swift_RfcComplianceException |
|
191 */ |
|
192 public function toString() |
|
193 { |
|
194 return $this->_tokensToString($this->toTokens()); |
|
195 } |
|
196 |
|
197 /** |
|
198 * Returns a string representation of this object. |
|
199 * |
|
200 * @return string |
|
201 * |
|
202 * @see toString() |
|
203 */ |
|
204 public function __toString() |
|
205 { |
|
206 return $this->toString(); |
|
207 } |
|
208 |
|
209 // -- Points of extension |
|
210 |
|
211 /** |
|
212 * Set the name of this Header field. |
|
213 * @param string $name |
|
214 * @access protected |
|
215 */ |
|
216 protected function setFieldName($name) |
|
217 { |
|
218 $this->_name = $name; |
|
219 } |
|
220 |
|
221 /** |
|
222 * Produces a compliant, formatted RFC 2822 'phrase' based on the string given. |
|
223 * @param Swift_Mime_Header $header |
|
224 * @param string $string as displayed |
|
225 * @param string $charset of the text |
|
226 * @param Swift_Mime_HeaderEncoder $encoder |
|
227 * @param boolean $shorten the first line to make remove for header name |
|
228 * @return string |
|
229 */ |
|
230 protected function createPhrase(Swift_Mime_Header $header, $string, $charset, |
|
231 Swift_Mime_HeaderEncoder $encoder = null, $shorten = false) |
|
232 { |
|
233 //Treat token as exactly what was given |
|
234 $phraseStr = $string; |
|
235 //If it's not valid |
|
236 if (!preg_match('/^' . $this->getGrammar()->getDefinition('phrase') . '$/D', $phraseStr)) |
|
237 { |
|
238 // .. but it is just ascii text, try escaping some characters |
|
239 // and make it a quoted-string |
|
240 if (preg_match('/^' . $this->getGrammar()->getDefinition('text') . '*$/D', $phraseStr)) |
|
241 { |
|
242 $phraseStr = $this->getGrammar()->escapeSpecials( |
|
243 $phraseStr, array('"'), $this->getGrammar()->getSpecials() |
|
244 ); |
|
245 $phraseStr = '"' . $phraseStr . '"'; |
|
246 } |
|
247 else // ... otherwise it needs encoding |
|
248 { |
|
249 //Determine space remaining on line if first line |
|
250 if ($shorten) |
|
251 { |
|
252 $usedLength = strlen($header->getFieldName() . ': '); |
|
253 } |
|
254 else |
|
255 { |
|
256 $usedLength = 0; |
|
257 } |
|
258 $phraseStr = $this->encodeWords($header, $string, $usedLength); |
|
259 } |
|
260 } |
|
261 |
|
262 return $phraseStr; |
|
263 } |
|
264 |
|
265 /** |
|
266 * Encode needed word tokens within a string of input. |
|
267 * @param string $input |
|
268 * @param string $usedLength, optional |
|
269 * @return string |
|
270 */ |
|
271 protected function encodeWords(Swift_Mime_Header $header, $input, |
|
272 $usedLength = -1) |
|
273 { |
|
274 $value = ''; |
|
275 |
|
276 $tokens = $this->getEncodableWordTokens($input); |
|
277 |
|
278 foreach ($tokens as $token) |
|
279 { |
|
280 //See RFC 2822, Sect 2.2 (really 2.2 ??) |
|
281 if ($this->tokenNeedsEncoding($token)) |
|
282 { |
|
283 //Don't encode starting WSP |
|
284 $firstChar = substr($token, 0, 1); |
|
285 switch($firstChar) |
|
286 { |
|
287 case ' ': |
|
288 case "\t": |
|
289 $value .= $firstChar; |
|
290 $token = substr($token, 1); |
|
291 } |
|
292 |
|
293 if (-1 == $usedLength) |
|
294 { |
|
295 $usedLength = strlen($header->getFieldName() . ': ') + strlen($value); |
|
296 } |
|
297 $value .= $this->getTokenAsEncodedWord($token, $usedLength); |
|
298 |
|
299 $header->setMaxLineLength(76); //Forefully override |
|
300 } |
|
301 else |
|
302 { |
|
303 $value .= $token; |
|
304 } |
|
305 } |
|
306 |
|
307 return $value; |
|
308 } |
|
309 |
|
310 /** |
|
311 * Test if a token needs to be encoded or not. |
|
312 * @param string $token |
|
313 * @return boolean |
|
314 */ |
|
315 protected function tokenNeedsEncoding($token) |
|
316 { |
|
317 return preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token); |
|
318 } |
|
319 |
|
320 /** |
|
321 * Splits a string into tokens in blocks of words which can be encoded quickly. |
|
322 * @param string $string |
|
323 * @return string[] |
|
324 */ |
|
325 protected function getEncodableWordTokens($string) |
|
326 { |
|
327 $tokens = array(); |
|
328 |
|
329 $encodedToken = ''; |
|
330 //Split at all whitespace boundaries |
|
331 foreach (preg_split('~(?=[\t ])~', $string) as $token) |
|
332 { |
|
333 if ($this->tokenNeedsEncoding($token)) |
|
334 { |
|
335 $encodedToken .= $token; |
|
336 } |
|
337 else |
|
338 { |
|
339 if (strlen($encodedToken) > 0) |
|
340 { |
|
341 $tokens[] = $encodedToken; |
|
342 $encodedToken = ''; |
|
343 } |
|
344 $tokens[] = $token; |
|
345 } |
|
346 } |
|
347 if (strlen($encodedToken)) |
|
348 { |
|
349 $tokens[] = $encodedToken; |
|
350 } |
|
351 |
|
352 return $tokens; |
|
353 } |
|
354 |
|
355 /** |
|
356 * Get a token as an encoded word for safe insertion into headers. |
|
357 * @param string $token to encode |
|
358 * @param int $firstLineOffset, optional |
|
359 * @return string |
|
360 */ |
|
361 protected function getTokenAsEncodedWord($token, $firstLineOffset = 0) |
|
362 { |
|
363 //Adjust $firstLineOffset to account for space needed for syntax |
|
364 $charsetDecl = $this->_charset; |
|
365 if (isset($this->_lang)) |
|
366 { |
|
367 $charsetDecl .= '*' . $this->_lang; |
|
368 } |
|
369 $encodingWrapperLength = strlen( |
|
370 '=?' . $charsetDecl . '?' . $this->_encoder->getName() . '??=' |
|
371 ); |
|
372 |
|
373 if ($firstLineOffset >= 75) //Does this logic need to be here? |
|
374 { |
|
375 $firstLineOffset = 0; |
|
376 } |
|
377 |
|
378 $encodedTextLines = explode("\r\n", |
|
379 $this->_encoder->encodeString( |
|
380 $token, $firstLineOffset, 75 - $encodingWrapperLength |
|
381 ) |
|
382 ); |
|
383 |
|
384 foreach ($encodedTextLines as $lineNum => $line) |
|
385 { |
|
386 $encodedTextLines[$lineNum] = '=?' . $charsetDecl . |
|
387 '?' . $this->_encoder->getName() . |
|
388 '?' . $line . '?='; |
|
389 } |
|
390 |
|
391 return implode("\r\n ", $encodedTextLines); |
|
392 } |
|
393 |
|
394 /** |
|
395 * Generates tokens from the given string which include CRLF as individual tokens. |
|
396 * @param string $token |
|
397 * @return string[] |
|
398 * @access protected |
|
399 */ |
|
400 protected function generateTokenLines($token) |
|
401 { |
|
402 return preg_split('~(\r\n)~', $token, -1, PREG_SPLIT_DELIM_CAPTURE); |
|
403 } |
|
404 |
|
405 /** |
|
406 * Set a value into the cache. |
|
407 * @param string $value |
|
408 * @access protected |
|
409 */ |
|
410 protected function setCachedValue($value) |
|
411 { |
|
412 $this->_cachedValue = $value; |
|
413 } |
|
414 |
|
415 /** |
|
416 * Get the value in the cache. |
|
417 * @return string |
|
418 * @access protected |
|
419 */ |
|
420 protected function getCachedValue() |
|
421 { |
|
422 return $this->_cachedValue; |
|
423 } |
|
424 |
|
425 /** |
|
426 * Clear the cached value if $condition is met. |
|
427 * @param boolean $condition |
|
428 * @access protected |
|
429 */ |
|
430 protected function clearCachedValueIf($condition) |
|
431 { |
|
432 if ($condition) |
|
433 { |
|
434 $this->setCachedValue(null); |
|
435 } |
|
436 } |
|
437 |
|
438 // -- Private methods |
|
439 |
|
440 /** |
|
441 * Generate a list of all tokens in the final header. |
|
442 * @param string $string input, optional |
|
443 * @return string[] |
|
444 * @access private |
|
445 */ |
|
446 protected function toTokens($string = null) |
|
447 { |
|
448 if (is_null($string)) |
|
449 { |
|
450 $string = $this->getFieldBody(); |
|
451 } |
|
452 |
|
453 $tokens = array(); |
|
454 |
|
455 //Generate atoms; split at all invisible boundaries followed by WSP |
|
456 foreach (preg_split('~(?=[ \t])~', $string) as $token) |
|
457 { |
|
458 $tokens = array_merge($tokens, $this->generateTokenLines($token)); |
|
459 } |
|
460 |
|
461 return $tokens; |
|
462 } |
|
463 |
|
464 /** |
|
465 * Takes an array of tokens which appear in the header and turns them into |
|
466 * an RFC 2822 compliant string, adding FWSP where needed. |
|
467 * @param string[] $tokens |
|
468 * @return string |
|
469 * @access private |
|
470 */ |
|
471 private function _tokensToString(array $tokens) |
|
472 { |
|
473 $lineCount = 0; |
|
474 $headerLines = array(); |
|
475 $headerLines[] = $this->_name . ': '; |
|
476 $currentLine =& $headerLines[$lineCount++]; |
|
477 |
|
478 //Build all tokens back into compliant header |
|
479 foreach ($tokens as $i => $token) |
|
480 { |
|
481 //Line longer than specified maximum or token was just a new line |
|
482 if (("\r\n" == $token) || |
|
483 ($i > 0 && strlen($currentLine . $token) > $this->_lineLength) |
|
484 && 0 < strlen($currentLine)) |
|
485 { |
|
486 $headerLines[] = ''; |
|
487 $currentLine =& $headerLines[$lineCount++]; |
|
488 } |
|
489 |
|
490 //Append token to the line |
|
491 if ("\r\n" != $token) |
|
492 { |
|
493 $currentLine .= $token; |
|
494 } |
|
495 } |
|
496 |
|
497 //Implode with FWS (RFC 2822, 2.2.3) |
|
498 return implode("\r\n", $headerLines) . "\r\n"; |
|
499 } |
|
500 |
|
501 } |