Added feature #15998: Create a new API to send mails based on SwiftMailer to replace...
[Packages/TYPO3.CMS.git] / typo3 / contrib / swiftmailer / classes / Swift / Mime / Headers / AbstractHeader.php
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 //@require 'Swift/Mime/Header.php';
12 //@require 'Swift/Mime/HeaderEncoder.php';
13 //@require 'Swift/RfcComplianceException.php';
14
15 /**
16 * An abstract base MIME Header.
17 * @package Swift
18 * @subpackage Mime
19 * @author Chris Corbyn
20 */
21 abstract class Swift_Mime_Headers_AbstractHeader implements Swift_Mime_Header
22 {
23
24 /**
25 * Special characters used in the syntax which need to be escaped.
26 * @var string[]
27 * @access private
28 */
29 private $_specials = array();
30
31 /**
32 * Tokens defined in RFC 2822 (and some related RFCs).
33 * @var string[]
34 * @access private
35 */
36 private $_grammar = array();
37
38 /**
39 * The name of this Header.
40 * @var string
41 * @access private
42 */
43 private $_name;
44
45 /**
46 * The Encoder used to encode this Header.
47 * @var Swift_Encoder
48 * @access private
49 */
50 private $_encoder;
51
52 /**
53 * The maximum length of a line in the header.
54 * @var int
55 * @access private
56 */
57 private $_lineLength = 78;
58
59 /**
60 * The language used in this Header.
61 * @var string
62 */
63 private $_lang;
64
65 /**
66 * The character set of the text in this Header.
67 * @var string
68 * @access private
69 */
70 private $_charset = 'utf-8';
71
72 /**
73 * The value of this Header, cached.
74 * @var string
75 * @access private
76 */
77 private $_cachedValue = null;
78
79 /**
80 * Set the character set used in this Header.
81 * @param string $charset
82 */
83 public function setCharset($charset)
84 {
85 $this->clearCachedValueIf($charset != $this->_charset);
86 $this->_charset = $charset;
87 if (isset($this->_encoder))
88 {
89 $this->_encoder->charsetChanged($charset);
90 }
91 }
92
93 /**
94 * Get the character set used in this Header.
95 * @return string
96 */
97 public function getCharset()
98 {
99 return $this->_charset;
100 }
101
102 /**
103 * Set the language used in this Header.
104 * For example, for US English, 'en-us'.
105 * This can be unspecified.
106 * @param string $lang
107 */
108 public function setLanguage($lang)
109 {
110 $this->clearCachedValueIf($this->_lang != $lang);
111 $this->_lang = $lang;
112 }
113
114 /**
115 * Get the language used in this Header.
116 * @return string
117 */
118 public function getLanguage()
119 {
120 return $this->_lang;
121 }
122
123 /**
124 * Set the encoder used for encoding the header.
125 * @param Swift_Mime_HeaderEncoder $encoder
126 */
127 public function setEncoder(Swift_Mime_HeaderEncoder $encoder)
128 {
129 $this->_encoder = $encoder;
130 $this->setCachedValue(null);
131 }
132
133 /**
134 * Get the encoder used for encoding this Header.
135 * @return Swift_Mime_HeaderEncoder
136 */
137 public function getEncoder()
138 {
139 return $this->_encoder;
140 }
141
142 /**
143 * Get the name of this header (e.g. charset).
144 * @return string
145 */
146 public function getFieldName()
147 {
148 return $this->_name;
149 }
150
151 /**
152 * Set the maximum length of lines in the header (excluding EOL).
153 * @param int $lineLength
154 */
155 public function setMaxLineLength($lineLength)
156 {
157 $this->clearCachedValueIf($this->_lineLength != $lineLength);
158 $this->_lineLength = $lineLength;
159 }
160
161 /**
162 * Get the maximum permitted length of lines in this Header.
163 * @return int
164 */
165 public function getMaxLineLength()
166 {
167 return $this->_lineLength;
168 }
169
170 /**
171 * Get this Header rendered as a RFC 2822 compliant string.
172 * @return string
173 * @throws Swift_RfcComplianceException
174 */
175 public function toString()
176 {
177 return $this->_tokensToString($this->toTokens());
178 }
179
180 /**
181 * Returns a string representation of this object.
182 *
183 * @return string
184 *
185 * @see toString()
186 */
187 public function __toString()
188 {
189 return $this->toString();
190 }
191
192 // -- Points of extension
193
194 /**
195 * Set the name of this Header field.
196 * @param string $name
197 * @access protected
198 */
199 protected function setFieldName($name)
200 {
201 $this->_name = $name;
202 }
203
204 /**
205 * Initialize some RFC 2822 (and friends) ABNF grammar definitions.
206 * @access protected
207 */
208 protected function initializeGrammar()
209 {
210 $this->_specials = array(
211 '(', ')', '<', '>', '[', ']',
212 ':', ';', '@', ',', '.', '"'
213 );
214
215 /*** Refer to RFC 2822 for ABNF grammar ***/
216
217 //All basic building blocks
218 $this->_grammar['NO-WS-CTL'] = '[\x01-\x08\x0B\x0C\x0E-\x19\x7F]';
219 $this->_grammar['WSP'] = '[ \t]';
220 $this->_grammar['CRLF'] = '(?:\r\n)';
221 $this->_grammar['FWS'] = '(?:(?:' . $this->_grammar['WSP'] . '*' .
222 $this->_grammar['CRLF'] . ')?' . $this->_grammar['WSP'] . ')';
223 $this->_grammar['text'] = '[\x00-\x08\x0B\x0C\x0E-\x7F]';
224 $this->_grammar['quoted-pair'] = '(?:\\\\' . $this->_grammar['text'] . ')';
225 $this->_grammar['ctext'] = '(?:' . $this->_grammar['NO-WS-CTL'] .
226 '|[\x21-\x27\x2A-\x5B\x5D-\x7E])';
227 //Uses recursive PCRE (?1) -- could be a weak point??
228 $this->_grammar['ccontent'] = '(?:' . $this->_grammar['ctext'] . '|' .
229 $this->_grammar['quoted-pair'] . '|(?1))';
230 $this->_grammar['comment'] = '(\((?:' . $this->_grammar['FWS'] . '|' .
231 $this->_grammar['ccontent']. ')*' . $this->_grammar['FWS'] . '?\))';
232 $this->_grammar['CFWS'] = '(?:(?:' . $this->_grammar['FWS'] . '?' .
233 $this->_grammar['comment'] . ')*(?:(?:' . $this->_grammar['FWS'] . '?' .
234 $this->_grammar['comment'] . ')|' . $this->_grammar['FWS'] . '))';
235 $this->_grammar['qtext'] = '(?:' . $this->_grammar['NO-WS-CTL'] .
236 '|[\x21\x23-\x5B\x5D-\x7E])';
237 $this->_grammar['qcontent'] = '(?:' . $this->_grammar['qtext'] . '|' .
238 $this->_grammar['quoted-pair'] . ')';
239 $this->_grammar['quoted-string'] = '(?:' . $this->_grammar['CFWS'] . '?"' .
240 '(' . $this->_grammar['FWS'] . '?' . $this->_grammar['qcontent'] . ')*' .
241 $this->_grammar['FWS'] . '?"' . $this->_grammar['CFWS'] . '?)';
242 $this->_grammar['atext'] = '[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]';
243 $this->_grammar['atom'] = '(?:' . $this->_grammar['CFWS'] . '?' .
244 $this->_grammar['atext'] . '+' . $this->_grammar['CFWS'] . '?)';
245 $this->_grammar['dot-atom-text'] = '(?:' . $this->_grammar['atext'] . '+' .
246 '(\.' . $this->_grammar['atext'] . '+)*)';
247 $this->_grammar['dot-atom'] = '(?:' . $this->_grammar['CFWS'] . '?' .
248 $this->_grammar['dot-atom-text'] . '+' . $this->_grammar['CFWS'] . '?)';
249 $this->_grammar['word'] = '(?:' . $this->_grammar['atom'] . '|' .
250 $this->_grammar['quoted-string'] . ')';
251 $this->_grammar['phrase'] = '(?:' . $this->_grammar['word'] . '+?)';
252 $this->_grammar['no-fold-quote'] = '(?:"(?:' . $this->_grammar['qtext'] .
253 '|' . $this->_grammar['quoted-pair'] . ')*")';
254 $this->_grammar['dtext'] = '(?:' . $this->_grammar['NO-WS-CTL'] .
255 '|[\x21-\x5A\x5E-\x7E])';
256 $this->_grammar['no-fold-literal'] = '(?:\[(?:' . $this->_grammar['dtext'] .
257 '|' . $this->_grammar['quoted-pair'] . ')*\])';
258
259 //Message IDs
260 $this->_grammar['id-left'] = '(?:' . $this->_grammar['dot-atom-text'] . '|' .
261 $this->_grammar['no-fold-quote'] . ')';
262 $this->_grammar['id-right'] = '(?:' . $this->_grammar['dot-atom-text'] . '|' .
263 $this->_grammar['no-fold-literal'] . ')';
264
265 //Addresses, mailboxes and paths
266 $this->_grammar['local-part'] = '(?:' . $this->_grammar['dot-atom'] . '|' .
267 $this->_grammar['quoted-string'] . ')';
268 $this->_grammar['dcontent'] = '(?:' . $this->_grammar['dtext'] . '|' .
269 $this->_grammar['quoted-pair'] . ')';
270 $this->_grammar['domain-literal'] = '(?:' . $this->_grammar['CFWS'] . '?\[(' .
271 $this->_grammar['FWS'] . '?' . $this->_grammar['dcontent'] . ')*?' .
272 $this->_grammar['FWS'] . '?\]' . $this->_grammar['CFWS'] . '?)';
273 $this->_grammar['domain'] = '(?:' . $this->_grammar['dot-atom'] . '|' .
274 $this->_grammar['domain-literal'] . ')';
275 $this->_grammar['addr-spec'] = '(?:' . $this->_grammar['local-part'] . '@' .
276 $this->_grammar['domain'] . ')';
277 }
278
279 /**
280 * Get the grammar defined for $name token.
281 * @param string $name execatly as written in the RFC
282 * @return string
283 */
284 protected function getGrammar($name)
285 {
286 if (array_key_exists($name, $this->_grammar))
287 {
288 return $this->_grammar[$name];
289 }
290 else
291 {
292 throw new Swift_RfcComplianceException(
293 "No such grammar '" . $name . "' defined."
294 );
295 }
296 }
297
298 /**
299 * Escape special characters in a string (convert to quoted-pairs).
300 * @param string $token
301 * @param string[] $include additonal chars to escape
302 * @param string[] $exclude chars from escaping
303 * @return string
304 */
305 protected function escapeSpecials($token, $include = array(),
306 $exclude = array())
307 {
308 foreach (
309 array_merge(array('\\'), array_diff($this->_specials, $exclude), $include) as $char)
310 {
311 $token = str_replace($char, '\\' . $char, $token);
312 }
313 return $token;
314 }
315
316 /**
317 * Produces a compliant, formatted RFC 2822 'phrase' based on the string given.
318 * @param Swift_Mime_Header $header
319 * @param string $string as displayed
320 * @param string $charset of the text
321 * @param Swift_Mime_HeaderEncoder $encoder
322 * @param boolean $shorten the first line to make remove for header name
323 * @return string
324 */
325 protected function createPhrase(Swift_Mime_Header $header, $string, $charset,
326 Swift_Mime_HeaderEncoder $encoder = null, $shorten = false)
327 {
328 //Treat token as exactly what was given
329 $phraseStr = $string;
330 //If it's not valid
331 if (!preg_match('/^' . $this->_grammar['phrase'] . '$/D', $phraseStr))
332 {
333 // .. but it is just ascii text, try escaping some characters
334 // and make it a quoted-string
335 if (preg_match('/^' . $this->_grammar['text'] . '*$/D', $phraseStr))
336 {
337 $phraseStr = $this->escapeSpecials(
338 $phraseStr, array('"'), $this->_specials
339 );
340 $phraseStr = '"' . $phraseStr . '"';
341 }
342 else // ... otherwise it needs encoding
343 {
344 //Determine space remaining on line if first line
345 if ($shorten)
346 {
347 $usedLength = strlen($header->getFieldName() . ': ');
348 }
349 else
350 {
351 $usedLength = 0;
352 }
353 $phraseStr = $this->encodeWords($header, $string, $usedLength);
354 }
355 }
356
357 return $phraseStr;
358 }
359
360 /**
361 * Encode needed word tokens within a string of input.
362 * @param string $input
363 * @param string $usedLength, optional
364 * @return string
365 */
366 protected function encodeWords(Swift_Mime_Header $header, $input,
367 $usedLength = -1)
368 {
369 $value = '';
370
371 $tokens = $this->getEncodableWordTokens($input);
372
373 foreach ($tokens as $token)
374 {
375 //See RFC 2822, Sect 2.2 (really 2.2 ??)
376 if ($this->tokenNeedsEncoding($token))
377 {
378 //Don't encode starting WSP
379 $firstChar = substr($token, 0, 1);
380 switch($firstChar)
381 {
382 case ' ':
383 case "\t":
384 $value .= $firstChar;
385 $token = substr($token, 1);
386 }
387
388 if (-1 == $usedLength)
389 {
390 $usedLength = strlen($header->getFieldName() . ': ') + strlen($value);
391 }
392 $value .= $this->getTokenAsEncodedWord($token, $usedLength);
393
394 $header->setMaxLineLength(76); //Forefully override
395 }
396 else
397 {
398 $value .= $token;
399 }
400 }
401
402 return $value;
403 }
404
405 /**
406 * Test if a token needs to be encoded or not.
407 * @param string $token
408 * @return boolean
409 */
410 protected function tokenNeedsEncoding($token)
411 {
412 return preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token);
413 }
414
415 /**
416 * Splits a string into tokens in blocks of words which can be encoded quickly.
417 * @param string $string
418 * @return string[]
419 */
420 protected function getEncodableWordTokens($string)
421 {
422 $tokens = array();
423
424 $encodedToken = '';
425 //Split at all whitespace boundaries
426 foreach (preg_split('~(?=[\t ])~', $string) as $token)
427 {
428 if ($this->tokenNeedsEncoding($token))
429 {
430 $encodedToken .= $token;
431 }
432 else
433 {
434 if (strlen($encodedToken) > 0)
435 {
436 $tokens[] = $encodedToken;
437 $encodedToken = '';
438 }
439 $tokens[] = $token;
440 }
441 }
442 if (strlen($encodedToken))
443 {
444 $tokens[] = $encodedToken;
445 }
446
447 return $tokens;
448 }
449
450 /**
451 * Get a token as an encoded word for safe insertion into headers.
452 * @param string $token to encode
453 * @param int $firstLineOffset, optional
454 * @return string
455 */
456 protected function getTokenAsEncodedWord($token, $firstLineOffset = 0)
457 {
458 //Adjust $firstLineOffset to account for space needed for syntax
459 $charsetDecl = $this->_charset;
460 if (isset($this->_lang))
461 {
462 $charsetDecl .= '*' . $this->_lang;
463 }
464 $encodingWrapperLength = strlen(
465 '=?' . $charsetDecl . '?' . $this->_encoder->getName() . '??='
466 );
467
468 if ($firstLineOffset >= 75) //Does this logic need to be here?
469 {
470 $firstLineOffset = 0;
471 }
472
473 $encodedTextLines = explode("\r\n",
474 $this->_encoder->encodeString(
475 $token, $firstLineOffset, 75 - $encodingWrapperLength
476 )
477 );
478
479 foreach ($encodedTextLines as $lineNum => $line)
480 {
481 $encodedTextLines[$lineNum] = '=?' . $charsetDecl .
482 '?' . $this->_encoder->getName() .
483 '?' . $line . '?=';
484 }
485
486 return implode("\r\n ", $encodedTextLines);
487 }
488
489 /**
490 * Generates tokens from the given string which include CRLF as individual tokens.
491 * @param string $token
492 * @return string[]
493 * @access protected
494 */
495 protected function generateTokenLines($token)
496 {
497 return preg_split('~(\r\n)~', $token, -1, PREG_SPLIT_DELIM_CAPTURE);
498 }
499
500 /**
501 * Set a value into the cache.
502 * @param string $value
503 * @access protected
504 */
505 protected function setCachedValue($value)
506 {
507 $this->_cachedValue = $value;
508 }
509
510 /**
511 * Get the value in the cache.
512 * @return string
513 * @access protected
514 */
515 protected function getCachedValue()
516 {
517 return $this->_cachedValue;
518 }
519
520 /**
521 * Clear the cached value if $condition is met.
522 * @param boolean $condition
523 * @access protected
524 */
525 protected function clearCachedValueIf($condition)
526 {
527 if ($condition)
528 {
529 $this->setCachedValue(null);
530 }
531 }
532
533 // -- Private methods
534
535 /**
536 * Generate a list of all tokens in the final header.
537 * @param string $string input, optional
538 * @return string[]
539 * @access private
540 */
541 protected function toTokens($string = null)
542 {
543 if (is_null($string))
544 {
545 $string = $this->getFieldBody();
546 }
547
548 $tokens = array();
549
550 //Generate atoms; split at all invisible boundaries followed by WSP
551 foreach (preg_split('~(?=[ \t])~', $string) as $token)
552 {
553 $tokens = array_merge($tokens, $this->generateTokenLines($token));
554 }
555
556 return $tokens;
557 }
558
559 /**
560 * Takes an array of tokens which appear in the header and turns them into
561 * an RFC 2822 compliant string, adding FWSP where needed.
562 * @param string[] $tokens
563 * @return string
564 * @access private
565 */
566 private function _tokensToString(array $tokens)
567 {
568 $lineCount = 0;
569 $headerLines = array();
570 $headerLines[] = $this->_name . ': ';
571 $currentLine =& $headerLines[$lineCount++];
572
573 //Build all tokens back into compliant header
574 foreach ($tokens as $i => $token)
575 {
576 //Line longer than specified maximum or token was just a new line
577 if (("\r\n" == $token) ||
578 ($i > 0 && strlen($currentLine . $token) > $this->_lineLength)
579 && 0 < strlen($currentLine))
580 {
581 $headerLines[] = '';
582 $currentLine =& $headerLines[$lineCount++];
583 }
584
585 //Append token to the line
586 if ("\r\n" != $token)
587 {
588 $currentLine .= $token;
589 }
590 }
591
592 //Implode with FWS (RFC 2822, 2.2.3)
593 return implode("\r\n", $headerLines) . "\r\n";
594 }
595
596 }