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 
12 require_once dirname(__FILE__) . "/../ClassLoader.php";
13 Swift_ClassLoader::load("Swift_Events_BeforeSendListener");
14 Swift_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  */
23 class 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