1<?php
2
3/**
4 * Swift Mailer MIME Library Headers component
5 * Please read the LICENSE file
6 * @copyright Chris Corbyn <chris@w3style.co.uk>
7 * @author Chris Corbyn <chris@w3style.co.uk>
8 * @package Swift_Message
9 * @license GNU Lesser General Public License
10 */
11
12require_once dirname(__FILE__) . "/../ClassLoader.php";
13
14/**
15 * Contains and constructs the headers for a MIME document
16 * @package Swift_Message
17 * @author Chris Corbyn <chris@w3style.co.uk>
18 */
19class Swift_Message_Headers
20{
21  /**
22   * Headers which may contain email addresses, and therefore should take notice when encoding
23   * @var array headers
24   */
25  protected $emailContainingHeaders = array(
26    "To", "From", "Reply-To", "Cc", "Bcc", "Return-Path", "Sender");
27  /**
28   * The encoding format used for the body of the document
29   * @var string format
30   */
31  protected $encoding = "B";
32  /**
33   * The charset used in the headers
34   * @var string
35   */
36  protected $charset = false;
37  /**
38   * A collection of headers
39   * @var array headers
40   */
41  protected $headers = array();
42  /**
43   * A container of references to the headers
44   * @var array
45   */
46  protected $lowerHeaders = array();
47  /**
48   * Attributes appended to headers
49   * @var array
50   */
51  protected $attributes = array();
52  /**
53   * If QP or Base64 encoding should be forced
54   * @var boolean
55   */
56  protected $forceEncoding = false;
57  /**
58   * The language used in the headers (doesn't really matter much)
59   * @var string
60   */
61  protected $language = "en-us";
62  /**
63   * Cached, pre-built headers
64   * @var string
65   */
66  protected $cached = array();
67  /**
68   * The line ending used in the headers
69   * @var string
70   */
71  protected $LE = "\r\n";
72
73  /**
74   * Set the line ending character to use
75   * @param string The line ending sequence
76   * @return boolean
77   */
78  public function setLE($le)
79  {
80    if (in_array($le, array("\r", "\n", "\r\n")))
81    {
82      foreach (array_keys($this->cached) as $k) $this->cached[$k] = null;
83      $this->LE = $le;
84      return true;
85    }
86    else return false;
87  }
88  /**
89   * Get the line ending sequence
90   * @return string
91   */
92  public function getLE()
93  {
94    return $this->LE;
95  }
96  /**
97   * Reset the cache state in these headers
98   */
99  public function uncacheAll()
100  {
101    foreach (array_keys($this->cached) as $k)
102    {
103      $this->cached[$k] = null;
104    }
105  }
106  /**
107   * Add a header or change an existing header value
108   * @param string The header name, for example "From" or "Subject"
109   * @param string The value to be inserted into the header.  This is safe from header injection.
110   */
111  public function set($name, $value)
112  {
113    $lname = strtolower($name);
114    if (!isset($this->lowerHeaders[$lname]))
115    {
116      $this->headers[$name] = null;
117      $this->lowerHeaders[$lname] =& $this->headers[$name];
118    }
119    $this->cached[$lname] = null;
120    Swift_ClassLoader::load("Swift_Message_Encoder");
121    if (is_array($value))
122    {
123      foreach ($value as $v)
124      {
125        if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($v))
126        {
127          $this->setCharset("utf-8");
128          break;
129        }
130      }
131    }
132    elseif ($value !== null)
133    {
134      if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($value))
135      {
136        $this->setCharset("utf-8");
137      }
138    }
139    if (!is_array($value) && $value !== null) $this->lowerHeaders[$lname] = (string) $value;
140    else $this->lowerHeaders[$lname] = $value;
141  }
142  /**
143   * Get the value at a given header
144   * @param string The name of the header, for example "From" or "Subject"
145   * @return string
146   * @throws Swift_Message_MimeException If no such header exists
147   * @see hasHeader
148   */
149  public function get($name)
150  {
151    $lname = strtolower($name);
152    if ($this->has($name))
153    {
154      return $this->lowerHeaders[$lname];
155    }
156  }
157  /**
158   * Remove a header from the list
159   * @param string The name of the header
160   */
161  public function remove($name)
162  {
163    $lname = strtolower($name);
164    if ($this->has($name))
165    {
166      unset($this->headers[$name]);
167      unset($this->lowerHeaders[$lname]);
168      unset($this->cached[$lname]);
169      if (isset($this->attributes[$lname])) unset($this->attributes[$lname]);
170    }
171  }
172  /**
173   * Just fetch the array containing the headers
174   * @return array
175   */
176  public function getList()
177  {
178    return $this->headers;
179  }
180  /**
181   * Check if a header has been set or not
182   * @param string The name of the header, for example "From" or "Subject"
183   * @return boolean
184   */
185  public function has($name)
186  {
187    $lname = strtolower($name);
188    return (array_key_exists($lname, $this->lowerHeaders) && $this->lowerHeaders[$lname] !== null);
189  }
190  /**
191   * Set the language used in the headers to $lang (e.g. en-us, en-gb, sv etc)
192   * @param string The language to use
193   */
194  public function setLanguage($lang)
195  {
196    $this->language = (string) $lang;
197  }
198  /**
199   * Get the language used in the headers to $lang (e.g. en-us, en-gb, sv etc)
200   * @return string
201   */
202  public function getLanguage()
203  {
204    return $this->language;
205  }
206  /**
207   * Set the charset used in the headers
208   * @param string The charset name
209   */
210  public function setCharset($charset)
211  {
212    $this->charset = (string) $charset;
213  }
214  /**
215   * Get the current charset used
216   * @return string
217   */
218  public function getCharset()
219  {
220    return $this->charset;
221  }
222  /**
223   * Specify the encoding to use for the headers if characters outside the 7-bit-printable ascii range are found
224   * This encoding will never be used if only 7-bit-printable characters are found in the headers.
225   * Possible values are:
226   *  - QP
227   *  - Q
228   *  - Quoted-Printable
229   *  - B
230   *  - Base64
231   * NOTE: Q, QP, Quoted-Printable are all the same; as are B and Base64
232   * @param string The encoding format to use
233   * @return boolean
234   */
235  public function setEncoding($encoding)
236  {
237    switch (strtolower($encoding))
238    {
239      case "qp": case "q": case "quoted-printable":
240      $this->encoding = "Q";
241      return true;
242      case "base64": case "b":
243      $this->encoding = "B";
244      return true;
245      default: return false;
246    }
247  }
248  /**
249   * Get the encoding format used in this document
250   * @return string
251   */
252  public function getEncoding()
253  {
254    return $this->encoding;
255  }
256  /**
257   * Turn on or off forced header encoding
258   * @param boolean On/Off
259   */
260  public function forceEncoding($force=true)
261  {
262    $this->forceEncoding = (boolean) $force;
263  }
264  /**
265   * Set an attribute in a major header
266   * For example $headers->setAttribute("Content-Type", "format", "flowed")
267   * @param string The main header these values exist in
268   * @param string The name for this value
269   * @param string The value to set
270   * @throws Swift_Message_MimeException If no such header exists
271   */
272  public function setAttribute($header, $name, $value)
273  {
274    $name = strtolower($name);
275    $lheader = strtolower($header);
276    $this->cached[$lheader] = null;
277    if (!$this->has($header))
278    {
279      throw new Swift_Message_MimeException(
280      "Cannot set attribute '" . $name . "' for header '" . $header . "' as the header does not exist. " .
281      "Consider using Swift_Message_Headers-&gt;has() to check.");
282    }
283    else
284    {
285      Swift_ClassLoader::load("Swift_Message_Encoder");
286      if (!$this->getCharset() && Swift_Message_Encoder::instance()->isUTF8($value)) $this->setCharset("utf-8");
287      if (!isset($this->attributes[$lheader])) $this->attributes[$lheader] = array();
288      if ($value !== null) $this->attributes[$lheader][$name] = (string) $value;
289      else $this->attributes[$lheader][$name] = $value;
290    }
291  }
292  /**
293   * Check if a header has a given attribute applied to it
294   * @param string The name of the main header
295   * @param string The name of the attribute
296   * @return boolean
297   */
298  public function hasAttribute($header, $name)
299  {
300    $name = strtolower($name);
301    $lheader = strtolower($header);
302    if (!$this->has($header))
303    {
304      return false;
305    }
306    else
307    {
308      return (isset($this->attributes[$lheader]) && isset($this->attributes[$lheader][$name]) && ($this->attributes[$lheader][$name] !== null));
309    }
310  }
311  /**
312   * Get the value for a given attribute on a given header
313   * @param string The name of the main header
314   * @param string The name of the attribute
315   * @return string
316   * @throws Swift_Message_MimeException If no header is set
317   */
318  public function getAttribute($header, $name)
319  {
320    if (!$this->has($header))
321    {
322      throw new Swift_Message_MimeException(
323      "Cannot locate attribute '" . $name . "' for header '" . $header . "' as the header does not exist. " .
324      "Consider using Swift_Message_Headers-&gt;has() to check.");
325    }
326
327    $name = strtolower($name);
328    $lheader = strtolower($header);
329
330    if ($this->hasAttribute($header, $name))
331    {
332      return $this->attributes[$lheader][$name];
333    }
334  }
335  /**
336   * Remove an attribute from a header
337   * @param string The name of the header to remove the attribute from
338   * @param string The name of the attribute to remove
339   */
340  public function removeAttribute($header, $name)
341  {
342    $name = strtolower($name);
343    $lheader = strtolower($header);
344    if ($this->has($header))
345    {
346      unset($this->attributes[$lheader][$name]);
347    }
348  }
349  /**
350   * Get a list of all the attributes in the given header.
351   * @param string The name of the header
352   * @return array
353   */
354  public function listAttributes($header)
355  {
356    $header = strtolower($header);
357    if (array_key_exists($header, $this->attributes))
358    {
359      return $this->attributes[$header];
360    }
361    else return array();
362  }
363  /**
364   * Get the header in it's compliant, encoded form
365   * @param string The name of the header
366   * @return string
367   * @throws Swift_Message_MimeException If the header doesn't exist
368   */
369  public function getEncoded($name)
370  {
371    if (!$this->getCharset()) $this->setCharset("iso-8859-1");
372    Swift_ClassLoader::load("Swift_Message_Encoder");
373    //I'll try as best I can to walk through this...
374
375    $lname = strtolower($name);
376
377    if ($this->cached[$lname] !== null) return $this->cached[$lname];
378
379    $value = $this->get($name);
380
381    $is_email = in_array($name, $this->emailContainingHeaders);
382
383    $encoded_value = (array) $value; //Turn strings into arrays (just to make the following logic simpler)
384
385    //Look at each value in this header
386    // There will only be 1 value if it was a string to begin with, and usually only address lists will be multiple
387    foreach ($encoded_value as $key => $row)
388    {
389      $spec = ""; //The bit which specifies the encoding of the header (if any)
390      $end = ""; //The end delimiter for an encoded header
391
392      //If the header is 7-bit printable it's at no risk of injection
393      if (Swift_Message_Encoder::instance()->isHeaderSafe($row) && !$this->forceEncoding)
394      {
395        //Keeps the total line length at less than 76 chars, taking into account the Header name length
396        $encoded_value[$key] = Swift_Message_Encoder::instance()->header7BitEncode(
397          $row, 72, ($key > 0 ? 0 : (75-(strlen($name)+5))), $this->LE);
398      }
399      elseif ($this->encoding == "Q") //QP encode required
400      {
401        $spec = "=?" . $this->getCharset() . "?Q?"; //e.g. =?iso-8859-1?Q?
402        $end = "?=";
403        //Calculate the length of, for example: "From: =?iso-8859-1?Q??="
404        $used_length = strlen($name) + 2 + strlen($spec) + 2;
405
406        //Encode to QP, excluding the specification for now but keeping the lines short enough to be compliant
407        $encoded_value[$key] = str_replace(" ", "_", Swift_Message_Encoder::instance()->QPEncode(
408          $row, (75-(strlen($spec)+6)), ($key > 0 ? 0 : (75-$used_length)), true, $this->LE));
409
410      }
411      elseif ($this->encoding == "B") //Need to Base64 encode
412      {
413        //See the comments in the elseif() above since the logic is the same (refactor?)
414        $spec = "=?" . $this->getCharset() . "?B?";
415        $end = "?=";
416        $used_length = strlen($name) + 2 + strlen($spec) + 2;
417        $encoded_value[$key] = Swift_Message_Encoder::instance()->base64Encode(
418          $row, (75-(strlen($spec)+5)), ($key > 0 ? 0 : (76-($used_length+3))), true, $this->LE);
419      }
420
421      if (false !== $p = strpos($encoded_value[$key], $this->LE))
422      {
423        $cb = 'str_replace("' . $this->LE . '", "", "<$1>");';
424        $encoded_value[$key] = preg_replace("/<([^>]+)>/e", $cb, $encoded_value[$key]);
425      }
426
427      //Turn our header into an array of lines ready for wrapping around the encoding specification
428      $lines = explode($this->LE, $encoded_value[$key]);
429
430      for ($i = 0, $len = count($lines); $i < $len; $i++)
431      {
432        //Don't allow commas in address fields without quotes unless they're encoded
433        if (empty($spec) && $is_email && (false !== $p = strpos($lines[$i], ",")))
434        {
435          $s = strpos($lines[$i], " <");
436          $e = strpos($lines[$i], ">");
437          if ($s < $e)
438          {
439            $addr = substr($lines[$i], $s);
440            $lines[$i] = "\"" . substr($lines[$i], 0, $s) . "\"" . $addr;
441          }
442          else
443          {
444            $lines[$i] = "\"" . $lines[$i] . "\"";
445          }
446        }
447
448        if ($this->encoding == "Q") $lines[$i] = rtrim($lines[$i], "=");
449
450        if ($lines[$i] == "" && $i > 0)
451        {
452          unset($lines[$i]); //Empty line, we'd rather not have these in the headers thank you!
453          continue;
454        }
455        if ($i > 0)
456        {
457          //Don't stick the specification part around the line if it's an address
458          if (substr($lines[$i], 0, 1) == '<' && substr($lines[$i], -1) == '>') $lines[$i] = " " . $lines[$i];
459          else $lines[$i] = " " . $spec . $lines[$i] . $end;
460        }
461        else
462        {
463          if (substr($lines[$i], 0, 1) != '<' || substr($lines[$i], -1) != '>') $lines[$i] = $spec . $lines[$i] . $end;
464        }
465      }
466      //Build back into a string, now includes the specification
467      $encoded_value[$key] = implode($this->LE, $lines);
468      $lines = null;
469    }
470
471    //If there are multiple values in this header, put them on separate lines, cleared by commas
472    $this->cached[$lname] = implode("," . $this->LE . " ", $encoded_value);
473
474    //Append attributes if there are any
475    if (!empty($this->attributes[$lname])) $this->cached[$lname] .= $this->buildAttributes($this->cached[$lname], $lname);
476
477    return $this->cached[$lname];
478  }
479  /**
480   * Build the list of attributes for appending to the given header
481   * This is RFC 2231 & 2047 compliant.
482   * A HUGE thanks to Joaquim Homrighausen for heaps of help, advice
483   * and testing to get this working rock solid.
484   * @param string The header built without attributes
485   * @param string The lowercase name of the header
486   * @return string
487   * @throws Swift_Message_MimeException If no such header exists or there are no attributes
488   */
489  protected function buildAttributes($header_line, $header_name)
490  {
491    Swift_ClassLoader::load("Swift_Message_Encoder");
492    $lines = explode($this->LE, $header_line);
493    $used_len = strlen($lines[count($lines)-1]);
494    $lines= null;
495    $ret = "";
496    foreach ($this->attributes[$header_name] as $attribute => $att_value)
497    {
498      if ($att_value === null) continue;
499      // 70 to account for LWSP, CRLF, quotes and a semi-colon
500      // + length of attribute
501      // + 4 for a 2 digit number and 2 asterisks
502      $avail_len = 70 - (strlen($attribute) + 4);
503      $encoded = Swift_Message_Encoder::instance()->rfc2047Encode($att_value, $this->charset, $this->language, $avail_len, $this->LE);
504      $lines = explode($this->LE, $encoded);
505      foreach ($lines as $i => $line)
506      {
507        //Add quotes if needed (RFC 2045)
508        if (preg_match("~[\\s\";,<>\\(\\)@:\\\\/\\[\\]\\?=]~", $line)) $lines[$i] = '"' . $line . '"';
509      }
510      $encoded = implode($this->LE, $lines);
511
512      //If we can fit this entire attribute onto the same line as the header then do it!
513      if ((strlen($encoded) + $used_len + strlen($attribute) + 4) < 74)
514      {
515        if (strpos($encoded, "'") !== false) $attribute .= "*";
516        $append = "; " . $attribute . "=" . $encoded;
517        $ret .= $append;
518        $used_len += strlen($append);
519      }
520      else //... otherwise list of underneath
521      {
522        $ret .= ";";
523        if (count($lines) > 1)
524        {
525          $loop = false;
526          $add_asterisk = false;
527          foreach ($lines as $i => $line)
528          {
529            $att_copy = $attribute; //Because it's multi-line it needs asterisks with decimal indices
530            $att_copy .= "*" . $i;
531            if ($add_asterisk || strpos($encoded, "'") !== false)
532            {
533              $att_copy .= "*"; //And if it's got a ' then it needs another asterisk
534              $add_asterisk = true;
535            }
536            $append = "";
537            if ($loop) $append .= ";";
538            $append .= $this->LE . " " . $att_copy . "=" . $line;
539            $ret .= $append;
540            $used_len = strlen($append)+1;
541            $loop = true;
542          }
543        }
544        else
545        {
546          if (strpos($encoded, "'") !== false) $attribute .= "*";
547          $append = $this->LE . " " . $attribute . "=" . $encoded;
548          $used_len = strlen($append)+1;
549          $ret .= $append;
550        }
551      }
552      $lines= null;
553    }
554    return $ret;
555  }
556  /**
557   * Compile the list of headers which have been set and return an ascii string
558   * The return value should always be 7-bit ascii and will have been cleaned for header injection
559   * If this looks complicated it's probably because it is!!  Keeping everything compliant is not easy.
560   * This is RFC 2822 compliant
561   * @return string
562   */
563  public function build()
564  {
565    $ret = "";
566    foreach ($this->headers as $name => $value) //Look at each header
567    {
568      if ($value === null) continue;
569      $ret .= ltrim($name, ".") . ": " . $this->getEncoded($name) . $this->LE;
570    }
571    return trim($ret);
572  }
573}
574