Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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
}