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