1<?php
2
3/**
4 * Swift Mailer Message Decorating Plugin.
5 * Please read the LICENSE file
6 * @author Chris Corbyn <chris@w3style.co.uk>
7 * @package Swift_Plugin
8 * @subpackage Decorator
9 * @license GNU Lesser General Public License
10 */
11
12require_once dirname(__FILE__) . "/../ClassLoader.php";
13Swift_ClassLoader::load("Swift_Events_BeforeSendListener");
14Swift_ClassLoader::load("Swift_Plugin_Decorator_Replacements");
15
16/**
17 * Swift Decorator Plugin.
18 * Allows messages to be slightly different for each recipient.
19 * @package Swift_Plugin
20 * @subpackage Decorator
21 * @author Chris Corbyn <chris@w3style.co.uk>
22 */
23class Swift_Plugin_Decorator implements Swift_Events_BeforeSendListener
24{
25  /**
26   * The replacements object.
27   * @var Swift_Plugin_Decorator_Replacements
28   */
29  protected $replacements;
30  /**
31   * Temporary storage so we can restore changes we make.
32   * @var array
33   */
34  protected $store;
35  /**
36   * A list of allowed mime types to replace bodies for.
37   * @var array
38   */
39  protected $permittedTypes = array("text/plain" => 1, "text/html" => 1);
40  /**
41   * True if values in the headers can be replaced
42   * @var boolean
43   */
44  protected $permittedInHeaders = true;
45
46  /**
47   * Ctor.
48   * @param mixed Replacements as a 2-d array or Swift_Plugin_Decorator_Replacements instance.
49   */
50  public function __construct($replacements=null)
51  {
52    $this->setReplacements($replacements);
53  }
54  /**
55   * Enable of disable the ability to replace values in the headers
56   * @param boolean
57   */
58  public function setPermittedInHeaders($bool)
59  {
60    $this->permittedInHeaders = (bool) $bool;
61  }
62  /**
63   * Check if replacements in headers are allowed.
64   * @return boolean
65   */
66  public function getPermittedInHeaders()
67  {
68    return $this->permittedInHeaders;
69  }
70  /**
71   * Add a mime type to the list of permitted type to replace values in the body.
72   * @param string The mime type (e.g. text/plain)
73   */
74  public function addPermittedType($type)
75  {
76    $type = strtolower($type);
77    $this->permittedTypes[$type] = 1;
78  }
79  /**
80   * Remove the ability to replace values in the body of the given mime type
81   * @param string The mime type
82   */
83  public function removePermittedType($type)
84  {
85    unset($this->permittedTypes[$type]);
86  }
87  /**
88   * Get the list of mime types for which the body can be changed.
89   * @return array
90   */
91  public function getPermittedTypes()
92  {
93    return array_keys($this->permittedTypes);
94  }
95  /**
96   * Check if the body can be replaced in the given mime type.
97   * @param string The mime type
98   * @return boolean
99   */
100  public function isPermittedType($type)
101  {
102    return array_key_exists(strtolower($type), $this->permittedTypes);
103  }
104  /**
105   * Called just before Swift sends a message.
106   * We perform operations on the message here.
107   * @param Swift_Events_SendEvent The event object for sending a message
108   */
109  public function beforeSendPerformed(Swift_Events_SendEvent $e)
110  {
111    $message = $e->getMessage();
112    $this->recursiveRestore($message, $this->store); //3.3.3 bugfix
113
114    $recipients = $e->getRecipients();
115    $to = array_keys($recipients->getTo());
116    if (count($to) > 0) $to = $to[0];
117    else return;
118
119    $replacements = (array)$this->replacements->getReplacementsFor($to);
120
121    $this->store = array(
122      "headers" => array(),
123      "body" => false,
124      "children" => array()
125    );
126    $this->recursiveReplace($message, $replacements, $this->store);
127  }
128  /**
129   * Replace strings in the message searching through all the allowed sub-parts.
130   * @param Swift_Message_Mime The message (or part)
131   * @param array The list of replacements
132   * @param array The array to cache original values into where needed
133   */
134  protected function recursiveReplace(Swift_Message_Mime $mime, $replacements, &$store)
135  {
136    //Check headers
137    if ($this->getPermittedInHeaders())
138    {
139      foreach ($mime->headers->getList() as $name => $value)
140      {
141        if (is_string($value) && ($replaced = $this->replace($replacements, $value)) != $value)
142        {
143          $mime->headers->set($name, $replaced);
144          $store["headers"][$name] = array();
145          $store["headers"][$name]["value"] = $value;
146          $store["headers"][$name]["attributes"] = array();
147        }
148        foreach ($mime->headers->listAttributes($name) as $att_name => $att_value)
149        {
150          if (is_string($att_value)
151            && ($att_replaced = $this->replace($replacements, $att_value)) != $att_value)
152          {
153            if (!isset($store["headers"][$name]))
154            {
155              $store["headers"][$name] = array("value" => false, "attributes" => array());
156            }
157            $mime->headers->setAttribute($name, $att_name, $att_replaced);
158            $store["headers"][$name]["attributes"][$att_name] = $att_value;
159          }
160        }
161      }
162    }
163    //Check body
164    $body = $mime->getData();
165    if ($this->isPermittedType($mime->getContentType())
166      && is_string($body) && ($replaced = $this->replace($replacements, $body)) != $body)
167    {
168      $mime->setData($replaced);
169      $store["body"] = $body;
170    }
171    //Check sub-parts
172    foreach ($mime->listChildren() as $id)
173    {
174      $store["children"][$id] = array(
175        "headers" => array(),
176        "body" => false,
177        "children" => array()
178      );
179      $child = $mime->getChild($id);
180      $this->recursiveReplace($child, $replacements, $store["children"][$id]);
181    }
182  }
183  /**
184   * Perform a str_replace() over the given value.
185   * @param array The list of replacements as (search => replacement)
186   * @param string The string to replace
187   * @return string
188   */
189  protected function replace($replacements, $value)
190  {
191    return str_replace(array_keys($replacements), array_values($replacements), $value);
192  }
193  /**
194   * Put the original values back in the message after it was modified before sending.
195   * @param Swift_Message_Mime The message (or part)
196   * @param array The location of the stored values
197   */
198  protected function recursiveRestore(Swift_Message_Mime $mime, &$store)
199  {
200    if (empty($store)) //3.3.3 bugfix
201    {
202      return;
203    }
204
205    //Restore headers
206    foreach ($store["headers"] as $name => $array)
207    {
208      if ($array["value"] !== false) $mime->headers->set($name, $array["value"]);
209      foreach ($array["attributes"] as $att_name => $att_value)
210      {
211        $mime->headers->setAttribute($name, $att_name, $att_value);
212      }
213    }
214    //Restore body
215    if ($store["body"] !== false)
216    {
217      $mime->setData($store["body"]);
218    }
219    //Restore children
220    foreach ($store["children"] as $id => $child_store)
221    {
222      $child = $mime->getChild($id);
223      $this->recursiveRestore($child, $child_store);
224    }
225  }
226  /**
227   * Set the replacements as a 2-d array or an instance of Swift_Plugin_Decorator_Replacements.
228   * @param mixed Array or Swift_Plugin_Decorator_Replacements
229   */
230  public function setReplacements($replacements)
231  {
232    if ($replacements === null)
233    {
234      $r = array();
235      $this->replacements = new Swift_Plugin_Decorator_Replacements($r);
236    }
237    elseif (is_array($replacements))
238    {
239      $this->replacements = new Swift_Plugin_Decorator_Replacements($replacements);
240    }
241    elseif ($replacements instanceof Swift_Plugin_Decorator_Replacements)
242    {
243      $this->replacements = $replacements;
244    }
245    else
246    {
247      throw new Exception(
248        "Decorator replacements must be array or instance of Swift_Plugin_Decorator_Replacements.");
249    }
250  }
251  /**
252   * Get the replacements object.
253   * @return Swift_Plugin_Decorator_Replacements
254   */
255  public function getReplacements()
256  {
257    return $this->replacements;
258  }
259}
260