UnstructuredHeaderTest.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <?php
  2. require_once 'Swift/Tests/SwiftUnitTestCase.php';
  3. require_once 'Swift/Mime/Headers/UnstructuredHeader.php';
  4. require_once 'Swift/Mime/HeaderEncoder.php';
  5. require_once 'Swift/Mime/Grammar.php';
  6. class Swift_Mime_Headers_UnstructuredHeaderTest
  7. extends Swift_Tests_SwiftUnitTestCase
  8. {
  9. private $_charset = 'utf-8';
  10. public function testTypeIsTextHeader()
  11. {
  12. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  13. $this->assertEqual(Swift_Mime_Header::TYPE_TEXT, $header->getFieldType());
  14. }
  15. public function testGetNameReturnsNameVerbatim()
  16. {
  17. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  18. $this->assertEqual('Subject', $header->getFieldName());
  19. }
  20. public function testGetValueReturnsValueVerbatim()
  21. {
  22. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  23. $header->setValue('Test');
  24. $this->assertEqual('Test', $header->getValue());
  25. }
  26. public function testBasicStructureIsKeyValuePair()
  27. {
  28. /* -- RFC 2822, 2.2
  29. Header fields are lines composed of a field name, followed by a colon
  30. (":"), followed by a field body, and terminated by CRLF.
  31. */
  32. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  33. $header->setValue('Test');
  34. $this->assertEqual('Subject: Test' . "\r\n", $header->toString());
  35. }
  36. public function testLongHeadersAreFoldedAtWordBoundary()
  37. {
  38. /* -- RFC 2822, 2.2.3
  39. Each header field is logically a single line of characters comprising
  40. the field name, the colon, and the field body. For convenience
  41. however, and to deal with the 998/78 character limitations per line,
  42. the field body portion of a header field can be split into a multiple
  43. line representation; this is called "folding". The general rule is
  44. that wherever this standard allows for folding white space (not
  45. simply WSP characters), a CRLF may be inserted before any WSP.
  46. */
  47. $value = 'The quick brown fox jumped over the fence, he was a very very ' .
  48. 'scary brown fox with a bushy tail';
  49. $header = $this->_getHeader('X-Custom-Header',
  50. $this->_getEncoder('Q', true)
  51. );
  52. $header->setValue($value);
  53. $header->setMaxLineLength(78); //A safe [RFC 2822, 2.2.3] default
  54. /*
  55. X-Custom-Header: The quick brown fox jumped over the fence, he was a very very
  56. scary brown fox with a bushy tail
  57. */
  58. $this->assertEqual(
  59. 'X-Custom-Header: The quick brown fox jumped over the fence, he was a' .
  60. ' very very' . "\r\n" . //Folding
  61. ' scary brown fox with a bushy tail' . "\r\n",
  62. $header->toString(), '%s: The header should have been folded at 78th char'
  63. );
  64. }
  65. public function testPrintableAsciiOnlyAppearsInHeaders()
  66. {
  67. /* -- RFC 2822, 2.2.
  68. A field name MUST be composed of printable US-ASCII characters (i.e.,
  69. characters that have values between 33 and 126, inclusive), except
  70. colon. A field body may be composed of any US-ASCII characters,
  71. except for CR and LF.
  72. */
  73. $nonAsciiChar = pack('C', 0x8F);
  74. $header = $this->_getHeader('X-Test', $this->_getEncoder('Q', true));
  75. $header->setValue($nonAsciiChar);
  76. $this->assertPattern(
  77. '~^[^:\x00-\x20\x80-\xFF]+: [^\x80-\xFF\r\n]+\r\n$~s',
  78. $header->toString()
  79. );
  80. }
  81. public function testEncodedWordsFollowGeneralStructure()
  82. {
  83. /* -- RFC 2047, 1.
  84. Generally, an "encoded-word" is a sequence of printable ASCII
  85. characters that begins with "=?", ends with "?=", and has two "?"s in
  86. between.
  87. */
  88. $nonAsciiChar = pack('C', 0x8F);
  89. $header = $this->_getHeader('X-Test', $this->_getEncoder('Q', true));
  90. $header->setValue($nonAsciiChar);
  91. $this->assertPattern(
  92. '~^X-Test: \=?.*?\?.*?\?.*?\?=\r\n$~s',
  93. $header->toString()
  94. );
  95. }
  96. public function testEncodedWordIncludesCharsetAndEncodingMethodAndText()
  97. {
  98. /* -- RFC 2047, 2.
  99. An 'encoded-word' is defined by the following ABNF grammar. The
  100. notation of RFC 822 is used, with the exception that white space
  101. characters MUST NOT appear between components of an 'encoded-word'.
  102. encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
  103. */
  104. $nonAsciiChar = pack('C', 0x8F);
  105. $encoder = $this->_getEncoder('Q');
  106. $this->_checking(Expectations::create()
  107. -> one($encoder)->encodeString($nonAsciiChar, any(), any()) -> returns('=8F')
  108. -> ignoring($encoder)
  109. );
  110. $header = $this->_getHeader('X-Test', $encoder);
  111. $header->setValue($nonAsciiChar);
  112. $this->assertEqual(
  113. 'X-Test: =?' . $this->_charset . '?Q?=8F?=' . "\r\n",
  114. $header->toString()
  115. );
  116. }
  117. public function testEncodedWordsAreUsedToEncodedNonPrintableAscii()
  118. {
  119. //SPACE and TAB permitted
  120. $nonPrintableBytes = array_merge(
  121. range(0x00, 0x08), range(0x10, 0x19), array(0x7F)
  122. );
  123. foreach ($nonPrintableBytes as $byte)
  124. {
  125. $char = pack('C', $byte);
  126. $encodedChar = sprintf('=%02X', $byte);
  127. $encoder = $this->_getEncoder('Q');
  128. $this->_checking(Expectations::create()
  129. -> one($encoder)->encodeString($char, any(), any()) -> returns($encodedChar)
  130. -> ignoring($encoder)
  131. );
  132. $header = $this->_getHeader('X-A', $encoder);
  133. $header->setValue($char);
  134. $this->assertEqual(
  135. 'X-A: =?' . $this->_charset . '?Q?' . $encodedChar . '?=' . "\r\n",
  136. $header->toString(), '%s: Non-printable ascii should be encoded'
  137. );
  138. }
  139. }
  140. public function testEncodedWordsAreUsedToEncode8BitOctets()
  141. {
  142. $_8BitBytes = range(0x80, 0xFF);
  143. foreach ($_8BitBytes as $byte)
  144. {
  145. $char = pack('C', $byte);
  146. $encodedChar = sprintf('=%02X', $byte);
  147. $encoder = $this->_getEncoder('Q');
  148. $this->_checking(Expectations::create()
  149. -> one($encoder)->encodeString($char, any(), any()) -> returns($encodedChar)
  150. -> ignoring($encoder)
  151. );
  152. $header = $this->_getHeader('X-A', $encoder);
  153. $header->setValue($char);
  154. $this->assertEqual(
  155. 'X-A: =?' . $this->_charset . '?Q?' . $encodedChar . '?=' . "\r\n",
  156. $header->toString(), '%s: 8-bit octets should be encoded'
  157. );
  158. }
  159. }
  160. public function testEncodedWordsAreNoMoreThan75CharsPerLine()
  161. {
  162. /* -- RFC 2047, 2.
  163. An 'encoded-word' may not be more than 75 characters long, including
  164. 'charset', 'encoding', 'encoded-text', and delimiters.
  165. ... SNIP ...
  166. While there is no limit to the length of a multiple-line header
  167. field, each line of a header field that contains one or more
  168. 'encoded-word's is limited to 76 characters.
  169. */
  170. $nonAsciiChar = pack('C', 0x8F);
  171. $encoder = $this->_getEncoder('Q');
  172. $this->_checking(Expectations::create()
  173. -> one($encoder)->encodeString($nonAsciiChar, 8, 63) -> returns('=8F')
  174. -> ignoring($encoder)
  175. );
  176. //Note that multi-line headers begin with LWSP which makes 75 + 1 = 76
  177. //Note also that =?utf-8?q??= is 12 chars which makes 75 - 12 = 63
  178. //* X-Test: is 8 chars
  179. $header = $this->_getHeader('X-Test', $encoder);
  180. $header->setValue($nonAsciiChar);
  181. $this->assertEqual(
  182. 'X-Test: =?' . $this->_charset . '?Q?=8F?=' . "\r\n",
  183. $header->toString()
  184. );
  185. }
  186. public function testFWSPIsUsedWhenEncoderReturnsMultipleLines()
  187. {
  188. /* --RFC 2047, 2.
  189. If it is desirable to encode more text than will fit in an 'encoded-word' of
  190. 75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may
  191. be used.
  192. */
  193. //Note the Mock does NOT return 8F encoded, the 8F merely triggers
  194. // encoding for the sake of testing
  195. $nonAsciiChar = pack('C', 0x8F);
  196. $encoder = $this->_getEncoder('Q');
  197. $this->_checking(Expectations::create()
  198. -> one($encoder)->encodeString($nonAsciiChar, 8, 63)
  199. -> returns('line_one_here' . "\r\n" . 'line_two_here')
  200. -> ignoring($encoder)
  201. );
  202. //Note that multi-line headers begin with LWSP which makes 75 + 1 = 76
  203. //Note also that =?utf-8?q??= is 12 chars which makes 75 - 12 = 63
  204. //* X-Test: is 8 chars
  205. $header = $this->_getHeader('X-Test', $encoder);
  206. $header->setValue($nonAsciiChar);
  207. $this->assertEqual(
  208. 'X-Test: =?' . $this->_charset . '?Q?line_one_here?=' . "\r\n" .
  209. ' =?' . $this->_charset . '?Q?line_two_here?=' . "\r\n",
  210. $header->toString()
  211. );
  212. }
  213. public function testAdjacentWordsAreEncodedTogether()
  214. {
  215. /* -- RFC 2047, 5 (1)
  216. Ordinary ASCII text and 'encoded-word's may appear together in the
  217. same header field. However, an 'encoded-word' that appears in a
  218. header field defined as '*text' MUST be separated from any adjacent
  219. 'encoded-word' or 'text' by 'linear-white-space'.
  220. -- RFC 2047, 2.
  221. IMPORTANT: 'encoded-word's are designed to be recognized as 'atom's
  222. by an RFC 822 parser. As a consequence, unencoded white space
  223. characters (such as SPACE and HTAB) are FORBIDDEN within an
  224. 'encoded-word'.
  225. */
  226. //It would be valid to encode all words needed, however it's probably
  227. // easiest to encode the longest amount required at a time
  228. $word = 'w' . pack('C', 0x8F) . 'rd';
  229. $text = 'start ' . $word . ' ' . $word . ' then end ' . $word;
  230. // 'start', ' word word', ' and end', ' word'
  231. $encoder = $this->_getEncoder('Q');
  232. $this->_checking(Expectations::create()
  233. -> one($encoder)->encodeString($word . ' ' . $word, any(), any())
  234. -> returns('w=8Frd_w=8Frd')
  235. -> one($encoder)->encodeString($word, any(), any()) -> returns('w=8Frd')
  236. -> ignoring($encoder)
  237. );
  238. $header = $this->_getHeader('X-Test', $encoder);
  239. $header->setValue($text);
  240. $headerString = $header->toString();
  241. $this->assertEqual('X-Test: start =?' . $this->_charset . '?Q?' .
  242. 'w=8Frd_w=8Frd?= then end =?' . $this->_charset . '?Q?'.
  243. 'w=8Frd?=' . "\r\n", $headerString,
  244. '%s: Adjacent encoded words should appear grouped with WSP encoded'
  245. );
  246. }
  247. public function testLanguageInformationAppearsInEncodedWords()
  248. {
  249. /* -- RFC 2231, 5.
  250. 5. Language specification in Encoded Words
  251. RFC 2047 provides support for non-US-ASCII character sets in RFC 822
  252. message header comments, phrases, and any unstructured text field.
  253. This is done by defining an encoded word construct which can appear
  254. in any of these places. Given that these are fields intended for
  255. display, it is sometimes necessary to associate language information
  256. with encoded words as well as just the character set. This
  257. specification extends the definition of an encoded word to allow the
  258. inclusion of such information. This is simply done by suffixing the
  259. character set specification with an asterisk followed by the language
  260. tag. For example:
  261. From: =?US-ASCII*EN?Q?Keith_Moore?= <moore@cs.utk.edu>
  262. */
  263. $value = 'fo' . pack('C', 0x8F) . 'bar';
  264. $encoder = $this->_getEncoder('Q');
  265. $this->_checking(Expectations::create()
  266. -> one($encoder)->encodeString($value, any(), any()) -> returns('fo=8Fbar')
  267. -> ignoring($encoder)
  268. );
  269. $header = $this->_getHeader('Subject', $encoder);
  270. $header->setLanguage('en');
  271. $header->setValue($value);
  272. $this->assertEqual("Subject: =?utf-8*en?Q?fo=8Fbar?=\r\n",
  273. $header->toString()
  274. );
  275. }
  276. public function testSetBodyModel()
  277. {
  278. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  279. $header->setFieldBodyModel('test');
  280. $this->assertEqual('test', $header->getValue());
  281. }
  282. public function testGetBodyModel()
  283. {
  284. $header = $this->_getHeader('Subject', $this->_getEncoder('Q', true));
  285. $header->setValue('test');
  286. $this->assertEqual('test', $header->getFieldBodyModel());
  287. }
  288. // -- Private methods
  289. private function _getHeader($name, $encoder)
  290. {
  291. $header = new Swift_Mime_Headers_UnstructuredHeader($name, $encoder, new Swift_Mime_Grammar());
  292. $header->setCharset($this->_charset);
  293. return $header;
  294. }
  295. private function _getEncoder($type, $stub = false)
  296. {
  297. $encoder = $this->_mock('Swift_Mime_HeaderEncoder');
  298. $this->_checking(Expectations::create()
  299. -> ignoring($encoder)->getName() -> returns($type)
  300. );
  301. if ($stub)
  302. {
  303. $this->_checking(Expectations::create()
  304. -> ignoring($encoder)
  305. );
  306. }
  307. return $encoder;
  308. }
  309. }