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