AbstractHeader.php 13KB

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